diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..2959201f4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.js linguist-language=java +*.css linguist-language=java +*.html linguist-language=java diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..c2eed7894 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,20 @@ +--- +name: BUG反馈 +about: 发现bug了,赶紧提一个 +title: '' +labels: 'bug' +assignees: '' + +--- + +**bug描述** + +可以再这里对bug进行的简单的描述,图文并茂更好 + +**复现** + +bug复现步骤: + + +bug产生原因(若您已发现具体的bug产生原因,请直接贴上,也可以提merge进行修复) + diff --git a/.github/ISSUE_TEMPLATE/discuss_report.md b/.github/ISSUE_TEMPLATE/discuss_report.md new file mode 100644 index 000000000..8a9a3291a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/discuss_report.md @@ -0,0 +1,11 @@ +--- +name: 讨论帖 +about: 文明讨论,和谐互助 +title: '' +labels: 'discuss' +assignees: '' + +--- + +**讨论主题** + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..449873c26 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: 新功能 +about: 描述一下希望新增的功能特点 +title: '' +labels: 'feature' +assignees: '' +--- + +**请说明一下新增支持的新功能** + + +**请描述下希望实现的功能特点** + + +**请描述下期望实现的方式** + + +**其他信息** \ No newline at end of file diff --git a/.gitignore b/.gitignore index b27158fec..67d6ae7c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -HELP.md !.gitignore target/ !.mvn/wrapper/maven-wrapper.jar @@ -33,5 +32,19 @@ build/ ### VS Code ### .vscode/ -## ignore logs -logs/ \ No newline at end of file +### ignore logs +logs/ +pid.log +*.jar +*.jar.bak +*.jar.tmp +*.log + +### .DS_Store + +**/.DS_Store + + +# 支付证书相关移除 +paicoding-web/src/main/resources/cert +/.mvn diff --git a/License b/License new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/License @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index e8e70cb6d..466a530f6 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,257 @@ -quick-forum ---- +

+ + 技术派,让技术也能很好玩 + +

+一个基于 Spring Boot、MyBatis-Plus、MySQL、Redis、ElasticSearch、MongoDB、Docker、RabbitMQ 等技术栈实现的社区系统,采用主流的互联网技术架构、全新的UI设计、支持一键源码部署,拥有完整的文章&教程发布/搜索/评论/统计流程等,代码完全开源,没有任何二次封装,是一个非常适合二次开发/实战的现代化社区项目👍 。 +

+

+ + + + +

-社区工程原型 -## 结构说明 +## 一、配套服务 + +1. **技术派网址**:[https://paicoding.com](https://paicoding.com) +2. **技术派教程**:[https://paicoding.com/column](https://paicoding.com/column) 目前已更新高并发手册、JVM 手册、Java 并发编程手册、二哥的 Java 进阶之路,以及技术派部分免费教程。我们的宗旨是:**学编程,就上技术派**😁 +3. **技术派管理端源码**:[paicoding-admin](https://github.com/itwanger/paicoding-admin) +4. **技术派专属学习圈子**:[不走弯路,少采坑,附 120 篇技术派全套教程](https://paicoding.com/article/detail/17) +5. **派聪明AI助手**:AI 时代,怎能掉队,欢迎体验 [技术派的派聪明 AI 助手](https://paicoding.com/chat) +6. **码云仓库**:[https://gitee.com/itwanger/paicoding](https://gitee.com/itwanger/paicoding) (国内访问速度更快) -- forum-web: web入口,权限身份校验,全局异常处理等 -- forum-ui:前端资源包 -- forum-service: 核心的服务包,db操作,服务封装在这里 -- forum-core: 通用模块,如工具包util, 如通用的组件放在这个模块(以包路径对模块功能进行拆分,如搜索、缓存、推荐等) +## 二、项目介绍 -## 初始化说明 +### 项目演示 -- 创建数据库, 命名为 forum -- 初始化表结构和demo数据, 可以直接导入 [test-data.sql](forum-web/src/main/resources/test-data.sql) +#### 前台社区系统 -## 部署教程 +- 项目仓库(GitHub):[https://github.com/itwanger/paicoding](https://github.com/itwanger/paicoding) +- 项目仓库(码云):[https://gitee.com/itwanger/paicoding](https://gitee.com/itwanger/paicoding) +- 项目演示地址:[https://paicoding.com](https://paicoding.com) -- [环境搭建 & 基于源码的部署教程](docs/安装环境.md) +![技术派首页](https://cdn.tobebetterjavaer.com/images/20230602/d7d341c557e7470d9fb41245e5bb4209.png) -## todo +#### Vue 版前后端分离版本 -1. 权限限制(包括菜单权限) +这个版本对技术派进行了二次开发,将用户端的前端 UI 使用 Vue3 重写,并且将后端升级到 Spring Boot 3 版本,喜欢 Vue3 或者 Spring Boot 3 版本的球友可以看看这个分支。 -- controller 很多接口,有一些是需要登录的,要有校验 - - @Auth(role = "login") - - @AUth(role = "admin") +- 项目仓库(GitHub):[https://github.com/itwanger/paicoding/tree/springboot3%26vue3](https://github.com/itwanger/paicoding/tree/springboot3%26vue3) +- 项目仓库(码云):[https://gitee.com/itwanger/paicoding/tree/springboot3%26vue3](https://gitee.com/itwanger/paicoding/tree/springboot3%26vue3) +- 项目演示地址(球友小灰飞):[https://www.xuyifei.site/](https://www.xuyifei.site/) -2. 文章阅读之后,各种计数、 评论目前还没有串起来 @楼仔 +![编程汇vue3+Spring Boot3](https://cdn.tobebetterjavaer.com/paicoding/README-1799f2f840fc4687a1cda4486782a07a.png) -- 第一版mysql -- 第二版mongodb -- 第三版redis -3. 用户登录、登出 (不存在用户注销) @一灰 +#### 后台社区系统 -- 个人公众号登录,只能拿到uuid,拿不到用户信息(用户名 + 头像) --》 随机分配一个,头像用户名,跳转用户详情 -- 扫公众号二维码,关注之后,输入 “关键词”, 我们返回一串 数字, 然后在登录界面输入数字之后,登录 +- 项目仓库(GitHub):[https://github.com/itwanger/paicoding-admin](https://github.com/itwanger/paicoding-admin) +- 项目仓库(码云):[https://gitee.com/itwanger/paicoding-admin](https://gitee.com/itwanger/paicoding-admin) +- 项目演示地址:[https://paicoding.com/admin-view](https://paicoding.com/admin/) -5. 图片上传 -- 需要一个独立的图片上传接口 (直接使用七牛云的oss) --> @楼仔 -6. 搜索 `一期可以考虑使用db的like语法` @楼仔 -- 第一版mysql -- 第二版es +![技术派后台管理系统](https://cdn.tobebetterjavaer.com/images/20230602/83139e13a4784c0fbf0adedd8e287c5b.png) +admin 端部署写在了 paicoding-admin 项目的 README.md 中,请注意查看⚠️。 + +#### 代码展示 + +![技术派源码结构](https://cdn.tobebetterjavaer.com/images/20231205/b8f76cb8e09f4ebca84b3ddd3b61c13e.png) + + +### 架构图 + +#### 系统架构图 + +![技术派系统架构图](https://cdn.tobebetterjavaer.com/paicoding/3da165adfcad0f03d40e13e941ed4afb.png) + + +#### 业务架构图 + +![技术派业务架构图](https://cdn.tobebetterjavaer.com/paicoding/main/paicoding-business.jpg) + +### 组织结构 + +``` +paicoding +├── paicoding-api -- 定义一些通用的枚举、实体类,定义 DO\DTO\VO 等 +├── paicoding-core -- 核心工具/组件相关模块,如工具包 util, 通用的组件都放在这个模块(以包路径对模块功能进行拆分,如搜索、缓存、推荐等) +├── paicoding-service -- 服务模块,业务相关的主要逻辑,DB 的操作都在这里 +├── paicoding-ui -- HTML 前端资源(包括 JavaScript、CSS、Thymeleaf 等) +├── paicoding-web -- Web模块、HTTP入口、项目启动入口,包括权限身份校验、全局异常处理等 +``` + +#### 环境配置说明 + +资源配置都放在 `paicoding-web` 模块的资源路径下,通过maven的env进行环境选择切换 + +当前提供了四种开发环境 + +- resources-env/dev: 本地开发环境,也是默认环境 +- resources-env/test: 测试环境 +- resources-env/pre: 预发环境 +- resources-env/prod: 生产环境 + +环境切换命令 + +```bash +# 如切换生产环境 +mvn clean install -DskipTests=true -Pprod +``` + +#### 配置文件说明 + +- resources + - application.yml: 主配置文件入口 + - application-config.yml: 全局的站点信息配置文件 + - logback-spring.xml: 日志打印相关配置文件 + - liquibase: 由liquibase进行数据库表结构管理 +- resources-env + - xxx/application-dal.yml: 定义数据库相关的配置信息 + - xxx/application-image.yml: 定义上传图片的相关配置信息 + - xxx/application-web.yml: 定义web相关的配置信息 + +[前端工程结构说明](docs/前端工程结构说明.md) + +### 技术选型 + +后端技术栈 + +| 技术 | 说明 | 官网 | +|:-------------------:|----------------------|----------------------------------------------------------------------------------------------------| +| Spring & SpringMVC | Java全栈应用程序框架和WEB容器实现 | [https://spring.io/](https://spring.io/) | +| SpringBoot | Spring应用简化集成开发框架 | [https://spring.io/projects/spring-boot](https://spring.io/projects/spring-boot) | +| mybatis | 数据库orm框架 | [https://mybatis.org](https://mybatis.org) | +| mybatis-plus | 数据库orm框架 | [https://baomidou.com/](https://baomidou.com/) | +| mybatis PageHelper | 数据库翻页插件 | [https://github.com/pagehelper/Mybatis-PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) | +| elasticsearch | 近实时文本搜索 | [https://www.elastic.co/cn/elasticsearch/service](https://www.elastic.co/cn/elasticsearch/service) | +| redis | 内存数据存储 | [https://redis.io](https://redis.io) | +| rabbitmq | 消息队列 | [https://www.rabbitmq.com](https://www.rabbitmq.com) | +| mongodb | NoSql数据库 | [https://www.mongodb.com/](https://www.mongodb.com/) | +| nginx | 服务器 | [https://nginx.org](https://nginx.org) | +| docker | 应用容器引擎 | [https://www.docker.com](https://www.docker.com) | +| hikariCP | 数据库连接 | [https://github.com/brettwooldridge/HikariCP](https://github.com/brettwooldridge/HikariCP) | +| oss | 对象存储 | [https://help.aliyun.com/document_detail/31883.html](https://help.aliyun.com/document_detail/31883.html) | +| https | 证书 | [https://letsencrypt.org/](https://letsencrypt.org/) | +| jwt | jwt登录 | [https://jwt.io](https://jwt.io) | +| lombok | Java语言增强库 | [https://projectlombok.org](https://projectlombok.org) | +| guava | google开源的java工具集 | [https://github.com/google/guava](https://github.com/google/guava) | +| thymeleaf | html5模板引擎 | [https://www.thymeleaf.org](https://www.thymeleaf.org) | +| swagger | API文档生成工具 | [https://swagger.io](https://swagger.io) | +| hibernate-validator | 验证框架 | [hibernate.org/validator/](hibernate.org/validator/) | +| quick-media | 多媒体处理 | [https://github.com/liuyueyi/quick-media](https://github.com/liuyueyi/quick-media) | +| liquibase | 数据库版本管理 | [https://www.liquibase.com](https://www.liquibase.com) | +| jackson | json/xml处理 | [https://www.jackson.com](https://www.jackson.com) | +| ip2region | ip地址 | [https://github.com/zoujingli/ip2region](https://github.com/zoujingli/ip2region) | +| websocket | 长连接 | [https://docs.spring.io/spring/reference/web/websocket.html](https://docs.spring.io/spring/reference/web/websocket.html) | +| sensitive-word | 敏感词 | [https://github.com/houbb/sensitive-word](https://github.com/houbb/sensitive-word) | +| chatgpt | chatgpt | [https://openai.com/blog/chatgpt](https://openai.com/blog/chatgpt) | +| 讯飞星火 | 讯飞星火大模型 | [https://www.xfyun.cn/doc/spark/Web.html](https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E) | + +## 三、技术派教程 + +技术派教程共 120+ 篇,从中整理出 20 篇,供大家免费学习。 +- [(🌟 新人必看)技术派系统架构&功能模块一览](https://paicoding.com/article/detail/15) +- [(🌟 新人必看)小白如何学习技术派](https://paicoding.com/article/detail/366) +- [(🌟 新人必看)如何将技术派写入简历](https://paicoding.com/article/detail/373) +- [(🌟 新人必看)技术派架构方案设计](https://paicoding.com/column/6/5) +- [(🌟 新人必看)技术派技术方案设计](https://paicoding.com/article/detail/208) +- [(🌟 新人必看)技术派项目管理流程](https://paicoding.com/article/detail/445) +- [(🌟 新人必看)技术派MVC分层架构](https://paicoding.com/article/detail/446) +- [(🌟 新人必看)技术派项目工程搭建手册](https://paicoding.com/article/detail/459) +- [(👍 强烈推荐)技术派微信公众号自动登录](https://paicoding.com/article/detail/448) +- [(👍 强烈推荐)技术派微信扫码登录实现](https://paicoding.com/article/detail/453) +- [(👍 强烈推荐)技术派Session/Cookie身份验证识别](https://paicoding.com/article/detail/449) +- [(👍 强烈推荐)技术派Mysql/Redis缓存一致性](https://paicoding.com/column/6/3) +- [(👍 强烈推荐)技术派Redis实现用户活跃排行榜](https://paicoding.com/article/detail/454) +- [(👍 强烈推荐)技术派消息队列RabbitMQ](https://paicoding.com/column/6/2) +- [(👍 强烈推荐)技术派消息队列RabbitMQ连接池](https://paicoding.com/column/6/1) +- [(👍 强烈推荐)技术派消息队列Kafka](https://paicoding.com/article/detail/460) +- [(👍 强烈推荐)技术派Cancal实现MySQL和ES同步](https://paicoding.com/column/6/8) +- [(👍 强烈推荐)技术派ES实现查询](https://paicoding.com/article/detail/341) +- [(👍 强烈推荐)技术派定时任务实现](https://paicoding.com/article/detail/457) +- [(👍 扬帆起航)送给坚持到最后的自己,一起杨帆起航](https://paicoding.com/article/detail/447) + + +## 四、环境搭建 + +### 开发工具 + +| 工具 | 说明 | 官网 | +|:----------------:|--------------|--------------------------------------------------------------------------------------------------------------| +| IDEA | java开发工具 | [https://www.jetbrains.com](https://www.jetbrains.com) | +| Webstorm | web开发工具 | [https://www.jetbrains.com/webstorm](https://www.jetbrains.com/webstorm) | +| Chrome | 浏览器 | [https://www.google.com/intl/zh-CN/chrome](https://www.google.com/intl/zh-CN/chrome) | +| ScreenToGif | gif录屏 | [https://www.screentogif.com](https://www.screentogif.com) | +| SniPaste | 截图 | [https://www.snipaste.com](https://www.snipaste.com) | +| PicPick | 图片处理工具 | [https://picpick.app](https://picpick.app) | +| MarkText | markdown编辑器 | [https://github.com/marktext/marktext](https://github.com/marktext/marktext) | +| curl | http终端请求 | [https://curl.se](https://curl.se) | +| Postman | API接口调试 | [https://www.postman.com](https://www.postman.com) | +| draw.io | 流程图、架构图绘制 | [https://www.diagrams.net/](https://www.diagrams.net/) | +| Axure | 原型图设计工具 | [https://www.axure.com](https://www.axure.com) | +| navicat | 数据库连接工具 | [https://www.navicat.com](https://www.navicat.com) | +| DBeaver | 免费开源的数据库连接工具 | [https://dbeaver.io](https://dbeaver.io) | +| iTerm2 | mac终端 | [https://iterm2.com](https://iterm2.com) | +| windows terminal | win终端 | [https://learn.microsoft.com/en-us/windows/terminal/install](https://learn.microsoft.com/en-us/windows/terminal/install) | +| SwitchHosts | host管理 | [https://github.com/oldj/SwitchHosts/releases](https://github.com/oldj/SwitchHosts/releases) | + + +### 开发环境 + +| 工具 | 版本 | 下载 | +|:-------------:|:----------|------------------------------------------------------------------------------------------------------------------------| +| jdk | 1.8+ | [https://www.oracle.com/java/technologies/downloads/#java8](https://www.oracle.com/java/technologies/downloads/#java8) | +| maven | 3.4+ | [https://maven.apache.org/](https://maven.apache.org/) | +| mysql | 5.7+/8.0+ | [https://www.mysql.com/downloads/](https://www.mysql.com/downloads/) | +| redis | 5.0+ | [https://redis.io/download/](https://redis.io/download/) | +| elasticsearch | 8.0.0+ | [https://www.elastic.co/cn/downloads/elasticsearch](https://www.elastic.co/cn/downloads/elasticsearch) | +| nginx | 1.10+ | [https://nginx.org/en/download.html](https://nginx.org/en/download.html) | +| rabbitmq | 3.10.14+ | [https://www.rabbitmq.com/news.html](https://www.rabbitmq.com/news.html) | +| ali-oss | 3.15.1 | [https://help.aliyun.com/document_detail/31946.html](https://help.aliyun.com/document_detail/31946.html) | +| git | 2.34.1 | [http://github.com/](http://github.com/) | +| docker | 4.10.0+ | [https://docs.docker.com/desktop/](https://docs.docker.com/desktop/) | +| let's encrypt | https证书 | [https://letsencrypt.org/](https://letsencrypt.org/) | + +### 搭建步骤 + +#### 本地部署教程 + +> [本地开发环境手把手教程](docs/本地开发环境配置教程.md) + +### 云服务器部署教程 + +> [环境搭建 & 基于源码的部署教程](docs/安装环境.md) +> [服务器启动教程](docs/服务器启动教程.md) + +## 五、友情链接 + +- [toBeBetterjavaer](https://github.com/itwanger/toBeBetterJavaer) :一份通俗易懂、风趣幽默的Java学习指南,内容涵盖Java基础、Java并发编程、Java虚拟机、Java企业级开发、Java面试等核心知识点。学Java,就认准二哥的Java进阶之路😄 +- [paicoding-admin](https://github.com/itwanger/paicoding-admin) :🚀🚀🚀 paicoding-admin,技术派管理端,基于 React18、React-Router v6、React-Hooks、Redux、TypeScript、Vite3、Ant-Design 5.x、Hook Admin、ECharts 的一套社区管理系统,够惊艳哦。 + +## 六、鸣谢 + +技术派收到了 [Jetbrains](https://jb.gg/OpenSourceSupport) 多份 Licenses(详情戳 [这里](https://paicoding.com/article/detail/331) ),并已分配给项目 [活跃开发者](https://github.com/itwanger/paicoding/graphs/contributors) ,非常感谢 Jetbrains 对开源社区的支持。 + +![JetBrains Logo (Main) logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg) + + +## 七、star 趋势图 + +[![Star History Chart](https://api.star-history.com/svg?repos=itwanger/paicoding&type=Date)](https://star-history.com/#itwanger/paicoding&Date) + +## 八、公众号 + +GitHub 上标星 13000+ 的开源知识库《 [二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer) 》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 13000+ 的 Java 教程](https://javabetter.cn/overview/) + +微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号,回复 **222** 即可免费领取。 + +![沉默王二公众号](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png) + +## 九、许可证 + +[Apache License 2.0](https://github.com/itwanger/paicoding/edit/main/README.md) + +Copyright (c) 2022-2024 技术派(楼仔、沉默王二、一灰、小超、小灰飞) -7. 消息模块 -8. 文章排序规则(目前只提供了按照时间的排序,后续需要添加热度、xxx排序)@一灰 -9. 公告侧边栏:先整一个写死的几个板块 @一灰 -10. admin后台 -- 先设计(前后端分离) -11. 添加文章时,自动保存,历史版本 -12. 定时发布 --> 定时任务 + 时间轮 + 延迟消息 -13. 评论前端页面 \ No newline at end of file diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 000000000..44bdf150f --- /dev/null +++ b/deploy.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash + +# pid file +PID_FILE_NAME="pid.log" + +# file to upload +WEB_PATH="paicoding-web" +EXECUTABLE_JAR_NAME="paicoding-web-0.0.1-SNAPSHOT.jar" +TMP_EXECUTABLE_JAR_NAME=${EXECUTABLE_JAR_NAME}".tmp" +BAK_EXECUTABLE_JAR_NAME=${EXECUTABLE_JAR_NAME}".bak" +EXECUTABLE_JAR_PATH="./${WEB_PATH}/target/${EXECUTABLE_JAR_NAME}" + +DEPLOY_SCRIPT="deploy.sh" +START_FUNC_NAME="start" +STOP_FUNC_NAME="stop" +RESTART_FUNC_NAME="restart" + +#env, ssh remote, work dir +ENV_PRO="prod" +SSH_HOST_PRO=("admin@39.105.208.175") +WORK_DIR_PRO="/home/admin/workspace/paicoding-forum/" + + +# log file +declare LOG_FILES +LOG_BACKUP_FOLDER="logs/" + +function stop() { + # kill + echo "--- 应用线下 ---" + if [ -f "${PID_FILE_NAME}" ]; then + pid=$(cat ${PID_FILE_NAME}) + echo "kill -9 ${pid}" + kill -9 ${pid} + fi + echo "----------------" +} + +function start() { + work_dir=`dirname $0` + cd ${work_dir} + + stop + + mv ${EXECUTABLE_JAR_NAME} ${BAK_EXECUTABLE_JAR_NAME} + mv ${TMP_EXECUTABLE_JAR_NAME} ${EXECUTABLE_JAR_NAME} + + chmod 755 ${EXECUTABLE_JAR_NAME} + # run + echo "===== 启动脚本:=====" + run +} + +function restart() { + work_dir=`dirname $0` + cd ${work_dir} + stop + # run + echo "===== 启动重启:=====" + run +} + +function run() { + echo "nohup java -server -Xms512m -Xmx512m -Xmn512m -XX:NativeMemoryTracking=detail -XX:-OmitStackTraceInFastThrow -jar ${EXECUTABLE_JAR_NAME} > /dev/null 2>&1 &" + echo "===================" + nohup java -server -Dspring.devtools.restart.enabled=false -Xms512m -Xmx512m -Xmn512m -XX:NativeMemoryTracking=detail -XX:-OmitStackTraceInFastThrow -jar ${EXECUTABLE_JAR_NAME} "$@" > /dev/null 2>&1 & + echo $! > ${PID_FILE_NAME} +} + +function compile() { + echo "---- start to build jar ----" + echo "安装依赖:mvn clean install -Dmaven.test.skip=True -P${1}" + mvn clean install -Dmaven.test.skip=True -P${1} + cd ${WEB_PATH} + echo "构建可运行jar:mvn clean package spring-boot:repackage -Dmaven.test.skip=true -P${1}" + mvn clean package spring-boot:repackage -Dmaven.test.skip=true -P${1} + cd - + ret=$? + if [[ ${ret} -ne 0 ]] ; then + return 1 + fi + echo "---------- jar包构建完成 -------------" +} + +function upload() { + # upload jar + # rename to *.jar.bak + scp ${EXECUTABLE_JAR_PATH} $1:$2${TMP_EXECUTABLE_JAR_NAME} + ret=$? + if [[ ${ret} -ne 0 ]] ; then + echo 'Failed to scp jar' + return 1 + fi + + # upload script + scp ${DEPLOY_SCRIPT} $1:$2 + ret=$? + if [[ ${ret} -ne 0 ]] ; then + echo 'Failed to scp deploy.sh' + return 1 + fi +} + +function deploy() { + # package + echo "*******Start to package*******" + compile $1 + ret=$? + if [[ ${ret} -ne 0 ]] ; then + echo 'Failed to compile' + exit ${ret} + fi + + if [ "$1" = "${ENV_PRO}" ]; then + SSH_HOST=${SSH_HOST_PRO[@]} + WORK_DIR=${WORK_DIR_PRO} + else + echo "Unknown env: $1" + exit + fi + + for host in ${SSH_HOST[@]} + do + # upload jar and deploy.sh + echo "*******Start to upload:${host} *******" + upload ${host} ${WORK_DIR} + ret=$? + if [[ ${ret} -ne 0 ]] ; then + echo 'Failed to upload files' + exit ${ret} + fi + done + + for host in ${SSH_HOST[@]} + do + # run + echo "*******Start service:${host} *******" + ssh ${host} "bash ${WORK_DIR}${DEPLOY_SCRIPT} ${START_FUNC_NAME}" + echo "*******Done*******" + done +} + +if [ "$1" = "${START_FUNC_NAME}" ]; then + start "$@" +elif [ "$1" = "${ENV_PRO}" ]; then + deploy $1 +elif [ "$1" = "${STOP_FUNC_NAME}" ]; then + stop +elif [ "$1" = "${RESTART_FUNC_NAME}" ]; then + restart +else + echo "部署jar到服务器: ./deploy.sh prod" + echo "服务器上应用重启: ./deploy.sh restart" + echo "服务器上应用关闭: ./deploy.sh stop" +fi \ No newline at end of file diff --git a/docs/imgs/init_00.jpg b/docs/imgs/init_00.jpg new file mode 100644 index 000000000..056e23b28 Binary files /dev/null and b/docs/imgs/init_00.jpg differ diff --git a/docs/imgs/init_01.jpg b/docs/imgs/init_01.jpg new file mode 100644 index 000000000..aa66eab84 Binary files /dev/null and b/docs/imgs/init_01.jpg differ diff --git a/docs/imgs/init_02.jpg b/docs/imgs/init_02.jpg new file mode 100644 index 000000000..30cd19072 Binary files /dev/null and b/docs/imgs/init_02.jpg differ diff --git a/docs/imgs/init_03.jpg b/docs/imgs/init_03.jpg new file mode 100644 index 000000000..5d71d23f0 Binary files /dev/null and b/docs/imgs/init_03.jpg differ diff --git a/docs/imgs/init_04.jpg b/docs/imgs/init_04.jpg new file mode 100644 index 000000000..2392ac64e Binary files /dev/null and b/docs/imgs/init_04.jpg differ diff --git a/docs/imgs/itwanger/tongzhishu.jpeg b/docs/imgs/itwanger/tongzhishu.jpeg new file mode 100644 index 000000000..eb8708d00 Binary files /dev/null and b/docs/imgs/itwanger/tongzhishu.jpeg differ diff --git a/docs/imgs/itwanger/tongzhishu1.jpeg b/docs/imgs/itwanger/tongzhishu1.jpeg new file mode 100644 index 000000000..9f85c4e61 Binary files /dev/null and b/docs/imgs/itwanger/tongzhishu1.jpeg differ diff --git a/docs/imgs/itwanger/tongzhishu1.pdf b/docs/imgs/itwanger/tongzhishu1.pdf new file mode 100644 index 000000000..b4cbb6326 Binary files /dev/null and b/docs/imgs/itwanger/tongzhishu1.pdf differ diff --git a/docs/version.md b/docs/version.md new file mode 100644 index 000000000..4624083f6 --- /dev/null +++ b/docs/version.md @@ -0,0 +1,6 @@ +## 技术派迭代关键历史进程 + +- 2023//06/30 基于ChatGPT,讯飞星火大模型的派聪明上线 +- 2023/06/09 mysql同步es,支持es查询集成 +- 2023/05 动态数据源方案集成 +- 2023/04 官宣上线p'r \ No newline at end of file diff --git "a/docs/\345\211\215\347\253\257\345\267\245\347\250\213\347\273\223\346\236\204\350\257\264\346\230\216.md" "b/docs/\345\211\215\347\253\257\345\267\245\347\250\213\347\273\223\346\236\204\350\257\264\346\230\216.md" new file mode 100644 index 000000000..f54c966e7 --- /dev/null +++ "b/docs/\345\211\215\347\253\257\345\267\245\347\250\213\347\273\223\346\236\204\350\257\264\346\230\216.md" @@ -0,0 +1,42 @@ +页面放在 ui 模块中: + +- resources/static: 静态资源文件,如css/js/image,放在这里 +- resources/templates: html相关页面 + - views: 业务相关的页面 + - 定义: + - 页面/index.html: 这个index.html表示的是这个业务对应的主页面 + - 页面/模块/xxx.html: 若主页面又可以拆分为多个模块页面进行组合,则在这个页面下,新建一个模块目录,下面放对应的html文件 + - article-category-list: 对应 分类文章列表页面, + - article-detail: 对应文章详情页 + - side-float-action-bar: 文章详情,左边的点赞/收藏/评论浮窗 + - side-recommend-bar: 文章详情右边侧边栏的sidebar + - article-edit: 对应文章发布页 + - article-search-list: 对应文章搜索页 + - article-tag-list: 对应标签文章列表 + - column-detail:对应专栏阅读详情页 + - column-home: 对应专栏首页 + - home: 全站主页 + - login: 登录页面 + - notice: 通知页面 + - user: 用户个人页 + - error: 错误页面 + - components: 公用的前端页面组件 + + +css 放在 static/css 中: + +- components: 公共组件的css + - navbar: 导航栏样式 + - footer: 底部样式 + - article-item: 文章块展示样式 + - article-footer: 文章底部(点赞、评论等) + - side-column: 侧边栏(公告等) +- views: 主页面css(直接在主页面内部引入) + - home: 主页样式 + - article-detail: 详情页样式 + - ... +- three: 第三方css + - index: 第三方css集合 + - ... +- common: 公共组件的css集合 (直接在公共组件components/layout/header/index.html内引入) +- global: 全局样式(全局的样式控制,注意覆盖问题,直接在公共组件components/layout/header/index.html内引入) diff --git "a/docs/\345\256\211\350\243\205\347\216\257\345\242\203.md" "b/docs/\345\256\211\350\243\205\347\216\257\345\242\203.md" index 5c90caf1e..2b88790db 100644 --- "a/docs/\345\256\211\350\243\205\347\216\257\345\242\203.md" +++ "b/docs/\345\256\211\350\243\205\347\216\257\345\242\203.md" @@ -132,6 +132,68 @@ server { 证书使用let's encrypt生成 +## 数据库创建 + +```bash +# ubuntu +sudo apt-get install mysql-server + +# centos +yum install mysql mysql-server mysql-libs +``` + +### 查询登录密码 + +```bash +grep "temporary password" /var/log/mysqld.log + +## 输出如下 +# A temporary password is generated for root@localhost: xxxx +``` + +### 密码修改: + +使用`set password` + +**格式:** + +``` +mysql> set password for 用户名@localhost = password('新密码'); +``` + +**例子:** + +``` +mysql> set password for root@localhost = password('123'); +``` + +update 方式 + +``` +mysql> use mysql; + +mysql> update user set password=password('123') where user='root' and host='localhost'; + +mysql> flush privileges; +``` + +添加用户 + +``` +alter user 'root'@'localhost' identified by 'test'; +create user 'test'@'%' IDENTIFIED BY 'test'; +``` + +授予权限 + +``` +# root 方式登录 +grant all PRIVILEGES on test.* to 'yihui'@'%' IDENTIFIED by 'test'; +flush privileges; +``` + +本项目在首次启动时,会自动创建数据库 + 表结构,无需额外操作;只是需要修改源码中的生产环境配置 + ## 配置调整 线上部署时,选择prod环境,因此需要设置对应的数据库相关配置信息 @@ -155,23 +217,26 @@ spring: ```bash #!/usr/bin/env bash -WEB_PATH="forum-web" -JAR_NAME="forum-web-0.0.1-SNAPSHOT.jar" +WEB_PATH="paicoding-web" +JAR_NAME="paicoding-web-0.0.1-SNAPSHOT.jar" # 部署 function start() { git pull - + # 杀掉之前的进程 cat pid.log| xargs -I {} kill {} - mv ${JAR_NAME} ${JAR_NAME}_bk - + mv ${JAR_NAME} ${JAR_NAME}.bak + mvn clean install -Dmaven.test.skip=True -Pprod cd ${WEB_PATH} mvn clean package spring-boot:repackage -Dmaven.test.skip=true -Pprod cd - - + mv ${WEB_PATH}/target/${JAR_NAME} ./ + echo "启动脚本:===========" + echo "nohup java -server -Xms512m -Xmx512m -Xmn512m -XX:NativeMemoryTracking=detail -XX:-OmitStackTraceInFastThrow -jar ${JAR_NAME} > /dev/null 2>&1 &" + echo "===========" nohup java -server -Xms512m -Xmx512m -Xmn512m -XX:NativeMemoryTracking=detail -XX:-OmitStackTraceInFastThrow -jar ${JAR_NAME} > /dev/null 2>&1 & echo $! 1> pid.log } @@ -181,7 +246,10 @@ function restart() { # 杀掉之前的进程 cat pid.log| xargs -I {} kill {} # 重新启动 - nohup java -server -Xms512m -Xmx512m -Xmn512m -XX:NativeMemoryTracking=detail -XX:-OmitStackTraceInFastThrow -jar ${JAR_NAME} > /dev/null 2>&1 & + echo "启动脚本:===========" + echo "nohup java -server -Xms512m -Xmx512m -Xmn512m -XX:NativeMemoryTracking=detail -XX:-OmitStackTraceInFastThrow -jar ${JAR_NAME} > /dev/null 2>&1 &" + echo "===========" + nohup java -server -Xmn512m -Xmn512m -Xmn512m -XX:NativeMemoryTracking=detail -XX:-OmitStackTraceInFastThrow -jar ${JAR_NAME} > /dev/null 2>&1 & echo $! 1> pid.log } @@ -192,7 +260,7 @@ elif [ $1 == 'start' ]; then elif [ $1 == 'restart' ];then restart else - echo 'illegal command, support cmd: start | restart' + echo 'illegal command, support cmd: start | restart' fi ``` diff --git "a/docs/\346\234\215\345\212\241\345\231\250\345\220\257\345\212\250\346\225\231\347\250\213.md" "b/docs/\346\234\215\345\212\241\345\231\250\345\220\257\345\212\250\346\225\231\347\250\213.md" new file mode 100644 index 000000000..6ae5d32c7 --- /dev/null +++ "b/docs/\346\234\215\345\212\241\345\231\250\345\220\257\345\212\250\346\225\231\347\250\213.md" @@ -0,0 +1,79 @@ +# 本文记录服务器启动方式 + +## 0. 环境配置 + +服务器上启动之前,请先准备好相应的环境配置,请查看 [安装环境](安装环境.md) 文档进行服务器环境初始化 + +## 1. 源码方式构建 +### 1.1 源码构建 + +进入工作目录,下来最新代码 + +```bash +cd /home/admin/workspace + +git clone git@github.com:itwanger/paicoding.git +``` + +线上部署时,选择prod环境,因此需要设置对应的数据库相关配置信息 + +- vim 进入 `resources-env/prod/application-dal.yml` + +```yml +spring: + datasource: + url: jdbc:mysql://xxx/forum?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai + username: xxx + password: xxx +``` + +根据实际的情况进行修改ip, 用户名密码 + +接下来就是编译启动 + +```bash +cd paicoding +./launch.sh start +``` + +提示: + +- 若launch.sh脚本没有执行权限,可以通过命令行 `chmod +x launch.sh` 添加 +- 启动之后,可以发现当前目录下新增一个 `pid.log` 文件,里面记录的是启动的服务进程号 +- 业务日志在当前目录的 `logs`下 + - 请求日志: logs/req-prod.log + - 业务日志: logs/forum-prod.log + + +### 1.2. 应用重启 + +若只是单纯的希望应用重启一下 + +```java +cd /home/admin/workspace/paicoding + +./launch.sh restart +``` + +### 1.3. 应用发布 + +当有新的改动时,若希望重新发布应用,执行下面的命令 + +```java +cd /home/admin/workspace/paicoding + +./launch.sh start +``` + +## 2. jar包上传 + +首先确保服务器配置已准备完毕 + +接下来确保本地生产环境的数据库等相关配置已更新为正确的配置 + +然后就是再项目根目录下执行 + +```bash +# 打包jar,并上传到服务器,关闭旧的应用,重新启动新的应用 +./deploy.sh prod +``` \ No newline at end of file diff --git "a/docs/\346\234\254\345\234\260\345\274\200\345\217\221\347\216\257\345\242\203\351\205\215\347\275\256\346\225\231\347\250\213.md" "b/docs/\346\234\254\345\234\260\345\274\200\345\217\221\347\216\257\345\242\203\351\205\215\347\275\256\346\225\231\347\250\213.md" new file mode 100644 index 000000000..4892d91db --- /dev/null +++ "b/docs/\346\234\254\345\234\260\345\274\200\345\217\221\347\216\257\345\242\203\351\205\215\347\275\256\346\225\231\347\250\213.md" @@ -0,0 +1,142 @@ +# 本地开发环境配置教程 + +## 1. 环境准备 + +首先准备好基础的开发环境,如 + +- jdk/jre: 请安装jdk8+以上版本 +- maven: 本项目基于maven作为项目管理工具,因此在启动之前请配置好maven相关环境 +- MySql数据库 + - 版本支持:8.x+ + - 说明:数据库可以使用本机的数据库,也可以使用非本机的(请注意本机能正常访问) +- git版本管理 +- 开发工具:建议idea,当然eclipse/vs也没有问题 + +## 2. 项目启动 + +当环境准备完毕之后,接下来就是下载项目,导入开发工具进行启动演示 + +### 2.1 项目获取 + +本项目所有源码开源,因此您可以在github/gitee上免费获取 + +**通过git方式拉取项目** + +```bash +# Git clone +git clone git@github.com:itwanger/paicoding.git +git clone https://github.com/itwanger/paicoding.git +``` + +**下载release包** + +若希望从一个稳定的版本进行尝试,推荐在release页下载zip包,然后本机解压 + +- [https://github.com/itwanger/paicoding/releases](https://github.com/itwanger/paicoding/releases) + +### 2.2 项目导入 + +以IDEA开发工具为例 + +- File -> Open +- 选择第一步clone的项目工程 + +项目导入成功之后,会自动下载依赖、构建索引,此过程用时取决于您的机器性能+网速,通常会持续一段时间,请耐心等待;当完成之后,一个正常的项目工程如下图所示 + +![](https://cdn.tobebetterjavaer.com/images/20240108/afeef4e1230c423d807e175dc8fbbb2a.png) + +如果发现项目 build 未成功或者无法运行 Java 程序,要立马检查一下自己 Intellij IDEA 中的 Maven 是否配置成功。 + +![](https://cdn.tobebetterjavaer.com/images/20240108/b9af8630c3bb4103bccf4f9c891994cc.png) + + +### 2.3 配置修改 + +在正式启动项目之前,还有几个前置步骤需要执行一下 + +#### 2.3.1 数据库准备 + +本项目会使用数据库,因此在本机启动时,请先指定数据库;项目中默认的数据库名为 `paicoding`,可以通过修改配置文件中的`database.name`参数来替换为您喜欢的数据库名 + +数据库名配置: [forum-web/src/main/resources/application.yml](../forum-web/src/main/resources/application.yml) + +```yaml +# 默认的数据库名 +database: + name: paicoding +``` + +本项目中所有使用的表定义放在 [liquibase](../forum-web/src/main/resources/liquibase) + +> 本项目提供了自动创建库表的功能,在项目启动之后,当库不存在时,会创建库;当表不存在时,会自动创建表,且会初始化一些测试数据 +> +> 因此不建议用户自己通过上面的sql进行创建表 + +#### 2.3.2 数据库配置 + +接下来我们需要做的就是设置数据库的相关连接配置 + +首先在进入之前,先简单了解一下配置,当前所有的配置放在`forum-web`模块内,我们做了环境区分, + +![](https://cdn.tobebetterjavaer.com/images/20240108/02c0500aba284f1aaeeb9663d844e020.png) + + +- dev: 本地开发环境 +- test: 测试环境 +- pre: 预发环境 +- prod: 生产环境 + +默认的环境选择是`dev`,可以通过下面两种方式进行环境切换 + +**case1: 命令切换** + +```bash +# 切换到test环境 +mvn clean package -DskipTests=true -Ptest +``` + +**case2: idea切换** + +![](https://cdn.tobebetterjavaer.com/images/20240108/ddd4af8aaf34448fa20ddb11789a94f7.png) + + +接下来以默认的dev环境配置为例,首先进入配置文件 [application-dal.yml](../forum-web/src/main/resources-env/dev/application-dal.yml) + +```yaml +spring: + datasource: + # 数据库名,从配置 database.name 中获取 + url: jdbc:mysql://127.0.0.1:3306/${database.name}?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: +``` + +上面的数据库中,通常来讲需要修改的有三个 + +- url: 主要修改的就是这个数据库的域名 + 端口号,即将`127.0.0.1:3306`替换为您实际使用的数据库地址 +- username: 数据库名 +- password: 数据库密码 + +#### 2.3.3 文件上传配置 + +暂时省略,后续补齐 + +### 2.4 启动 + +接下来就可以直接启动项目了 + +进入启动类: `QuickForumApplication` + +![](https://cdn.tobebetterjavaer.com/images/20240108/6238d17c93d640deb0e1123d02fa270d.png) + + +启动完毕之后,将会在控制台看到如下输出 + +![](https://cdn.tobebetterjavaer.com/images/20240108/3fddd6712f0846879b445d378813a15f.png) + + +点击控制台中的链接进入首页, 默认首页为: [http://127.0.0.1:8080](http://127.0.0.1:8080) + +然后就可以开始愉快的玩耍了,对了,记得启动 Redis。 + +![](https://cdn.tobebetterjavaer.com/images/20240108/9b1242b0ef6a4aec83b1951e5dd5b3b6.png) diff --git "a/docs/\347\272\246\345\256\232.md" "b/docs/\347\272\246\345\256\232.md" index 6adf221d8..d73b10f5f 100644 --- "a/docs/\347\272\246\345\256\232.md" +++ "b/docs/\347\272\246\345\256\232.md" @@ -12,7 +12,7 @@ ### 1.1 接口层区分两类:返回数据 + 返回视图 -建议:不要讲两类接口放在同一个Controller文件中,分开存放 +建议:不要将两类接口放在同一个Controller文件中,分开存放 - RestController: - 返回json/xml/string格式数据, diff --git "a/docs/\351\205\215\345\245\227\346\225\231\347\250\213.md" "b/docs/\351\205\215\345\245\227\346\225\231\347\250\213.md" new file mode 100644 index 000000000..5593778dd --- /dev/null +++ "b/docs/\351\205\215\345\245\227\346\225\231\347\250\213.md" @@ -0,0 +1,117 @@ +论坛技术教程 +--- + +## 序 + +1. 基本功能盘点,演示 +2. 架构 + 知识体系 + +## 设计篇 +> 这一节将主要介绍一个成熟的团队,一个新的项目在立项之前,会做哪些事情 + +1. 产品设计 +2. 技术调研 +3. 方案设计 + 1. 架构设计 + 2. 库表设计 + 3. 技术选型 +4. 项目管理,产品功能按版本拆分,任务拆分,排期,周期进度同步 + +## 技术实现 +> 开始正式技术实现 + +0. 代码规范、编码风格、Git工作流程、约定等信息同步 +1. 项目工程搭建 +2. 结构分层(MVC) +3. DAO层实现 + 1. SpringBoot集成Mybatis-Plus + 2. 配合现有的代码实现,介绍Mybatis-Plus的CURD使用姿势,以此讲解db操作相关知识点 + 3. db相关知识点: + - 单表curd使用姿势 + - 复杂姿势:连表查询 + 聚合 + 分组 + 类型转换(如数据库中的Data如何映射Java中long) + - 事务(声明事务、编程事务) + 分布式事务 + - 其他知识点:如输出sql执行日志 + 数据脱敏 + 悲观锁/乐观锁等 + 分库分表 + 全局唯一递增id方案 +4. Service层实现 + 1. service主要做具体的业务逻辑,大量引入的技术都在这一层进行应用得体现,所以service的相关内容,可以结合知识点来进行展开 + 2. 缓存操作姿势 + 1. 本地缓存 Guava + Caffeine + 2. redis缓存 + 3. Spring 的 Cacheable注解使用姿势 (基于此可以科普介绍AOP) + 3. ES搜索相关 + 4. Redis计数器相关 + 5. MongoDB相关 + 6. 消息通知 + 1. 进程内的Spring的事件机制 SpringEvent/Listener + 2. 进程间的MQ姿势 + 7. 定时任务 + 1. 进程内的Spring的Schedule + 2. 进程间的xxl-job,Elastic-Job,Quartz Cluster + 3. 自研实现:如系统消息,通知百万用户,如何实现 + 8. 分布式锁 +5. Web层 + 1. Controller基本知识点 + 1. 传参 + 返回 + 2. RestTemplate 网络请求相关 + 2. 用户登录权限管理 + 1. 自定义基于AOP实现权限管理 + 2. SpringSecurity实现权限管理 + 3. session/cookie身份校验, JWT、Token等机制 + 4. 分布式会话 + 3. 全局异常处理 + 4. 日志相关 + 5. Thymeleaf渲染引擎 + 1. 基本语法 + 2. SpEL使用姿势 + 6. 文件上传下载 + 7. 跨域 +6. 通用 + 1. 多环境管理(dev, test, pre, pro) + 2. 配置相关 + 1. 本地配置,如何取,动态刷新 + 2. 集成Apollo,Nacos,SpringCloudConfig,Zookeeper + 3. 大厂应用日志规范 + 4. 应用状态信息监控 + 1. SpringBootAdmin + 2. prometheus + 5. Swagger接口文档管理 + 6. 邮件通知 + 7. 序列化 + +## 知识点 +> 上面实现过程中介绍的知识点,可能并不全面,这里则可以介绍更深入一些的知识要点 + +1. SpringSecurity实现的权限管理 +2. 第三方授权登录,扫码登录原理,OAuth相关知识点,分布式session,单点登录,JWT等 +3. MyBatis/MybatisPlus 系统的教程 +4. 参数校验Validate +5. 定时任务的各种方案 +6. 网站统计功能(pv, uv),排行榜,计数 +7. 缓存相关 +8. 分布式锁的实现方式,使用姿势 +9. 消息队列,各种MQ的集成介绍 +10. 搜索的知识点ES/Solr +11. ELK搭建日志 +12. Prometheus + Grafana搭建应用监控体系 +13. AOP/Filter两种实现请求日志打印的方式 +14. 全链路监控方案 (这个放在后面做服务拆分之后再引入) +15. 明文密码改造(项目中不存密文密码,可以怎么搞?) + +## 实战技能 +> 介绍一些实战的技巧提高大家的编程水平 + +1. jdk8流式用法 +2. 使用工具包,提高工作效率 + 1. 如idea的各种插件 + 2. gauva, hutool,apache 等工具包 + 3. 打造自己的工具集 +3. 如何写出更好看的代码 +4. 池化技术,并发管理 +5. 如何在项目中使用合适的设计模式 +6. xxx + +## 部署 + +1. 不同环境的部署上线教程 +2. jar包部署姿势、war包部署姿势 +3. docker部署姿势 +4. jenkins自动化部署 diff --git a/form-api/pom.xml b/form-api/pom.xml deleted file mode 100644 index bd86b2472..000000000 --- a/form-api/pom.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - quick-forum - com.github.liuyueyi.quick-forum - 0.0.1-SNAPSHOT - - 4.0.0 - - form-api - - - 8 - 8 - - - - - org.projectlombok - lombok - - - - com.baomidou - mybatis-plus-boot-starter - provided - - - - com.fasterxml.jackson.dataformat - jackson-dataformat-xml - provided - - - - \ No newline at end of file diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/context/ReqInfoContext.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/context/ReqInfoContext.java deleted file mode 100644 index 863f39b74..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/context/ReqInfoContext.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.github.liueyueyi.forum.api.model.context; - -import com.github.liueyueyi.forum.api.model.vo.user.dto.BaseUserInfoDTO; -import lombok.Data; - -/** - * 请求上下文,携带用户身份相关信息 - * - * @author YiHui - * @date 2022/7/6 - */ -public class ReqInfoContext { - - /** - * fixme 注意,下面这种方式导致在子线程中拿不到用户信息 - */ - private static ThreadLocal contexts = new ThreadLocal<>(); - - public static void addReqInfo(ReqInfo reqInfo) { - contexts.set(reqInfo); - } - - public static void clear() { - contexts.remove(); - } - - public static ReqInfo getReqInfo() { - return contexts.get(); - } - - @Data - public static class ReqInfo { - /** - * appKey - */ - private String appKey; - /** - * 访问的域名 - */ - private String host; - /** - * 访问路径 - */ - private String path; - /** - * 客户端ip - */ - private String clientIp; - /** - * referer - */ - private String referer; - /** - * post 表单参数 - */ - private String payload; - /** - * 设备信息 - */ - private String userAgent; - - /** - * 登录的会话 - */ - private String session; - - /** - * 用户id - */ - private Long userId; - /** - * 用户信息 - */ - private BaseUserInfoDTO user; - } -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/entity/BaseDTO.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/entity/BaseDTO.java deleted file mode 100644 index 8032b933f..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/entity/BaseDTO.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.github.liueyueyi.forum.api.model.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import lombok.Data; - -import java.util.Date; - -@Data -public class BaseDTO { - - private Long id; - - private Date createTime; - - private Date updateTime; -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/OperateTypeEnum.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/OperateTypeEnum.java deleted file mode 100644 index 6c82b8f0b..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/OperateTypeEnum.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.github.liueyueyi.forum.api.model.enums; - -import lombok.Getter; - -/** - * 操作类型 - * - * @author louzai - * @since 2022/7/19 - */ -@Getter -public enum OperateTypeEnum { - - EMPTY(0, "") { - @Override - public int getDbStatCode() { - return 0; - } - }, - READ(1, "阅读") { - @Override - public int getDbStatCode() { - return ReadStatEnum.READ.getCode(); - } - }, - PRAISE(2, "点赞") { - @Override - public int getDbStatCode() { - return PraiseStatEnum.PRAISE.getCode(); - } - }, - COLLECTION(3, "收藏") { - @Override - public int getDbStatCode() { - return CollectionStatEnum.COLLECTION.getCode(); - } - }, - CANCEL_PRAISE(4, "取消点赞") { - @Override - public int getDbStatCode() { - return PraiseStatEnum.CANCEL_PRAISE.getCode(); - } - }, - CANCEL_COLLECTION(5, "取消收藏") { - @Override - public int getDbStatCode() { - return CollectionStatEnum.CANCEL_COLLECTION.getCode(); - } - }, - COMMENT(6, "评论") { - @Override - public int getDbStatCode() { - return CommentStatEnum.COMMENT.getCode(); - } - }, - DELETE_COMMENT(7, "删除评论") { - @Override - public int getDbStatCode() { - return CommentStatEnum.DELETE_COMMENT.getCode(); - } - }, - ; - - OperateTypeEnum(Integer code, String desc) { - this.code = code; - this.desc = desc; - } - - private final Integer code; - private final String desc; - - public static OperateTypeEnum fromCode(Integer code) { - for (OperateTypeEnum value : OperateTypeEnum.values()) { - if (value.getCode().equals(code)) { - return value; - } - } - return OperateTypeEnum.EMPTY; - } - - public abstract int getDbStatCode(); -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/exception/ExceptionUtil.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/exception/ExceptionUtil.java deleted file mode 100644 index e1fac9718..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/exception/ExceptionUtil.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.github.liueyueyi.forum.api.model.exception; - -import com.github.liueyueyi.forum.api.model.vo.constants.StatusEnum; - -/** - * @author YiHui - * @date 2022/9/2 - */ -public class ExceptionUtil { - - public static ForumException of(StatusEnum status, Object... args) { - return new ForumException(status, args); - } - -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/exception/ForumException.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/exception/ForumException.java deleted file mode 100644 index 0f79115aa..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/exception/ForumException.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.liueyueyi.forum.api.model.exception; - -import com.github.liueyueyi.forum.api.model.vo.Status; -import com.github.liueyueyi.forum.api.model.vo.constants.StatusEnum; - -/** - * 业务异常 - * - * @author YiHui - * @date 2022/9/2 - */ -public class ForumException extends RuntimeException { - private Status status; - - public ForumException(Status status) { - this.status = status; - } - - public ForumException(int code, String msg) { - this.status = Status.newStatus(code, msg); - } - - public ForumException(StatusEnum statusEnum, Object... args) { - this.status = Status.newStatus(statusEnum, args); - } - -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/package-info.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/package-info.java deleted file mode 100644 index 42b4351aa..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -/** - * @author YiHui - * @date 2022/7/6 - */ -package com.github.liueyueyi.forum.api.model; \ No newline at end of file diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/PageParam.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/PageParam.java deleted file mode 100644 index a4e57c567..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/PageParam.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.github.liueyueyi.forum.api.model.vo; - -import lombok.Data; - -/** - * 数据库分页参数 - * - * @author louzai - * @date 2022-07-120 - */ -@Data -public class PageParam { - - public static final Long DEFAULT_PAGE_NUM = 1L; - public static final Long DEFAULT_PAGE_SIZE = 10L; - - - /** - * 请求页数,从1开始计数 - */ - private long pageNum; - private long pageSize; - private long offset; - private long limit; - - public static PageParam newPageInstance() { - return newPageInstance(DEFAULT_PAGE_NUM ,DEFAULT_PAGE_SIZE); - } - - public static PageParam newPageInstance(Long pageNum, Long pageSize) { - if (pageNum == null || pageSize == null) { - return null; - } - - final PageParam pageParam = new PageParam(); - pageParam.pageNum = pageNum; - pageParam.pageSize = pageSize; - - pageParam.offset = (pageNum - 1) * pageSize; - pageParam.limit = pageSize; - - return pageParam; - } - - public static String getLimitSql(PageParam pageParam) { - return String.format("limit %s,%s", pageParam.offset, pageParam.limit); - } -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/ResVo.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/ResVo.java deleted file mode 100644 index 8eb5c5b27..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/ResVo.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.github.liueyueyi.forum.api.model.vo; - -import com.github.liueyueyi.forum.api.model.vo.constants.StatusEnum; -import lombok.Data; - -import java.io.Serializable; - -/** - * @author YiHui - * @date 2022/7/6 - */ -@Data -public class ResVo implements Serializable { - private static final long serialVersionUID = -510306209659393854L; - private Status status; - - private T result; - - - public ResVo() { - } - - public ResVo(Status status) { - this.status = status; - } - - public ResVo(T t) { - status = Status.newStatus(StatusEnum.SUCCESS); - this.result = t; - } - - public static ResVo ok(T t) { - return new ResVo(t); - } - - @SuppressWarnings("unchecked") - public static ResVo fail(StatusEnum status, Object... args) { - return new ResVo<>(Status.newStatus(status, args)); - } -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/Status.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/Status.java deleted file mode 100644 index 0dccf2080..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/Status.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.github.liueyueyi.forum.api.model.vo; - -import com.github.liueyueyi.forum.api.model.vo.constants.StatusEnum; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * @author YiHui - * @date 2022/7/6 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class Status { - - /** - * 业务状态码 - */ - private int code; - - /** - * 描述信息 - */ - private String msg; - - public static Status newStatus(int code, String msg) { - return new Status(code, msg); - } - - public static Status newStatus(StatusEnum status, Object... msgs) { - String msg; - if (msgs.length > 0) { - msg = String.format(status.getMsg(), msgs); - } else { - msg = status.getMsg(); - } - return newStatus(status.getCode(), msg); - } -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/ArticlePostReq.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/ArticlePostReq.java deleted file mode 100644 index 6c20bd76a..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/ArticlePostReq.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.github.liueyueyi.forum.api.model.vo.article; - -import com.github.liueyueyi.forum.api.model.enums.PushStatusEnum; -import lombok.Data; - -import java.io.Serializable; -import java.util.Set; - -/** - * 发布文章请求参数 - * - * @author YiHui - * @date 2022/7/24 - */ -@Data -public class ArticlePostReq implements Serializable { - /** - * 文章ID, 当存在时,表示更新文章 - */ - private Long articleId; - /** - * 文章标题 - */ - private String title; - - /** - * 文章短标题 - */ - private String subTitle; - - /** - * 分类 - */ - private Long categoryId; - - /** - * 标签 - */ - private Set tagIds; - - /** - * 简介 - */ - private String summary; - - /** - * 正文内容 - */ - private String content; - - /** - * 封面 - */ - private String cover; - - /** - * 文本类型 - * - * @see com.github.liueyueyi.forum.api.model.enums.ArticleTypeEnum - */ - private String articleType; - - - /** - * 来源:1-转载,2-原创,3-翻译 - * - * @see com.github.liueyueyi.forum.api.model.enums.SourceTypeEnum - */ - private Integer source; - - /** - * 原文地址 - */ - private String sourceUrl; - - /** - * POST 发表, SAVE 暂存 DELETE 删除 - */ - private String actionType; - - public PushStatusEnum pushStatus() { - if ("post".equalsIgnoreCase(actionType)) { - return PushStatusEnum.ONLINE; - } else { - return PushStatusEnum.OFFLINE; - } - } - - public boolean deleted() { - return "delete".equalsIgnoreCase(actionType); - } -} \ No newline at end of file diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/dto/ArticleDTO.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/dto/ArticleDTO.java deleted file mode 100644 index 0ad7fc372..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/dto/ArticleDTO.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.github.liueyueyi.forum.api.model.vo.article.dto; - -import com.github.liueyueyi.forum.api.model.vo.user.dto.ArticleFootCountDTO; -import lombok.Data; - -import java.io.Serializable; -import java.util.List; - -/** - * 文章信息 - *

- * DTO 定义返回给web前端的实体类 (VO) - * - * @author YiHui - * @date 2022/7/24 - */ -@Data -public class ArticleDTO implements Serializable { - private static final long serialVersionUID = -793906904770296838L; - - private Long articleId; - - /** - * 文章类型:1-博文,2-问答 - */ - private Integer articleType; - - /** - * 作者uid - */ - private Long author; - - /** - * 作者名 - */ - private String authorName; - - /** - * 文章标题 - */ - private String title; - - /** - * 短标题 - */ - private String shortTitle; - - /** - * 简介 - */ - private String summary; - - /** - * 封面 - */ - private String cover; - - /** - * 正文 - */ - private String content; - - /** - * 文章来源 - * - * @see com.github.liueyueyi.forum.api.model.enums.SourceTypeEnum - */ - private String sourceType; - - /** - * 原文地址 - */ - private String sourceUrl; - - /** - * 0 未发布 1 已发布 - */ - private Integer status; - - /** - * 创建时间 - */ - private Long createTime; - - /** - * 最后更新时间 - */ - private Long lastUpdateTime; - - /** - * 分类 - */ - private CategoryDTO category; - - /** - * 标签 - */ - private List tags; - - /** - * 表示当前查看的用户是否已经点赞过 - */ - private Boolean praised; - - /** - * 表示当用户是否评论过 - */ - private Boolean commented; - - /** - * 表示当前用户是否收藏过 - */ - private Boolean collected; - - /** - * 文章对应的统计计数 - */ - private ArticleFootCountDTO count; -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/dto/ArticleListDTO.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/dto/ArticleListDTO.java deleted file mode 100644 index 3bf4fd555..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/dto/ArticleListDTO.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.github.liueyueyi.forum.api.model.vo.article.dto; - -import lombok.Data; - -import java.util.List; - -/** - * 文章列表信息 - * - * @author louzai - * @date 2022/7/31 - */ -@Data -public class ArticleListDTO { - - /** - * 文章列表 - */ - List articleList; - - /** - * 是否有更多 - */ - private Boolean isMore; -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/dto/CategoryDTO.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/dto/CategoryDTO.java deleted file mode 100644 index 4486583c6..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/dto/CategoryDTO.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.liueyueyi.forum.api.model.vo.article.dto; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; - -/** - * @author YiHui - * @date 2022/7/24 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class CategoryDTO implements Serializable { - public static final String DEFAULT_TOTAL_CATEGORY = "全部"; - public static final CategoryDTO DEFAULT_CATEGORY = new CategoryDTO(0L, "全部"); - - private static final long serialVersionUID = 8272116638231812207L; - public static CategoryDTO EMPTY = new CategoryDTO(-1L, "illegal"); - - private Long categoryId; - - private String category; - - private Boolean selected; - - public CategoryDTO(Long categoryId, String category) { - this.categoryId = categoryId; - this.category = category; - this.selected = false; - } -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/dto/TagDTO.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/dto/TagDTO.java deleted file mode 100644 index b081d82f6..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/dto/TagDTO.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.liueyueyi.forum.api.model.vo.article.dto; - -import lombok.Data; - -import java.io.Serializable; - -/** - * @author YiHui - * @date 2022/7/24 - */ -@Data -public class TagDTO implements Serializable { - private static final long serialVersionUID = -8614833588325787479L; - - private Long categoryId; - - private Long tagId; - - private String tag; -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/dto/UserFollowDTO.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/dto/UserFollowDTO.java deleted file mode 100644 index edc72a231..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/dto/UserFollowDTO.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.github.liueyueyi.forum.api.model.vo.comment.dto; - -import lombok.Data; - -/** - * 关注用户 - * - * @author louzai - * @since 2022/7/19 - */ -@Data -public class UserFollowDTO { - - /** - * 关系ID - */ - private Long userRelationId; - - /** - * 用户ID - */ - private Long userId; - - /** - * 用户名 - */ - private String userName; - - /** - * 用户图像 - */ - private String photo; - - /** - * 个人简介 - */ - private String profile; -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/dto/UserFollowListDTO.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/dto/UserFollowListDTO.java deleted file mode 100644 index b4ebdf1c5..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/dto/UserFollowListDTO.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.github.liueyueyi.forum.api.model.vo.comment.dto; - -import lombok.Data; - -import java.util.Collections; -import java.util.List; - -/** - * 关注用户 - * - * @author louzai - * @since 2022/7/19 - */ -@Data -public class UserFollowListDTO { - - /** - * 用户列表 - */ - List userFollowList; - - /** - * 是否有更多 - */ - private Boolean isMore; - - public static UserFollowListDTO emptyInstance() { - UserFollowListDTO res = new UserFollowListDTO(); - res.setUserFollowList(Collections.emptyList()); - res.setIsMore(false); - return res; - } -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/constants/StatusEnum.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/constants/StatusEnum.java deleted file mode 100644 index a388998ff..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/constants/StatusEnum.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.github.liueyueyi.forum.api.model.vo.constants; - -import lombok.Getter; - -/** - * 异常码规范: - * xxx - xxx - xxx - * 业务 - 状态 - code - *

- * 业务取值 - * - 100 全局 - * - 200 文章相关 - * - 300 评论相关 - * - 400 用户相关 - *

- * 状态:基于http status的含义 - * - 4xx 调用方使用姿势问题 - * - 5xx 服务内部问题 - *

- * code: 具体的业务code - * - * @author YiHui - * @date 2022/7/27 - */ -@Getter -public enum StatusEnum { - SUCCESS(0, "OK"), - - // 全局传参异常 - ILLEGAL_ARGUMENTS(100_400_001, "参数异常"), - ILLEGAL_ARGUMENTS_MIXED(100_400_002, "参数异常:%s"), - - // 全局权限相关 - FORBID_ERROR(100_403_001, "无权限"), - - FORBID_ERROR_MIXED(100_403_002, "无权限:%s"), - - // 全局,数据不存在 - RECORDS_NOT_EXISTS(100_500_001, "记录不存在:%s"), - - - // 用户相关异常 - LOGIN_FAILED_MIXED(400_403_001, "登录失败:%s"), - USER_NOT_EXISTS(400_500_001, "用户不存在:%s"), - - - ; - - private int code; - - private String msg; - - StatusEnum(int code, String msg) { - this.code = code; - this.msg = msg; - } -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/UserInfoSaveReq.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/UserInfoSaveReq.java deleted file mode 100644 index 311fabdd2..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/UserInfoSaveReq.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.github.liueyueyi.forum.api.model.vo.user; - -import lombok.Data; - -/** - * 用户信息入参 - * - * @author louzai - * @date 2022-07-24 - */ -@Data -public class UserInfoSaveReq { - - /** - * 用户ID - */ - private Long userId; - - /** - * 用户名 - */ - private String userName; - - /** - * 用户图像 - */ - private String photo; - - /** - * 职位 - */ - private String position; - - /** - * 公司 - */ - private String company; - - /** - * 个人简介 - */ - private String profile; -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/UserRelationReq.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/UserRelationReq.java deleted file mode 100644 index b2dae0cb7..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/UserRelationReq.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.liueyueyi.forum.api.model.vo.user; - -import lombok.Data; - -/** - * 用户关系入参 - * - * @author louzai - * @date 2022-07-24 - */ -@Data -public class UserRelationReq { - - /** - * 用户ID - */ - private Long userId; - - /** - * 是否关注当前用户 - */ - private Boolean followed; -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/dto/BaseUserInfoDTO.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/dto/BaseUserInfoDTO.java deleted file mode 100644 index de88acefb..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/dto/BaseUserInfoDTO.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.github.liueyueyi.forum.api.model.vo.user.dto; - -import com.github.liueyueyi.forum.api.model.entity.BaseDTO; -import lombok.Data; -import lombok.experimental.Accessors; - -/** - * @author YiHui - * @date 2022/8/15 - */ -@Data -@Accessors(chain = true) -public class BaseUserInfoDTO extends BaseDTO { - /** - * 用户id - */ - private Long userId; - - /** - * 用户名 - */ - private String userName; - - /** - * 用户角色 admin, normal - */ - private String role; - - /** - * 用户图像 - */ - private String photo; - /** - * 个人简介 - */ - private String profile; - /** - * 职位 - */ - private String position; - - /** - * 公司 - */ - private String company; - - /** - * 扩展字段 - */ - private String extend; - - /** - * 是否删除 - */ - private Integer deleted; -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/dto/UserStatisticInfoDTO.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/dto/UserStatisticInfoDTO.java deleted file mode 100644 index e1091337d..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/dto/UserStatisticInfoDTO.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.github.liueyueyi.forum.api.model.vo.user.dto; - -import lombok.Data; -import lombok.ToString; - -/** - * 用户主页信息 - * - * @author louzai - * @since 2022/7/19 - */ -@Data -@ToString(callSuper = true) -public class UserStatisticInfoDTO extends BaseUserInfoDTO { - - /** - * 关注数 - */ - private Integer followCount; - - /** - * 粉丝数 - */ - private Integer fansCount; - - /** - * 已发布文章数 - */ - private Integer articleCount; - - /** - * 文章点赞数 - */ - private Integer praiseCount; - - /** - * 文章被阅读数 - */ - private Integer readCount; - - /** - * 文章被收藏数 - */ - private Integer collectionCount; - - /** - * 是否关注当前用户 - */ - private Boolean followed; -} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/wx/WxTxtMsgResVo.java b/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/wx/WxTxtMsgResVo.java deleted file mode 100644 index 8e6d267c4..000000000 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/wx/WxTxtMsgResVo.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.liueyueyi.forum.api.model.vo.user.wx; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; -import lombok.Data; - -/** - * 返回的数据结构体 - *

- * - * @author yihui - * @link - * @date 2022/6/20 - */ -@Data -@JacksonXmlRootElement(localName = "xml") -public class WxTxtMsgResVo { - - @JacksonXmlProperty(localName = "ToUserName") - private String toUserName; - @JacksonXmlProperty(localName = "FromUserName") - private String fromUserName; - @JacksonXmlProperty(localName = "CreateTime") - private Long createTime; - @JacksonXmlProperty(localName = "MsgType") - private String msgType; - @JacksonXmlProperty(localName = "Content") - private String content; - -} diff --git a/forum-core/pom.xml b/forum-core/pom.xml deleted file mode 100644 index 2bef089de..000000000 --- a/forum-core/pom.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - quick-forum - com.github.liuyueyi.quick-forum - 0.0.1-SNAPSHOT - - 4.0.0 - - forum-core - - - 8 - 8 - - - - - com.github.liuyueyi.quick-forum - form-api - - - org.springframework - spring-context - - - javax.servlet - javax.servlet-api - - - org.slf4j - slf4j-api - - - ch.qos.logback - logback-classic - - - org.apache.commons - commons-lang3 - - - - com.google.guava - guava - - - \ No newline at end of file diff --git a/forum-core/src/main/java/com/github/liuyueyi/forum/core/package-info.java b/forum-core/src/main/java/com/github/liuyueyi/forum/core/package-info.java deleted file mode 100644 index 56b8f36bd..000000000 --- a/forum-core/src/main/java/com/github/liuyueyi/forum/core/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * 公共依赖的核心包模块 - * - * @author YiHui - * @date 2022/7/6 - */ -package com.github.liuyueyi.forum.core; \ No newline at end of file diff --git a/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/CodeGenerateUtil.java b/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/CodeGenerateUtil.java deleted file mode 100644 index 39b359133..000000000 --- a/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/CodeGenerateUtil.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.github.liuyueyi.forum.core.util; - -import java.util.Random; - -/** - * @author YiHui - * @date 2022/8/15 - */ -public class CodeGenerateUtil { - - private static final Random random = new Random(); - - public static String genCode() { - return String.format("%06d", random.nextInt(1000_000)); - } -} diff --git a/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/DateUtil.java b/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/DateUtil.java deleted file mode 100644 index 6ed890620..000000000 --- a/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/DateUtil.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.github.liuyueyi.forum.core.util; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; - -/** - * @author YiHui - * @date 2022/8/25 - */ -public class DateUtil { - - /** - * 毫秒转日期 - * - * @param timestamp - * @return - */ - public static String time2day(long timestamp) { - DateTimeFormatter ftf = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm"); - return ftf.format(LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault())); - } -} diff --git a/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/IpUtil.java b/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/IpUtil.java deleted file mode 100644 index 0f7db58ae..000000000 --- a/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/IpUtil.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.github.liuyueyi.forum.core.util; - -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; - -import javax.servlet.http.HttpServletRequest; - -/** - * @author YiHui - * @date 2022/7/6 - */ -@Slf4j -public class IpUtil { - private static final String UNKNOWN = "unKnown"; - - public static String getClientIp(HttpServletRequest request) { - try { - String xIp = request.getHeader("X-Real-IP"); - String xFor = request.getHeader("X-Forwarded-For"); - if (StringUtils.isNotEmpty(xFor) && !UNKNOWN.equalsIgnoreCase(xFor)) { - //多次反向代理后会有多个ip值,第一个ip才是真实ip - int index = xFor.indexOf(","); - if (index != -1) { - return xFor.substring(0, index); - } else { - return xFor; - } - } - xFor = xIp; - if (StringUtils.isNotEmpty(xFor) && !UNKNOWN.equalsIgnoreCase(xFor)) { - return xFor; - } - if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) { - xFor = request.getHeader("Proxy-Client-IP"); - } - if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) { - xFor = request.getHeader("WL-Proxy-Client-IP"); - } - if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) { - xFor = request.getHeader("HTTP_CLIENT_IP"); - } - if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) { - xFor = request.getHeader("HTTP_X_FORWARDED_FOR"); - } - if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) { - xFor = request.getRemoteAddr(); - } - return xFor; - } catch (Exception e) { - log.error("get remote ip error!", e); - return "x.0.0.1"; - } - } - -} diff --git a/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/SpringUtil.java b/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/SpringUtil.java deleted file mode 100644 index 9ea909b47..000000000 --- a/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/SpringUtil.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.github.liuyueyi.forum.core.util; - -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.EnvironmentAware; -import org.springframework.core.env.Environment; -import org.springframework.stereotype.Component; - -/** - * @author YiHui - * @date 2022/8/29 - */ -@Component -public class SpringUtil implements ApplicationContextAware, EnvironmentAware { - private static ApplicationContext context; - private static Environment environment; - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - SpringUtil.context = applicationContext; - } - - @Override - public void setEnvironment(Environment environment) { - SpringUtil.environment = environment; - } - - /** - * 获取bean - * - * @param bean - * @param - * @return - */ - public static T getBean(Class bean) { - return context.getBean(bean); - } - - public static Object getBean(String beanName) { - return context.getBean(beanName); - } - - /** - * 获取配置 - * - * @param key - * @return - */ - public static String getConfig(String key) { - return environment.getProperty(key); - } -} diff --git a/forum-service/pom.xml b/forum-service/pom.xml deleted file mode 100644 index 8df8ddd26..000000000 --- a/forum-service/pom.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - quick-forum - com.github.liuyueyi.quick-forum - 0.0.1-SNAPSHOT - - 4.0.0 - - forum-service - - - - com.github.liuyueyi.quick-forum - forum-core - - - org.springframework - spring-context - - - - com.fasterxml.jackson.core - jackson-databind - - - - com.baomidou - mybatis-plus-boot-starter - - - mysql - mysql-connector-java - - - - \ No newline at end of file diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/ServiceAutoConfig.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/ServiceAutoConfig.java deleted file mode 100644 index e404bb120..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/ServiceAutoConfig.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.github.liuyueyi.forum.service; - -import org.mybatis.spring.annotation.MapperScan; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; - -/** - * @author YiHui - * @date 2022/7/6 - */ -@Configuration -@ComponentScan("com.github.liuyueyi.forum.service") -@MapperScan(basePackages = { - "com.github.liuyueyi.forum.service.article.repository.mapper", - "com.github.liuyueyi.forum.service.user.repository.mapper", - "com.github.liuyueyi.forum.service.comment.repository.mapper",}) -public class ServiceAutoConfig { -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/conveter/ArticleConverter.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/conveter/ArticleConverter.java deleted file mode 100644 index c20b0f30f..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/conveter/ArticleConverter.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.github.liuyueyi.forum.service.article.conveter; - -import com.github.liueyueyi.forum.api.model.enums.ArticleTypeEnum; -import com.github.liueyueyi.forum.api.model.enums.SourceTypeEnum; -import com.github.liueyueyi.forum.api.model.enums.YesOrNoEnum; -import com.github.liueyueyi.forum.api.model.vo.article.ArticlePostReq; -import com.github.liueyueyi.forum.api.model.vo.article.dto.ArticleDTO; -import com.github.liueyueyi.forum.api.model.vo.article.dto.CategoryDTO; -import com.github.liueyueyi.forum.api.model.vo.article.dto.TagDTO; -import com.github.liuyueyi.forum.service.article.repository.entity.ArticleDO; -import com.github.liuyueyi.forum.service.article.repository.entity.CategoryDO; -import com.github.liuyueyi.forum.service.article.repository.entity.TagDO; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * 文章转换 - *

- * - * @author louzai - * @date 2022-07-31 - */ -public class ArticleConverter { - - public static ArticleDO toArticleDo(ArticlePostReq req, Long author) { - ArticleDO article = new ArticleDO(); - // 设置作者ID - article.setUserId(author); - article.setId(req.getArticleId()); - article.setTitle(req.getTitle()); - article.setShortTitle(req.getSubTitle()); - article.setArticleType(ArticleTypeEnum.valueOf(req.getArticleType().toUpperCase()).getCode()); - article.setPicture(req.getCover() == null ? "" : req.getCover()); - article.setCategoryId(req.getCategoryId()); - article.setSource(req.getSource()); - article.setSourceUrl(req.getSourceUrl()); - article.setSummary(req.getSummary()); - article.setStatus(req.pushStatus().getCode()); - article.setDeleted(req.deleted() ? YesOrNoEnum.YES.getCode() : YesOrNoEnum.NO.getCode()); - return article; - } - - public static ArticleDTO toDto(ArticleDO articleDO) { - if (articleDO == null) { - return null; - } - ArticleDTO articleDTO = new ArticleDTO(); - articleDTO.setAuthor(articleDO.getUserId()); - articleDTO.setArticleId(articleDO.getId()); - articleDTO.setArticleType(articleDO.getArticleType()); - articleDTO.setTitle(articleDO.getTitle()); - articleDTO.setShortTitle(articleDO.getShortTitle()); - articleDTO.setSummary(articleDO.getSummary()); - articleDTO.setCover(articleDO.getPicture()); - articleDTO.setSourceType(SourceTypeEnum.formCode(articleDO.getSource()).getDesc()); - articleDTO.setSourceUrl(articleDO.getSourceUrl()); - articleDTO.setStatus(articleDO.getStatus()); - articleDTO.setCreateTime(articleDO.getCreateTime().getTime()); - articleDTO.setLastUpdateTime(articleDO.getUpdateTime().getTime()); - - // 设置类目id - articleDTO.setCategory(new CategoryDTO(articleDO.getCategoryId(), null)); - return articleDTO; - } - - - /** - * do转换 - * - * @param tag - * @return - */ - public static TagDTO toDto(TagDO tag) { - TagDTO dto = new TagDTO(); - dto.setTag(tag.getTagName()); - dto.setTagId(tag.getId()); - dto.setCategoryId(tag.getCategoryId()); - return dto; - } - - public static List toDtoList(List tags) { - return tags.stream().map(ArticleConverter::toDto).collect(Collectors.toList()); - } - - - public static CategoryDTO toDto(CategoryDO category) { - CategoryDTO dto = new CategoryDTO(); - dto.setCategory(category.getCategoryName()); - dto.setCategoryId(category.getId()); - dto.setSelected(false); - return dto; - } -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/dao/ArticleDao.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/dao/ArticleDao.java deleted file mode 100644 index a54ec3301..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/dao/ArticleDao.java +++ /dev/null @@ -1,173 +0,0 @@ -package com.github.liuyueyi.forum.service.article.repository.dao; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.toolkit.StringUtils; -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.github.liueyueyi.forum.api.model.enums.DocumentTypeEnum; -import com.github.liueyueyi.forum.api.model.enums.PushStatusEnum; -import com.github.liueyueyi.forum.api.model.enums.YesOrNoEnum; -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.article.dto.ArticleDTO; -import com.github.liuyueyi.forum.service.article.conveter.ArticleConverter; -import com.github.liuyueyi.forum.service.article.repository.entity.ArticleDO; -import com.github.liuyueyi.forum.service.article.repository.entity.ArticleDetailDO; -import com.github.liuyueyi.forum.service.article.repository.entity.ReadCountDO; -import com.github.liuyueyi.forum.service.article.repository.mapper.ArticleDetailMapper; -import com.github.liuyueyi.forum.service.article.repository.mapper.ArticleMapper; -import com.github.liuyueyi.forum.service.article.repository.mapper.ReadCountMapper; -import org.springframework.stereotype.Repository; - -import javax.annotation.Resource; -import java.util.List; -import java.util.Optional; - -/** - * 文章相关DB操作 - *

- * 多表结构的操作封装,只与DB操作相关 - * - * @author louzai - * @date 2022-07-18 - */ -@Repository -public class ArticleDao extends ServiceImpl { - @Resource - private ArticleDetailMapper articleDetailMapper; - @Resource - private ReadCountMapper readCountMapper; - - /** - * 查询文章详情 - * - * @param articleId - * @return - */ - public ArticleDTO queryArticleDetail(Long articleId) { - // 查询文章记录 - ArticleDO article = baseMapper.selectById(articleId); - if (article == null) { - return null; - } - - // 查询文章正文 - ArticleDTO dto = ArticleConverter.toDto(article); - ArticleDetailDO detail = findLatestDetail(articleId); - dto.setContent(detail.getContent()); - return dto; - } - - - // ------------ article content ---------------- - - private ArticleDetailDO findLatestDetail(long articleId) { - // 查询文章内容 - LambdaQueryWrapper contentQuery = Wrappers.lambdaQuery(); - contentQuery.eq(ArticleDetailDO::getDeleted, YesOrNoEnum.NO.getCode()) - .eq(ArticleDetailDO::getArticleId, articleId) - .orderByDesc(ArticleDetailDO::getVersion); - return articleDetailMapper.selectOne(contentQuery); - } - - /** - * 保存文章正文 - * - * @param articleId - * @param content - * @return - */ - public Long saveArticleContent(Long articleId, String content) { - ArticleDetailDO detail = new ArticleDetailDO(); - detail.setArticleId(articleId); - detail.setContent(content); - detail.setVersion(1L); - articleDetailMapper.insert(detail); - return detail.getId(); - } - - /** - * 更正文章正文 - * - * @param articleId - * @param content - */ - public void updateArticleContent(Long articleId, String content) { - articleDetailMapper.updateContent(articleId, content); - } - - public List listArticlesByUserId(Long userId, PageParam pageParam) { - LambdaQueryWrapper query = Wrappers.lambdaQuery(); - query.eq(ArticleDO::getDeleted, YesOrNoEnum.NO.getCode()) - .eq(ArticleDO::getStatus, PushStatusEnum.ONLINE.getCode()) - .eq(ArticleDO::getUserId, userId) - .last(PageParam.getLimitSql(pageParam)) - .orderByDesc(ArticleDO::getId); - return baseMapper.selectList(query); - } - - - public List listArticlesByCategoryId(Long categoryId, PageParam pageParam) { - if (categoryId != null && categoryId <= 0) { - // 分类不存在时,表示查所有 - categoryId = null; - } - LambdaQueryWrapper query = Wrappers.lambdaQuery(); - query.eq(ArticleDO::getDeleted, YesOrNoEnum.NO.getCode()) - .eq(ArticleDO::getStatus, PushStatusEnum.ONLINE.getCode()); - Optional.ofNullable(categoryId).ifPresent(cid -> query.eq(ArticleDO::getCategoryId, cid)); - query.last(PageParam.getLimitSql(pageParam)) - .orderByDesc(ArticleDO::getId); - return baseMapper.selectList(query); - } - - - public List listArticlesByBySearchKey(String key, PageParam pageParam) { - LambdaQueryWrapper query = Wrappers.lambdaQuery(); - query.eq(ArticleDO::getDeleted, YesOrNoEnum.NO.getCode()) - .eq(ArticleDO::getStatus, PushStatusEnum.ONLINE.getCode()) - .and(!StringUtils.isEmpty(key), - v -> v.like(ArticleDO::getTitle, key) - .or() - .like(ArticleDO::getShortTitle, key) - .or() - .like(ArticleDO::getSummary, key)); - query.last(PageParam.getLimitSql(pageParam)) - .orderByDesc(ArticleDO::getId); - return baseMapper.selectList(query); - } - - - /** - * 阅读计数 - * - * @param articleId - * @return - */ - public int incrReadCount(Long articleId) { - LambdaQueryWrapper query = Wrappers.lambdaQuery(); - query.eq(ReadCountDO::getDocumentId, articleId).eq(ReadCountDO::getDocumentType, DocumentTypeEnum.ARTICLE.getCode()); - ReadCountDO record = readCountMapper.selectOne(query); - if (record == null) { - record = new ReadCountDO().setDocumentId(articleId).setDocumentType(DocumentTypeEnum.ARTICLE.getCode()).setCnt(1); - readCountMapper.insert(record); - } else { - // fixme: 这里存在并发覆盖问题,推荐使用 update read_count set cnt = cnt + 1 where id = xxx - record.setCnt(record.getCnt() + 1); - readCountMapper.updateById(record); - } - return record.getCnt(); - } - - /** - * 统计用户的文章计数 - * - * @param userId - * @return - */ - public int countArticleByUser(Long userId) { - return lambdaQuery().eq(ArticleDO::getUserId, userId) - .eq(ArticleDO::getStatus, PushStatusEnum.ONLINE.getCode()) - .eq(ArticleDO::getDeleted, YesOrNoEnum.NO.getCode()) - .count().intValue(); - } -} \ No newline at end of file diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/dao/CategoryDao.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/dao/CategoryDao.java deleted file mode 100644 index 5ce2ef71d..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/dao/CategoryDao.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.github.liuyueyi.forum.service.article.repository.dao; - -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.github.liueyueyi.forum.api.model.enums.PushStatusEnum; -import com.github.liueyueyi.forum.api.model.enums.YesOrNoEnum; -import com.github.liuyueyi.forum.service.article.repository.entity.CategoryDO; -import com.github.liuyueyi.forum.service.article.repository.mapper.CategoryMapper; -import org.springframework.stereotype.Repository; - -import java.util.List; - -/** - * 类目Service - * - * @author louzai - * @date 2022-07-20 - */ -@Repository -public class CategoryDao extends ServiceImpl { - /** - * @return - */ - public List listAllCategoriesFromDb() { - return lambdaQuery() - .eq(CategoryDO::getDeleted, YesOrNoEnum.NO.getCode()) - .eq(CategoryDO::getStatus, PushStatusEnum.ONLINE.getCode()) - .list(); - } -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/dao/TagDao.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/dao/TagDao.java deleted file mode 100644 index eebfbe5f1..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/dao/TagDao.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.liuyueyi.forum.service.article.repository.dao; - -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.github.liueyueyi.forum.api.model.enums.YesOrNoEnum; -import com.github.liueyueyi.forum.api.model.vo.article.dto.TagDTO; -import com.github.liuyueyi.forum.service.article.conveter.ArticleConverter; -import com.github.liuyueyi.forum.service.article.repository.entity.TagDO; -import com.github.liuyueyi.forum.service.article.repository.mapper.TagMapper; -import org.springframework.stereotype.Repository; - -import java.util.List; - -/** - * @author YiHui - * @date 2022/9/2 - */ -@Repository -public class TagDao extends ServiceImpl { - public List listTagsByCategoryId(Long categoryId) { - List list = lambdaQuery() - .eq(TagDO::getDeleted, YesOrNoEnum.NO.getCode()) - .eq(TagDO::getCategoryId, categoryId) - .list(); - return ArticleConverter.toDtoList(list); - } -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/ArticleDO.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/ArticleDO.java deleted file mode 100644 index 8b1d5b6a8..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/ArticleDO.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.github.liuyueyi.forum.service.article.repository.entity; - -import com.baomidou.mybatisplus.annotation.TableName; -import com.github.liueyueyi.forum.api.model.entity.BaseDO; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * 文章表 - * - * @author louzai - * @date 2022-07-18 - */ -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("article") -public class ArticleDO extends BaseDO { - private static final long serialVersionUID = 1L; - - /** - * 作者 - */ - private Long userId; - - /** - * 文章类型:1-博文,2-问答 - */ - private Integer articleType; - - /** - * 文章标题 - */ - private String title; - - /** - * 短标题 - */ - private String shortTitle; - - /** - * 文章头图 - */ - private String picture; - - /** - * 文章摘要 - */ - private String summary; - - /** - * 类目ID - */ - private Long categoryId; - - /** - * 来源:1-转载,2-原创,3-翻译 - * - * @see com.github.liueyueyi.forum.api.model.enums.SourceTypeEnum - */ - private Integer source; - - /** - * 原文地址 - */ - private String sourceUrl; - - /** - * 状态:0-未发布,1-已发布 - * - * @see com.github.liueyueyi.forum.api.model.enums.PushStatusEnum - */ - private Integer status; - - private Integer deleted; -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/CategoryDO.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/CategoryDO.java deleted file mode 100644 index a6e9917b9..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/CategoryDO.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.liuyueyi.forum.service.article.repository.entity; - -import com.baomidou.mybatisplus.annotation.TableName; -import com.github.liueyueyi.forum.api.model.entity.BaseDO; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * 类目管理表 - * - * @author louzai - * @date 2022-07-18 - */ -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("category") -public class CategoryDO extends BaseDO { - - private static final long serialVersionUID = 1L; - - /** - * 类目名称 - */ - private String categoryName; - - /** - * 状态:0-未发布,1-已发布 - */ - private Integer status; - - private Integer deleted; -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/ArticleMapper.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/ArticleMapper.java deleted file mode 100644 index 6dba72177..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/ArticleMapper.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.liuyueyi.forum.service.article.repository.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.github.liuyueyi.forum.service.article.repository.entity.ArticleDO; - -/** - * 文章mapper接口 - * - * @author louzai - * @date 2022-07-18 - */ -public interface ArticleMapper extends BaseMapper { -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/ArticleTagMapper.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/ArticleTagMapper.java deleted file mode 100644 index b8a770118..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/ArticleTagMapper.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.liuyueyi.forum.service.article.repository.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.github.liueyueyi.forum.api.model.vo.article.dto.TagDTO; -import com.github.liuyueyi.forum.service.article.repository.entity.ArticleTagDO; -import org.apache.ibatis.annotations.Insert; -import org.apache.ibatis.annotations.Param; - -import java.util.List; - -/** - * 文章标签映mapper接口 - * - * @author louzai - * @date 2022-07-18 - */ -public interface ArticleTagMapper extends BaseMapper { - - /** - * 查询文章标签 - * - * @param articleId - * @return - */ - List listArticleTagDetails(@Param("articleId") Long articleId); - -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/CategoryMapper.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/CategoryMapper.java deleted file mode 100644 index dec1e586e..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/CategoryMapper.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.liuyueyi.forum.service.article.repository.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.github.liuyueyi.forum.service.article.repository.entity.CategoryDO; - -/** - * 类目管理mapper接口 - * - * @author louzai - * @date 2022-07-18 - */ -public interface CategoryMapper extends BaseMapper { -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/ReadCountMapper.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/ReadCountMapper.java deleted file mode 100644 index b1be166d0..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/ReadCountMapper.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.liuyueyi.forum.service.article.repository.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.github.liuyueyi.forum.service.article.repository.entity.ReadCountDO; - -/** - * 标签mapper接口 - * - * @author louzai - * @date 2022-07-18 - */ -public interface ReadCountMapper extends BaseMapper { -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/TagMapper.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/TagMapper.java deleted file mode 100644 index 66368e962..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/TagMapper.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.liuyueyi.forum.service.article.repository.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.github.liuyueyi.forum.service.article.repository.entity.TagDO; - -/** - * 标签mapper接口 - * - * @author louzai - * @date 2022-07-18 - */ -public interface TagMapper extends BaseMapper { -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/ArticleReadService.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/ArticleReadService.java deleted file mode 100644 index 30dcbd0e8..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/ArticleReadService.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.github.liuyueyi.forum.service.article.service; - -import com.github.liueyueyi.forum.api.model.enums.HomeSelectEnum; -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.article.dto.ArticleDTO; -import com.github.liueyueyi.forum.api.model.vo.article.dto.ArticleListDTO; -import com.github.liuyueyi.forum.service.article.repository.entity.ArticleDO; - -public interface ArticleReadService { - - /** - * 查询基础的文章信息 - * - * @param articleId - * @return - */ - ArticleDO queryBasicArticle(Long articleId); - - - /** - * 查询文章详情,包括正文内容,分类、标签等信息 - * - * @param articleId - * @return - */ - ArticleDTO queryDetailArticleInfo(Long articleId); - - /** - * 查询文章所有的关联信息,正文,分类,标签,阅读计数+1,当前登录用户是否点赞、评论过 - * - * @param articleId 文章id - * @param currentUser 当前查看的用户ID - * @return - */ - ArticleDTO queryTotalArticleInfo(Long articleId, Long currentUser); - - /** - * 查询某个分类下的文章,支持翻页 - * - * @param categoryId - * @param page - * @return - */ - ArticleListDTO queryArticlesByCategory(Long categoryId, PageParam page); - - /** - * 根据查询条件查询文章列表,支持翻页 - * - * @param key - * @param page - * @return - */ - ArticleListDTO queryArticlesBySearchKey(String key, PageParam page); - - /** - * 查询用户的文章列表 - * - * @param userId - * @param pageParam - * @param select - * @return - */ - ArticleListDTO queryArticlesByUserAndType(Long userId, PageParam pageParam, HomeSelectEnum select); - - /** - * 查询作者的文章数 - * - * @param authorId - * @return - */ - int queryArticleCount(long authorId); -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/ArticleWriteService.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/ArticleWriteService.java deleted file mode 100644 index 3076950d6..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/ArticleWriteService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.github.liuyueyi.forum.service.article.service; - -import com.github.liueyueyi.forum.api.model.enums.PushStatusEnum; -import com.github.liueyueyi.forum.api.model.vo.article.ArticlePostReq; - -public interface ArticleWriteService { - - /** - * 保存or更新文章 - * - * @param req - * @param author 作者 - * @return - */ - Long saveArticle(ArticlePostReq req, Long author); - - /** - * 删除文章 - * - * @param articleId - */ - void deleteArticle(Long articleId); - - /** - * 上线/下线文章 - * - * @param articleId - * @param pushStatusEnum - */ - void operateArticle(Long articleId, PushStatusEnum pushStatusEnum); -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/CategoryService.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/CategoryService.java deleted file mode 100644 index 9a26e4723..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/CategoryService.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.github.liuyueyi.forum.service.article.service; - -import com.github.liueyueyi.forum.api.model.vo.article.dto.CategoryDTO; - -import java.util.List; - -/** - * 标签Service - * - * @author louzai - * @date 2022-07-20 - */ -public interface CategoryService { - /** - * 查询类目名 - * - * @param categoryId - * @return - */ - String queryCategoryName(Long categoryId); - - - /** - * 查询所有的分离 - * - * @return - */ - List loadAllCategories(); -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/TagService.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/TagService.java deleted file mode 100644 index c4d14bda6..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/TagService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.liuyueyi.forum.service.article.service; - -import com.github.liueyueyi.forum.api.model.vo.article.dto.TagDTO; - -import java.util.List; - -/** - * 标签Service - * - * @author louzai - * @date 2022-07-20 - */ -public interface TagService { - /** - * 根据类目ID查询标签列表 - * - * @param categoryId - * @return - */ - List queryTagsByCategoryId(Long categoryId); -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/impl/ArticleReadServiceImpl.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/impl/ArticleReadServiceImpl.java deleted file mode 100644 index 165e6ff0e..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/impl/ArticleReadServiceImpl.java +++ /dev/null @@ -1,188 +0,0 @@ -package com.github.liuyueyi.forum.service.article.service.impl; - -import com.github.liueyueyi.forum.api.model.enums.*; -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.article.dto.ArticleDTO; -import com.github.liueyueyi.forum.api.model.vo.article.dto.ArticleListDTO; -import com.github.liueyueyi.forum.api.model.vo.article.dto.CategoryDTO; -import com.github.liuyueyi.forum.service.article.conveter.ArticleConverter; -import com.github.liuyueyi.forum.service.article.repository.dao.ArticleDao; -import com.github.liuyueyi.forum.service.article.repository.dao.ArticleTagDao; -import com.github.liuyueyi.forum.service.article.repository.entity.ArticleDO; -import com.github.liuyueyi.forum.service.article.service.ArticleReadService; -import com.github.liuyueyi.forum.service.article.service.CategoryService; -import com.github.liuyueyi.forum.service.user.repository.entity.UserFootDO; -import com.github.liuyueyi.forum.service.user.service.CountService; -import com.github.liuyueyi.forum.service.user.service.UserFootService; -import com.github.liuyueyi.forum.service.user.service.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; - -import java.util.*; -import java.util.stream.Collectors; - -/** - * 文章查询相关服务类 - * - * @author louzai - * @date 2022-07-20 - */ -@Service -public class ArticleReadServiceImpl implements ArticleReadService { - - @Autowired - private ArticleDao articleDao; - - @Autowired - private ArticleTagDao articleTagDao; - - @Autowired - private CategoryService categoryService; - - /** - * 在一个项目中,UserFootService 就是内部服务调用 - * 拆微服务时,这个会作为远程服务访问 - */ - @Autowired - private UserFootService userFootService; - - @Autowired - private CountService countService; - - @Autowired - private UserService userService; - - @Override - public ArticleDO queryBasicArticle(Long articleId) { - return articleDao.getById(articleId); - } - - @Override - public ArticleDTO queryDetailArticleInfo(Long articleId) { - ArticleDTO article = articleDao.queryArticleDetail(articleId); - if (article == null) { - throw new IllegalArgumentException("文章不存在"); - } - // 更新分类相关信息 - CategoryDTO category = article.getCategory(); - category.setCategory(categoryService.queryCategoryName(category.getCategoryId())); - - // 更新标签信息 - article.setTags(articleTagDao.queryArticleTagDetails(articleId)); - return article; - } - - /** - * 查询文章所有的关联信息,正文,分类,标签,阅读计数,当前登录用户是否点赞、评论过 - * - * @param articleId - * @param readUser - * @return - */ - @Override - public ArticleDTO queryTotalArticleInfo(Long articleId, Long readUser) { - ArticleDTO article = queryDetailArticleInfo(articleId); - - // 文章阅读计数+1 - articleDao.incrReadCount(articleId); - - // 文章的操作标记 - if (readUser != null) { - // 更新用于足迹,并判断是否点赞、评论、收藏 - UserFootDO foot = userFootService.saveOrUpdateUserFoot(DocumentTypeEnum.ARTICLE, articleId, article.getAuthor(), readUser, OperateTypeEnum.READ); - article.setPraised(Objects.equals(foot.getPraiseStat(), PraiseStatEnum.PRAISE.getCode())); - article.setCommented(Objects.equals(foot.getCommentStat(), CommentStatEnum.COMMENT.getCode())); - article.setCollected(Objects.equals(foot.getCollectionStat(), CollectionStatEnum.COLLECTION.getCode())); - } else { - // 未登录,全部设置为未处理 - article.setPraised(false); - article.setCommented(false); - article.setCollected(false); - } - - // 更新文章统计计数 - article.setCount(countService.queryArticleCountInfoByArticleId(articleId)); - return article; - } - - - @Override - public ArticleListDTO queryArticlesByCategory(Long categoryId, PageParam page) { - List records = articleDao.listArticlesByCategoryId(categoryId, page); - return buildArticleListVo(records, page.getPageSize()); - } - - @Override - public ArticleListDTO queryArticlesBySearchKey(String key, PageParam page) { - List records = articleDao.listArticlesByBySearchKey(key, page); - return buildArticleListVo(records, page.getPageSize()); - } - - @Override - public ArticleListDTO queryArticlesByUserAndType(Long userId, PageParam pageParam, HomeSelectEnum select) { - List records = null; - if (select == HomeSelectEnum.ARTICLE) { - // 用户的文章列表 - records = articleDao.listArticlesByUserId(userId, pageParam); - } else if (select == HomeSelectEnum.READ) { - // 用户的阅读记录 - List articleIds = userFootService.queryUserReadArticleList(userId, pageParam); - records = CollectionUtils.isEmpty(articleIds) ? Collections.emptyList() : articleDao.listByIds(articleIds); - records = sortByIds(articleIds, records); - } else if (select == HomeSelectEnum.COLLECTION) { - // 用户的收藏列表 - List articleIds = userFootService.queryUserCollectionArticleList(userId, pageParam); - records = CollectionUtils.isEmpty(articleIds) ? Collections.emptyList() : articleDao.listByIds(articleIds); - records = sortByIds(articleIds, records); - } - - if (CollectionUtils.isEmpty(records)) { - return new ArticleListDTO(); - } - return buildArticleListVo(records, pageParam.getPageSize()); - } - - private List sortByIds(List articleIds, List records) { - List articleDOS = new ArrayList<>(); - Map articleDOMap = records.stream().collect(Collectors.toMap(ArticleDO::getId, t -> t)); - articleIds.forEach(articleId -> { - if (articleDOMap.containsKey(articleId)) { - articleDOS.add(articleDOMap.get(articleId)); - } - }); - return articleDOS; - } - - private ArticleListDTO buildArticleListVo(List records, long pageSize) { - List result = records.stream().map(this::fillArticleRelatedInfo).collect(Collectors.toList()); - ArticleListDTO dto = new ArticleListDTO(); - dto.setArticleList(result); - dto.setIsMore(result.size() == pageSize); - return dto; - } - - /** - * 补全文章的阅读计数、作者、分类、标签等信息 - * - * @param record - * @return - */ - private ArticleDTO fillArticleRelatedInfo(ArticleDO record) { - ArticleDTO dto = ArticleConverter.toDto(record); - // 分类信息 - dto.getCategory().setCategory(categoryService.queryCategoryName(record.getCategoryId())); - // 标签列表 - dto.setTags(articleTagDao.queryArticleTagDetails(record.getId())); - // 阅读计数统计 - dto.setCount(countService.queryArticleCountInfoByArticleId(record.getId())); - // 作者信息 - dto.setAuthorName(userService.queryBasicUserInfo(dto.getAuthor()).getUserName()); - return dto; - } - - @Override - public int queryArticleCount(long authorId) { - return articleDao.countArticleByUser(authorId); - } -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/impl/ArticleWriteServiceImpl.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/impl/ArticleWriteServiceImpl.java deleted file mode 100644 index 6b7b64397..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/impl/ArticleWriteServiceImpl.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.github.liuyueyi.forum.service.article.service.impl; - -import com.github.liueyueyi.forum.api.model.enums.DocumentTypeEnum; -import com.github.liueyueyi.forum.api.model.enums.OperateTypeEnum; -import com.github.liueyueyi.forum.api.model.enums.PushStatusEnum; -import com.github.liueyueyi.forum.api.model.enums.YesOrNoEnum; -import com.github.liueyueyi.forum.api.model.vo.article.ArticlePostReq; -import com.github.liuyueyi.forum.core.util.NumUtil; -import com.github.liuyueyi.forum.service.article.conveter.ArticleConverter; -import com.github.liuyueyi.forum.service.article.repository.dao.ArticleDao; -import com.github.liuyueyi.forum.service.article.repository.dao.ArticleTagDao; -import com.github.liuyueyi.forum.service.article.repository.entity.ArticleDO; -import com.github.liuyueyi.forum.service.article.service.ArticleWriteService; -import com.github.liuyueyi.forum.service.user.service.UserFootService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Set; - -/** - * 文章操作相关服务类 - * - * @author louzai - * @date 2022-07-20 - */ -@Service -public class ArticleWriteServiceImpl implements ArticleWriteService { - - private final ArticleDao articleDao; - - private final ArticleTagDao articleTagDao; - - @Autowired - private UserFootService userFootService; - - public ArticleWriteServiceImpl(ArticleDao articleDao, ArticleTagDao articleTagDao) { - this.articleDao = articleDao; - this.articleTagDao = articleTagDao; - } - - /** - * 保存文章,当articleId存在时,表示更新记录; 不存在时,表示插入 - * - * @param req - * @return - */ - @Transactional(rollbackFor = Exception.class) - @Override - public Long saveArticle(ArticlePostReq req, Long author) { - ArticleDO article = ArticleConverter.toArticleDo(req, author); - if (NumUtil.nullOrZero(req.getArticleId())) { - return insertArticle(article, req.getContent(), req.getTagIds()); - } else { - return updateArticle(article, req.getContent(), req.getTagIds()); - } - } - - /** - * 新建文章 - * - * @param article - * @param content - * @param tags - * @return - */ - private Long insertArticle(ArticleDO article, String content, Set tags) { - // article + article_detail + tag 三张表的数据变更 - articleDao.save(article); - Long articleId = article.getId(); - - articleDao.saveArticleContent(articleId, content); - - articleTagDao.batchSave(articleId, tags); - - // 发布文章,阅读计数+1 - userFootService.saveOrUpdateUserFoot(DocumentTypeEnum.ARTICLE, articleId, article.getUserId(), article.getUserId(), OperateTypeEnum.READ); - return articleId; - } - - /** - * 更新文章 - * - * @param article - * @param content - * @param tags - * @return - */ - private Long updateArticle(ArticleDO article, String content, Set tags) { - // 更新文章 - articleDao.updateById(article); - - // 更新内容 - articleDao.updateArticleContent(article.getId(), content); - - // 标签更新 - articleTagDao.updateTags(article.getId(), tags); - return article.getId(); - } - - - /** - * 删除文章 - * - * @param articleId - */ - @Override - public void deleteArticle(Long articleId) { - ArticleDO dto = articleDao.getById(articleId); - if (dto != null && dto.getDeleted() != YesOrNoEnum.YES.getCode()) { - dto.setDeleted(YesOrNoEnum.YES.getCode()); - articleDao.updateById(dto); - } - } - - /** - * 文章上下线 - * - * @param articleId - * @param pushStatusEnum - */ - @Override - public void operateArticle(Long articleId, PushStatusEnum pushStatusEnum) { - ArticleDO dto = articleDao.getById(articleId); - if (dto != null && dto.getStatus() != pushStatusEnum.getCode()) { - dto.setStatus(pushStatusEnum.getCode()); - articleDao.updateById(dto); - } - } -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/impl/CategoryServiceImpl.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/impl/CategoryServiceImpl.java deleted file mode 100644 index 9dfd035c6..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/impl/CategoryServiceImpl.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.github.liuyueyi.forum.service.article.service.impl; - -import com.github.liueyueyi.forum.api.model.enums.YesOrNoEnum; -import com.github.liueyueyi.forum.api.model.vo.article.dto.CategoryDTO; -import com.github.liuyueyi.forum.service.article.conveter.ArticleConverter; -import com.github.liuyueyi.forum.service.article.repository.dao.CategoryDao; -import com.github.liuyueyi.forum.service.article.repository.entity.CategoryDO; -import com.github.liuyueyi.forum.service.article.service.CategoryService; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import org.jetbrains.annotations.NotNull; -import org.springframework.stereotype.Service; - -import javax.annotation.PostConstruct; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; - -/** - * 类目Service - * - * @author louzai - * @date 2022-07-20 - */ -@Service -public class CategoryServiceImpl implements CategoryService { - /** - * 分类数一般不会特别多,如编程领域可以预期的分类将不会超过30,所以可以做一个全量的内存缓存 - * todo 后续可改为Guava -> Redis - */ - private LoadingCache categoryCaches; - - private CategoryDao categoryDao; - - public CategoryServiceImpl(CategoryDao categoryDao) { - this.categoryDao = categoryDao; - } - - @PostConstruct - public void init() { - categoryCaches = CacheBuilder.newBuilder().maximumSize(300).build(new CacheLoader() { - @Override - public CategoryDTO load(@NotNull Long categoryId) throws Exception { - CategoryDO category = categoryDao.getById(categoryId); - if (category == null || category.getDeleted() == YesOrNoEnum.YES.getCode()) { - return CategoryDTO.EMPTY; - } - return new CategoryDTO(categoryId, category.getCategoryName()); - } - }); - // 预热全量缓存 - refreshCache(); - } - - /** - * 查询类目名 - * - * @param categoryId - * @return - */ - @Override - public String queryCategoryName(Long categoryId) { - return categoryCaches.getUnchecked(categoryId).getCategory(); - } - - /** - * 查询所有的分类 - * - * @return - */ - public List loadAllCategories() { - List list = new ArrayList<>(categoryCaches.asMap().values()); - list.sort(Comparator.comparingLong(CategoryDTO::getCategoryId)); - return list; - } - - /** - * 刷新缓存 - */ - private void refreshCache() { - List list = categoryDao.listAllCategoriesFromDb(); - categoryCaches.invalidateAll(); - categoryCaches.cleanUp(); - list.forEach(s -> categoryCaches.put(s.getId(), ArticleConverter.toDto(s))); - } -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/impl/TagServiceImpl.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/impl/TagServiceImpl.java deleted file mode 100644 index f3a96a9dc..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/service/impl/TagServiceImpl.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.github.liuyueyi.forum.service.article.service.impl; - -import com.github.liueyueyi.forum.api.model.vo.article.dto.TagDTO; -import com.github.liuyueyi.forum.service.article.repository.dao.TagDao; -import com.github.liuyueyi.forum.service.article.service.TagService; -import org.springframework.stereotype.Service; - -import java.util.List; - -/** - * 标签Service - * - * @author louzai - * @date 2022-07-20 - */ -@Service -public class TagServiceImpl implements TagService { - private final TagDao tagDao; - - public TagServiceImpl(TagDao tagDao) { - this.tagDao = tagDao; - } - - @Override - public List queryTagsByCategoryId(Long categoryId) { - return tagDao.listTagsByCategoryId(categoryId); - } -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/package-info.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/package-info.java deleted file mode 100644 index 0cbd61dfa..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * 评论相关服务包 - * - * @author YiHui - * @date 2022/7/6 - */ -package com.github.liuyueyi.forum.service.comment; \ No newline at end of file diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/repository/dao/CommentDao.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/repository/dao/CommentDao.java deleted file mode 100644 index 31749c364..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/repository/dao/CommentDao.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.github.liuyueyi.forum.service.comment.repository.dao; - -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.github.liueyueyi.forum.api.model.enums.YesOrNoEnum; -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liuyueyi.forum.service.comment.repository.entity.CommentDO; -import com.github.liuyueyi.forum.service.comment.repository.mapper.CommentMapper; -import org.springframework.stereotype.Repository; - -import java.util.Collection; -import java.util.List; - -/** - * @author YiHui - * @date 2022/9/2 - */ -@Repository -public class CommentDao extends ServiceImpl { - - /** - * 获取评论列表 - * - * @param pageParam - * @return - */ - public List listTopCommentList(Long articleId, PageParam pageParam) { - return lambdaQuery() - .eq(CommentDO::getTopCommentId, 0) - .eq(CommentDO::getArticleId, articleId) - .eq(CommentDO::getDeleted, YesOrNoEnum.NO.getCode()) - .last(PageParam.getLimitSql(pageParam)) - .orderByDesc(CommentDO::getId).list(); - } - - /** - * 查询所有的子评论 - * - * @param articleId - * @return - */ - public List listSubCommentIdMappers(Long articleId, Collection topCommentIds) { - return lambdaQuery() - .in(CommentDO::getTopCommentId, topCommentIds) - .eq(CommentDO::getArticleId, articleId) - .eq(CommentDO::getDeleted, YesOrNoEnum.NO.getCode()).list(); - } - - - /** - * 查询有效评论数 - * - * @param articleId - * @return - */ - public int commentCount(Long articleId) { - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.lambda() - .eq(CommentDO::getArticleId, articleId) - .eq(CommentDO::getDeleted, YesOrNoEnum.NO.getCode()); - return baseMapper.selectCount(queryWrapper).intValue(); - } - -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/repository/mapper/CommentMapper.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/repository/mapper/CommentMapper.java deleted file mode 100644 index 93e20ed96..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/repository/mapper/CommentMapper.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.liuyueyi.forum.service.comment.repository.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.github.liuyueyi.forum.service.comment.repository.entity.CommentDO; - -/** - * 评论mapper接口 - * - * @author louzai - * @date 2022-07-18 - */ -public interface CommentMapper extends BaseMapper { -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/service/CommentReadService.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/service/CommentReadService.java deleted file mode 100644 index 80ca9e90e..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/service/CommentReadService.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.liuyueyi.forum.service.comment.service; - -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.TopCommentDTO; - -import java.util.List; - -/** - * 评论Service接口 - * - * @author louzai - * @date 2022-07-24 - */ -public interface CommentReadService { - - /** - * 查询文章评论列表 - * - * @param articleId - * @param page - * @return - */ - List getArticleComments(Long articleId, PageParam page); - - /** - * 文章的有效评论数 - * - * @param articleId - * @return - */ - int queryCommentCount(Long articleId); -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/service/CommentWriteService.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/service/CommentWriteService.java deleted file mode 100644 index 3f8d2108f..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/service/CommentWriteService.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.github.liuyueyi.forum.service.comment.service; - -import com.github.liueyueyi.forum.api.model.vo.comment.CommentSaveReq; - -/** - * 评论Service接口 - * - * @author louzai - * @date 2022-07-24 - */ -public interface CommentWriteService { - - /** - * 更新/保存评论 - * - * @param commentSaveReq - * @return - */ - Long saveComment(CommentSaveReq commentSaveReq); - - /** - * 删除评论 - * - * @param commentId - * @throws Exception - */ - void deleteComment(Long commentId); - -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/service/impl/CommentReadServiceImpl.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/service/impl/CommentReadServiceImpl.java deleted file mode 100644 index a39b9e5ad..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/service/impl/CommentReadServiceImpl.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.github.liuyueyi.forum.service.comment.service.impl; - -import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.BaseCommentDTO; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.SubCommentDTO; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.TopCommentDTO; -import com.github.liueyueyi.forum.api.model.vo.user.dto.BaseUserInfoDTO; -import com.github.liuyueyi.forum.service.comment.converter.CommentConverter; -import com.github.liuyueyi.forum.service.comment.repository.dao.CommentDao; -import com.github.liuyueyi.forum.service.comment.repository.entity.CommentDO; -import com.github.liuyueyi.forum.service.comment.service.CommentReadService; -import com.github.liuyueyi.forum.service.user.service.CountService; -import com.github.liuyueyi.forum.service.user.service.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.*; -import java.util.stream.Collectors; - -/** - * 评论Service - * - * @author louzai - * @date 2022-07-24 - */ -@Service -public class CommentReadServiceImpl implements CommentReadService { - - @Autowired - private CommentDao commentDao; - - @Autowired - private UserService userService; - - @Autowired - private CountService countService; - - @Override - public List getArticleComments(Long articleId, PageParam page) { - // 1.查询一级评论 - List comments = commentDao.listTopCommentList(articleId, page); - if (CollectionUtils.isEmpty(comments)) { - return Collections.emptyList(); - } - // map 存 commentId -> 评论 - Map topComments = comments.stream().collect(Collectors.toMap(CommentDO::getId, CommentConverter::toTopDto)); - - // 2.查询非一级评论 - List subComments = commentDao.listSubCommentIdMappers(articleId, topComments.keySet()); - - // 3.构建一级评论的子评论 - buildCommentRelation(subComments, topComments); - - // 4.挑出需要返回的数据,排序,并补齐对应的用户信息,最后排序返回 - List result = new ArrayList<>(); - comments.forEach(comment -> { - TopCommentDTO dto = topComments.get(comment.getId()); - fillCommentInfo(dto); - dto.getChildComments().forEach(this::fillCommentInfo); - Collections.sort(dto.getChildComments()); - result.add(dto); - }); - - // 返回结果根据时间进行排序 - Collections.sort(result); - return result; - } - - /** - * 构建父子评论关系 - */ - private void buildCommentRelation(List subComments, Map topComments) { - Map subCommentMap = subComments.stream().collect(Collectors.toMap(CommentDO::getId, CommentConverter::toSubDto)); - subComments.forEach(comment -> { - TopCommentDTO top = topComments.get(comment.getTopCommentId()); - if (top == null) { - return; - } - SubCommentDTO sub = subCommentMap.get(comment.getId()); - top.getChildComments().add(sub); - if (Objects.equals(comment.getTopCommentId(), comment.getParentCommentId())) { - return; - } - - SubCommentDTO parent = subCommentMap.get(comment.getParentCommentId()); - sub.setParentContent(parent == null ? "~~已删除~~" : parent.getCommentContent()); - }); - } - - /** - * 填充评论对应的信息,如用户信息,点赞数等 - * - * @param comment - */ - private void fillCommentInfo(BaseCommentDTO comment) { - BaseUserInfoDTO userInfoDO = userService.queryBasicUserInfo(comment.getUserId()); - if (userInfoDO == null) { - // 如果用户注销,给一个默认的用户 - comment.setUserName("默认用户"); - comment.setUserPhoto(""); - if (comment instanceof TopCommentDTO) { - ((TopCommentDTO) comment).setCommentCount(0); - } - } else { - comment.setUserName(userInfoDO.getUserName()); - comment.setUserPhoto(userInfoDO.getPhoto()); - if (comment instanceof TopCommentDTO) { - ((TopCommentDTO) comment).setCommentCount(((TopCommentDTO) comment).getChildComments().size()); - } - } - - // 查询点赞数 - Long praiseCount = countService.queryCommentPraiseCount(comment.getCommentId()); - comment.setPraiseCount(praiseCount.intValue()); - } - - @Override - public int queryCommentCount(Long articleId) { - return commentDao.commentCount(articleId); - } -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/service/impl/CommentWriteServiceImpl.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/service/impl/CommentWriteServiceImpl.java deleted file mode 100644 index c0e48600b..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/service/impl/CommentWriteServiceImpl.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.github.liuyueyi.forum.service.comment.service.impl; - -import com.github.liueyueyi.forum.api.model.enums.YesOrNoEnum; -import com.github.liueyueyi.forum.api.model.exception.ExceptionUtil; -import com.github.liueyueyi.forum.api.model.vo.article.dto.ArticleDTO; -import com.github.liueyueyi.forum.api.model.vo.comment.CommentSaveReq; -import com.github.liueyueyi.forum.api.model.vo.constants.StatusEnum; -import com.github.liuyueyi.forum.core.util.NumUtil; -import com.github.liuyueyi.forum.service.article.repository.dao.ArticleDao; -import com.github.liuyueyi.forum.service.article.repository.entity.ArticleDO; -import com.github.liuyueyi.forum.service.comment.converter.CommentConverter; -import com.github.liuyueyi.forum.service.comment.repository.dao.CommentDao; -import com.github.liuyueyi.forum.service.comment.repository.entity.CommentDO; -import com.github.liuyueyi.forum.service.comment.service.CommentWriteService; -import com.github.liuyueyi.forum.service.user.service.UserFootService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Date; - -/** - * 评论Service - * - * @author louzai - * @date 2022-07-24 - */ -@Service -public class CommentWriteServiceImpl implements CommentWriteService { - - @Autowired - private CommentDao commentDao; - - @Autowired - private ArticleDao articleDao; - - @Autowired - private UserFootService userFootWriteService; - - @Override - @Transactional(rollbackFor = Exception.class) - public Long saveComment(CommentSaveReq commentSaveReq) { - // 保存评论 - CommentDO comment; - if (NumUtil.nullOrZero(commentSaveReq.getCommentId())) { - comment = addComment(commentSaveReq); - } else { - comment = updateComment(commentSaveReq); - } - return comment.getId(); - } - - private CommentDO addComment(CommentSaveReq commentSaveReq) { - // 0.校验父评论是否存在 - getParentCommentUser(commentSaveReq.getParentCommentId()); - - // 1. 保存评论内容 - CommentDO commentDO = CommentConverter.toDo(commentSaveReq); - Date now = new Date(); - commentDO.setCreateTime(now); - commentDO.setUpdateTime(now); - commentDao.save(commentDO); - - // 2. 获取文章信息 - ArticleDTO articleDTO = articleDao.queryArticleDetail(commentSaveReq.getArticleId()); - if (articleDTO == null) { - throw ExceptionUtil.of(StatusEnum.RECORDS_NOT_EXISTS, "文章=" + commentSaveReq.getArticleId()); - } - - // 2. 保存足迹信息 : 文章的已评信息 + 评论的已评信息 - userFootWriteService.saveCommentFoot(commentDO, articleDTO); - return commentDO; - } - - private CommentDO updateComment(CommentSaveReq commentSaveReq) { - // 更新评论 - CommentDO commentDO = commentDao.getById(commentSaveReq.getCommentId()); - if (commentDO == null) { - throw new RuntimeException("未查询到该评论"); - } - commentDO.setContent(commentSaveReq.getCommentContent()); - commentDao.updateById(commentDO); - return commentDO; - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void deleteComment(Long commentId) { - CommentDO commentDO = commentDao.getById(commentId); - if (commentDO == null) { - throw ExceptionUtil.of(StatusEnum.RECORDS_NOT_EXISTS, "评论ID=" + commentId); - } - commentDO.setDeleted(YesOrNoEnum.YES.getCode()); - commentDao.updateById(commentDO); - - // 获取文章信息 - ArticleDTO articleDTO = articleDao.queryArticleDetail(commentDO.getArticleId()); - if (articleDTO == null) { - throw ExceptionUtil.of(StatusEnum.RECORDS_NOT_EXISTS, "文章=" + commentDO.getArticleId()); - } - - userFootWriteService.removeCommentFoot(commentDO, articleDTO); - } - - - private Long getParentCommentUser(Long parentCommentId) { - if (NumUtil.nullOrZero(parentCommentId)) { - return null; - - } - CommentDO parent = commentDao.getById(parentCommentId); - if (parent == null) { - throw ExceptionUtil.of(StatusEnum.RECORDS_NOT_EXISTS, "父评论=" + parentCommentId); - } - return parent.getUserId(); - } - -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/converter/UserConverter.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/converter/UserConverter.java deleted file mode 100644 index 8444ed7d1..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/converter/UserConverter.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.github.liuyueyi.forum.service.user.converter; - -import com.github.liueyueyi.forum.api.model.context.ReqInfoContext; -import com.github.liueyueyi.forum.api.model.enums.FollowStateEnum; -import com.github.liueyueyi.forum.api.model.vo.user.UserInfoSaveReq; -import com.github.liueyueyi.forum.api.model.vo.user.UserRelationReq; -import com.github.liueyueyi.forum.api.model.vo.user.UserSaveReq; -import com.github.liueyueyi.forum.api.model.vo.user.dto.BaseUserInfoDTO; -import com.github.liueyueyi.forum.api.model.vo.user.dto.UserStatisticInfoDTO; -import com.github.liuyueyi.forum.service.user.repository.entity.UserDO; -import com.github.liuyueyi.forum.service.user.repository.entity.UserInfoDO; -import com.github.liuyueyi.forum.service.user.repository.entity.UserRelationDO; -import org.springframework.beans.BeanUtils; - -/** - * 用户转换 - * - * @author louzai - * @date 2022-07-20 - */ -public class UserConverter { - - public static UserDO toDO(UserSaveReq req) { - if (req == null) { - return null; - } - UserDO userDO = new UserDO(); - userDO.setId(req.getUserId()); - userDO.setThirdAccountId(req.getThirdAccountId()); - userDO.setLoginType(req.getLoginType()); - return userDO; - } - - public static UserInfoDO toDO(UserInfoSaveReq req) { - if (req == null) { - return null; - } - UserInfoDO userInfoDO = new UserInfoDO(); - userInfoDO.setUserId(req.getUserId()); - userInfoDO.setUserName(req.getUserName()); - userInfoDO.setPhoto(req.getPhoto()); - userInfoDO.setPosition(req.getPosition()); - userInfoDO.setCompany(req.getCompany()); - userInfoDO.setProfile(req.getProfile()); - return userInfoDO; - } - - public static BaseUserInfoDTO toDTO(UserInfoDO info) { - if (info == null) { - return null; - } - BaseUserInfoDTO user = new BaseUserInfoDTO(); - // todo 知识点,bean属性拷贝的几种方式, 直接get/set方式,使用BeanUtil工具类(spring, cglib, apache, objectMapper),序列化方式等 - BeanUtils.copyProperties(info, user); - return user; - } - - public static UserRelationDO toDO(UserRelationReq req) { - if (req == null) { - return null; - } - UserRelationDO userRelationDO = new UserRelationDO(); - userRelationDO.setUserId(req.getUserId()); - userRelationDO.setFollowUserId(ReqInfoContext.getReqInfo().getUserId()); - userRelationDO.setFollowState(req.getFollowed() ? FollowStateEnum.FOLLOW.getCode() : FollowStateEnum.CANCEL_FOLLOW.getCode()); - return userRelationDO; - } - - public static UserStatisticInfoDTO toUserHomeDTO(BaseUserInfoDTO baseUserInfoDTO) { - if (baseUserInfoDTO == null) { - return null; - } - UserStatisticInfoDTO userHomeDTO = new UserStatisticInfoDTO(); - userHomeDTO.setUserId(baseUserInfoDTO.getUserId()); - userHomeDTO.setUserName(baseUserInfoDTO.getUserName()); - userHomeDTO.setRole(baseUserInfoDTO.getRole()); - userHomeDTO.setPhoto(baseUserInfoDTO.getPhoto()); - userHomeDTO.setProfile(baseUserInfoDTO.getProfile()); - userHomeDTO.setPosition(baseUserInfoDTO.getPosition()); - userHomeDTO.setCompany(baseUserInfoDTO.getCompany()); - userHomeDTO.setExtend(baseUserInfoDTO.getExtend()); - userHomeDTO.setDeleted(baseUserInfoDTO.getDeleted()); - userHomeDTO.setId(baseUserInfoDTO.getId()); - userHomeDTO.setCreateTime(baseUserInfoDTO.getCreateTime()); - userHomeDTO.setUpdateTime(baseUserInfoDTO.getUpdateTime()); - return userHomeDTO; - } -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/package-info.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/package-info.java deleted file mode 100644 index 604916fa6..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * 用户相关包 - * - * @author YiHui - * @date 2022/7/6 - */ -package com.github.liuyueyi.forum.service.user; \ No newline at end of file diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/dao/UserDao.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/dao/UserDao.java deleted file mode 100644 index dab437f2f..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/dao/UserDao.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.github.liuyueyi.forum.service.user.repository.dao; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.github.liueyueyi.forum.api.model.enums.YesOrNoEnum; -import com.github.liuyueyi.forum.service.user.repository.entity.UserDO; -import com.github.liuyueyi.forum.service.user.repository.entity.UserInfoDO; -import com.github.liuyueyi.forum.service.user.repository.mapper.UserInfoMapper; -import com.github.liuyueyi.forum.service.user.repository.mapper.UserMapper; -import org.springframework.stereotype.Repository; - -import javax.annotation.Resource; - -/** - * @author YiHui - * @date 2022/9/2 - */ -@Repository -public class UserDao extends ServiceImpl { - @Resource - private UserMapper userMapper; - - public UserDO getByThirdAccountId(String accountId) { - return userMapper.getByThirdAccountId(accountId); - } - - public void saveUser(UserDO user) { - userMapper.insert(user); - } - - public UserInfoDO getByUserId(Long userId) { - LambdaQueryWrapper query = Wrappers.lambdaQuery(); - query.eq(UserInfoDO::getUserId, userId) - .eq(UserInfoDO::getDeleted, YesOrNoEnum.NO.getCode()); - return baseMapper.selectOne(query); - } -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/dao/UserFootDao.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/dao/UserFootDao.java deleted file mode 100644 index f4ee6d29c..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/dao/UserFootDao.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.github.liuyueyi.forum.service.user.repository.dao; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.github.liueyueyi.forum.api.model.enums.DocumentTypeEnum; -import com.github.liueyueyi.forum.api.model.enums.PraiseStatEnum; -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.user.dto.ArticleFootCountDTO; -import com.github.liuyueyi.forum.service.user.repository.entity.UserFootDO; -import com.github.liuyueyi.forum.service.user.repository.mapper.UserFootMapper; -import org.springframework.stereotype.Repository; - -import java.util.List; - -/** - * @author YiHui - * @date 2022/9/2 - */ -@Repository -public class UserFootDao extends ServiceImpl { - public UserFootDO getByDocumentAndUserId(Long documentId, Integer type, Long userId) { - LambdaQueryWrapper query = Wrappers.lambdaQuery(); - query.eq(UserFootDO::getDocumentId, documentId) - .eq(UserFootDO::getDocumentType, type) - .eq(UserFootDO::getUserId, userId); - return baseMapper.selectOne(query); - } - - /** - * 查询用户收藏的文章列表 - * - * @param userId - * @param pageParam - * @return - */ - public List listCollectedArticlesByUserId(Long userId, PageParam pageParam) { - return baseMapper.listCollectedArticlesByUserId(userId, pageParam); - } - - - /** - * 查询用户阅读的文章列表 - * - * @param userId - * @param pageParam - * @return - */ - public List listReadArticleByUserId(Long userId, PageParam pageParam) { - return baseMapper.listReadArticleByUserId(userId, pageParam); - } - - /** - * 查询文章计数信息 - * - * @param articleId - * @return - */ - public ArticleFootCountDTO countArticleByArticleId(Long articleId) { - return baseMapper.countArticleByArticleId(articleId); - } - - /** - * 查询作者的文章统计 - * - * @param author - * @return - */ - public ArticleFootCountDTO countArticleByUserId(Long author) { - return baseMapper.countArticleByUserId(author); - } - - /** - * 查询评论的点赞数 - * - * @param commentId - * @return - */ - public Long countCommentPraise(Long commentId) { - return lambdaQuery() - .eq(UserFootDO::getDocumentId, commentId) - .eq(UserFootDO::getDocumentType, DocumentTypeEnum.COMMENT.getCode()) - .eq(UserFootDO::getPraiseStat, PraiseStatEnum.PRAISE.getCode()) - .count(); - } -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/dao/UserRelationDao.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/dao/UserRelationDao.java deleted file mode 100644 index cab5d4236..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/dao/UserRelationDao.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.github.liuyueyi.forum.service.user.repository.dao; - -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.github.liueyueyi.forum.api.model.enums.FollowStateEnum; -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.UserFollowDTO; -import com.github.liuyueyi.forum.service.user.repository.entity.UserRelationDO; -import com.github.liuyueyi.forum.service.user.repository.mapper.UserRelationMapper; -import org.springframework.stereotype.Repository; - -import java.util.List; - -/** - * 用户相关DB操作 - * - * @author louzai - * @date 2022-07-18 - */ -@Repository -public class UserRelationDao extends ServiceImpl { - - - public List queryUserFollowList(Long followUserId, PageParam pageParam) { - return baseMapper.queryUserFollowList(followUserId, pageParam); - } - - public List queryUserFansList(Long userId, PageParam pageParam) { - return baseMapper.queryUserFansList(userId, pageParam); - } - - public Long queryUserFollowCount(Long userId) { - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.lambda() - .eq(UserRelationDO::getFollowUserId, userId) - .eq(UserRelationDO::getFollowState, FollowStateEnum.FOLLOW.getCode()); - return baseMapper.selectCount(queryWrapper); - } - - public Long queryUserFansCount(Long userId) { - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.lambda() - .eq(UserRelationDO::getUserId, userId) - .eq(UserRelationDO::getFollowState, FollowStateEnum.FOLLOW.getCode()); - return baseMapper.selectCount(queryWrapper); - } - - /** - * 获取关注信息 - * - * @param userId 登录用户 - * @param followUserId 关注的用户 - * @return - */ - public UserRelationDO getUserRelationByUserId(Long userId, Long followUserId) { - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.lambda() - .eq(UserRelationDO::getUserId, userId) - .eq(UserRelationDO::getFollowUserId, followUserId) - .eq(UserRelationDO::getFollowState, FollowStateEnum.FOLLOW.getCode()); - return baseMapper.selectOne(queryWrapper); - } - - /** - * 获取关注记录 - * - * @param userId 登录用户 - * @param followUserId 关注的用户 - * @return - */ - public UserRelationDO getUserRelationRecord(Long userId, Long followUserId) { - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.lambda() - .eq(UserRelationDO::getUserId, userId) - .eq(UserRelationDO::getFollowUserId, followUserId); - return baseMapper.selectOne(queryWrapper); - } -} \ No newline at end of file diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/entity/UserDO.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/entity/UserDO.java deleted file mode 100644 index 34a8201cd..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/entity/UserDO.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.github.liuyueyi.forum.service.user.repository.entity; - -import com.baomidou.mybatisplus.annotation.TableName; -import com.github.liueyueyi.forum.api.model.entity.BaseDO; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * 用户登录表 - * - * @author louzai - * @date 2022-07-18 - */ -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("user") -public class UserDO extends BaseDO { - - private static final long serialVersionUID = 1L; - - /** - * 第三方用户ID - */ - private String thirdAccountId; - - /** - * 登录方式: 0-微信登录,1-账号密码登录 - */ - private Integer loginType; - - /** - * 删除标记 - */ - private Integer deleted; -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/entity/UserInfoDO.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/entity/UserInfoDO.java deleted file mode 100644 index ebfd841a6..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/entity/UserInfoDO.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.github.liuyueyi.forum.service.user.repository.entity; - -import com.baomidou.mybatisplus.annotation.TableName; -import com.github.liueyueyi.forum.api.model.entity.BaseDO; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * 用户个人信息表 - * - * @author louzai - * @date 2022-07-18 - */ -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("user_info") -public class UserInfoDO extends BaseDO { - - private static final long serialVersionUID = 1L; - - /** - * 用户ID - */ - private Long userId; - - /** - * 用户名 - */ - private String userName; - - /** - * 用户图像 - */ - private String photo; - - /** - * 职位 - */ - private String position; - - /** - * 公司 - */ - private String company; - - /** - * 个人简介 - */ - private String profile; - - /** - * 扩展字段 - */ - private String extend; - - /** - * 删除标记 - */ - private Integer deleted; -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/mapper/UserFootMapper.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/mapper/UserFootMapper.java deleted file mode 100644 index 71585008e..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/mapper/UserFootMapper.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.github.liuyueyi.forum.service.user.repository.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.user.dto.ArticleFootCountDTO; -import com.github.liuyueyi.forum.service.article.repository.entity.ArticleDO; -import com.github.liuyueyi.forum.service.user.repository.entity.UserFootDO; -import org.apache.ibatis.annotations.Param; - -import java.util.List; - -/** - * 用户足迹mapper接口 - * - * @author louzai - * @date 2022-07-18 - */ -public interface UserFootMapper extends BaseMapper { - - /** - * 查询文章计数信息 - * - * @param articleId - * @return - */ - ArticleFootCountDTO countArticleByArticleId(@Param("articleId") Long articleId); - - /** - * 查询作者的文章统计 - * - * @param author - * @return - */ - ArticleFootCountDTO countArticleByUserId(@Param("userId") Long author); - - /** - * 查询用户收藏的文章列表 - * - * @param userId - * @param pageParam - * @return - */ - List listCollectedArticlesByUserId(@Param("userId") Long userId, @Param("pageParam") PageParam pageParam); - - - /** - * 查询用户阅读的文章列表 - * - * @param userId - * @param pageParam - * @return - */ - List listReadArticleByUserId(@Param("userId") Long userId, @Param("pageParam") PageParam pageParam); -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/mapper/UserInfoMapper.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/mapper/UserInfoMapper.java deleted file mode 100644 index 4864d5849..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/mapper/UserInfoMapper.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.liuyueyi.forum.service.user.repository.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.github.liuyueyi.forum.service.user.repository.entity.UserInfoDO; - -/** - * 用户个人信息mapper接口 - * - * @author louzai - * @date 2022-07-18 - */ -public interface UserInfoMapper extends BaseMapper { -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/mapper/UserMapper.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/mapper/UserMapper.java deleted file mode 100644 index 72b1db5c5..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/mapper/UserMapper.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.liuyueyi.forum.service.user.repository.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.github.liuyueyi.forum.service.user.repository.entity.UserDO; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; - -/** - * 用户登录mapper接口 - * - * @author louzai - * @date 2022-07-18 - */ -public interface UserMapper extends BaseMapper { - /** - * 根据三方唯一id进行查询 - * - * @param accountId - * @return - */ - @Select("select * from user where third_account_id = #{account_id} limit 1") - UserDO getByThirdAccountId(@Param("account_id") String accountId); -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/mapper/UserRelationMapper.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/mapper/UserRelationMapper.java deleted file mode 100644 index 4ce918b20..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/mapper/UserRelationMapper.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.liuyueyi.forum.service.user.repository.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.UserFollowDTO; -import com.github.liuyueyi.forum.service.user.repository.entity.UserRelationDO; -import org.apache.ibatis.annotations.Param; - -import java.util.List; - -/** - * 用户关系mapper接口 - * - * @author louzai - * @date 2022-07-18 - */ -public interface UserRelationMapper extends BaseMapper { - - /** - * 我关注的用户 - * @param followUserId - * @param pageParam - * @return - */ - List queryUserFollowList(@Param("followUserId") Long followUserId, @Param("pageParam") PageParam pageParam); - - /** - * 关注我的粉丝 - * @param userId - * @param pageParam - * @return - */ - List queryUserFansList(@Param("userId") Long userId, @Param("pageParam") PageParam pageParam); -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/CountService.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/CountService.java deleted file mode 100644 index 4b9eff236..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/CountService.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.github.liuyueyi.forum.service.user.service; - -import com.github.liueyueyi.forum.api.model.vo.user.dto.ArticleFootCountDTO; - -/** - * 计数统计相关 - * - * @author YiHui - * @date 2022/9/2 - */ -public interface CountService { - /** - * 根据文章ID查询文章计数 - * - * @param articleId - * @return - */ - ArticleFootCountDTO queryArticleCountInfoByArticleId(Long articleId); - - - /** - * 查询做的总阅读相关计数(当前返回评论数) - * - * @param userId - * @return - */ - ArticleFootCountDTO queryArticleCountInfoByUserId(Long userId); - - /** - * 获取评论点赞数量 - * - * @param commentId - * @return - */ - Long queryCommentPraiseCount(Long commentId); -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/LoginService.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/LoginService.java deleted file mode 100644 index 700e6bddd..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/LoginService.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.github.liuyueyi.forum.service.user.service; - -import com.github.liueyueyi.forum.api.model.vo.user.dto.BaseUserInfoDTO; -import com.google.common.collect.Sets; - -import java.util.Set; - -/** - * @author YiHui - * @date 2022/8/15 - */ -public interface LoginService { - String SESSION_KEY = "f-session"; - Set LOGIN_CODE_KEY = Sets.newHashSet("登录", "login"); - - - /** - * 获取登录验证码 - * - * @param uuid - * @return - */ - String getVerifyCode(String uuid); - - /** - * 登录 - * - * @param code - * @return - */ - String login(String code); - - /** - * 登出 - * - * @param session - */ - void logout(String session); - - - /** - * 获取登录的用户信息 - * - * @param session - * @return - */ - BaseUserInfoDTO getUserBySessionId(String session); -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/UserFootService.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/UserFootService.java deleted file mode 100644 index ad373343b..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/UserFootService.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.github.liuyueyi.forum.service.user.service; - - -import com.github.liueyueyi.forum.api.model.enums.DocumentTypeEnum; -import com.github.liueyueyi.forum.api.model.enums.OperateTypeEnum; -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.article.dto.ArticleDTO; -import com.github.liuyueyi.forum.service.comment.repository.entity.CommentDO; -import com.github.liuyueyi.forum.service.user.repository.entity.UserFootDO; - -import java.util.List; - -/** - * 用户足迹Service接口 - * - * @author louzai - * @date 2022-07-20 - */ -public interface UserFootService { - /** - * 保存或更新状态信息 - * - * @param documentType 文档类型:博文 + 评论 - * @param documentId 文档id - * @param authorId 作者 - * @param userId 操作人 - * @param operateTypeEnum 操作类型:点赞,评论,收藏等 - * @return - */ - UserFootDO saveOrUpdateUserFoot(DocumentTypeEnum documentType, Long documentId, Long authorId, Long userId, OperateTypeEnum operateTypeEnum); - - /** - * 保存评论足迹 - * 1. 用户文章记录上,设置为已评论 - * 2. 若改评论为回复别人的评论,则针对父评论设置为已评论 - * - * @param comment 保存评论入参 - * @param article 文章信息 - */ - void saveCommentFoot(CommentDO comment, ArticleDTO article); - - /** - * 删除评论足迹 - * - * @param comment 保存评论入参 - * @param article 文章信息 - */ - void removeCommentFoot(CommentDO comment, ArticleDTO article); - - - /** - * 查询已读文章列表 - * - * @param userId - * @param pageParam - * @return - */ - List queryUserReadArticleList(Long userId, PageParam pageParam); - - /** - * 查询收藏文章列表 - * - * @param userId - * @param pageParam - * @return - */ - List queryUserCollectionArticleList(Long userId, PageParam pageParam); -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/UserRelationService.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/UserRelationService.java deleted file mode 100644 index a76dca3eb..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/UserRelationService.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.github.liuyueyi.forum.service.user.service; - -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.UserFollowListDTO; -import com.github.liueyueyi.forum.api.model.vo.user.UserRelationReq; - -/** - * 用户关系Service接口 - * - * @author louzai - * @date 2022-07-20 - */ -public interface UserRelationService { - - /** - * 我关注的用户 - * - * @param userId - * @param pageParam - * @return - */ - UserFollowListDTO getUserFollowList(Long userId, PageParam pageParam); - - - /** - * 关注我的粉丝 - * - * @param userId - * @param pageParam - * @return - */ - UserFollowListDTO getUserFansList(Long userId, PageParam pageParam); - - - /** - * 保存用户关系 - * - * @param req - * @throws Exception - */ - void saveUserRelation(UserRelationReq req); -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/UserService.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/UserService.java deleted file mode 100644 index d09e48d4c..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/UserService.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.github.liuyueyi.forum.service.user.service; - -import com.github.liueyueyi.forum.api.model.vo.user.UserInfoSaveReq; -import com.github.liueyueyi.forum.api.model.vo.user.UserSaveReq; -import com.github.liueyueyi.forum.api.model.vo.user.dto.BaseUserInfoDTO; -import com.github.liueyueyi.forum.api.model.vo.user.dto.UserStatisticInfoDTO; - -/** - * 用户Service接口 - * - * @author louzai - * @date 2022-07-20 - */ -public interface UserService { - - /** - * 用户存在时,直接返回;不存在时,则初始化 - * - * @param req - */ - void registerOrGetUserInfo(UserSaveReq req); - - /** - * 保存用户详情 - * - * @param req - */ - void saveUserInfo(UserInfoSaveReq req); - - /** - * 查询用户基本信息 - * todo: 可以做缓存优化 - * - * @param userId - * @return - */ - BaseUserInfoDTO queryBasicUserInfo(Long userId); - - - /** - * 查询用户主页信息 - * - * @param userId - * @return - * @throws Exception - */ - UserStatisticInfoDTO queryUserInfoWithStatistic(Long userId); -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/count/CountServiceImpl.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/count/CountServiceImpl.java deleted file mode 100644 index 7c174d53d..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/count/CountServiceImpl.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.github.liuyueyi.forum.service.user.service.count; - -import com.github.liueyueyi.forum.api.model.vo.user.dto.ArticleFootCountDTO; -import com.github.liuyueyi.forum.service.comment.service.CommentReadService; -import com.github.liuyueyi.forum.service.user.repository.dao.UserFootDao; -import com.github.liuyueyi.forum.service.user.service.CountService; -import org.springframework.stereotype.Service; - -import javax.annotation.Resource; - -/** - * 计数服务,后续计数相关的可以考虑基于redis来做 - * - * @author YiHui - * @date 2022/9/2 - */ -@Service -public class CountServiceImpl implements CountService { - - private final UserFootDao userFootDao; - - @Resource - private CommentReadService commentReadService; - - public CountServiceImpl(UserFootDao userFootDao) { - this.userFootDao = userFootDao; - } - - @Override - public ArticleFootCountDTO queryArticleCountInfoByArticleId(Long articleId) { - ArticleFootCountDTO res = userFootDao.countArticleByArticleId(articleId); - if (res == null) { - res = new ArticleFootCountDTO(); - } else { - res.setCommentCount(commentReadService.queryCommentCount(articleId)); - } - return res; - } - - - @Override - public ArticleFootCountDTO queryArticleCountInfoByUserId(Long userId) { - return userFootDao.countArticleByUserId(userId); - } - - /** - * 查询评论的点赞数 - * - * @param commentId - * @return - */ - @Override - public Long queryCommentPraiseCount(Long commentId) { - return userFootDao.countCommentPraise(commentId); - } -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/relation/UserRelationServiceImpl.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/relation/UserRelationServiceImpl.java deleted file mode 100644 index 861013b44..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/relation/UserRelationServiceImpl.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.github.liuyueyi.forum.service.user.service.relation; - -import com.github.liueyueyi.forum.api.model.context.ReqInfoContext; -import com.github.liueyueyi.forum.api.model.enums.FollowStateEnum; -import com.github.liueyueyi.forum.api.model.exception.ExceptionUtil; -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.UserFollowDTO; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.UserFollowListDTO; -import com.github.liueyueyi.forum.api.model.vo.constants.StatusEnum; -import com.github.liueyueyi.forum.api.model.vo.user.UserRelationReq; -import com.github.liuyueyi.forum.core.util.NumUtil; -import com.github.liuyueyi.forum.service.user.converter.UserConverter; -import com.github.liuyueyi.forum.service.user.repository.dao.UserRelationDao; -import com.github.liuyueyi.forum.service.user.repository.entity.UserRelationDO; -import com.github.liuyueyi.forum.service.user.service.UserRelationService; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; - -import javax.annotation.Resource; -import java.util.List; - -/** - * 用户关系Service - * - * @author louzai - * @date 2022-07-20 - */ -@Service -public class UserRelationServiceImpl implements UserRelationService { - @Resource - private UserRelationDao userRelationDao; - - - @Override - public UserFollowListDTO getUserFollowList(Long userId, PageParam pageParam) { - List userRelationList = userRelationDao.queryUserFollowList(userId, pageParam); - return buildRes(userRelationList, pageParam); - } - - @Override - public UserFollowListDTO getUserFansList(Long userId, PageParam pageParam) { - List userRelationList = userRelationDao.queryUserFansList(userId, pageParam); - return buildRes(userRelationList, pageParam); - } - - private UserFollowListDTO buildRes(List records, PageParam param) { - if (CollectionUtils.isEmpty(records)) { - return UserFollowListDTO.emptyInstance(); - } - - UserFollowListDTO userFollowListDTO = new UserFollowListDTO(); - userFollowListDTO.setUserFollowList(records); - userFollowListDTO.setIsMore(records.size() == param.getPageSize()); - return userFollowListDTO; - } - - @Override - public void saveUserRelation(UserRelationReq req) { - // 查询是否存在 - UserRelationDO userRelationDO = userRelationDao.getUserRelationRecord(req.getUserId(), ReqInfoContext.getReqInfo().getUserId()); - if (userRelationDO == null) { - userRelationDao.save(UserConverter.toDO(req)); - return; - } - userRelationDO.setFollowState(req.getFollowed() ? FollowStateEnum.FOLLOW.getCode() : FollowStateEnum.CANCEL_FOLLOW.getCode()); - userRelationDao.updateById(userRelationDO); - } -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/user/UserServiceImpl.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/user/UserServiceImpl.java deleted file mode 100644 index 2e80c136a..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/user/UserServiceImpl.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.github.liuyueyi.forum.service.user.service.user; - -import com.github.liueyueyi.forum.api.model.context.ReqInfoContext; -import com.github.liueyueyi.forum.api.model.exception.ExceptionUtil; -import com.github.liueyueyi.forum.api.model.vo.constants.StatusEnum; -import com.github.liueyueyi.forum.api.model.vo.user.UserInfoSaveReq; -import com.github.liueyueyi.forum.api.model.vo.user.UserSaveReq; -import com.github.liueyueyi.forum.api.model.vo.user.dto.ArticleFootCountDTO; -import com.github.liueyueyi.forum.api.model.vo.user.dto.BaseUserInfoDTO; -import com.github.liueyueyi.forum.api.model.vo.user.dto.UserStatisticInfoDTO; -import com.github.liuyueyi.forum.service.article.service.ArticleReadService; -import com.github.liuyueyi.forum.service.user.converter.UserConverter; -import com.github.liuyueyi.forum.service.user.repository.dao.UserDao; -import com.github.liuyueyi.forum.service.user.repository.dao.UserRelationDao; -import com.github.liuyueyi.forum.service.user.repository.entity.UserDO; -import com.github.liuyueyi.forum.service.user.repository.entity.UserInfoDO; -import com.github.liuyueyi.forum.service.user.repository.entity.UserRelationDO; -import com.github.liuyueyi.forum.service.user.service.CountService; -import com.github.liuyueyi.forum.service.user.service.UserService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import javax.annotation.Resource; - -/** - * 用户Service - * - * @author louzai - * @date 2022-07-20 - */ -@Service -public class UserServiceImpl implements UserService { - - @Resource - private UserDao userDao; - - @Resource - private UserRelationDao userRelationDao; - - @Autowired - private ArticleReadService articleReadService; - - @Autowired - private CountService countService; - - - /** - * 用户存在时,直接返回;不存在时,则初始化 - * - * @param req - */ - @Override - @Transactional(rollbackFor = Exception.class) - public void registerOrGetUserInfo(UserSaveReq req) { - UserDO record = userDao.getByThirdAccountId(req.getThirdAccountId()); - if (record != null) { - // 用户存在,不需要注册 - req.setUserId(record.getId()); - return; - } - - // 用户不存在,则需要注册 - record = UserConverter.toDO(req); - userDao.saveUser(record); - req.setUserId(record.getId()); - - // 初始化用户信息 - UserInfoDO userInfo = new UserInfoDO(); - userInfo.setUserId(req.getUserId()); - userInfo.setUserName(String.format("小侠%06d", (int) (Math.random() * 1000000))); - userInfo.setPhoto(""); - userDao.save(userInfo); - } - - @Override - public void saveUserInfo(UserInfoSaveReq req) { - UserInfoDO userInfoDO = UserConverter.toDO(req); - userDao.updateById(userInfoDO); - } - - @Override - public BaseUserInfoDTO queryBasicUserInfo(Long userId) { - UserInfoDO user = userDao.getByUserId(userId); - if (user == null) { - throw ExceptionUtil.of(StatusEnum.USER_NOT_EXISTS, "userId=" + userId); - } - return UserConverter.toDTO(user); - } - - @Override - public UserStatisticInfoDTO queryUserInfoWithStatistic(Long userId) { - BaseUserInfoDTO userInfoDTO = queryBasicUserInfo(userId); - UserStatisticInfoDTO userHomeDTO = UserConverter.toUserHomeDTO(userInfoDTO); - userHomeDTO.setRole("normal"); - - // 获取文章相关统计 - ArticleFootCountDTO articleFootCountDTO = countService.queryArticleCountInfoByUserId(userId); - if (articleFootCountDTO != null) { - userHomeDTO.setPraiseCount(articleFootCountDTO.getPraiseCount()); - userHomeDTO.setReadCount(articleFootCountDTO.getReadCount()); - userHomeDTO.setCollectionCount(articleFootCountDTO.getCollectionCount()); - } else { - userHomeDTO.setPraiseCount(0); - userHomeDTO.setReadCount(0); - userHomeDTO.setCollectionCount(0); - } - - // 获取发布文章总数 - int articleCount = articleReadService.queryArticleCount(userId); - userHomeDTO.setArticleCount(articleCount); - - // 获取关注数 - Long followCount = userRelationDao.queryUserFollowCount(userId); - userHomeDTO.setFollowCount(followCount.intValue()); - - // 粉丝数 - Long fansCount = userRelationDao.queryUserFansCount(userId); - userHomeDTO.setFansCount(fansCount.intValue()); - - // 是否关注 - Long followUserId = ReqInfoContext.getReqInfo().getUserId(); - if (followUserId != null) { - UserRelationDO userRelationDO = userRelationDao.getUserRelationByUserId(userId, followUserId); - userHomeDTO.setFollowed((userRelationDO == null) ? Boolean.FALSE : Boolean.TRUE); - } else { - userHomeDTO.setFollowed(Boolean.FALSE); - } - return userHomeDTO; - } - -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/user/WxLoginServiceImpl.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/user/WxLoginServiceImpl.java deleted file mode 100644 index 6e7634228..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/user/WxLoginServiceImpl.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.github.liuyueyi.forum.service.user.service.user; - -import com.github.liueyueyi.forum.api.model.exception.NoVlaInGuavaException; -import com.github.liueyueyi.forum.api.model.vo.user.UserSaveReq; -import com.github.liueyueyi.forum.api.model.vo.user.dto.BaseUserInfoDTO; -import com.github.liuyueyi.forum.core.util.CodeGenerateUtil; -import com.github.liuyueyi.forum.service.user.service.LoginService; -import com.github.liuyueyi.forum.service.user.service.UserService; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import javax.annotation.PostConstruct; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -/** - * 基于微信公众号的登录方式 - * - * @author YiHui - * @date 2022/8/15 - */ -@Service -public class WxLoginServiceImpl implements LoginService { - /** - * key = userId, value = 验证码 - */ - private LoadingCache verifyCodeCache; - /** - * key = 验证码 value = userId - */ - private LoadingCache codeUserIdCache; - /** - * key = session, value = userId - */ - private LoadingCache sessionMap; - @Autowired - private UserService userService; - - /** - * todo 知识点:bean完成之后的初始化方式,除了 @PostConstruct 之外还有构造方法方式、实现BeanPostProcessor接口方式 - */ - @PostConstruct - public void init() { - // 五分钟内,最多只支持300个用户登录;注意当服务多台机器部署时,基于本地缓存会有问题;请改成redis/memcache缓存 - verifyCodeCache = CacheBuilder.newBuilder().maximumSize(300).expireAfterWrite(5, TimeUnit.MINUTES).build(new CacheLoader() { - @Override - public String load(Long userId) { - String code = CodeGenerateUtil.genCode(); - codeUserIdCache.put(code, userId); - return code; - } - }); - codeUserIdCache = CacheBuilder.newBuilder().maximumSize(300).expireAfterWrite(5, TimeUnit.MINUTES).build(new CacheLoader() { - @Override - public Long load(String s) throws Exception { - throw new NoVlaInGuavaException("not hit!"); - } - }); - - sessionMap = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.DAYS).build(new CacheLoader() { - @Override - public Long load(String userId) { - throw new NoVlaInGuavaException("not hit!"); - } - }); - } - - /** - * 获取验证码,注意一个验证码只使用一次;每次请求验证码时,重新生成一个 - * - * @param userId - * @return - */ - private String getVerifyCodeFromCache(Long userId) { - String code = verifyCodeCache.getUnchecked(userId); - verifyCodeCache.invalidate(userId); - return code; - } - - @Override - public String getVerifyCode(String uuid) { - UserSaveReq req = new UserSaveReq().setLoginType(0).setThirdAccountId(uuid); - userService.registerOrGetUserInfo(req); - return getVerifyCodeFromCache(req.getUserId()); - } - - @Override - public String login(String code) { - Long userId = codeUserIdCache.getIfPresent(code); - if (userId == null) { - return null; - } - - String session = "s-" + UUID.randomUUID(); - sessionMap.put(session, userId); - // 验证完之后,移除掉,避免重复使用 - codeUserIdCache.invalidate(code); - return session; - } - - @Override - public void logout(String session) { - sessionMap.invalidate(session); - sessionMap.cleanUp(); - } - - - @Override - public BaseUserInfoDTO getUserBySessionId(String session) { - if (StringUtils.isBlank(session)) { - return null; - } - - Long userId = sessionMap.getIfPresent(session); - return userId == null ? null : userService.queryBasicUserInfo(userId); - } -} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/userfoot/UserFootServiceImpl.java b/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/userfoot/UserFootServiceImpl.java deleted file mode 100644 index e70f484c0..000000000 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/service/userfoot/UserFootServiceImpl.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.github.liuyueyi.forum.service.user.service.userfoot; - -import com.github.liueyueyi.forum.api.model.enums.DocumentTypeEnum; -import com.github.liueyueyi.forum.api.model.enums.OperateTypeEnum; -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.article.dto.ArticleDTO; -import com.github.liuyueyi.forum.service.comment.repository.entity.CommentDO; -import com.github.liuyueyi.forum.service.user.repository.dao.UserFootDao; -import com.github.liuyueyi.forum.service.user.repository.entity.UserFootDO; -import com.github.liuyueyi.forum.service.user.service.UserFootService; -import org.springframework.stereotype.Service; - -import java.util.Date; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.Supplier; - -/** - * 用户足迹Service - * - * @author louzai - * @date 2022-07-20 - */ -@Service -public class UserFootServiceImpl implements UserFootService { - private final UserFootDao userFootDao; - - public UserFootServiceImpl(UserFootDao userFootDao) { - this.userFootDao = userFootDao; - } - - /** - * 保存或更新状态信息 - * - * @param documentType 文档类型:博文 + 评论 - * @param documentId 文档id - * @param authorId 作者 - * @param userId 操作人 - * @param operateTypeEnum 操作类型:点赞,评论,收藏等 - */ - @Override - public UserFootDO saveOrUpdateUserFoot(DocumentTypeEnum documentType, Long documentId, Long authorId, Long userId, OperateTypeEnum operateTypeEnum) { - // 查询是否有该足迹;有则更新,没有则插入 - UserFootDO readUserFootDO = userFootDao.getByDocumentAndUserId(documentId, documentType.getCode(), userId); - if (readUserFootDO == null) { - readUserFootDO = new UserFootDO(); - readUserFootDO.setUserId(userId); - readUserFootDO.setDocumentId(documentId); - readUserFootDO.setDocumentType(documentType.getCode()); - readUserFootDO.setDocumentUserId(authorId); - setUserFootStat(readUserFootDO, operateTypeEnum); - userFootDao.save(readUserFootDO); - } else if (setUserFootStat(readUserFootDO, operateTypeEnum)) { - readUserFootDO.setUpdateTime(new Date()); - userFootDao.updateById(readUserFootDO); - } - return readUserFootDO; - } - - - @Override - public void saveCommentFoot(CommentDO comment, ArticleDTO article) { - // 保存文章对应的评论足迹 - saveOrUpdateUserFoot(DocumentTypeEnum.ARTICLE, article.getArticleId(), article.getAuthor(), comment.getUserId(), OperateTypeEnum.COMMENT); - // 如果是子评论,则找到父评论的记录,然后设置为已评 - if (comment.getParentCommentId() != null &&comment.getParentCommentId() != 0) { - // 如果需要展示父评论的子评论数量,authorId 需要传父评论的 userId - saveOrUpdateUserFoot(DocumentTypeEnum.COMMENT, comment.getParentCommentId(), 0L, comment.getUserId(), OperateTypeEnum.COMMENT); - } - } - - @Override - public void removeCommentFoot(CommentDO comment, ArticleDTO article) { - saveOrUpdateUserFoot(DocumentTypeEnum.ARTICLE, article.getArticleId(), article.getAuthor(), comment.getUserId(), OperateTypeEnum.DELETE_COMMENT); - if (comment.getParentCommentId() != null) { - // 如果需要展示父评论的子评论数量,authorId 需要传父评论的 userId - saveOrUpdateUserFoot(DocumentTypeEnum.COMMENT, comment.getParentCommentId(), 0L, comment.getUserId(), OperateTypeEnum.DELETE_COMMENT); - } - } - - - private boolean setUserFootStat(UserFootDO userFootDO, OperateTypeEnum operate) { - switch (operate) { - case READ: - return true; - case PRAISE: - case CANCEL_PRAISE: - return compareAndUpdate(userFootDO::getPraiseStat, userFootDO::setPraiseStat, operate.getDbStatCode()); - case COLLECTION: - case CANCEL_COLLECTION: - return compareAndUpdate(userFootDO::getCollectionStat, userFootDO::setCollectionStat, operate.getDbStatCode()); - case COMMENT: - case DELETE_COMMENT: - return compareAndUpdate(userFootDO::getCommentStat, userFootDO::setCommentStat, operate.getDbStatCode()); - default: - return false; - } - } - - /** - * 相同则直接返回false不用更新;不同则更新,返回true - * - * @param supplier - * @param consumer - * @param input - * @param - * @return - */ - private boolean compareAndUpdate(Supplier supplier, Consumer consumer, T input) { - if (Objects.equals(supplier.get(), input)) { - return false; - } - consumer.accept(input); - return true; - } - - @Override - public List queryUserReadArticleList(Long userId, PageParam pageParam) { - return userFootDao.listReadArticleByUserId(userId, pageParam); - } - - @Override - public List queryUserCollectionArticleList(Long userId, PageParam pageParam) { - return userFootDao.listCollectedArticlesByUserId(userId, pageParam); - } -} diff --git a/forum-service/src/main/resources/META-INF/spring.factories b/forum-service/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 409ca19be..000000000 --- a/forum-service/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,3 +0,0 @@ -# Auto Configure -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -com.github.liuyueyi.forum.service.ServiceAutoConfig \ No newline at end of file diff --git a/forum-service/src/main/resources/mapper/ArticleTagMapper.xml b/forum-service/src/main/resources/mapper/ArticleTagMapper.xml deleted file mode 100644 index e60ea77e5..000000000 --- a/forum-service/src/main/resources/mapper/ArticleTagMapper.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/forum-service/src/main/resources/mapper/UserFootMapper.xml b/forum-service/src/main/resources/mapper/UserFootMapper.xml deleted file mode 100644 index e0e9b448d..000000000 --- a/forum-service/src/main/resources/mapper/UserFootMapper.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/forum-service/src/main/resources/mapper/UserRelationMapper.xml b/forum-service/src/main/resources/mapper/UserRelationMapper.xml deleted file mode 100644 index c3ea95db4..000000000 --- a/forum-service/src/main/resources/mapper/UserRelationMapper.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - limit #{pageParam.offset}, #{pageParam.limit} - - - - - - - - - diff --git a/forum-ui/README.md b/forum-ui/README.md deleted file mode 100644 index 6b9e4de5e..000000000 --- a/forum-ui/README.md +++ /dev/null @@ -1,4 +0,0 @@ -forum-ui -=== - -存储前段资源文件 \ No newline at end of file diff --git a/forum-ui/pom.xml b/forum-ui/pom.xml deleted file mode 100644 index d0461be5b..000000000 --- a/forum-ui/pom.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - quick-forum - com.github.liuyueyi.quick-forum - 0.0.1-SNAPSHOT - - 4.0.0 - - forum-ui - - - - org.springframework.boot - spring-boot-starter-thymeleaf - - - - \ No newline at end of file diff --git a/forum-ui/src/main/resources/static/css/content.css b/forum-ui/src/main/resources/static/css/content.css deleted file mode 100644 index b77c4e796..000000000 --- a/forum-ui/src/main/resources/static/css/content.css +++ /dev/null @@ -1,315 +0,0 @@ -.article-content img { - max-width: 100%; - max-height: 500px; - box-sizing: content-box; - background-color: #fff; - margin: 0 auto; -} - -.article-content h1, -.article-content h2, -.article-content h3, -.article-content h4, -.article-content h5 { - color: #333; - margin-bottom: 10px; - padding-bottom: 7px; -} - -.article-content h1 { - border-bottom: 1px solid #eaecef; - font-size: 1.7em; -} - -.article-content h2 { - font-size: 1.5em; -} - -.article-content h3 { - font-size: 1.3em; -} - -.article-content h4 { - font-size: 1.1em; -} - -.article-content h5 { - font-size: 1em; -} - -.article-content p, -.article-content ol, -.article-content ul, -.article-content table, -.article-content pre, -.article-content blockquote { - /* font-weight: 400; */ - line-height: 1.8; - margin-bottom: 15px; -} - -.article-content blockquote { - padding: 0 1em; - color: #6a737d; - border-left: .25em solid #dfe2e5; -} - -.article-content ol, -.article-content ul { - padding-left: 20px; -} - -.article-content table { - display: table; - border-collapse: separate; - border-spacing: 2px; - border-color: grey; - border-spacing: 0; - border-collapse: collapse; - font-size: 14px; -} - -.article-content table th, -.article-content table tr, -.article-content table td { - padding: 6px 13px; - border: 1px solid #dfe2e5; -} - -.article-content pre { - padding: 5px; - overflow: auto; - font-size: 85%; - line-height: 1.45; - background-color: #fafafa; - border-radius: 3px; - word-wrap: normal; -} - -.article-content pre div { - background-color: #fafafa; -} - -.article-content li { - /* font-weight: 400; */ - line-height: 1.4; - font-size: 15px; - margin-bottom: 5px; -} - -.article-content .hljs-center { - text-align: center; -} - -.article-content .hljs-left { - text-align: left; -} - -.article-content .hljs-right { - text-align: right; -} - -.article-suspended-panel { - position: fixed; - margin-left: -7rem; - top: 140px; - z-index: 2; -} - -.panel-btn { - position: relative; - margin-bottom: 1.667rem; - width: 3rem; - height: 3rem; - background-color: #fff; - background-position: 50%; - background-repeat: no-repeat; - border-radius: 50%; - box-shadow: 0 2px 4px 0 rgba(0,0,0,.04); - cursor: pointer; - text-align: center; - font-size: 1.67rem -} - -.panel-btn .sprite-icon { - color: #8a919f; - height: 100% -} - -.panel-btn:hover .sprite-icon { - color: #515767 -} - -.panel-btn:not(.share-btn).active .sprite-icon { - color: #1e80ff -} - -.panel-btn:not(.share-btn).active .sprite-icon.icon-collect { - color: #ffb800 -} - -.panel-btn:not(.share-btn).active .sprite-icon { - color: #1e80ff; -} - - -.panel-btn:not(.share-btn).active.with-badge:after { - background-color: #1e80ff -} - -.panel-btn.with-badge:after { - content: attr(badge); - position: absolute; - top: 0; - left: 75%; - height: 17px; - line-height: 17px; - padding: 0 5px; - border-radius: 9px; - font-size: 11px; - text-align: center; - white-space: nowrap; - background-color: #c2c8d1; - color: #fff -} - -.panel-btn.share-btn:after { - display: block; - content: " "; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 50% -} - -.panel-btn.share-btn:hover .share-popup { - display: flex -} - -.panel-btn.share-btn .share-popup { - display: none; - position: absolute; - top: 0; - flex-direction: column; - left: calc(100% + 14px); - z-index: 30; - background: #fff; - border-radius: 4px; - padding: 9px 0; - width: -webkit-max-content; - width: -moz-max-content; - width: max-content; - box-shadow: 0 8px 24px rgba(81,87,103,.16) -} - -.panel-btn.share-btn .share-popup:after { - position: absolute; - width: 0; - height: 0; - content: " "; - right: 100%; - top: 14px; - border: 12px solid transparent; - border-right-color: #fff -} - -.panel-btn.share-btn .share-popup .share-item { - display: flex; - align-items: center; - height: 44px; - padding: 0 15px -} - -.panel-btn.share-btn .share-popup .share-item:hover { - background-color: #f2f3f5 -} - -.panel-btn.share-btn .share-popup .share-item:hover.wechat .wechat-qrcode { - display: flex -} - -.panel-btn.share-btn .share-popup .share-item:hover .share-icon { - color: #515767 -} - -.panel-btn.share-btn .share-popup .share-item .share-item-title { - margin-left: 8px; - font-size: 14px; - color: #515767 -} - -.panel-btn.share-btn .share-popup .share-item .share-icon { - color: #8a919f; - width: 20px; - height: 20px; - font-size: 1.67rem -} - -.share-title { - margin: 2.5rem 0 1rem; - font-size: 1rem; - text-align: center; - color: #c6c6c6; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none -} - -.collect-popover { - width: 200px; - box-sizing: border-box; - background-color: #fff; - padding: 12px 20px; - border-radius: 4px; - font-size: 13px; - color: #8a919f; - line-height: 22px; - text-align: left; - position: absolute; - left: 4rem; - top: -10px; - margin-left: 15px; - box-shadow: 0 8px 24px rgba(81,87,103,.26) -} - -.collect-popover:after { - content: ""; - display: block; - width: 0; - height: 0; - border-top: 12px solid #fff; - border-left: 12px solid #fff; - transform: rotate(45deg); - position: absolute; - top: 30px; - left: -6px -} - -.collect-popover-title { - color: #252933; - font-weight: 500; - font-size: 14px; - margin-bottom: 4px -} - -.collect-popover-content { - display: flex; - flex-direction: row -} - -.collect-popover-button { - color: #1e80ff; - font-weight: 500; - cursor: pointer; - margin-right: 4px -} - -.sprite-icon { - width: 1em; - height: 1em; - fill: currentColor; - vertical-align: middle; - transition: all .15s linear -} \ No newline at end of file diff --git a/forum-ui/src/main/resources/static/css/global.css b/forum-ui/src/main/resources/static/css/global.css deleted file mode 100644 index 2ea7af9ee..000000000 --- a/forum-ui/src/main/resources/static/css/global.css +++ /dev/null @@ -1,521 +0,0 @@ -body { - -webkit-font-smoothing: antialiased; -} - -.custom-empty { - text-align: center; - margin-top: 20px; - width: 100%; - font-size: 1rem; -} - -html, -.custom-bg-color { - background-color: #EEEEEE; -} - -.posts-comment-input-box, -.posts-author-box, -.posts-box, -.page-box, -.user-info-box { - background-color: #fff; -} - -.btn-outline-primary:hover, -.page-item.active .page-link, -.current-page { - color: #fff !important; -} - -.bottom-line, -.list-group, -.editor-title { - border-bottom: 1px solid rgba(0, 0, 0, .125); -} - -.faq-solution-box, -.posts-comment-input-box { - background-color: #fafbfc; -} - -.posts-comment-input-box { - margin-top: -20px; -} - -.posts-comment-box, -.posts-author-box, -.posts-box, -.editor-form-box, -.editor-title, -.card-body, -.card-header { - padding: 20px; -} - -.type-box { - padding: 10px; -} - -.tag-box, -.no-comment-box, -.posts-author-box, -.posts-box, -.card, -.user-info-box, -.page-box, -.carousel { - margin-bottom: 0px; -} - -.custom-theme-bg-color, -.btn-outline-primary:hover, -.page-item.active .page-link, -.btn-primary, -.btn-primary:active, -.btn-primary:focus, -.btn-primary:hover, -.current-page { - background-color: #007fff !important; -} - -.btn-outline-primary:hover, -.page-item.active .page-link, -.btn-primary, -.btn-primary:active, -.btn-primary:focus, -.btn-primary:hover, -.btn-outline-primary { - border-color: #007fff; -} - -.page-link, -.page-link:hover, -.btn-outline-primary, -.posts-admin-tag-official, -a:hover, -.custom-font-color { - color: #007fff; -} - -a { - color: #212529; -} - -.dropdown-menu, -.card { - border: 0; -} - -.input-group-text, -.navbar-toggler, -.modal-content, -.card { - margin-bottom: 10px; -} -.form-control, -.btn, -.dropdown-menu, -.list-group-item:first-child, -.list-group-item:last-child, -.pagination { - border-radius: 0; -} - -/* */ -html { - padding-top: 82px; -} - -.posts-list-desc, -a { - color: rgba(0, 0, 0, .87); -} - -.custom-by-both { - padding-left: 10px; - padding-right: 10px; -} - -.carousel-inner img { - width: 100%; - height: 100%; -} - -.foot { - height: 70px; -} - -.foot-link { - list-style: none; - padding: 25px 0; - width: 80%; - margin: 0 auto; - text-align: left; - font-size: 0; - border-top: 1px solid rgba(0, 0, 0, .1); -} - -.foot li { - font-size: 14px; - padding: 0 10px; - display: inline-block; - vertical-align: middle; - line-height: 1em; -} - -.foot li:last-child { - border-left: none; - float: right; - padding-right: 0; -} - -.posts-list-desc { - display: inline; - max-height: 48px; - text-overflow: -o-ellipsis-lastline; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; -} - -.posts-list-title { - font-size: 18px; - font-weight: 700; - line-height: 1.5; - margin-bottom: 5px; - color: rgba(0, 0, 0, .85); -} - -.posts-list-payload-item, -.posts-list-payload-item a, -.posts-list-payload-box-author { - color: #6c757d !important; -} - -.posts-list-payload-item { - padding: 3px 8px; -} - -.page-box { - padding: 10px; -} - -.faq-solution-box { - margin-top: 10px; - padding: 15px; -} - -.faq-solution-box, -.posts-list-desc { - font-size: 13px; - line-height: 24px; -} - -.posts-admin-tag { - margin-top: 4px; - height: 16px; - padding: 2px; - border-radius: 2px; - line-height: 1; - font-size: 12px; - margin-right: 6px; - vertical-align: middle; - -webkit-transform: translateY(1px); - -ms-transform: translateY(1px); - transform: translateY(1px); -} - -.posts-admin-tag-official { - background: rgba(101, 212, 117, 0.1); -} - -.posts-admin-tag-top { - color: #f85959; - background: rgba(248, 89, 89, 0.1); -} - -.posts-admin-tag-marrow { - color: #3c8cff; - background: rgba(60, 140, 255, 0.1); -} - -.selected-domain { - border-bottom: 0 solid #3973ff; -} - -.selected-domain a { - color: #3973ff; -} - -.user-info-box { - height: 200px; - width: 100%; - margin-left: 0; - margin-right: 0; -} - -.user-info-date-box { - height: 80px; - padding-top: 40px; - padding-left: 40px; -} - -.user-info-date-box > p { - display: inline-block; -} - -.user-info-desc-box { - margin-top: 15px; - padding-left: 40px; - padding-right: 40px; -} - -/* 覆盖框架默认样式 */ -.navbar { - padding: .5rem 4rem; -} - -.container-bg-light { - background: #ffffff; -} - -.input-icon { - position: relative; -} - -.input-icon input { - border-radius: 26px; -} - -.input-icon-addon { - position: absolute; - top: 0; - bottom: 0; - left: 0; - display: flex; - align-items: center; - justify-content: center; - min-width: 2.5rem; - color: rgb(179 174 174 / 60%); - pointer-events: none; - font-size: 1.2em; -} - -.input-icon .form-control:not(:first-child), .input-icon .form-select:not(:last-child) { - padding-left: 2.5rem; -} - - -.list-group-item { - border: none; - padding: 0.75rem 1.25rem; -} - -.btn { - padding-left: 25px; - padding-right: 25px; -} - -.btn-sm { - padding-left: 15px; - padding-right: 15px; -} - -.page-item:first-child .page-link, -.page-item:last-child .page-link { - border-radius: 0; -} - -.comment-avatar-box { - width: 40px; - float: left; -} - -.posts-comment-input-box-btn { - width: 100%; - display: none; -} - -.posts-comment-input-box-textarea { - padding: 4px 10px; - font-size: 13px; - line-height: 1.7; -} - -.posts-comment-input-box-textarea, -.comment-content-box { - width: calc(100% - 40px); - float: right; -} - -.best-answer { - margin-left: 20px; -} - -.best-answer:hover, -.reply-comment:hover { - cursor: pointer; -} - -.comment-content-box-title { - font-size: 16px; - color: #3d464d; - font-weight: 300; -} - -.comment-content-box-content { - color: #505050; - font-size: 14px; - margin: 12px 0; -} - -.comment-content-box-foot { - color: #b2b2b2; - font-size: 14px; -} - -.navbar-count-msg-box { - position: relative; -} - -.navbar-count-msg { - position: absolute; - top: 8px; - right: 0; - width: 10px; - height: 10px; - border-radius: 50%; - background-color: #f85959; -} - -.nav-item { - clear: both; - margin: 0; - padding: 5px 10px; - color: rgba(0, 0, 0, .65); - font-weight: 600; - font-size: 1.1em; - line-height: 22px; - white-space: nowrap; - cursor: pointer; - transition: all .3s; -} - -.navbar-light .navbar-nav .nav-link { - color: rgba(0, 0, 0, .85); -} - -.message-block { - margin-right: 10px; -} - -.third-oauth-login-box::before { - content: '三方账号登录'; - position: absolute; - left: 50%; - bottom: 55px; - font-size: 10px; - transform: translateX(-50%); - -webkit-transform: translate(-50%, -50%); - padding: 0 10px; - background-color: #fff; -} - -.third-oauth-login-box { - display: block; - text-align: center; -} - -/* 设备适配样式 */ -@media (max-width: 768px) { - html { - padding-top: 68px; - } - - .navbar { - padding: .5rem 1rem; - } - - .foot-link { - padding: 10px 0; - } - - .foot li { - display: block; - padding: 10px 0 0 0; - } - - .foot li:last-child { - float: none; - } - - .tag-box, - .no-comment-box, - .posts-author-box, - .posts-box, - .card { - margin-bottom: 10px; - } - .carousel - .page-box, - .user-info-box { - margin-bottom: 10px; - } - - .user-info-date-box { - display: none; - } - - .user-info-desc-box { - padding-left: 10px; - padding-right: 0; - } - - .best-answer { - margin-left: 10px; - } - - .posts-comment-box, - .posts-box, - .editor-form-box, - .card-body, - .card-header, - .list-group-item, - .faq-solution-box, - .editor-title { - padding: 10px; - } - - .type-box { - padding: 0; - } - - .posts-comment-input-box { - margin-top: -10px; - } - - .user-edit-btn { - display: none; - } - - .message-block { - margin-right: 5px; - } -} -.parent-wrapper { - display: flex; - background: #f2f3f5; - border: 1px solid #e4e6eb; - box-sizing: border-box; - border-radius: 4px; - padding: 0 12px; - line-height: 36px; - height: 36px; - font-size: 14px; - color: #8a919f; - margin-top: 8px; -} \ No newline at end of file diff --git a/forum-ui/src/main/resources/static/js/biz/forum.js b/forum-ui/src/main/resources/static/js/biz/forum.js deleted file mode 100644 index f188236ab..000000000 --- a/forum-ui/src/main/resources/static/js/biz/forum.js +++ /dev/null @@ -1,44 +0,0 @@ -const post = function (path, data, callback) { - $.ajax({ - method: 'POST', - url: path, - contentType: 'application/json', - data: JSON.stringify(data), - success: function (data) { - console.log("data", data); - if (!data || !data.status || data.status.code != 0) { - toastr.error(data.message); - } else if (callback) { - callback(data.result); - } - }, - error: function (data) { - toastr.error(data); - } - }); -}; - -const loadScript = function (url, callback) { - const secScript = document.createElement("script"); - if (secScript.readyState) { // IE - secScript.onreadystatechange = function () { - if (secScript.readyState === 'loaded' || secScript.readyState === 'complete') { - secScript.onreadystatechange = null; - callback(); - } - } - } else { // 其他浏览器 - secScript.onload = function () { - callback(); - } - } - secScript.setAttribute("type", "text/javascript"); - secScript.setAttribute("src", url); - document.body.insertBefore(secScript, document.body.lastChild); -}; - -const loadLink = function (url) { - let headHTML = document.getElementsByTagName('head')[0].innerHTML; - headHTML += ''; - document.getElementsByTagName('head')[0].innerHTML = headHTML; -}; diff --git a/forum-ui/src/main/resources/static/js/biz/login.js b/forum-ui/src/main/resources/static/js/biz/login.js deleted file mode 100644 index a77c2e278..000000000 --- a/forum-ui/src/main/resources/static/js/biz/login.js +++ /dev/null @@ -1,37 +0,0 @@ -$('#logoutBtn').click(function () { - $.ajax({ - url: "/logout", - dataType: "json", - type: "get", - success: function (data) { - toastr.success("已退出登录") - window.location.href = "/"; - } - }) -}) - -$('#loginBtn').click(function () { - const code = $('#loginCode').val(); - console.log("开始登录:" + code); - $.ajax({ - url: "/login?code=" + code, //请求的url地址 - dataType: "json", //返回格式为json - async: false,//请求是否异步,默认为异步,这也是ajax重要特性 - type: "GET", //请求方式 - success: function (data) { - //请求成功时处理 - console.log("response data:", data); - if (!data || !data.status || data.status.code !== 0) { - toastr.error(data.status.msg); - } else { - // 登录成功,刷新 - window.location.reload(); - toastr.success("登录成功"); - } - }, - error: function () { - //请求出错处理 - toastr.error("登录错误"); - } - }); -}); \ No newline at end of file diff --git a/forum-ui/src/main/resources/static/js/biz/toolaction.js b/forum-ui/src/main/resources/static/js/biz/toolaction.js deleted file mode 100644 index 2ddee21ed..000000000 --- a/forum-ui/src/main/resources/static/js/biz/toolaction.js +++ /dev/null @@ -1,41 +0,0 @@ -// 文章点赞 -const praiseArticle = function (articleId, action, callback) { - // 2 点赞, 4 取消点赞 - const type = action ? 2 : 4; - $.get('/article/api/favor?articleId=' + articleId + "&type=" + type, function (data) { - console.log("response:", data); - if (!data || !data.status || data.status.code !== 0) { - toastr.error(data.message); - } else if (callback) { - callback(data.result); - } - }); -} - -// 评论点赞 -const praiseComment = function (commentId, action, callback) { - // 2 点赞, 4 取消点赞 - const type = action ? 2 : 4; - $.get('/comment/api/favor?commentId=' + commentId + "&type=" + type, function (data) { - console.log("response:", data); - if (!data || !data.status || data.status.code !== 0) { - toastr.error(data.message); - } else if (callback) { - callback(data.result); - } - }); -} - -// 文章收藏 -const collectArticle = function (articleId, action, callback) { - // 3 收藏, 5 取消收藏 - const type = action ? 3 : 5; - $.get('/article/api/favor?articleId=' + articleId + "&type=" + type, function (data) { - console.log("response:", data); - if (!data || !data.status || data.status.code !== 0) { - toastr.error(data.message); - } else if (callback) { - callback(data.result); - } - }); -} diff --git a/forum-ui/src/main/resources/static/js/jquery.min.js b/forum-ui/src/main/resources/static/js/jquery.min.js deleted file mode 100644 index 644d35e27..000000000 --- a/forum-ui/src/main/resources/static/js/jquery.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */ -!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S), -a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), -null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" - - - - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/biz/article/edit.html b/forum-ui/src/main/resources/templates/biz/article/edit.html deleted file mode 100644 index a7b2f8b10..000000000 --- a/forum-ui/src/main/resources/templates/biz/article/edit.html +++ /dev/null @@ -1,183 +0,0 @@ - - -

- QuickFrom社区 - 文章发表 -
- - - -
- - - -
- - -
- - - - -
-
- -
- - - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/biz/article/search.html b/forum-ui/src/main/resources/templates/biz/article/search.html deleted file mode 100644 index 200402e64..000000000 --- a/forum-ui/src/main/resources/templates/biz/article/search.html +++ /dev/null @@ -1,34 +0,0 @@ - - -
- QuickFrom社区 -
- - - -
- - -
- - -
-
-
- 正文 -
-
- - -
-
-
- - - -
- - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/biz/user/achievement.html b/forum-ui/src/main/resources/templates/biz/user/achievement.html deleted file mode 100644 index 6937ab957..000000000 --- a/forum-ui/src/main/resources/templates/biz/user/achievement.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - -
-
-
标题
-
    -
  • 已发布文章
  • -
  • 文章被点赞
  • -
  • 文章被阅读
  • -
  • 文章被收藏
  • -
-
-
- - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/biz/user/home.html b/forum-ui/src/main/resources/templates/biz/user/home.html deleted file mode 100644 index f8fba934d..000000000 --- a/forum-ui/src/main/resources/templates/biz/user/home.html +++ /dev/null @@ -1,182 +0,0 @@ - - -
- QuickFrom社区 -
- - - -
- -
- -
-
-
-
- -
-
-
一灰灰
-
-
-
    -
  • 关注数
  • -
  • 粉丝数
  • -
- - - - -
-
- -
-
-
- - - -
-
-

个人简介

-
-
- - - - -
- > -
- -
- - - - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/biz/user/process.html b/forum-ui/src/main/resources/templates/biz/user/process.html deleted file mode 100644 index 8df08d294..000000000 --- a/forum-ui/src/main/resources/templates/biz/user/process.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - -
-
-
标题
-

描述

-
-
- - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/index.html b/forum-ui/src/main/resources/templates/index.html deleted file mode 100644 index aab18e24e..000000000 --- a/forum-ui/src/main/resources/templates/index.html +++ /dev/null @@ -1,51 +0,0 @@ - - -
- QuickFrom社区 -
- - - -
- - - -
- - -
- - -
- - - -
-
-
- 正文 -
-
-
-
-
- - -
-
-
- - -
-
-
- -
- - - -
- - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/layout/footer.html b/forum-ui/src/main/resources/templates/layout/footer.html deleted file mode 100644 index f503ced7b..000000000 --- a/forum-ui/src/main/resources/templates/layout/footer.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/layout/header.html b/forum-ui/src/main/resources/templates/layout/header.html deleted file mode 100644 index 0b79d5422..000000000 --- a/forum-ui/src/main/resources/templates/layout/header.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - Quick社区 - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/layout/navbar.html b/forum-ui/src/main/resources/templates/layout/navbar.html deleted file mode 100644 index 4d129985e..000000000 --- a/forum-ui/src/main/resources/templates/layout/navbar.html +++ /dev/null @@ -1,132 +0,0 @@ - - - -
- - - - - - - -
- \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/plugins/article-card.html b/forum-ui/src/main/resources/templates/plugins/article-card.html deleted file mode 100644 index acac57523..000000000 --- a/forum-ui/src/main/resources/templates/plugins/article-card.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - -
- -
-
-
- -
- 作者 -
-
- | -
10小时前
- | - -
- -
- - - - 第一个文章 -
-

- 文章简介 -

-
-

- - 阅读: 10 - - - 点赞: 10 - - - 评论: 10 - -

-
-
-
- -
- -
- -
- - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/plugins/article-info.html b/forum-ui/src/main/resources/templates/plugins/article-info.html deleted file mode 100644 index 6fdc6eedc..000000000 --- a/forum-ui/src/main/resources/templates/plugins/article-info.html +++ /dev/null @@ -1,93 +0,0 @@ - - - - -
-
标题
-
- -   - 作者 - - - - 更新时间 - - - · - - - - - 520 - - - · - - - - - 521 - - - - 编辑 - - - - - 删除 - - -
-
- -
- -
- -
- 最近更新于 2022年09月01日 - - - - - - -
- - -
- - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/plugins/carousel.html b/forum-ui/src/main/resources/templates/plugins/carousel.html deleted file mode 100644 index 0564738c3..000000000 --- a/forum-ui/src/main/resources/templates/plugins/carousel.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/plugins/categories.html b/forum-ui/src/main/resources/templates/plugins/categories.html deleted file mode 100644 index 63b7b45c1..000000000 --- a/forum-ui/src/main/resources/templates/plugins/categories.html +++ /dev/null @@ -1,16 +0,0 @@ - - - -
-
- 全部 -
-
- - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/plugins/comment-list.html b/forum-ui/src/main/resources/templates/plugins/comment-list.html deleted file mode 100644 index e2f009035..000000000 --- a/forum-ui/src/main/resources/templates/plugins/comment-list.html +++ /dev/null @@ -1,143 +0,0 @@ - - - - -
-
- -
- -
- -
-
-
- -
-
-
- - - -
-
-

- - - -

-

-

-       - - 回复 - -

- -
-
- - - -
-
-

- - - - -

-

-

- - - -

- - - 回复 - -

-
-
- -
-
-
- - - -
- - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/plugins/custom-empty.html b/forum-ui/src/main/resources/templates/plugins/custom-empty.html deleted file mode 100644 index 1301cb6d5..000000000 --- a/forum-ui/src/main/resources/templates/plugins/custom-empty.html +++ /dev/null @@ -1,8 +0,0 @@ - - - -
-

等待热评 ~ ~

-
- - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/plugins/follow-select-tag.html b/forum-ui/src/main/resources/templates/plugins/follow-select-tag.html deleted file mode 100644 index 298325cb3..000000000 --- a/forum-ui/src/main/resources/templates/plugins/follow-select-tag.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/plugins/home-select-tag.html b/forum-ui/src/main/resources/templates/plugins/home-select-tag.html deleted file mode 100644 index be795888b..000000000 --- a/forum-ui/src/main/resources/templates/plugins/home-select-tag.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/plugins/normal-card.html b/forum-ui/src/main/resources/templates/plugins/normal-card.html deleted file mode 100644 index 4e5c227b0..000000000 --- a/forum-ui/src/main/resources/templates/plugins/normal-card.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - -
-
-
标题
-

描述

-
-
- - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/plugins/user-article-card.html b/forum-ui/src/main/resources/templates/plugins/user-article-card.html deleted file mode 100644 index bf75beacd..000000000 --- a/forum-ui/src/main/resources/templates/plugins/user-article-card.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - -
- -
-
-
- -
- 作者 -
-
- | -
10小时前
- | - -
- - -

- 文章简介 -

-
-

- - 阅读: 10 - - - 点赞: 10 - - - 评论: 10 - -

-
-
-
- -
- -
- -
- - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/plugins/user-card.html b/forum-ui/src/main/resources/templates/plugins/user-card.html deleted file mode 100644 index 08f8d8925..000000000 --- a/forum-ui/src/main/resources/templates/plugins/user-card.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - -
-
-
-
- -
-
-
一灰灰
-

- 伪全栈*真后端(求👍,求🌟 求👋 ) - 微信公众号:一灰灰Blog -

-
-
-
-
    -
  • 点赞数
  • -
  • 阅读数
  • -
- - -
-
- - \ No newline at end of file diff --git a/forum-ui/src/main/resources/templates/plugins/user-follow-card.html b/forum-ui/src/main/resources/templates/plugins/user-follow-card.html deleted file mode 100644 index 4dc1b4f82..000000000 --- a/forum-ui/src/main/resources/templates/plugins/user-follow-card.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/forum-web/pom.xml b/forum-web/pom.xml deleted file mode 100644 index 96bf1db1e..000000000 --- a/forum-web/pom.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - quick-forum - com.github.liuyueyi.quick-forum - 0.0.1-SNAPSHOT - - 4.0.0 - - forum-web - - - - forum-ui - com.github.liuyueyi.quick-forum - - - forum-service - com.github.liuyueyi.quick-forum - - - - - org.springframework.boot - spring-boot-starter - - - org.springframework.boot - spring-boot-starter-web - - - com.fasterxml.jackson.dataformat - jackson-dataformat-xml - - - org.projectlombok - lombok - - - - org.springframework.boot - spring-boot-starter-test - test - - - - junit - junit - test - - - \ No newline at end of file diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/QuickForumApplication.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/QuickForumApplication.java deleted file mode 100644 index 553268858..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/QuickForumApplication.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.liuyueyi.forum.web; - -import com.github.liuyueyi.forum.web.hook.interceptor.GlobalViewInterceptor; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.servlet.ServletComponentScan; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -import javax.annotation.Resource; - -/** - * 入口 - * - * @author yihui - * @date 2022/7/6 - */ -@ServletComponentScan -@SpringBootApplication -public class QuickForumApplication implements WebMvcConfigurer { - - @Resource - private GlobalViewInterceptor globalViewInterceptor; - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(globalViewInterceptor).addPathPatterns("/**"); - } - - public static void main(String[] args) { - SpringApplication.run(QuickForumApplication.class, args); - } - -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/admin/AdminController.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/admin/AdminController.java deleted file mode 100644 index 72cd30d0c..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/admin/AdminController.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.github.liuyueyi.forum.web.admin; - -import org.springframework.stereotype.Controller; - -/** - * @author YiHui - * @date 2022/9/1 - */ -@Controller -public class AdminController { -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/admin/package-info.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/admin/package-info.java deleted file mode 100644 index bfd2d3b83..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/admin/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * 管理员用户操作路径 - * - * @author yihui - * @date 2022/7/6 - */ -package com.github.liuyueyi.forum.web.admin; \ No newline at end of file diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/config/GlobalViewConfig.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/config/GlobalViewConfig.java deleted file mode 100644 index b4271a33b..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/config/GlobalViewConfig.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.liuyueyi.forum.web.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -/** - * @author yihui - * @date 2022/6/15 - */ -@Data -@ConfigurationProperties(prefix = "view.site") -@Component -public class GlobalViewConfig { - - private String cdnImgStyle; - - private String websiteRecord; - - private Integer pageSize; - - private String websiteName; - - private String websiteLogoUrl; - - private String websiteFaviconIconUrl; - - private String contactMeWxQrCode; - - private String contactMeTitle; - -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/config/XmlWebConfig.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/config/XmlWebConfig.java deleted file mode 100644 index 73ae2e764..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/config/XmlWebConfig.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.github.liuyueyi.forum.web.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.http.MediaType; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; -import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -import java.util.List; - -/** - * 注册xml解析器 - * - * @author yihui - * @date 2022/6/20 - */ -@Configuration -public class XmlWebConfig implements WebMvcConfigurer { - @Override - public void configureMessageConverters(List> converters) { - converters.add(new MappingJackson2HttpMessageConverter()); - converters.add(new MappingJackson2XmlHttpMessageConverter()); - } - - /** - * fixme 返回数据类型的配置, 相关知识点可以查看 - * - * @param configurer - */ - @Override - public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { - configurer.favorParameter(true) - .defaultContentType(MediaType.APPLICATION_JSON, MediaType.TEXT_XML, MediaType.APPLICATION_XML) - .parameterName("mediaType") - .mediaType("json", MediaType.APPLICATION_JSON) - .mediaType("xml", MediaType.APPLICATION_XML) - .mediaType("html", MediaType.TEXT_HTML) - .mediaType("text", MediaType.TEXT_PLAIN) - ; - } -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/IndexController.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/IndexController.java deleted file mode 100644 index c4fcea50d..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/IndexController.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.github.liuyueyi.forum.web.front; - -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.article.dto.ArticleListDTO; -import com.github.liueyueyi.forum.api.model.vo.article.dto.CategoryDTO; -import com.github.liuyueyi.forum.core.util.MapUtils; -import com.github.liuyueyi.forum.service.article.service.ArticleReadService; -import com.github.liuyueyi.forum.service.article.service.CategoryService; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; - -import javax.servlet.http.HttpServletRequest; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicReference; - -/** - * @author YiHui - * @date 2022/7/6 - */ -@Controller -public class IndexController { - @Autowired - private CategoryService categoryService; - - @Autowired - private ArticleReadService articleService; - - @GetMapping(path = {"/", "", "/index"}) - public String index(Model model, HttpServletRequest request) { - String activeTab = request.getParameter("category"); - Long categoryId = categories(model, activeTab); - articleList(model, request, categoryId); - homeCarouselList(model); - sideBarItems(model); - model.addAttribute("currentDomain", "article"); - return "index"; - } - - /** - * 查询文章列表 - * - * @param model - */ - @GetMapping(path = "search") - public String searchArticleList(@RequestParam(name = "key") String key, Model model) { - if (!StringUtils.isBlank(key)) { - PageParam page = PageParam.newPageInstance(1L, 10L); - ArticleListDTO list = articleService.queryArticlesBySearchKey(key, page); - model.addAttribute("articles", list); - sideBarItems(model); - } - return "biz/article/search"; - } - - /** - * 返回分类列表 - * - * @param active - * @return - */ - private Long categories(Model model, String active) { - List list = categoryService.loadAllCategories(); - list.add(0, new CategoryDTO(0L, CategoryDTO.DEFAULT_TOTAL_CATEGORY, false)); - Long selectCategoryId = null; - for (CategoryDTO c : list) { - if (c.getCategory().equalsIgnoreCase(active)) { - selectCategoryId = c.getCategoryId(); - c.setSelected(true); - } else { - c.setSelected(false); - } - } - - if (selectCategoryId == null) { - // 未匹配时,默认选全部 - list.get(0).setSelected(true); - } - model.addAttribute("categories", list); - return selectCategoryId; - } - - /** - * 文章列表 - * - * @param model - * @param request - * @param categoryId - */ - private void articleList(Model model, HttpServletRequest request, Long categoryId) { - AtomicReference page = new AtomicReference<>(1L); - AtomicReference pageNum = new AtomicReference<>(20L); - Optional.ofNullable(request.getParameter("page")).ifPresent(p -> page.set(Long.parseLong(p))); - Optional.ofNullable(request.getParameter("size")).ifPresent(p -> pageNum.set(Long.parseLong(p))); - ArticleListDTO list = articleService.queryArticlesByCategory(categoryId, PageParam.newPageInstance(page.get(), pageNum.get())); - model.addAttribute("articles", list); - } - - /** - * 轮播图 - * - * @return - */ - private void homeCarouselList(Model model) { - List> list = new ArrayList<>(); - list.add(MapUtils.create("imgUrl", "https://spring.hhui.top/spring-blog/imgs/220425/logo.jpg", "name", "spring社区", "actionUrl", "https://spring.hhui.top/")); - list.add(MapUtils.create("imgUrl", "https://spring.hhui.top/spring-blog/imgs/220422/logo.jpg", "name", "一灰灰", "actionUrl", "https://blog.hhui.top/")); - model.addAttribute("homeCarouselList", list); - } - - - /** - * 侧边栏信息 - *

- * fixme: 后续调整为由运营推广模块返回 - * - * @return - */ - private void sideBarItems(Model model) { - List> res = new ArrayList<>(); - res.add(MapUtils.create("title", "公告", "desc", "简单的公告内容")); - res.add(MapUtils.create("title", "标签云", "desc", "java, web, html")); - model.addAttribute("sideBarItems", res); - } - - - @GetMapping(path = "/403") - public String _403() { - return "403"; - } - - @GetMapping(path = "/500") - public String _500() { - return "500"; - } -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/article/rest/ArticleRestController.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/article/rest/ArticleRestController.java deleted file mode 100644 index 7ddb33675..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/article/rest/ArticleRestController.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.github.liuyueyi.forum.web.front.article.rest; - -import com.github.liueyueyi.forum.api.model.context.ReqInfoContext; -import com.github.liueyueyi.forum.api.model.enums.DocumentTypeEnum; -import com.github.liueyueyi.forum.api.model.enums.OperateTypeEnum; -import com.github.liueyueyi.forum.api.model.vo.ResVo; -import com.github.liueyueyi.forum.api.model.vo.article.ArticlePostReq; -import com.github.liueyueyi.forum.api.model.vo.article.dto.CategoryDTO; -import com.github.liueyueyi.forum.api.model.vo.article.dto.TagDTO; -import com.github.liueyueyi.forum.api.model.vo.constants.StatusEnum; -import com.github.liuyueyi.forum.core.permission.Permission; -import com.github.liuyueyi.forum.core.permission.UserRole; -import com.github.liuyueyi.forum.service.article.repository.entity.ArticleDO; -import com.github.liuyueyi.forum.service.article.service.ArticleReadService; -import com.github.liuyueyi.forum.service.article.service.ArticleWriteService; -import com.github.liuyueyi.forum.service.article.service.CategoryService; -import com.github.liuyueyi.forum.service.article.service.TagService; -import com.github.liuyueyi.forum.service.user.service.UserFootService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.*; - -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.List; - -/** - * 返回json格式数据 - * - * @author YiHui - * @date 2022/9/2 - */ -@RequestMapping(path = "article/api") -@RestController -public class ArticleRestController { - @Autowired - private ArticleReadService articleService; - @Autowired - private UserFootService userFootService; - @Autowired - private CategoryService categoryService; - @Autowired - private TagService tagService; - @Autowired - private ArticleWriteService articleWriteService; - - - /** - * 查询所有的标签 - * - * @return - */ - @ResponseBody - @GetMapping(path = "tag/list") - public ResVo> queryTags(Long categoryId) { - if (categoryId == null || categoryId <= 0L) { - return ResVo.fail(StatusEnum.ILLEGAL_ARGUMENTS, categoryId); - } - - List list = tagService.queryTagsByCategoryId(categoryId); - return ResVo.ok(list); - } - - /** - * 获取所有的分类 - * - * @return - */ - @ResponseBody - @GetMapping(path = "category/list") - public ResVo> getCategoryList(@RequestParam(name = "categoryId", required = false) Long categoryId) { - List list = categoryService.loadAllCategories(); - list.forEach(c -> c.setSelected(c.getCategoryId().equals(categoryId))); - return ResVo.ok(list); - } - - - /** - * 收藏、点赞等相关操作 - * - * @param articleId - * @param type 取值来自于 OperateTypeEnum#code - * @return - */ - @ResponseBody - @Permission(role = UserRole.LOGIN) - @GetMapping(path = "favor") - public ResVo favor(@RequestParam(name = "articleId") Long articleId, - @RequestParam(name = "type") Integer type) { - OperateTypeEnum operate = OperateTypeEnum.fromCode(type); - if (operate == OperateTypeEnum.EMPTY) { - return ResVo.fail(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, type + "非法"); - } - - // 要求文章必须存在 - ArticleDO article = articleService.queryBasicArticle(articleId); - if (article == null) { - return ResVo.fail(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "文章不存在!"); - } - - userFootService.saveOrUpdateUserFoot(DocumentTypeEnum.ARTICLE, articleId, article.getUserId(), - ReqInfoContext.getReqInfo().getUserId(), - operate); - return ResVo.ok(true); - } - - - /** - * 发布文章,完成后跳转到详情页 - * - 这里有一个重定向的知识点 - * - fixme 博文:* [5.请求重定向 | 一灰灰Learning](https://hhui.top/spring-web/02.response/05.190929-springboot%E7%B3%BB%E5%88%97%E6%95%99%E7%A8%8Bweb%E7%AF%87%E4%B9%8B%E9%87%8D%E5%AE%9A%E5%90%91/) - * - * @return - */ - @Permission(role = UserRole.LOGIN) - @PostMapping(path = "post") - @ResponseBody - @Transactional(rollbackFor = Exception.class) - public ResVo post(@RequestBody ArticlePostReq req, HttpServletResponse response) throws IOException { - Long id = articleWriteService.saveArticle(req, ReqInfoContext.getReqInfo().getUserId()); -// return "redirect:/article/detail/" + id; -// response.sendRedirect("/article/detail/" + id); - // 这里采用前端重定向策略 - return ResVo.ok(id); - } -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/article/view/ArticleViewController.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/article/view/ArticleViewController.java deleted file mode 100644 index 21a1c7140..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/article/view/ArticleViewController.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.github.liuyueyi.forum.web.front.article.view; - -import com.github.liueyueyi.forum.api.model.context.ReqInfoContext; -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.article.dto.ArticleDTO; -import com.github.liueyueyi.forum.api.model.vo.article.dto.CategoryDTO; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.TopCommentDTO; -import com.github.liueyueyi.forum.api.model.vo.user.dto.UserStatisticInfoDTO; -import com.github.liuyueyi.forum.core.permission.Permission; -import com.github.liuyueyi.forum.core.permission.UserRole; -import com.github.liuyueyi.forum.service.article.service.ArticleReadService; -import com.github.liuyueyi.forum.service.article.service.CategoryService; -import com.github.liuyueyi.forum.service.article.service.TagService; -import com.github.liuyueyi.forum.service.comment.service.CommentReadService; -import com.github.liuyueyi.forum.service.user.service.UserService; -import com.github.liuyueyi.forum.web.front.article.vo.ArticleDetailVo; -import com.github.liuyueyi.forum.web.front.article.vo.ArticleEditVo; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -/** - * 文章 - * todo: 所有的入口都放在一个Controller,会导致功能划分非常混乱 - * : 文章列表 - * : 文章编辑 - * : 文章详情 - * --- - * - 返回视图 view - * - 返回json数据 - * - * @author yihui - */ -@Controller -@RequestMapping(path = "article") -public class ArticleViewController { - @Autowired - private ArticleReadService articleService; - - @Autowired - private CategoryService categoryService; - - @Autowired - private TagService tagService; - - @Autowired - private UserService userService; - - @Autowired - private CommentReadService commentService; - - /** - * 文章编辑页 - * - * @param articleId - * @return - */ - @Permission(role = UserRole.LOGIN) - @GetMapping(path = "edit") - public String edit(@RequestParam(required = false) Long articleId, Model model) { - ArticleEditVo vo = new ArticleEditVo(); - if (articleId != null) { - ArticleDTO article = articleService.queryDetailArticleInfo(articleId); - vo.setArticle(article); - if (!Objects.equals(article.getAuthor(), ReqInfoContext.getReqInfo().getUserId())) { - // 没有权限 - model.addAttribute("toast", "内容不存在"); - return "redirect:403"; - } - - List categoryList = categoryService.loadAllCategories(); - categoryList.forEach(s -> { - s.setSelected(s.getCategoryId().equals(article.getCategory().getCategoryId())); - }); - vo.setCategories(categoryList); - vo.setTags(tagService.queryTagsByCategoryId(article.getCategory().getCategoryId())); - } else { - List categoryList = categoryService.loadAllCategories(); - vo.setCategories(categoryList); - vo.setTags(Collections.emptyList()); - } - model.addAttribute("vo", vo); - return "biz/article/edit"; - } - - - /** - * 文章详情页 - * - 参数解析知识点 - * - fixme * [1.Get请求参数解析姿势汇总 | 一灰灰Learning](https://hhui.top/spring-web/01.request/01.190824-springboot%E7%B3%BB%E5%88%97%E6%95%99%E7%A8%8Bweb%E7%AF%87%E4%B9%8Bget%E8%AF%B7%E6%B1%82%E5%8F%82%E6%95%B0%E8%A7%A3%E6%9E%90%E5%A7%BF%E5%8A%BF%E6%B1%87%E6%80%BB/) - * - * @param articleId - * @return - */ - @GetMapping("detail/{articleId}") - public String detail(@PathVariable(name = "articleId") Long articleId, Model model) { - ArticleDetailVo vo = new ArticleDetailVo(); - ArticleDTO articleDTO = articleService.queryTotalArticleInfo(articleId, ReqInfoContext.getReqInfo().getUserId()); - vo.setArticle(articleDTO); - - // 评论信息 - List comments = commentService.getArticleComments(articleId, PageParam.newPageInstance(1L, 10L)); - vo.setComments(comments); - - // 作者信息 - UserStatisticInfoDTO user = userService.queryUserInfoWithStatistic(articleDTO.getAuthor()); - articleDTO.setAuthorName(user.getUserName()); - vo.setAuthor(user); - model.addAttribute("vo", vo); - return "biz/article/detail"; - } - - -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/article/vo/ArticleDetailVo.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/article/vo/ArticleDetailVo.java deleted file mode 100644 index 01352b551..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/article/vo/ArticleDetailVo.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.github.liuyueyi.forum.web.front.article.vo; - -import com.github.liueyueyi.forum.api.model.vo.article.dto.ArticleDTO; -import com.github.liueyueyi.forum.api.model.vo.article.dto.CategoryDTO; -import com.github.liueyueyi.forum.api.model.vo.article.dto.TagDTO; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.TopCommentDTO; -import com.github.liueyueyi.forum.api.model.vo.user.dto.UserStatisticInfoDTO; -import lombok.Data; - -import java.util.List; - -/** - * @author YiHui - * @date 2022/9/2 - */ -@Data -public class ArticleDetailVo { - - private ArticleDTO article; - - private List comments; - - private UserStatisticInfoDTO author; - -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/article/vo/ArticleEditVo.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/article/vo/ArticleEditVo.java deleted file mode 100644 index db869190a..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/article/vo/ArticleEditVo.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.liuyueyi.forum.web.front.article.vo; - -import com.github.liueyueyi.forum.api.model.vo.article.dto.ArticleDTO; -import com.github.liueyueyi.forum.api.model.vo.article.dto.CategoryDTO; -import com.github.liueyueyi.forum.api.model.vo.article.dto.TagDTO; -import lombok.Data; - -import java.util.List; - -/** - * @author YiHui - * @date 2022/9/2 - */ -@Data -public class ArticleEditVo { - - private ArticleDTO article; - - private List categories; - - private List tags; - -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/comment/rest/CommentRestController.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/comment/rest/CommentRestController.java deleted file mode 100644 index 267e16143..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/comment/rest/CommentRestController.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.github.liuyueyi.forum.web.front.comment.rest; - -import com.github.liueyueyi.forum.api.model.context.ReqInfoContext; -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.ResVo; -import com.github.liueyueyi.forum.api.model.vo.comment.CommentSaveReq; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.TopCommentDTO; -import com.github.liueyueyi.forum.api.model.vo.constants.StatusEnum; -import com.github.liuyueyi.forum.core.permission.Permission; -import com.github.liuyueyi.forum.core.permission.UserRole; -import com.github.liuyueyi.forum.core.util.NumUtil; -import com.github.liuyueyi.forum.service.comment.service.CommentReadService; -import com.github.liuyueyi.forum.service.comment.service.CommentWriteService; -import org.apache.commons.lang3.StringEscapeUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.Optional; - -/** - * 评论 - * - * @author lvmenglou - * @date : 2022/4/22 10:56 - **/ -@RestController -@RequestMapping(path = "comment/api") -public class CommentRestController { - - @Autowired - private CommentReadService commentReadService; - - @Autowired - private CommentWriteService commentWriteService; - - /** - * 评论列表页 - * - * @param articleId - * @return - */ - @ResponseBody - @RequestMapping(path = "list") - public ResVo> list(Long articleId, Long pageNum, Long pageSize) { - if (NumUtil.nullOrZero(articleId)) { - return ResVo.fail(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "文章id为空"); - } - pageNum = Optional.ofNullable(pageNum).orElse(PageParam.DEFAULT_PAGE_NUM); - pageSize = Optional.ofNullable(pageSize).orElse(PageParam.DEFAULT_PAGE_SIZE); - List result = commentReadService.getArticleComments(articleId, PageParam.newPageInstance(pageNum, pageSize)); - return ResVo.ok(result); - } - - /** - * 保存评论 - * - * @param req - * @return - */ - @Permission(role = UserRole.LOGIN) - @PostMapping(path = "post") - @ResponseBody - public ResVo save(@RequestBody CommentSaveReq req) { - if (req.getArticleId() == null) { - return ResVo.fail(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "文章id为空"); - } - req.setUserId(ReqInfoContext.getReqInfo().getUserId()); - req.setCommentContent(StringEscapeUtils.escapeHtml3(req.getCommentContent())); - Long commentId = commentWriteService.saveComment(req); - return ResVo.ok(NumUtil.upZero(commentId)); - } - - /** - * 删除评论 - * - * @param commentId - * @return - */ - @Permission(role = UserRole.LOGIN) - @RequestMapping(path = "delete") - public ResVo delete(Long commentId) { - commentWriteService.deleteComment(commentId); - return ResVo.ok(true); - } -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/notice/NoticeController.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/notice/NoticeController.java deleted file mode 100644 index 20531c8d3..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/notice/NoticeController.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.github.liuyueyi.forum.web.front.notice; - -public class NoticeController { -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/package-info.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/package-info.java deleted file mode 100644 index e0de2dda7..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/package-info.java +++ /dev/null @@ -1,14 +0,0 @@ -/** - * 前台页用户包路径 - * - * 入口层,不做复杂的业务逻辑,主要干的事情 - * 1. 参数解析 - * 2. 视图数据封装,就是往Model中写数据 - * 3. 重定向控制 - * 4. todo: 权限判断(个人页,需要登录...) - * - * - * @author yihui - * @date 2022/7/6 - */ -package com.github.liuyueyi.forum.web.front; \ No newline at end of file diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/user/rest/LoginRestController.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/user/rest/LoginRestController.java deleted file mode 100644 index f1fadcdf2..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/user/rest/LoginRestController.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.github.liuyueyi.forum.web.front.user.rest; - -import com.github.liueyueyi.forum.api.model.context.ReqInfoContext; -import com.github.liueyueyi.forum.api.model.vo.ResVo; -import com.github.liueyueyi.forum.api.model.vo.constants.StatusEnum; -import com.github.liuyueyi.forum.core.permission.Permission; -import com.github.liuyueyi.forum.core.permission.UserRole; -import com.github.liuyueyi.forum.service.user.service.LoginService; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletResponse; -import java.util.Optional; - -/** - * 登录/登出的入口 - * - * @author YiHui - * @date 2022/8/15 - */ -@RestController -@RequestMapping -public class LoginRestController { - @Autowired - private LoginService loginService; - - @RequestMapping("/login") - public ResVo login(@RequestParam(name = "code") String code, - HttpServletResponse response) { - String session = loginService.login(code); - if (StringUtils.isNotBlank(session)) { - // cookie中写入用户登录信息 - response.addCookie(new Cookie(LoginService.SESSION_KEY, session)); - return ResVo.ok(true); - } else { - return ResVo.fail(StatusEnum.LOGIN_FAILED_MIXED, "登录码异常,请重新输入"); - } - } - - @Permission(role = UserRole.LOGIN) - @RequestMapping("logout") - public ResVo logOut() { - Optional.ofNullable(ReqInfoContext.getReqInfo()).ifPresent(s -> loginService.logout(s.getSession())); - return ResVo.ok(true); - } -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/user/rest/UserRestController.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/user/rest/UserRestController.java deleted file mode 100644 index 4f19c33c1..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/user/rest/UserRestController.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.github.liuyueyi.forum.web.front.user.rest; - -import com.github.liueyueyi.forum.api.model.context.ReqInfoContext; -import com.github.liueyueyi.forum.api.model.exception.ExceptionUtil; -import com.github.liueyueyi.forum.api.model.vo.ResVo; -import com.github.liueyueyi.forum.api.model.vo.constants.StatusEnum; -import com.github.liueyueyi.forum.api.model.vo.user.UserInfoSaveReq; -import com.github.liueyueyi.forum.api.model.vo.user.UserRelationReq; -import com.github.liuyueyi.forum.core.permission.Permission; -import com.github.liuyueyi.forum.core.permission.UserRole; -import com.github.liuyueyi.forum.service.user.service.UserRelationService; -import com.github.liuyueyi.forum.service.user.service.relation.UserRelationServiceImpl; -import com.github.liuyueyi.forum.service.user.service.user.UserServiceImpl; -import org.springframework.transaction.annotation.Transactional; -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; - -/** - * @author YiHui - * @date 2022/9/2 - */ -@RestController -@RequestMapping(path = "user/api") -public class UserRestController { - - @Resource - private UserServiceImpl userService; - - @Resource - private UserRelationServiceImpl userRelationService; - - /** - * 保存用户关系 - * - * @param req - * @return - * @throws Exception - */ - @Permission(role = UserRole.LOGIN) - @PostMapping(path = "saveUserRelation") - public ResVo saveUserRelation(@RequestBody UserRelationReq req) { - userRelationService.saveUserRelation(req); - return ResVo.ok(true); - } - - - /** - * 保存用户详情 - * - * @param req - * @return - * @throws Exception - */ - @Permission(role = UserRole.LOGIN) - @PostMapping(path = "saveUserInfo") - @Transactional(rollbackFor = Exception.class) - public ResVo saveUserInfo(@RequestBody UserInfoSaveReq req) { - if (!(req.getUserId() != null && req.getUserId().equals(ReqInfoContext.getReqInfo().getUserId()))) { - // 不能修改其他用户的信息 - throw ExceptionUtil.of(StatusEnum.FORBID_ERROR_MIXED, "无权修改"); - } - userService.saveUserInfo(req); - return ResVo.ok(true); - } - -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/user/rest/WxRestController.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/user/rest/WxRestController.java deleted file mode 100644 index 18cc3ad80..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/user/rest/WxRestController.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.github.liuyueyi.forum.web.front.user.rest; - -import com.github.liueyueyi.forum.api.model.vo.user.wx.WxTxtMsgReqVo; -import com.github.liueyueyi.forum.api.model.vo.user.wx.WxTxtMsgResVo; -import com.github.liuyueyi.forum.service.user.service.LoginService; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import javax.servlet.http.HttpServletRequest; - -/** - * 微信公众号登录相关 - * - * @author YiHui - * @date 2022/9/2 - */ -@RequestMapping(path = "wx") -@RestController -public class WxRestController { - @Autowired - private LoginService loginService; - - /** - * 微信的公众号接入 token 验证,即返回echostr的参数值 - * - * @param request - * @return - */ - @GetMapping(path = "callback") - public String check(HttpServletRequest request) { - String echoStr = request.getParameter("echostr"); - if (StringUtils.isNoneEmpty(echoStr)) { - return echoStr; - } - return ""; - } - - /** - * fixme: 需要做防刷校验 - * 微信的响应返回 - * 本地测试访问: curl -X POST 'http://localhost:8080/wx/callback' -H 'content-type:application/xml' -d '165570057911111111' -i - * - * @param msg - * @return - */ - @PostMapping(path = "callback", - consumes = {"application/xml", "text/xml"}, - produces = "application/xml;charset=utf-8") - public WxTxtMsgResVo callBack(@RequestBody WxTxtMsgReqVo msg) { - String content = msg.getContent(); - WxTxtMsgResVo res = new WxTxtMsgResVo(); - res.setFromUserName(msg.getToUserName()); - res.setToUserName(msg.getFromUserName()); - res.setCreateTime(System.currentTimeMillis() / 1000); - res.setMsgType("text"); - if (loginSymbol(content)) { - res.setContent("登录验证码: 【" + loginService.getVerifyCode(msg.getFromUserName()) + "】 五分钟内有效"); - } else { - res.setContent("输入关键词不对!"); - } - return res; - } - - /** - * 判断是否为登录指令,后续扩展其他的响应 - * - * @param msg - * @return - */ - private boolean loginSymbol(String msg) { - if (StringUtils.isBlank(msg)) { - return false; - } - - msg = msg.trim(); - for (String key : LoginService.LOGIN_CODE_KEY) { - if (msg.equalsIgnoreCase(key)) { - return true; - } - } - return false; - } -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/user/view/UserViewController.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/user/view/UserViewController.java deleted file mode 100644 index be49cef76..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/user/view/UserViewController.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.github.liuyueyi.forum.web.front.user.view; - -import com.github.liueyueyi.forum.api.model.enums.FollowSelectEnum; -import com.github.liueyueyi.forum.api.model.enums.FollowTypeEnum; -import com.github.liueyueyi.forum.api.model.enums.HomeSelectEnum; -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.article.dto.ArticleListDTO; -import com.github.liueyueyi.forum.api.model.vo.article.dto.TagSelectDTO; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.UserFollowListDTO; -import com.github.liueyueyi.forum.api.model.vo.user.dto.UserStatisticInfoDTO; -import com.github.liuyueyi.forum.service.article.service.impl.ArticleReadServiceImpl; -import com.github.liuyueyi.forum.service.user.service.relation.UserRelationServiceImpl; -import com.github.liuyueyi.forum.service.user.service.user.UserServiceImpl; -import com.github.liuyueyi.forum.web.front.user.vo.UserHomeVo; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; - -import javax.annotation.Resource; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - - -/** - * 用户注册、取消,登录、登出 - * - * @author lvmenglou - * @date : 2022/8/3 10:56 - **/ -@Controller -@RequestMapping(path = "user") -@Slf4j -public class UserViewController { - - @Resource - private UserServiceImpl userService; - - @Resource - private UserRelationServiceImpl userRelationService; - - @Resource - private ArticleReadServiceImpl articleReadService; - - private static final List homeSelectTags = Arrays.asList("article", "read", "follow", "collection"); - private static final List followSelectTags = Arrays.asList("follow", "fans"); - - /** - * 获取用户主页信息 - * - * @return - */ - @GetMapping(path = "home") - public String getUserHome(@RequestParam(name = "userId") Long userId, - @RequestParam(name = "homeSelectType", required = false) String homeSelectType, - @RequestParam(name = "followSelectType", required = false) String followSelectType, - Model model) { - UserHomeVo vo = new UserHomeVo(); - vo.setHomeSelectType(StringUtils.isBlank(homeSelectType) ? HomeSelectEnum.ARTICLE.getCode() : homeSelectType); - vo.setFollowSelectType(StringUtils.isBlank(followSelectType) ? FollowTypeEnum.FOLLOW.getCode() : followSelectType); - - UserStatisticInfoDTO userInfo = userService.queryUserInfoWithStatistic(userId); - vo.setUserHome(userInfo); - - List homeSelectTags = homeSelectTags(vo.getHomeSelectType()); - vo.setHomeSelectTags(homeSelectTags); - - userHomeSelectList(vo, userId); - model.addAttribute("vo", vo); - return "biz/user/home"; - } - - /** - * 返回Home页选择列表标签 - * - * @param selectType - * @return - */ - private List homeSelectTags(String selectType) { - List tags = new ArrayList<>(); - homeSelectTags.forEach(tag -> { - TagSelectDTO tagSelectDTO = new TagSelectDTO(); - tagSelectDTO.setSelectType(tag); - tagSelectDTO.setSelectDesc(HomeSelectEnum.fromCode(tag).getDesc()); - tagSelectDTO.setSelected(selectType.equals(tag)); - tags.add(tagSelectDTO); - }); - return tags; - } - - /** - * 返回关注用户选择列表标签 - * - * @param selectType - * @return - */ - private List followSelectTags(String selectType) { - List tags = new ArrayList<>(); - followSelectTags.forEach(tag -> { - TagSelectDTO tagSelectDTO = new TagSelectDTO(); - tagSelectDTO.setSelectType(tag); - tagSelectDTO.setSelectDesc(FollowSelectEnum.fromCode(tag).getDesc()); - tagSelectDTO.setSelected(selectType.equals(tag)); - tags.add(tagSelectDTO); - }); - return tags; - } - - /** - * 返回选择列表 - * - * @param vo - * @param userId - */ - private void userHomeSelectList(UserHomeVo vo, Long userId) { - PageParam pageParam = PageParam.newPageInstance(); - HomeSelectEnum select = HomeSelectEnum.fromCode(vo.getHomeSelectType()); - if (select == null) { - return; - } - - switch (select) { - case ARTICLE: - case READ: - case COLLECTION: - ArticleListDTO dto = articleReadService.queryArticlesByUserAndType(userId, pageParam, select); - vo.setHomeSelectList(dto); - return; - case FOLLOW: - // 关注用户与被关注用户 - // 获取选择标签 - List followSelectTags = followSelectTags(vo.getFollowSelectType()); - vo.setFollowSelectTags(followSelectTags); - initFollowFansList(vo, userId, pageParam); - return; - default: - } - } - - private void initFollowFansList(UserHomeVo vo, long userId, PageParam pageParam) { - if (vo.getFollowSelectType().equals(FollowTypeEnum.FOLLOW.getCode())) { - UserFollowListDTO userFollowListDTO = userRelationService.getUserFollowList(userId, pageParam); - vo.setFollowList(userFollowListDTO); - vo.setFansList(UserFollowListDTO.emptyInstance()); - } else { - UserFollowListDTO userFollowListDTO = userRelationService.getUserFansList(userId, pageParam); - vo.setFansList(userFollowListDTO); - vo.setFollowList(UserFollowListDTO.emptyInstance()); - } - } -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/user/vo/UserHomeVo.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/user/vo/UserHomeVo.java deleted file mode 100644 index 1b16be3bc..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/front/user/vo/UserHomeVo.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.liuyueyi.forum.web.front.user.vo; - -import com.github.liueyueyi.forum.api.model.vo.article.dto.ArticleListDTO; -import com.github.liueyueyi.forum.api.model.vo.article.dto.TagSelectDTO; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.UserFollowListDTO; -import com.github.liueyueyi.forum.api.model.vo.user.dto.UserStatisticInfoDTO; -import lombok.Data; - -import java.util.List; - -/** - * @author YiHui - * @date 2022/9/2 - */ -@Data -public class UserHomeVo { - String homeSelectType; - List homeSelectTags; - UserFollowListDTO fansList; - UserFollowListDTO followList; - String followSelectType; - List followSelectTags; - UserStatisticInfoDTO userHome; - - ArticleListDTO homeSelectList; -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/hook/filter/ReqRecordFilter.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/hook/filter/ReqRecordFilter.java deleted file mode 100644 index 108c27602..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/hook/filter/ReqRecordFilter.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.github.liuyueyi.forum.web.hook.filter; - -import com.github.liueyueyi.forum.api.model.context.ReqInfoContext; -import com.github.liueyueyi.forum.api.model.vo.user.dto.BaseUserInfoDTO; -import com.github.liuyueyi.forum.core.util.CrossUtil; -import com.github.liuyueyi.forum.core.util.IpUtil; -import com.github.liuyueyi.forum.service.user.service.LoginService; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpMethod; - -import javax.servlet.*; -import javax.servlet.annotation.WebFilter; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.net.URLDecoder; - -/** - * 1. 请求参数日志输出过滤器 - * 2. 判断用户是否登录 - * - * @author YiHui - * @date 2022/7/6 - */ -@Slf4j -@WebFilter(urlPatterns = "/*", filterName = "selfProcessBeforeFilter") -public class ReqRecordFilter implements Filter { - private static Logger REQ_LOG = LoggerFactory.getLogger("req"); - - @Autowired - private LoginService loginService; - - @Override - public void init(FilterConfig filterConfig) { - } - - @Override - public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { - long start = System.currentTimeMillis(); - HttpServletRequest request = null; - try { - request = this.initReqInfo((HttpServletRequest) servletRequest); - CrossUtil.buildCors(request, (HttpServletResponse) servletResponse); - filterChain.doFilter(request, servletResponse); - } finally { - buildRequestLog(ReqInfoContext.getReqInfo(), request, System.currentTimeMillis() - start); - ReqInfoContext.clear(); - } - } - - @Override - public void destroy() { - } - - private HttpServletRequest initReqInfo(HttpServletRequest request) { - try { - ReqInfoContext.ReqInfo reqInfo = new ReqInfoContext.ReqInfo(); - reqInfo.setHost(request.getHeader("host")); - reqInfo.setPath(request.getPathInfo()); - reqInfo.setReferer(request.getHeader("referer")); - reqInfo.setClientIp(IpUtil.getClientIp(request)); - reqInfo.setUserAgent(request.getHeader("User-Agent")); - - request = this.wrapperRequest(request, reqInfo); - ReqInfoContext.addReqInfo(reqInfo); - - for (Cookie cookie : request.getCookies()) { - if (LoginService.SESSION_KEY.equalsIgnoreCase(cookie.getName())) { - String session = cookie.getValue(); - BaseUserInfoDTO user = loginService.getUserBySessionId(session); - reqInfo.setSession(session); - reqInfo.setUserId(user.getUserId()); - reqInfo.setUser(user); - break; - } - } - } catch (Exception e) { - log.error("init reqInfo error!", e); - } - - return request; - } - - private void buildRequestLog(ReqInfoContext.ReqInfo req, HttpServletRequest request, long costTime) { - // fixme 过滤不需要记录请求日志的场景 - if (request == null - || request.getRequestURI().endsWith("css") - || request.getRequestURI().endsWith("js") - || request.getRequestURI().endsWith("png") - || request.getRequestURI().endsWith("ico")) { - return; - } - - StringBuilder msg = new StringBuilder(); - msg.append("method=").append(request.getMethod()).append("; "); - if (StringUtils.isNotBlank(req.getReferer())) { - msg.append("referer=").append(URLDecoder.decode(req.getReferer())).append("; "); - } - msg.append("remoteIp=").append(req.getClientIp()); - msg.append("; agent=").append(req.getUserAgent()); - - if (req.getUserId() != null) { - // 打印用户信息 - msg.append("; user=").append(req.getUserId()); - } - - msg.append("; uri=").append(request.getRequestURI()); - if (StringUtils.isNotBlank(request.getQueryString())) { - msg.append('?').append(URLDecoder.decode(request.getQueryString())); - } - - msg.append("; payload=").append(req.getPayload()); - msg.append("; cost=").append(costTime); - REQ_LOG.info("{}", msg); - } - - - private HttpServletRequest wrapperRequest(HttpServletRequest request, ReqInfoContext.ReqInfo reqInfo) { - if (!HttpMethod.POST.name().equalsIgnoreCase(request.getMethod())) { - return request; - } - - BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper(request); - reqInfo.setPayload(requestWrapper.getBodyString()); - return requestWrapper; - } - -} diff --git a/forum-web/src/main/java/com/github/liuyueyi/forum/web/hook/interceptor/GlobalViewInterceptor.java b/forum-web/src/main/java/com/github/liuyueyi/forum/web/hook/interceptor/GlobalViewInterceptor.java deleted file mode 100644 index 36cac2a4f..000000000 --- a/forum-web/src/main/java/com/github/liuyueyi/forum/web/hook/interceptor/GlobalViewInterceptor.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.github.liuyueyi.forum.web.hook.interceptor; - -import com.github.liueyueyi.forum.api.model.context.ReqInfoContext; -import com.github.liuyueyi.forum.core.permission.Permission; -import com.github.liuyueyi.forum.core.permission.UserRole; -import com.github.liuyueyi.forum.service.user.service.UserService; -import com.github.liuyueyi.forum.web.config.GlobalViewConfig; -import lombok.Data; -import lombok.experimental.Accessors; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.util.ObjectUtils; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.servlet.AsyncHandlerInterceptor; -import org.springframework.web.servlet.ModelAndView; - -import javax.annotation.Resource; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.util.Arrays; - -/** - * 注入全局的配置信息: - * - thymleaf 站点信息,基本信息,在这里注入 - * - * @author yihui - * @date 2022/6/15 - */ -@Slf4j -@Component -public class GlobalViewInterceptor implements AsyncHandlerInterceptor { - @Resource - private GlobalViewConfig globalViewConfig; - @Resource - private UserService userService; - @Value("${env.name}") - private String env; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - if (handler instanceof HandlerMethod) { - HandlerMethod handlerMethod = (HandlerMethod) handler; - Permission permission = handlerMethod.getMethod().getAnnotation(Permission.class); - if (permission == null) { - permission = handlerMethod.getBeanType().getAnnotation(Permission.class); - } - - if (permission == null || permission.role() == UserRole.ALL) { - return true; - } - if (ReqInfoContext.getReqInfo() == null || ReqInfoContext.getReqInfo().getUserId() == null) { - // 跳转到登录界面 - response.sendRedirect("/403"); - return false; - } - - if (permission.role() == UserRole.ADMIN && !"admin".equalsIgnoreCase(ReqInfoContext.getReqInfo().getUser().getRole())) { - response.sendRedirect("/403"); - return false; - } - } - return true; - } - - @Override - public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { - // 重定向请求不需要添加 - if (!ObjectUtils.isEmpty(modelAndView)) { - modelAndView.getModel().put("env", env); - modelAndView.getModel().put("siteInfo", globalViewConfig); - if (ReqInfoContext.getReqInfo() == null || ReqInfoContext.getReqInfo().getUserId() == null) { - modelAndView.getModel().put("isLogin", false); - - } else { - modelAndView.getModel().put("isLogin", true); - modelAndView.getModel().put("user", userService.queryUserInfoWithStatistic(ReqInfoContext.getReqInfo().getUserId())); - // 消息数 fixme 消息信息改由消息模块处理 - modelAndView.getModel().put("msgs", Arrays.asList(new UserMsg().setMsgId(100L).setMsgType(1).setMsg("模拟通知消息"))); - } - } - } - - @Data - @Accessors(chain = true) - private static class UserMsg { - private long msgId; - private int msgType; - private String msg; - } -} diff --git a/forum-web/src/main/resources-env/dev/application-dal.yml b/forum-web/src/main/resources-env/dev/application-dal.yml deleted file mode 100644 index 0b6d930c1..000000000 --- a/forum-web/src/main/resources-env/dev/application-dal.yml +++ /dev/null @@ -1,5 +0,0 @@ -spring: - datasource: - url: jdbc:mysql://127.0.0.1:3306/forum?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai - username: root - password: diff --git a/forum-web/src/main/resources-env/dev/application-web.yml b/forum-web/src/main/resources-env/dev/application-web.yml deleted file mode 100644 index a3b059dc6..000000000 --- a/forum-web/src/main/resources-env/dev/application-web.yml +++ /dev/null @@ -1,12 +0,0 @@ -log: - path: logs -env: - name: dev - -spring: - thymeleaf: - mode: HTML - encoding: UTF-8 - servlet: - content-type: text/html - cache: false \ No newline at end of file diff --git a/forum-web/src/main/resources-env/pre/application-dal.yml b/forum-web/src/main/resources-env/pre/application-dal.yml deleted file mode 100644 index ab71ced79..000000000 --- a/forum-web/src/main/resources-env/pre/application-dal.yml +++ /dev/null @@ -1,5 +0,0 @@ -spring: - datasource: - url: jdbc:mysql://pre.hhui.top/forum?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai - username: pre_root - password: diff --git a/forum-web/src/main/resources-env/pre/application-web.yml b/forum-web/src/main/resources-env/pre/application-web.yml deleted file mode 100644 index 01831f7c0..000000000 --- a/forum-web/src/main/resources-env/pre/application-web.yml +++ /dev/null @@ -1,12 +0,0 @@ -log: - path: ~/logs -env: - name: pre - -spring: - thymeleaf: - mode: HTML - encoding: UTF-8 - servlet: - content-type: text/html - cache: true \ No newline at end of file diff --git a/forum-web/src/main/resources-env/prod/application-dal.yml b/forum-web/src/main/resources-env/prod/application-dal.yml deleted file mode 100644 index 5a6c1e678..000000000 --- a/forum-web/src/main/resources-env/prod/application-dal.yml +++ /dev/null @@ -1,5 +0,0 @@ -spring: - datasource: - url: jdbc:mysql://prod.hhui.top/forum?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai - username: prod_root - password: diff --git a/forum-web/src/main/resources-env/prod/application-web.yml b/forum-web/src/main/resources-env/prod/application-web.yml deleted file mode 100644 index b2f50800c..000000000 --- a/forum-web/src/main/resources-env/prod/application-web.yml +++ /dev/null @@ -1,13 +0,0 @@ -log: - path: /home/admin/workspace/quick-forum/logs # 请使用实际的地址进行替换 -env: - name: prod - - -spring: - thymeleaf: - mode: HTML - encoding: UTF-8 - servlet: - content-type: text/html - cache: true # 开启缓存 \ No newline at end of file diff --git a/forum-web/src/main/resources-env/test/application-dal.yml b/forum-web/src/main/resources-env/test/application-dal.yml deleted file mode 100644 index 494c7e1ba..000000000 --- a/forum-web/src/main/resources-env/test/application-dal.yml +++ /dev/null @@ -1,5 +0,0 @@ -spring: - datasource: - url: jdbc:mysql://test.hhui.top/forum?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai - username: test_root - password: diff --git a/forum-web/src/main/resources-env/test/application-web.yml b/forum-web/src/main/resources-env/test/application-web.yml deleted file mode 100644 index 5cfd4e631..000000000 --- a/forum-web/src/main/resources-env/test/application-web.yml +++ /dev/null @@ -1,12 +0,0 @@ -log: - path: ~/logs -env: - name: test - -spring: - thymeleaf: - mode: HTML - encoding: UTF-8 - servlet: - content-type: text/html - cache: true \ No newline at end of file diff --git a/forum-web/src/main/resources/application-config.yml b/forum-web/src/main/resources/application-config.yml deleted file mode 100644 index fa9892c8a..000000000 --- a/forum-web/src/main/resources/application-config.yml +++ /dev/null @@ -1,10 +0,0 @@ -view: - site: - websiteName: Quick社区 - websiteLogoUrl: https://blog.hhui.top/hexblog/images/avatar.jpg - websiteFaviconIconUrl: https://blog.hhui.top/hexblog/images/avatar.jpg - contactMeTitle: 联系我 - liuyueyi25 - contactMeWxQrCode: https://blog.hhui.top/hexblog/imgs/info/wx.jpg - pageSize: 20 - cdnImgStyle: - websiteRecord: 备案记录 \ No newline at end of file diff --git a/forum-web/src/main/resources/application.yml b/forum-web/src/main/resources/application.yml deleted file mode 100644 index 3fcad9361..000000000 --- a/forum-web/src/main/resources/application.yml +++ /dev/null @@ -1,14 +0,0 @@ -server: - port: 8080 - -spring: - profiles: - active: dal,web,config - main: - allow-circular-references: true - -# mybatis 相关统一配置 -mybatis-plus: - configuration: - #开启下划线转驼峰 - map-underscore-to-camel-case: true diff --git a/forum-web/src/main/resources/logback-spring.xml b/forum-web/src/main/resources/logback-spring.xml deleted file mode 100644 index 843a9d376..000000000 --- a/forum-web/src/main/resources/logback-spring.xml +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - %d [%t] %-5level %logger{36}.%M\(%file:%line\) - %msg%n - - UTF-8 - - - - - - - - - - INFO - - ${log.path}/${log.service.name}-${log.env}.log - - - - - - ${log.path}/arch/${log.service.name}-${log.env}.%d.%i.log - - 3 - - - 100MB - - - - - - [%d{yyyy-MM-dd HH:mm:ss}] {"logger":"%logger{36}", "thread":"%thread", "msg":"%msg %replace(%ex){'\n', ' '}%nopex"}%n - - - UTF-8 - - - - - - ${log.path}/${log.req.name}-${log.env}.log - - - - ${log.path}/arch/req/req.%d{yyyy-MM-dd}.%i.log.gz - - 100MB - - 10 - - 1GB - - - - UTF-8 - [%d{yyyy-MM-dd HH:mm:ss}] - %msg%n - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/forum-web/src/main/resources/schema.sql b/forum-web/src/main/resources/schema.sql deleted file mode 100644 index 55b1d8978..000000000 --- a/forum-web/src/main/resources/schema.sql +++ /dev/null @@ -1,207 +0,0 @@ --- forum.article definition - -CREATE TABLE `article` -( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `user_id` int unsigned NOT NULL COMMENT '用户ID', - `article_type` tinyint NOT NULL DEFAULT '1' COMMENT '文章类型:1-博文,2-问答', - `title` varchar(120) NOT NULL COMMENT '文章标题', - `short_title` varchar(120) NOT NULL COMMENT '短标题', - `picture` varchar(128) NOT NULL DEFAULT '' COMMENT '文章头图', - `summary` varchar(300) NOT NULL DEFAULT '' COMMENT '文章摘要', - `category_id` int unsigned NOT NULL DEFAULT '0' COMMENT '类目ID', - `source` tinyint NOT NULL DEFAULT '1' COMMENT '来源:1-转载,2-原创,3-翻译', - `source_url` varchar(128) NOT NULL DEFAULT '1' COMMENT '原文链接', - `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-未发布,1-已发布', - `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - KEY `idx_category_id` (`category_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章表'; - - --- forum.article_detail definition - -CREATE TABLE `article_detail` -( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `article_id` int unsigned NOT NULL COMMENT '文章ID', - `version` int unsigned NOT NULL COMMENT '版本号', - `content` text COMMENT '文章内容', - `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `idx_article_version` (`article_id`,`version`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章详情表'; - - --- forum.article_tag definition - -CREATE TABLE `article_tag` -( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `article_id` int unsigned NOT NULL DEFAULT '0' COMMENT '文章ID', - `tag_id` int NOT NULL DEFAULT '0' COMMENT '标签', - `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - KEY `idx_tag_id` (`tag_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签映射'; - - --- forum.category definition - -CREATE TABLE `category` -( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `category_name` varchar(64) NOT NULL COMMENT '类目名称', - `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-未发布,1-已发布', - `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='类目管理表'; - - --- forum.comment definition - -CREATE TABLE `comment` -( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `article_id` int unsigned NOT NULL COMMENT '文章ID', - `user_id` int unsigned NOT NULL COMMENT '用户ID', - `content` varchar(300) NOT NULL DEFAULT '' COMMENT '评论内容', - `top_comment_id` int unsigned NOT NULL DEFAULT '0' COMMENT '顶级评论ID', - `parent_comment_id` int unsigned NOT NULL DEFAULT '0' COMMENT '父评论ID', - `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - KEY `idx_article_id_parent_comment_id` (`article_id`, `parent_comment_id`), - KEY `idx_user_id` (`user_id`), - KEY `idx_article_id` (`top_comment_id`), -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表'; - - --- forum.tag definition - -CREATE TABLE `tag` -( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `tag_name` varchar(120) NOT NULL COMMENT '标签名称', - `tag_type` tinyint NOT NULL DEFAULT '1' COMMENT '标签类型:1-系统标签,2-自定义标签', - `category_id` int unsigned NOT NULL DEFAULT '0' COMMENT '类目ID', - `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-未发布,1-已发布', - `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - KEY `idx_category_id` (`category_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签管理表'; - --- forum.read_count 访问计数 - -CREATE TABLE `read_count` ( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `document_id` int unsigned NOT NULL COMMENT '文档ID(文章/评论)', - `document_type` tinyint NOT NULL DEFAULT '1' COMMENT '文档类型:1-文章,2-评论', - `cnt` int unsigned NOT NULL COMMENT '访问计数', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `idx_document_id_type` (`document_id`,`document_type`) -) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='计数表'; - --- forum.`user` definition - -CREATE TABLE `user` -( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `third_account_id` varchar(128) NOT NULL DEFAULT '' COMMENT '第三方用户ID', - `login_type` tinyint NOT NULL DEFAULT '0' COMMENT '登录方式: 0-微信登录,1-账号密码登录', - `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - KEY `key_third_account_id` (`third_account_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户登录表'; - - --- forum.user_foot definition - -CREATE TABLE `user_foot` -( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `user_id` int unsigned NOT NULL COMMENT '用户ID', - `document_id` int unsigned NOT NULL COMMENT '文档ID(文章/评论)', - `document_type` tinyint NOT NULL DEFAULT '1' COMMENT '文档类型:1-文章,2-评论', - `document_user_id` int unsigned NOT NULL DEFAULT '0' COMMENT '发布该文档的用户ID', - `collection_stat` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '收藏状态: 0-未收藏,1-已收藏,2-取消收藏', - `read_stat` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '阅读状态: 0-未读,1-已读', - `comment_stat` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '评论状态: 0-未评论,1-已评论,2-删除评论', - `praise_stat` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '点赞状态: 0-未点赞,1-已点赞,2-取消点赞', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `idx_user_document` (`user_id`,`document_id`,`document_type`,`comment_id`), - KEY `idx_document_id` (`document_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户足迹表'; - - --- forum.user_info definition - -CREATE TABLE `user_info` -( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `user_id` int unsigned NOT NULL COMMENT '用户ID', - `user_name` varchar(50) NOT NULL DEFAULT '' COMMENT '用户名', - `photo` varchar(128) NOT NULL DEFAULT '' COMMENT '用户图像', - `position` varchar(50) NOT NULL DEFAULT '' COMMENT '职位', - `company` varchar(50) NOT NULL DEFAULT '' COMMENT '公司', - `profile` varchar(225) NOT NULL DEFAULT '' COMMENT '个人简介', - `extend` varchar(1024) NOT NULL DEFAULT '' COMMENT '扩展字段', - `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - KEY `key_user_id` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户个人信息表'; - - --- forum.user_relation definition - -CREATE TABLE `user_relation` -( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `user_id` int unsigned NOT NULL COMMENT '用户ID', - `follow_user_id` int unsigned NOT NULL COMMENT '关注用户ID', - `follow_state` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '阅读状态: 0-未关注,1-已关注,2-取消关注', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_user_follow` (`user_id`,`follow_user_id`), - KEY `key_follow_user_id` (`follow_user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户关系表'; - --- 变更记录 -# alter table user_relation -# add `follow_state` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '阅读状态: 0-未关注,1-已关注,2-取消关注'; -# alter table comment change parent_comment_id `parent_comment_id` int unsigned NOT NULL DEFAULT '0' COMMENT '父评论ID'; -# alter table user_foot add `document_user_id` int unsigned NOT NULL COMMENT '发布该文档的用户ID'; -# alter table user_foot -# add `comment_id` int unsigned NOT NULL DEFAULT '0' COMMENT '当前发起评论的ID'; -# alter table user_foot change praise_stat `praise_stat` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '点赞状态: 0-未点赞,1-已点赞'; -# alter table user_foot change collection_stat `collection_stat` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '收藏状态: 0-未收藏,1-已收藏'; -# alter table user_foot change comment_stat `comment_stat` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '评论状态: 0-未评论,1-已评论'; -# drop index idx_user_document on user_foot; -# alter table user_foot add unique index `idx_user_document` (`user_id`,`document_id`,`document_type`,`comment_id`); -# alter table user_foot rename column doucument_id to document_id; -# alter table user_foot rename column doucument_type to document_type; -# alter table user_foot rename column doucument_user_id to document_user_id; --- 删除用户足迹中的评论id -# alter table user_foot drop column comment_id; -# alter table `comment` add column `top_comment_id` int not null default '0' comment '顶级评论ID' after `content`; -# alter table `comment` add column `deleted` tinyint not null default '0' comment '0有效1删除' after `parent_comment_id`; \ No newline at end of file diff --git a/forum-web/src/main/resources/test-data.sql b/forum-web/src/main/resources/test-data.sql deleted file mode 100644 index b29efe7c5..000000000 --- a/forum-web/src/main/resources/test-data.sql +++ /dev/null @@ -1,339 +0,0 @@ --- MySQL dump 10.13 Distrib 8.0.29, for Linux (x86_64) --- --- Host: localhost Database: forum --- ------------------------------------------------------ --- Server version 8.0.29-0ubuntu0.20.04.3 - -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -/*!50503 SET NAMES utf8mb4 */; -/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; -/*!40103 SET TIME_ZONE='+00:00' */; -/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; -/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; -/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; - --- --- Table structure for table `article` --- - -DROP TABLE IF EXISTS `article`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `article` ( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `user_id` int unsigned NOT NULL COMMENT '用户ID', - `article_type` tinyint NOT NULL DEFAULT '1' COMMENT '文章类型:1-博文,2-问答', - `title` varchar(120) NOT NULL COMMENT '文章标题', - `short_title` varchar(120) NOT NULL COMMENT '短标题', - `picture` varchar(128) NOT NULL DEFAULT '' COMMENT '文章头图', - `summary` varchar(300) NOT NULL DEFAULT '' COMMENT '文章摘要', - `category_id` int unsigned NOT NULL DEFAULT '0' COMMENT '类目ID', - `source` tinyint NOT NULL DEFAULT '1' COMMENT '来源:1-转载,2-原创,3-翻译', - `source_url` varchar(128) NOT NULL DEFAULT '1' COMMENT '原文链接', - `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-未发布,1-已发布', - `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - KEY `idx_category_id` (`category_id`) -) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='文章表'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `article` --- - -LOCK TABLES `article` WRITE; -/*!40000 ALTER TABLE `article` DISABLE KEYS */; -INSERT INTO `article` VALUES (3,1,1,'Java小技巧:巧用函数方法实现二维数组遍历','巧用函数方法实现二维数组遍历','','对于数组遍历,基本上每个开发者都写过,遍历本身没什么好说的,但是当我们在遍历的过程中,有一些复杂的业务逻辑时,将会发现代码的层级会逐渐加深',1,2,'',1,0,'2022-08-06 11:52:13','2022-08-07 02:11:42'),(4,1,1,'SpringBoot系列之xml传参与返回实战演练','xml传参与返回实战','','asd',1,2,'',1,0,'2022-08-06 11:55:04','2022-08-07 03:53:37'); -/*!40000 ALTER TABLE `article` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `article_detail` --- - -DROP TABLE IF EXISTS `article_detail`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `article_detail` ( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `article_id` int unsigned NOT NULL COMMENT '文章ID', - `version` int unsigned NOT NULL COMMENT '版本号', - `content` text COMMENT '文章内容', - `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `idx_article_version` (`article_id`,`version`) -) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='文章详情表'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `article_detail` --- - -LOCK TABLES `article_detail` WRITE; -/*!40000 ALTER TABLE `article_detail` DISABLE KEYS */; -INSERT INTO `article_detail` VALUES (3,3,1,'对于数组遍历,基本上每个开发者都写过,遍历本身没什么好说的,但是当我们在遍历的过程中,有一些复杂的业务逻辑时,将会发现代码的层级会逐渐加深\r\n\r\n如一个简单的case,将一个二维数组中的偶数找出来,保存到一个列表中\r\n\r\n二维数组遍历,每个元素判断下是否为偶数,很容易就可以写出来,如\r\n\r\n```java\r\npublic void getEven() {\r\n int[][] cells = new int[][]{{1, 2, 3, 4}, {11, 12, 13, 14}, {21, 22, 23, 24}};\r\n List ans = new ArrayList<>();\r\n for (int i = 0; i < cells.length; i ++) {\r\n for (int j = 0; j < cells[0].length; j++) {\r\n if ((cells[i][j] & 1) == 0) {\r\n ans.add(cells[i][j]);\r\n }\r\n }\r\n }\r\n System.out.println(ans);\r\n}\r\n```\r\n\r\n上面这个实现没啥问题,但是这个代码的深度很容易就有三层了;当上面这个if中如果再有其他的判定条件,那么这个代码层级很容易增加了;二维数组还好,如果是三维数组,一个遍历就是三层;再加点逻辑,四层、五层不也是分分钟的事情么\r\n\r\n那么问题来了,代码层级变多之后会有什么问题呢?\r\n\r\n> 只要代码能跑,又能有什么问题呢?!\r\n\r\n## 1. 函数方法消减代码层级\r\n\r\n由于多维数组的遍历层级天然就很深,那么有办法进行消减么?\r\n\r\n要解决这个问题,关键是要抓住重点,遍历的重点是什么?获取每个元素的坐标!那么我们可以怎么办?\r\n\r\n> 定义一个函数方法,输入的就是函数坐标,在这个函数体中执行我们的遍历逻辑即可\r\n\r\n基于上面这个思路,相信我们可以很容易写一个二维的数组遍历通用方法\r\n\r\n```java\r\npublic static void scan(int maxX, int maxY, BiConsumer consumer) {\r\n for (int i = 0; i < maxX; i++) {\r\n for (int j = 0; j < maxY; j++) {\r\n consumer.accept(i, j);\r\n }\r\n }\r\n}\r\n```\r\n\r\n主要上面的实现,函数方法直接使用了JDK默认提供的BiConsumer,两个传参,都是int 数组下表;无返回值\r\n\r\n那么上面这个怎么用呢?\r\n\r\n同样是上面的例子,改一下之后,如\r\n\r\n```java\r\npublic void getEven() {\r\n int[][] cells = new int[][]{{1, 2, 3, 4}, {11, 12, 13, 14}, {21, 22, 23, 24}};\r\n List ans = new ArrayList<>();\r\n scan(cells.length, cells[0].length, (i, j) -> {\r\n if ((cells[i][j] & 1) == 0) {\r\n ans.add(cells[i][j]);\r\n }\r\n });\r\n System.out.println(ans);\r\n}\r\n```\r\n\r\n相比于前面的,貌似也就少了一层而已,好像也没什么了不起的\r\n\r\n但是,当数组变为三维、四维、无维时,这个改动的写法层级都不会变哦\r\n\r\n## 2. 遍历中return支持\r\n\r\n前面的实现对于正常的遍历没啥问题;但是当我们在遍历过程中,遇到某个条件直接返回,能支持么?\r\n\r\n如一个遍历二维数组,我们希望判断其中是否有偶数,那么可以怎么整?\r\n\r\n仔细琢磨一下我们的scan方法,希望可以支持return,主要的问题点就是这个函数方法执行之后,我该怎么知道是继续循环还是直接return呢?\r\n\r\n很容易想到的就是执行逻辑中,添加一个额外的返回值,用于标记是否中断循环直接返回\r\n\r\n基于此思路,我们可以实现一个简单的demo版本\r\n\r\n定义一个函数方法,接受循环的下标 + 返回值\r\n\r\n```java\r\n@FunctionalInterface\r\npublic interface ScanProcess {\r\n ImmutablePair accept(int i, int j);\r\n}\r\n```\r\n\r\n循环通用方法就可以相应的改成\r\n\r\n```java\r\npublic static T scanReturn(int x, int y, ScanProcess func) {\r\n for (int i = 0; i < x; i++) {\r\n for (int j = 0; j < y; j++) {\r\n ImmutablePair ans = func.accept(i, j);\r\n if (ans != null && ans.left) {\r\n return ans.right;\r\n }\r\n }\r\n }\r\n return null;\r\n}\r\n```\r\n\r\n基于上面这种思路,我们的实际使用姿势如下\r\n\r\n```java\r\n@Test\r\npublic void getEven() {\r\n int[][] cells = new int[][]{{1, 2, 3, 4}, {11, 12, 13, 14}, {21, 22, 23, 24}};\r\n List ans = new ArrayList<>();\r\n scanReturn(cells.length, cells[0].length, (i, j) -> {\r\n if ((cells[i][j] & 1) == 0) {\r\n return ImmutablePair.of(true, i + \"_\" + j);\r\n }\r\n return ImmutablePair.of(false, null);\r\n });\r\n System.out.println(ans);\r\n}\r\n```\r\n\r\n上面这个实现可满足我们的需求,唯一有个别扭的地方就是返回,总有点不太优雅;那么除了这种方式之外,还有其他的方式么?\r\n\r\n既然考虑了返回值,那么再考虑一下传参呢?通过一个定义的参数来装在是否中断以及返回结果,是否可行呢?\r\n\r\n\r\n基于这个思路,我们可以先定义一个参数包装类\r\n\r\n```java\r\npublic static class Ans {\r\n private T ans;\r\n private boolean tag = false;\r\n\r\n public Ans setAns(T ans) {\r\n tag = true;\r\n this.ans = ans;\r\n return this;\r\n }\r\n\r\n public T getAns() {\r\n return ans;\r\n }\r\n}\r\n\r\npublic interface ScanFunc {\r\n void accept(int i, int j, Ans ans)\r\n}\r\n```\r\n\r\n我们希望通过Ans这个类来记录循环结果,其中tag=true,则表示不用继续循环了,直接返回ans结果吧\r\n\r\n与之对应的方法改造及实例如下\r\n\r\n```java\r\npublic static T scanReturn(int x, int y, ScanFunc func) {\r\n Ans ans = new Ans<>();\r\n for (int i = 0; i < x; i++) {\r\n for (int j = 0; j < y; j++) {\r\n func.accept(i, j, ans);\r\n if (ans.tag) {\r\n return ans.ans;\r\n }\r\n }\r\n }\r\n return null;\r\n}\r\n \r\npublic void getEven() {\r\n int[][] cells = new int[][]{{1, 2, 3, 4}, {11, 12, 13, 14}, {21, 22, 23, 24}};\r\n String ans = scanReturn(cells.length, cells[0].length, (i, j, a) -> {\r\n if ((cells[i][j] & 1) == 0) {\r\n a.setAns(i + \"_\" + j);\r\n }\r\n });\r\n System.out.println(ans);\r\n}\r\n```\r\n\r\n这样看起来就比前面的要好一点了\r\n\r\n实际跑一下,看下输出是否和我们预期的一致;\r\n\r\n![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/546a699ae4334df4b6525332da4e5770~tplv-k3u1fbpfcp-watermark.image?)\r\n\r\n## 3.小结\r\n\r\n到此一个小的技巧就分享完毕了,各位感兴趣的小伙伴可以关注我的公众号“一灰灰blog”\r\n\r\n最近正在整理的 * [分布式设计模式综述 | 一灰灰Learning](https://hhui.top/%E5%88%86%E5%B8%83%E5%BC%8F/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/01.%E5%88%86%E5%B8%83%E5%BC%8F%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E7%BB%BC%E8%BF%B0/) 欢迎各位大佬点评\r\n\r\n* [万字总结:分布式系统的38个知识点 - 掘金](https://juejin.cn/post/7125383856651239432)\r\n* [万字详解:MySql,Redis,Mq,ES的高可用方案解析 - 掘金](https://juejin.cn/post/7126864114806177822)\r\n\r\n\r\n\r\n\r\n',0,'2022-08-06 11:52:13','2022-08-07 02:13:51'),(4,4,8,'![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7224ef4796684b6b8ba597716f8cc172~tplv-k3u1fbpfcp-zoom-1.image)\n\n> SpringBoot系列之xml传参与返回实战演练\n\n最近在准备使用微信公众号来做个人站点的登录,发现微信的回调协议居然是xml格式的,之前使用json传输的较多,结果发现换成xml之后,好像并没有想象中的那么顺利,比如回传的数据始终拿不到,返回的数据对方不认等\n\n接下来我们来实际看一下,一个传参和返回都是xml的SpringBoot应用,究竟是怎样的\n\n\n\n## I. 项目搭建\n\n\n本文创建的实例工程采用`SpringBoot 2.2.1.RELEASE` + `maven 3.5.3` + `idea`进行开发\n\n### 1. pom依赖\n\n具体的SpringBoot项目工程创建就不赘述了,对于pom文件中,需要重点关注下面两个依赖类\n\n```xml\n\n \n org.springframework.boot\n spring-boot-starter-web\n \n \n com.fasterxml.jackson.dataformat\n jackson-dataformat-xml\n \n\n```\n\n### 2. 接口调研\n\n我们直接使用微信公众号的回调传参、返回来搭建项目服务,微信开发平台文档如: [基础消息能力](https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html)\n\n\n其定义的推送参数如下\n\n```xml\n\n \n \n 1348831860\n \n \n 1234567890123456\n xxxx\n xxxx\n\n```\n\n要求返回的结果如下\n\n```xml\n\n \n \n 12345678\n \n \n\n```\n\n上面的结构看起来还好,但是需要注意的是外层标签为`xml`,内层标签都是大写开头的;而微信识别返回是大小写敏感的\n\n## II. 实战\n\n项目工程搭建完毕之后,首先定义一个接口,用于接收xml传参,并返回xml对象;\n\n那么核心的问题就是如何定义传参为xml,返回也是xml呢?\n\n> 没错:就是请求头 + 返回头\n\n### 1.REST接口\n\n```java\n@RestController\npublic class XmlRest {\n\n /**\n * curl -X POST \'http://localhost:8080/xml/callback\' -H \'content-type:application/xml\' -d \'165570057911111111\' -i\n *\n * @param msg\n * @param request\n * @return\n */\n @PostMapping(path = \"xml/callback\",\n consumes = {\"application/xml\", \"text/xml\"},\n produces = \"application/xml;charset=utf-8\")\n public WxTxtMsgResVo callBack(@RequestBody WxTxtMsgReqVo msg, HttpServletRequest request) {\n WxTxtMsgResVo res = new WxTxtMsgResVo();\n res.setFromUserName(msg.getToUserName());\n res.setToUserName(msg.getFromUserName());\n res.setCreateTime(System.currentTimeMillis() / 1000);\n res.setMsgType(\"text\");\n res.setContent(\"hello: \" + LocalDateTime.now());\n return res;\n }\n}\n```\n\n注意上面的接口定义,POST传参,请求头和返回头都是 `application/xml`\n\n### 2.请求参数与返回结果对象定义\n\n上面的接口中定义了`WxTxtMsgReqVo`来接收传参,定义`WxTxtMsgResVo`来返回结果,由于我们采用的是xml协议传输数据,这里需要借助`JacksonXmlRootElement`和`JacksonXmlProperty`注解;它们的实际作用与json传输时,使用`JsonProperty`来指定json key的作用相仿\n\n\n下面是具体的实体定义\n\n```java\n@Data\n@JacksonXmlRootElement(localName = \"xml\")\npublic class WxTxtMsgReqVo {\n @JacksonXmlProperty(localName = \"ToUserName\")\n private String toUserName;\n @JacksonXmlProperty(localName = \"FromUserName\")\n private String fromUserName;\n @JacksonXmlProperty(localName = \"CreateTime\")\n private Long createTime;\n @JacksonXmlProperty(localName = \"MsgType\")\n private String msgType;\n @JacksonXmlProperty(localName = \"Content\")\n private String content;\n @JacksonXmlProperty(localName = \"MsgId\")\n private String msgId;\n @JacksonXmlProperty(localName = \"MsgDataId\")\n private String msgDataId;\n @JacksonXmlProperty(localName = \"Idx\")\n private String idx;\n}\n\n@Data\n@JacksonXmlRootElement(localName = \"xml\")\npublic class WxTxtMsgResVo {\n\n @JacksonXmlProperty(localName = \"ToUserName\")\n private String toUserName;\n @JacksonXmlProperty(localName = \"FromUserName\")\n private String fromUserName;\n @JacksonXmlProperty(localName = \"CreateTime\")\n private Long createTime;\n @JacksonXmlProperty(localName = \"MsgType\")\n private String msgType;\n @JacksonXmlProperty(localName = \"Content\")\n private String content;\n}\n```\n\n重点说明:\n\n\n- JacksonXmlRootElement 注解,定义返回的xml文档中最外层的标签名\n- JacksonXmlProperty 注解,定义每个属性值对应的标签名\n- 无需额外添加``,这个会自动添加,防转义\n\n\n\n### 3.测试\n\n然后访问测试一下,直接通过curl来发送xml请求\n\n```bash\ncurl -X POST \'http://localhost:8080/xml/callback\' -H \'content-type:application/xml\' -d \'165570057911111111\' -i\n```\n\n实际响应如下\n\n```bash\nHTTP/1.1 200\nContent-Type: application/xml;charset=utf-8\nTransfer-Encoding: chunked\nDate: Tue, 05 Jul 2022 01:20:32 GMT\n\n123一灰灰blog1656984032texthello: 2022-07-05T09:20:32.155% \n```\n\n\n### 4.问题记录\n\n#### 4.1 HttpMediaTypeNotSupportedException异常\n\n通过前面的方式搭建项目之后,在实际测试时,可能会遇到下面的异常情况`Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type \'application/xml;charset=UTF-8\' not supported]`\n\n\n当出现这个问题时,表明是没有对应的Convert来处理`application/xml`格式的请求头\n\n对应的解决方案则是主动注册上\n\n```java\n@Configuration\npublic class XmlWebConfig implements WebMvcConfigurer {\n @Override\n public void configureMessageConverters(List> converters) {\n converters.add(new MappingJackson2XmlHttpMessageConverter());\n }\n}\n```\n\n#### 4.2 其他json接口也返回xml数据\n\n另外一个场景则是配置了前面的xml之后,导致项目中其他正常的json传参、返回的接口也开始返回xml格式的数据了,此时解决方案如下\n\n```java\n@Configuration\npublic class XmlWebConfig implements WebMvcConfigurer {\n /**\n * 配置这个,默认返回的是json格式数据;若指定了xml返回头,则返回xml格式数据\n *\n * @param configurer\n */\n @Override\n public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {\n configurer.defaultContentType(MediaType.APPLICATION_JSON, MediaType.TEXT_XML, MediaType.APPLICATION_XML);\n }\n}\n```\n\n#### 4.3 微信实际回调参数一直拿不到\n\n这个问题是在实际测试回调的时候遇到的,接口定义之后始终拿不到结果,主要原因就在于最开始没有在定义的实体类上添加 `@JacksonXmlProperty`\n\n当我们没有指定这个注解时,接收的xml标签名与实体对象的fieldName完全相同,既区分大小写\n\n所以为了解决这个问题,就是老老实实如上面的写法,在每个成员上添加注解,如下\n\n```java\n@JacksonXmlProperty(localName = \"ToUserName\")\nprivate String toUserName;\n@JacksonXmlProperty(localName = \"FromUserName\")\nprivate String fromUserName;\n```\n\n### 5.小结\n\n本文主要介绍的是SpringBoot如何支持xml格式的传参与返回,大体上使用姿势与json格式并没有什么区别,但是在实际使用的时候需要注意上面提出的几个问题,避免采坑\n\n关键知识点提炼如下:\n\n- Post接口上,指定请求头和返回头:\n - `consumes = {\"application/xml\", \"text/xml\"},`\n - `produces = \"application/xml;charset=utf-8\"`\n- 实体对象,通过`JacksonXmlRootElement`和`JacksonXmlProperty`来重命名返回的标签名\n- 注册`MappingJackson2XmlHttpMessageConverter`解决HttpMediaTypeNotSupportedException异常\n- 指定`ContentNegotiationConfigurer.defaultContentType` 避免出现所有接口返回xml文档\n\n\n## III. 其他\n\n### 0. 项目与源码\n\n- 工程:[https://github.com/liuyueyi/spring-boot-demo](https://github.com/liuyueyi/spring-boot-demo)\n- 源码:[https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/204-web-xml](https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/204-web-xml)\n\n\n',0,'2022-08-06 11:55:04','2022-08-07 03:53:37'); -/*!40000 ALTER TABLE `article_detail` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `article_tag` --- - -DROP TABLE IF EXISTS `article_tag`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `article_tag` ( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `article_id` int unsigned NOT NULL DEFAULT '0' COMMENT '文章ID', - `tag_id` int NOT NULL DEFAULT '0' COMMENT '标签', - `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - KEY `idx_tag_id` (`tag_id`) -) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='文章标签映射'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `article_tag` --- - -LOCK TABLES `article_tag` WRITE; -/*!40000 ALTER TABLE `article_tag` DISABLE KEYS */; -INSERT INTO `article_tag` VALUES (1,3,1,0,'2022-08-06 11:52:13','2022-08-06 11:52:13'),(9,4,1,0,'2022-08-07 03:52:14','2022-08-07 03:52:14'); -/*!40000 ALTER TABLE `article_tag` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `category` --- - -DROP TABLE IF EXISTS `category`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `category` ( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `category_name` varchar(64) NOT NULL COMMENT '类目名称', - `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-未发布,1-已发布', - `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='类目管理表'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `category` --- - -LOCK TABLES `category` WRITE; -/*!40000 ALTER TABLE `category` DISABLE KEYS */; -INSERT INTO `category` VALUES (1,'后端',1,0,'2022-07-20 06:58:20','2022-07-20 06:58:20'),(2,'前端',1,0,'2022-07-28 03:35:56','2022-07-28 03:35:56'),(3,'大数据',1,0,'2022-07-28 03:36:03','2022-07-28 03:36:03'),(4,'Android',1,0,'2022-07-28 03:36:19','2022-07-28 03:36:19'),(5,'IOS',1,0,'2022-07-28 03:36:24','2022-07-28 03:37:26'),(6,'人工智能',1,0,'2022-07-28 03:36:30','2022-07-28 03:37:26'),(7,'开发工具',1,0,'2022-07-28 03:36:33','2022-07-28 03:37:27'),(8,'代码人生',1,0,'2022-07-28 03:36:37','2022-07-28 03:37:27'),(9,'阅读',1,0,'2022-07-28 03:36:40','2022-07-28 03:37:27'); -/*!40000 ALTER TABLE `category` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `comment` --- - -DROP TABLE IF EXISTS `comment`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `comment` ( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `article_id` int unsigned NOT NULL COMMENT '文章ID', - `user_id` int unsigned NOT NULL COMMENT '用户ID', - `content` varchar(300) NOT NULL DEFAULT '' COMMENT '评论内容', - `parent_comment_id` int unsigned NOT NULL COMMENT '父评论ID', - `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - KEY `idx_article_id` (`article_id`), - KEY `idx_user_id` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='评论表'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `comment` --- - -LOCK TABLES `comment` WRITE; -/*!40000 ALTER TABLE `comment` DISABLE KEYS */; -/*!40000 ALTER TABLE `comment` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `tag` --- - -DROP TABLE IF EXISTS `tag`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `tag` ( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `tag_name` varchar(120) NOT NULL COMMENT '标签名称', - `tag_type` tinyint NOT NULL DEFAULT '1' COMMENT '标签类型:1-系统标签,2-自定义标签', - `category_id` int unsigned NOT NULL DEFAULT '0' COMMENT '类目ID', - `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-未发布,1-已发布', - `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - KEY `idx_category_id` (`category_id`) -) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='标签管理表'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `tag` --- - -LOCK TABLES `tag` WRITE; -/*!40000 ALTER TABLE `tag` DISABLE KEYS */; -INSERT INTO `tag` VALUES (1,'Java',1,1,0,0,'2022-07-20 07:03:56','2022-07-20 07:03:56'),(2,'Go',1,1,0,0,'2022-07-28 03:38:30','2022-07-28 03:38:30'),(3,'Python',1,1,0,0,'2022-07-28 03:38:36','2022-07-28 03:38:36'),(4,'Spring Boot',1,1,0,0,'2022-07-28 03:38:48','2022-07-28 03:38:48'),(5,'Spring',1,1,0,0,'2022-07-28 03:39:01','2022-07-28 03:39:01'),(6,'Redis',1,1,0,0,'2022-07-28 03:39:05','2022-07-28 03:39:05'),(7,'Linux',1,1,0,0,'2022-07-28 03:39:10','2022-07-28 03:39:10'),(8,'JavaScript',1,2,0,0,'2022-07-28 03:39:37','2022-07-28 03:39:37'),(9,'React.js',1,2,0,0,'2022-07-28 03:39:41','2022-07-28 03:39:41'),(10,'Vue.js',1,2,0,0,'2022-07-28 03:41:37','2022-07-28 03:41:37'),(11,'Angular.js',1,2,0,0,'2022-07-28 03:41:51','2022-07-28 03:41:51'),(12,'小程序',1,2,0,0,'2022-07-28 03:42:44','2022-07-28 03:42:44'); -/*!40000 ALTER TABLE `tag` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `user` --- - -DROP TABLE IF EXISTS `user`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `user` ( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `third_account_id` varchar(128) NOT NULL DEFAULT '' COMMENT '第三方用户ID', - `login_type` tinyint NOT NULL DEFAULT '0' COMMENT '登录方式: 0-微信登录,1-账号密码登录', - `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - KEY `key_third_account_id` (`third_account_id`) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户登录表'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `user` --- - -LOCK TABLES `user` WRITE; -/*!40000 ALTER TABLE `user` DISABLE KEYS */; -INSERT INTO `user` VALUES (1,'a7cb7228-0f85-4dd5-845c-7c5df3746e92',0,0,'2022-08-06 12:45:22','2022-08-06 12:45:22'); -/*!40000 ALTER TABLE `user` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `user_foot` --- - -DROP TABLE IF EXISTS `user_foot`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `user_foot` ( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `user_id` int unsigned NOT NULL COMMENT '用户ID', - `doucument_id` int unsigned NOT NULL COMMENT '文档ID(文章/评论)', - `doucument_type` tinyint NOT NULL DEFAULT '1' COMMENT '文档类型:1-文章,2-评论', - `doucument_user_id` int unsigned NOT NULL COMMENT '发布该文档的用户ID', - `comment_id` int unsigned NOT NULL DEFAULT '0' COMMENT '当前发起评论的ID', - `collection_stat` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '收藏状态: 0-未收藏,1-已收藏,2-取消收藏', - `read_stat` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '阅读状态: 0-未读,1-已读', - `comment_stat` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '评论状态: 0-未评论,1-已评论,2-删除评论', - `praise_stat` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '点赞状态: 0-未点赞,1-已点赞,2-取消点赞', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `idx_user_doucument` (`user_id`,`doucument_id`,`doucument_type`,`comment_id`), - KEY `idx_doucument_id` (`doucument_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户足迹表'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `user_foot` --- - -LOCK TABLES `user_foot` WRITE; -/*!40000 ALTER TABLE `user_foot` DISABLE KEYS */; -/*!40000 ALTER TABLE `user_foot` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `user_info` --- - -DROP TABLE IF EXISTS `user_info`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `user_info` ( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `user_id` int unsigned NOT NULL COMMENT '用户ID', - `user_name` varchar(50) NOT NULL DEFAULT '' COMMENT '用户名', - `photo` varchar(128) NOT NULL DEFAULT '' COMMENT '用户图像', - `position` varchar(50) NOT NULL DEFAULT '' COMMENT '职位', - `company` varchar(50) NOT NULL DEFAULT '' COMMENT '公司', - `profile` varchar(225) NOT NULL DEFAULT '' COMMENT '个人简介', - `extend` varchar(1024) NOT NULL DEFAULT '' COMMENT '扩展字段', - `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - KEY `key_user_id` (`user_id`) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户个人信息表'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `user_info` --- - -LOCK TABLES `user_info` WRITE; -/*!40000 ALTER TABLE `user_info` DISABLE KEYS */; -INSERT INTO `user_info` VALUES (1,1,'一灰灰','https://spring.hhui.top/spring-blog/css/images/avatar.jpg','java','xm','码农','',0,'2022-08-06 12:45:22','2022-08-06 12:45:22'); -/*!40000 ALTER TABLE `user_info` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `user_relation` --- - -DROP TABLE IF EXISTS `user_relation`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `user_relation` ( - `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `user_id` int unsigned NOT NULL COMMENT '用户ID', - `follow_user_id` int unsigned NOT NULL COMMENT '关注用户ID', - `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_user_follow` (`user_id`,`follow_user_id`), - KEY `key_follow_user_id` (`follow_user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户关系表'; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `user_relation` --- - -LOCK TABLES `user_relation` WRITE; -/*!40000 ALTER TABLE `user_relation` DISABLE KEYS */; -/*!40000 ALTER TABLE `user_relation` ENABLE KEYS */; -UNLOCK TABLES; -/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; - -/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; -/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; -/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; -/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; -/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; - --- Dump completed on 2022-08-07 11:59:37 diff --git a/forum-web/src/test/java/com/github/liueyueyi/forum/test/DemoTest.java b/forum-web/src/test/java/com/github/liueyueyi/forum/test/DemoTest.java deleted file mode 100644 index 7e3dc8650..000000000 --- a/forum-web/src/test/java/com/github/liueyueyi/forum/test/DemoTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.github.liueyueyi.forum.test; - -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.BiConsumer; - -/** - * @author YiHui - * @date 2022/8/6 - */ -public class DemoTest { - - public static void scan(int maxX, int maxY, BiConsumer consumer) { - for (int i = 0; i < maxX; i++) { - for (int j = 0; j < maxY; j++) { - consumer.accept(i, j); - } - } - } - - public static T scanReturn(int x, int y, ScanProcess func) { - for (int i = 0; i < x; i++) { - for (int j = 0; j < y; j++) { - ImmutablePair ans = func.accept(i, j); - if (ans != null && ans.left) { - return ans.right; - } - } - } - return null; - } - - @FunctionalInterface - public interface ScanProcess { - ImmutablePair accept(int i, int j); - } - - public static T scanReturn(int x, int y, ScanFunc func) { - Ans ans = new Ans<>(); - for (int i = 0; i < x; i++) { - for (int j = 0; j < y; j++) { - func.accept(i, j, ans); - if (ans.tag) { - return ans.ans; - } - } - } - return null; - } - - public interface ScanFunc { - void accept(int i, int j, Ans ans); - } - - public static class Ans { - private T ans; - private boolean tag = false; - - public Ans setAns(T ans) { - tag = true; - this.ans = ans; - return this; - } - - public T getAns() { - return ans; - } - } - - @Test - public void testScan() { - int[][] cells = new int[][]{{1, 2, 3, 4}, {11, 12, 13, 14}, {21, 22, 23, 24}}; - scan(cells.length, cells[0].length, (i, j) -> { - System.out.println(cells[i][j]); - }); - - String ans = scanReturn(cells.length, cells[0].length, (i, j) -> cells[i][j] % 2 == 0 ? - ImmutablePair.of(true, "\"index:\" " + i + " + \"_\" " + j + ";") : - null); - System.out.println(ans); - } - - @Test - public void getEven() { - int[][] cells = new int[][]{{1, 2, 3, 4}, {11, 12, 13, 14}, {21, 22, 23, 24}}; - String ans = scanReturn(cells.length, cells[0].length, (i, j, a) -> { - if ((cells[i][j] & 1) == 0) { - a.setAns(i + "_" + j); - } - }); - System.out.println(ans); - } -} diff --git a/forum-web/src/test/java/com/github/liueyueyi/forum/test/dao/ArticleDaoTest.java b/forum-web/src/test/java/com/github/liueyueyi/forum/test/dao/ArticleDaoTest.java deleted file mode 100644 index 98c1e9ce7..000000000 --- a/forum-web/src/test/java/com/github/liueyueyi/forum/test/dao/ArticleDaoTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.github.liueyueyi.forum.test.dao; - -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.article.dto.ArticleListDTO; -import com.github.liueyueyi.forum.test.BasicTest; -import com.github.liuyueyi.forum.service.article.repository.dao.CategoryDao; -import com.github.liuyueyi.forum.service.article.repository.dao.TagDao; -import com.github.liuyueyi.forum.service.article.repository.entity.CategoryDO; -import com.github.liuyueyi.forum.service.article.repository.entity.TagDO; -import com.github.liuyueyi.forum.service.article.service.ArticleReadService; -import lombok.extern.slf4j.Slf4j; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; - -/** - * @author YiHui - * @date 2022/7/20 - */ -@Slf4j -public class ArticleDaoTest extends BasicTest { - - @Autowired - private TagDao tagDao; - - @Autowired - private CategoryDao categoryDao; - - @Autowired - private ArticleReadService articleService; - - @Test - public void testCategory() { - CategoryDO category = new CategoryDO(); - category.setCategoryName("后端"); - category.setStatus(1); - categoryDao.save(category); - log.info("save category:{} -> id:{}", category, category.getId()); - } - - @Test - public void testTag() { - TagDO tag = new TagDO(); - tag.setTagName("Java"); - tag.setTagType(1); - tag.setCategoryId(1L); - tagDao.save(tag); - log.info("tagId: {}", tag.getId()); - } - - @Test - public void testArticle() { - ArticleListDTO articleListDTO = articleService.queryArticlesByCategory(1L, PageParam.newPageInstance(1L, 10L)); - log.info("articleListDTO: {}", articleListDTO); - } - -} diff --git a/forum-web/src/test/java/com/github/liueyueyi/forum/test/dao/UserDaoTest.java b/forum-web/src/test/java/com/github/liueyueyi/forum/test/dao/UserDaoTest.java deleted file mode 100644 index c5cf7ac31..000000000 --- a/forum-web/src/test/java/com/github/liueyueyi/forum/test/dao/UserDaoTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.github.liueyueyi.forum.test.dao; - -import com.github.liueyueyi.forum.test.BasicTest; -import com.github.liuyueyi.forum.service.user.service.UserService; -import com.github.liueyueyi.forum.api.model.vo.user.dto.UserStatisticInfoDTO; -import lombok.extern.slf4j.Slf4j; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; - -/** - * @author YiHui - * @date 2022/7/20 - */ -@Slf4j -public class UserDaoTest extends BasicTest { - - @Autowired - private UserService userService; - - @Test - public void testUserHome() throws Exception { - UserStatisticInfoDTO userHomeDTO = userService.queryUserInfoWithStatistic(1L); - log.info("query userPageDTO: {}", userHomeDTO); - } -} diff --git a/forum-web/src/test/java/com/github/liueyueyi/forum/test/dao/UserRelationDaoTest.java b/forum-web/src/test/java/com/github/liueyueyi/forum/test/dao/UserRelationDaoTest.java deleted file mode 100644 index ef456bfa5..000000000 --- a/forum-web/src/test/java/com/github/liueyueyi/forum/test/dao/UserRelationDaoTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.github.liueyueyi.forum.test.dao; - -import com.github.liueyueyi.forum.api.model.enums.FollowStateEnum; -import com.github.liueyueyi.forum.api.model.vo.PageParam; -import com.github.liueyueyi.forum.api.model.vo.user.UserRelationReq; -import com.github.liueyueyi.forum.test.BasicTest; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.UserFollowListDTO; -import com.github.liuyueyi.forum.service.user.service.UserRelationService; -import com.github.liuyueyi.forum.service.user.service.UserService; -import lombok.extern.slf4j.Slf4j; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; - -/** - * @author YiHui - * @date 2022/7/20 - */ -@Slf4j -public class UserRelationDaoTest extends BasicTest { - - @Autowired - private UserRelationService userRelationService; - - @Autowired - private UserService userService; - - @Test - public void saveUserRelation() throws Exception { - -// UserRelationReq req1 = new UserRelationReq(); -// req1.setUserId(1L); -// req1.setFollowUserId(2L); -// req1.setFollowState(FollowStateEnum.FOLLOW.getCode()); -// userRelationService.saveUserRelation(req1); -// -// UserRelationReq req2 = new UserRelationReq(); -// req2.setUserId(1L); -// req2.setFollowUserId(3L); -// req2.setFollowState(FollowStateEnum.FOLLOW.getCode()); -// userRelationService.saveUserRelation(req2); -// -// UserRelationReq req3 = new UserRelationReq(); -// req3.setUserId(2L); -// req3.setFollowUserId(1L); -// req3.setFollowState(FollowStateEnum.FOLLOW.getCode()); -// userRelationService.saveUserRelation(req3); - } - - @Test - public void testCancelUserRelation() throws Exception { -// UserRelationReq req = new UserRelationReq(); -// req.setUserRelationId(7L); -// req.setFollowState(FollowStateEnum.CANCEL_FOLLOW.getCode()); -// userRelationService.saveUserRelation(req); - } - - @Test - public void testUserRelation() { - UserFollowListDTO userFollowListDTO = userRelationService.getUserFollowList(1L, PageParam.newPageInstance(1L, 10L)); - log.info("query userFollowDTOS: {}", userFollowListDTO); - - UserFollowListDTO userFansListDTO = userRelationService.getUserFansList(1L, PageParam.newPageInstance(1L, 10L)); - log.info("query userFansList: {}", userFansListDTO); - } -} diff --git a/forum-web/src/test/java/com/github/liueyueyi/forum/test/user/UserServiceTest.java b/forum-web/src/test/java/com/github/liueyueyi/forum/test/user/UserServiceTest.java deleted file mode 100644 index 723b65b6c..000000000 --- a/forum-web/src/test/java/com/github/liueyueyi/forum/test/user/UserServiceTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.github.liueyueyi.forum.test.user; - -import com.github.liueyueyi.forum.api.model.vo.user.UserInfoSaveReq; -import com.github.liueyueyi.forum.api.model.vo.user.UserSaveReq; -import com.github.liueyueyi.forum.test.BasicTest; -import com.github.liuyueyi.forum.service.user.service.UserService; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.UUID; - -/** - * @author YiHui - * @date 2022/8/6 - */ -public class UserServiceTest extends BasicTest { - - @Autowired - private UserService userService; - - /** - * 注册一个用户 - */ - @Test - public void testRegister() { - UserSaveReq req = new UserSaveReq(); - req.setThirdAccountId(UUID.randomUUID().toString()); - req.setLoginType(0); - userService.registerOrGetUserInfo(req); - long userId = req.getUserId(); - - UserInfoSaveReq save = new UserInfoSaveReq(); - save.setUserId(userId); - save.setUserName("一灰灰"); - save.setPhoto("https://spring.hhui.top/spring-blog/css/images/avatar.jpg"); - save.setCompany("xm"); - save.setPosition("java"); - save.setProfile("码农"); - userService.saveUserInfo(save); - } - -} diff --git a/forum-web/src/test/resources/logback-spring.xml b/forum-web/src/test/resources/logback-spring.xml deleted file mode 100644 index 93399c686..000000000 --- a/forum-web/src/test/resources/logback-spring.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - %d [%t] %-5level %logger{36}.%M\(%file:%line\) - %msg%n - - UTF-8 - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/image.md b/image.md new file mode 100644 index 000000000..45240c16c --- /dev/null +++ b/image.md @@ -0,0 +1,8 @@ +![891723526443_.pic.jpg](https://cdn.tobebetterjavaer.com/paicoding/image-fdae281151294c18a14563846393465d.jpg) + + +![itwanger-zsxq-small.png](https://cdn.tobebetterjavaer.com/paicoding/image-f709477e8bbe44dcb10b13770afaff9d.png) + +![zijie-kouzi-fabu.gif](https://cdn.tobebetterjavaer.com/paicoding/image-727b20dd2440447e915f56d81d206466.gif) + +![xiaochengxu.gif](https://cdn.tobebetterjavaer.com/paicoding/image-85ab47515aa94ab99e4a7508b570140b.gif) diff --git a/launch.sh b/launch.sh index 4cf1f38f3..13b13284f 100644 --- a/launch.sh +++ b/launch.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -WEB_PATH="forum-web" -JAR_NAME="forum-web-0.0.1-SNAPSHOT.jar" +WEB_PATH="paicoding-web" +JAR_NAME="paicoding-web-0.0.1-SNAPSHOT.jar" # 部署 function start() { @@ -9,7 +9,7 @@ function start() { # 杀掉之前的进程 cat pid.log| xargs -I {} kill {} - mv ${JAR_NAME} ${JAR_NAME}_bk + mv ${JAR_NAME} ${JAR_NAME}.bak mvn clean install -Dmaven.test.skip=True -Pprod cd ${WEB_PATH} @@ -17,11 +17,7 @@ function start() { cd - mv ${WEB_PATH}/target/${JAR_NAME} ./ - echo "启动脚本:===========" - echo "nohup java -server -Xms512m -Xmx512m -Xmn512m -XX:NativeMemoryTracking=detail -XX:-OmitStackTraceInFastThrow -jar ${JAR_NAME} > /dev/null 2>&1 &" - echo "===========" - nohup java -server -Xms512m -Xmx512m -Xmn512m -XX:NativeMemoryTracking=detail -XX:-OmitStackTraceInFastThrow -jar ${JAR_NAME} > /dev/null 2>&1 & - echo $! 1> pid.log + run } # 重启 @@ -29,11 +25,16 @@ function restart() { # 杀掉之前的进程 cat pid.log| xargs -I {} kill {} # 重新启动 - echo "启动脚本:===========" - echo "nohup java -server -Xms512m -Xmx512m -Xmn512m -XX:NativeMemoryTracking=detail -XX:-OmitStackTraceInFastThrow -jar ${JAR_NAME} > /dev/null 2>&1 &" - echo "===========" - nohup java -server -Xmn512m -Xmn512m -Xmn512m -XX:NativeMemoryTracking=detail -XX:-OmitStackTraceInFastThrow -jar ${JAR_NAME} > /dev/null 2>&1 & - echo $! 1> pid.log + run +} + +function run() { + echo "启动脚本:===========" + echo "nohup java -server -Xms1g -Xmx1g -Xmn512m -XX:NativeMemoryTracking=detail -XX:-OmitStackTraceInFastThrow -jar ${JAR_NAME} > /dev/null 2>&1 &" + echo "===========" + # ms 堆大小 mx 最大堆大小 mn 新生代大小 + nohup java -server -Dspring.devtools.restart.enabled=false -Xms1g -Xmx1g -Xmn256m -XX:NativeMemoryTracking=detail -XX:-OmitStackTraceInFastThrow -jar ${JAR_NAME} > /dev/null 2>&1 & + echo $! 1> pid.log } if [ $# == 0 ]; then diff --git a/paicoding-api/pom.xml b/paicoding-api/pom.xml new file mode 100644 index 000000000..cff7c0265 --- /dev/null +++ b/paicoding-api/pom.xml @@ -0,0 +1,50 @@ + + + + paicoding-forum + com.github.paicoding.forum + 0.0.1-SNAPSHOT + + 4.0.0 + + paicoding-api + + + 8 + 8 + + + + + org.projectlombok + lombok + + + + com.baomidou + mybatis-plus-boot-starter + provided + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + provided + + + + com.github.xiaoymin + knife4j-openapi2-spring-boot-starter + provided + + + com.alibaba + transmittable-thread-local + 2.14.5 + provided + + + + \ No newline at end of file diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/context/ReqInfoContext.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/context/ReqInfoContext.java new file mode 100644 index 000000000..ff720f2de --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/context/ReqInfoContext.java @@ -0,0 +1,94 @@ +package com.github.paicoding.forum.api.model.context; + +import com.alibaba.ttl.TransmittableThreadLocal; +import com.github.paicoding.forum.api.model.vo.seo.Seo; +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; +import lombok.Data; + +import java.security.Principal; + +/** + * 请求上下文,携带用户身份相关信息 + * + * @author YiHui + * @date 2022/7/6 + */ +public class ReqInfoContext { + private static TransmittableThreadLocal contexts = new TransmittableThreadLocal<>(); + + public static void addReqInfo(ReqInfo reqInfo) { + contexts.set(reqInfo); + } + + public static void clear() { + contexts.remove(); + } + + public static ReqInfo getReqInfo() { + return contexts.get(); + } + + @Data + public static class ReqInfo implements Principal { + /** + * appKey + */ + private String appKey; + /** + * 访问的域名 + */ + private String host; + /** + * 访问路径 + */ + private String path; + /** + * 客户端ip + */ + private String clientIp; + /** + * referer + */ + private String referer; + /** + * post 表单参数 + */ + private String payload; + /** + * 设备信息 + */ + private String userAgent; + + /** + * 登录的会话 + */ + private String session; + + /** + * 用户id + */ + private Long userId; + /** + * 用户信息 + */ + private BaseUserInfoDTO user; + /** + * 消息数量 + */ + private Integer msgNum; + + private Seo seo; + + private String deviceId; + + /** + * 当前聊天的会话id + */ + private String chatId; + + @Override + public String getName() { + return session; + } + } +} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/entity/BaseDO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/entity/BaseDO.java similarity index 86% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/entity/BaseDO.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/entity/BaseDO.java index 7197675c3..e6b7fd768 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/entity/BaseDO.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/entity/BaseDO.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.entity; +package com.github.paicoding.forum.api.model.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/entity/BaseDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/entity/BaseDTO.java new file mode 100644 index 000000000..894c77f62 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/entity/BaseDTO.java @@ -0,0 +1,18 @@ +package com.github.paicoding.forum.api.model.entity; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.Date; + +@Data +public class BaseDTO { + @ApiModelProperty(value = "业务主键") + private Long id; + + @ApiModelProperty(value = "创建时间") + private Date createTime; + + @ApiModelProperty(value = "最后编辑时间") + private Date updateTime; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ArticleEventEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ArticleEventEnum.java new file mode 100644 index 000000000..de08b75f8 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ArticleEventEnum.java @@ -0,0 +1,48 @@ +package com.github.paicoding.forum.api.model.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * 文章操作枚举 + * + * @author YiHui + * @date 2022/9/3 + */ +@Getter +public enum ArticleEventEnum { + CREATE(1, "创建"), + ONLINE(2, "发布"), + REVIEW(3, "审核"), + DELETE(4, "删除"), + OFFLINE(5, "下线"), + ; + + + private int type; + private String msg; + + private static Map mapper; + + static { + mapper = new HashMap<>(); + for (ArticleEventEnum type : values()) { + mapper.put(type.type, type); + } + } + + ArticleEventEnum(int type, String msg) { + this.type = type; + this.msg = msg; + } + + public static ArticleEventEnum typeOf(int type) { + return mapper.get(type); + } + + public static ArticleEventEnum typeOf(String type) { + return valueOf(type.toUpperCase().trim()); + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ArticleReadTypeEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ArticleReadTypeEnum.java new file mode 100644 index 000000000..4e8944254 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ArticleReadTypeEnum.java @@ -0,0 +1,39 @@ +package com.github.paicoding.forum.api.model.enums; + +import lombok.Getter; + +import java.util.Objects; + +/** + * 文章阅读类型枚举 + * + * @author YiHui + * @date 2024/10/29 + */ +@Getter +public enum ArticleReadTypeEnum { + NORMAL(0, "直接阅读"), + LOGIN(1, "登录阅读"), + TIME_READ(2, "限时阅读"), + STAR_READ(3, "星球阅读"), + PAY_READ(4, "付费阅读"), + ; + + private Integer type; + + private String desc; + + ArticleReadTypeEnum(Integer type, String desc) { + this.type = type; + this.desc = desc; + } + + public static ArticleReadTypeEnum typeOf(Integer type) { + for (ArticleReadTypeEnum t : values()) { + if (Objects.equals(type, t.type)) { + return t; + } + } + return null; + } +} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/ArticleTypeEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ArticleTypeEnum.java similarity index 84% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/ArticleTypeEnum.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ArticleTypeEnum.java index a6dab4163..cec9fa920 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/ArticleTypeEnum.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ArticleTypeEnum.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.enums; +package com.github.paicoding.forum.api.model.enums; import lombok.Getter; @@ -13,7 +13,9 @@ public enum ArticleTypeEnum { EMPTY(0, ""), BLOG(1, "博文"), - ANSWER(2, "问答"); + ANSWER(2, "问答"), + COLUMN(3, "专栏文章"), + ; ArticleTypeEnum(Integer code, String desc) { this.code = code; diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ChatAnswerTypeEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ChatAnswerTypeEnum.java new file mode 100644 index 000000000..042a034b6 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ChatAnswerTypeEnum.java @@ -0,0 +1,42 @@ +package com.github.paicoding.forum.api.model.enums; + +import lombok.Getter; + +@Getter +public enum ChatAnswerTypeEnum { + // 纯文本 + TEXT(0, "TEXT"), + // JSON, + JSON(1, "JSON"), + /** + * 流式返回 + */ + STREAM(2, "STREAM"), + /** + * 流式结束 + */ + STREAM_END(3, "STREAM_END") + ; + + private Integer code; + private String desc; + + + ChatAnswerTypeEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static ChatAnswerTypeEnum typeOf(int type) { + for (ChatAnswerTypeEnum value : ChatAnswerTypeEnum.values()) { + if (value.code.equals(type)) { + return value; + } + } + return null; + } + + public static ChatAnswerTypeEnum typeOf(String type) { + return valueOf(type.toUpperCase().trim()); + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ChatSocketStateEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ChatSocketStateEnum.java new file mode 100644 index 000000000..a32b3d23c --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ChatSocketStateEnum.java @@ -0,0 +1,44 @@ +package com.github.paicoding.forum.api.model.enums; + +import lombok.Getter; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 6/7/23 + */ +@Getter +public enum ChatSocketStateEnum { + // code desc + // 连接成功 + Established(0, "Established"), + // payload 消息 + Payload(1, "Payload"), + // Closed 关闭 + Closed(2, "Closed"), + ; + + private final Integer code; + private final String desc; + + ChatSocketStateEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + public static ChatSocketStateEnum typeOf(int type) { + for (ChatSocketStateEnum value : ChatSocketStateEnum.values()) { + if (value.code.equals(type)) { + return value; + } + } + return null; + } + + public static ChatSocketStateEnum typeOf(String type) { + return valueOf(type.toUpperCase().trim()); + } + + +} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/CollectionStatEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/CollectionStatEnum.java similarity index 93% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/CollectionStatEnum.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/CollectionStatEnum.java index 55de52b92..3df2048d4 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/CollectionStatEnum.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/CollectionStatEnum.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.enums; +package com.github.paicoding.forum.api.model.enums; import lombok.Getter; diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/CommentStatEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/CommentStatEnum.java similarity index 92% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/CommentStatEnum.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/CommentStatEnum.java index 78349ef4e..be95969d5 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/CommentStatEnum.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/CommentStatEnum.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.enums; +package com.github.paicoding.forum.api.model.enums; import lombok.Getter; diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ConfigTagEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ConfigTagEnum.java new file mode 100644 index 000000000..bca660c3a --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ConfigTagEnum.java @@ -0,0 +1,36 @@ +package com.github.paicoding.forum.api.model.enums; + +import lombok.Getter; + +/** + * 配置类型枚举 + * + * @author louzai + * @since 2022/7/19 + */ +@Getter +public enum ConfigTagEnum { + + EMPTY(0, ""), + HOT(1, "热门"), + OFFICAL(2, "官方"), + COMMENT(3, "推荐"), + ; + + ConfigTagEnum(Integer code, String desc) { + this.code = code; + this.desc = desc; + } + + private final Integer code; + private final String desc; + + public static ConfigTagEnum formCode(Integer code) { + for (ConfigTagEnum value : ConfigTagEnum.values()) { + if (value.getCode().equals(code)) { + return value; + } + } + return ConfigTagEnum.EMPTY; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ConfigTypeEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ConfigTypeEnum.java new file mode 100644 index 000000000..418a8056a --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ConfigTypeEnum.java @@ -0,0 +1,38 @@ +package com.github.paicoding.forum.api.model.enums; + +import lombok.Getter; + +/** + * 配置类型枚举 + * + * @author louzai + * @since 2022/7/19 + */ +@Getter +public enum ConfigTypeEnum { + + EMPTY(0, ""), + HOME_PAGE(1, "首页Banner"), + SIDE_PAGE(2, "侧边Banner"), + ADVERTISEMENT(3, "广告Banner"), + NOTICE(4, "公告"), + COLUMN(5, "教程"), + PDF(6, "电子书"); + + ConfigTypeEnum(Integer code, String desc) { + this.code = code; + this.desc = desc; + } + + private final Integer code; + private final String desc; + + public static ConfigTypeEnum formCode(Integer code) { + for (ConfigTypeEnum value : ConfigTypeEnum.values()) { + if (value.getCode().equals(code)) { + return value; + } + } + return ConfigTypeEnum.EMPTY; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/CreamStatEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/CreamStatEnum.java new file mode 100644 index 000000000..eae36ab99 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/CreamStatEnum.java @@ -0,0 +1,33 @@ +package com.github.paicoding.forum.api.model.enums; + +import lombok.Getter; + +/** + * 加精状态枚举 + * + * @author louzai + * @since 2022/7/19 + */ +@Getter +public enum CreamStatEnum { + + NOT_CREAM(0, "不加精"), + CREAM(1, "加精"); + + CreamStatEnum(Integer code, String desc) { + this.code = code; + this.desc = desc; + } + + private final Integer code; + private final String desc; + + public static CreamStatEnum formCode(Integer code) { + for (CreamStatEnum value : CreamStatEnum.values()) { + if (value.getCode().equals(code)) { + return value; + } + } + return CreamStatEnum.NOT_CREAM; + } +} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/DocumentTypeEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/DocumentTypeEnum.java similarity index 92% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/DocumentTypeEnum.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/DocumentTypeEnum.java index bae850dd5..066091e0f 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/DocumentTypeEnum.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/DocumentTypeEnum.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.enums; +package com.github.paicoding.forum.api.model.enums; import lombok.Getter; diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/FollowSelectEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/FollowSelectEnum.java similarity index 81% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/FollowSelectEnum.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/FollowSelectEnum.java index eb4ca02da..bc7b5bf3a 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/FollowSelectEnum.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/FollowSelectEnum.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.enums; +package com.github.paicoding.forum.api.model.enums; import lombok.Getter; @@ -11,8 +11,8 @@ @Getter public enum FollowSelectEnum { - FOLLOW("follow", "我关注的用户"), - FANS("fans", "关注我的粉丝"); + FOLLOW("follow", "关注列表"), + FANS("fans", "粉丝列表"); FollowSelectEnum(String code, String desc) { this.code = code; diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/FollowStateEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/FollowStateEnum.java similarity index 92% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/FollowStateEnum.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/FollowStateEnum.java index 7a08d8295..56fde96c5 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/FollowStateEnum.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/FollowStateEnum.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.enums; +package com.github.paicoding.forum.api.model.enums; import lombok.Getter; diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/FollowTypeEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/FollowTypeEnum.java similarity index 92% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/FollowTypeEnum.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/FollowTypeEnum.java index 094141518..25b5343f3 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/FollowTypeEnum.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/FollowTypeEnum.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.enums; +package com.github.paicoding.forum.api.model.enums; import lombok.Getter; diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/HomeSelectEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/HomeSelectEnum.java similarity index 93% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/HomeSelectEnum.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/HomeSelectEnum.java index 722f18b03..50cb276ce 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/HomeSelectEnum.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/HomeSelectEnum.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.enums; +package com.github.paicoding.forum.api.model.enums; import lombok.Getter; diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/NotifyStatEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/NotifyStatEnum.java new file mode 100644 index 000000000..acfb1be8f --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/NotifyStatEnum.java @@ -0,0 +1,22 @@ +package com.github.paicoding.forum.api.model.enums; + +import lombok.Getter; + +/** + * @author YiHui + * @date 2022/9/3 + */ +@Getter +public enum NotifyStatEnum { + UNREAD(0, "未读"), + READ(1, "已读"); + + + private int stat; + private String msg; + + NotifyStatEnum(int type, String msg) { + this.stat = type; + this.msg = msg; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/NotifyTypeEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/NotifyTypeEnum.java new file mode 100644 index 000000000..603024370 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/NotifyTypeEnum.java @@ -0,0 +1,63 @@ +package com.github.paicoding.forum.api.model.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author YiHui + * @date 2022/9/3 + */ +@Getter +public enum NotifyTypeEnum { + COMMENT(1, "评论"), + REPLY(2, "回复"), + PRAISE(3, "点赞"), + COLLECT(4, "收藏"), + FOLLOW(5, "关注消息"), + SYSTEM(6, "系统消息"), + DELETE_COMMENT(1, "删除评论"), + DELETE_REPLY(2, "删除回复"), + CANCEL_PRAISE(3, "取消点赞"), + CANCEL_COLLECT(4, "取消收藏"), + CANCEL_FOLLOW(5, "取消关注"), + + // 注册、登录添加系统相关提示消息 + REGISTER(6, "用户注册"), + BIND(6, "绑定星球"), + LOGIN(6, "用户登录"), + + PAYING(6, "支付中通知"), + PAY(6, "支付结果通知"), + ; + + + /** + * 表示消息类型: 1-6 对应的时评论/回复/点赞/关注消息/系统消息等 + */ + private int type; + private String msg; + + private static Map mapper; + + static { + mapper = new HashMap<>(); + for (NotifyTypeEnum type : values()) { + mapper.put(type.type, type); + } + } + + NotifyTypeEnum(int type, String msg) { + this.type = type; + this.msg = msg; + } + + public static NotifyTypeEnum typeOf(int type) { + return mapper.get(type); + } + + public static NotifyTypeEnum typeOf(String type) { + return valueOf(type.toUpperCase().trim()); + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/OfficalStatEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/OfficalStatEnum.java new file mode 100644 index 000000000..11a3d6d9b --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/OfficalStatEnum.java @@ -0,0 +1,33 @@ +package com.github.paicoding.forum.api.model.enums; + +import lombok.Getter; + +/** + * 官方状态枚举 + * + * @author louzai + * @since 2022/7/19 + */ +@Getter +public enum OfficalStatEnum { + + NOT_OFFICAL(0, "非官方"), + OFFICAL(1, "官方"); + + OfficalStatEnum(Integer code, String desc) { + this.code = code; + this.desc = desc; + } + + private final Integer code; + private final String desc; + + public static OfficalStatEnum formCode(Integer code) { + for (OfficalStatEnum value : OfficalStatEnum.values()) { + if (value.getCode().equals(code)) { + return value; + } + } + return OfficalStatEnum.NOT_OFFICAL; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/OperateArticleEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/OperateArticleEnum.java new file mode 100644 index 000000000..f7490ff7c --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/OperateArticleEnum.java @@ -0,0 +1,75 @@ +package com.github.paicoding.forum.api.model.enums; + +import lombok.Getter; + +/** + * 操作文章 + * + * @author louzai + * @since 2022/7/19 + */ +@Getter +public enum OperateArticleEnum { + + EMPTY(0, "") { + @Override + public int getDbStatCode() { + return 0; + } + }, + OFFICAL(1, "官方") { + @Override + public int getDbStatCode() { + return OfficalStatEnum.OFFICAL.getCode(); + } + }, + CANCEL_OFFICAL(2, "非官方"){ + @Override + public int getDbStatCode() { + return OfficalStatEnum.NOT_OFFICAL.getCode(); + } + }, + TOPPING(3, "置顶"){ + @Override + public int getDbStatCode() { + return ToppingStatEnum.TOPPING.getCode(); + } + }, + CANCEL_TOPPING(4, "不置顶"){ + @Override + public int getDbStatCode() { + return ToppingStatEnum.NOT_TOPPING.getCode(); + } + }, + CREAM(5, "加精"){ + @Override + public int getDbStatCode() { + return CreamStatEnum.CREAM.getCode(); + } + }, + CANCEL_CREAM(6, "不加精"){ + @Override + public int getDbStatCode() { + return CreamStatEnum.NOT_CREAM.getCode(); + } + }; + + OperateArticleEnum(Integer code, String desc) { + this.code = code; + this.desc = desc; + } + + private final Integer code; + private final String desc; + + public static OperateArticleEnum fromCode(Integer code) { + for (OperateArticleEnum value : OperateArticleEnum.values()) { + if (value.getCode().equals(code)) { + return value; + } + } + return OperateArticleEnum.OFFICAL; + } + + public abstract int getDbStatCode(); +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/OperateTypeEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/OperateTypeEnum.java new file mode 100644 index 000000000..503d2e1c8 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/OperateTypeEnum.java @@ -0,0 +1,107 @@ +package com.github.paicoding.forum.api.model.enums; + +import lombok.Getter; + +/** + * 操作类型 + * + * @author louzai + * @since 2022/7/19 + */ +@Getter +public enum OperateTypeEnum { + + EMPTY(0, "") { + @Override + public int getDbStatCode() { + return 0; + } + }, + READ(1, "阅读") { + @Override + public int getDbStatCode() { + return ReadStatEnum.READ.getCode(); + } + }, + PRAISE(2, "点赞") { + @Override + public int getDbStatCode() { + return PraiseStatEnum.PRAISE.getCode(); + } + }, + COLLECTION(3, "收藏") { + @Override + public int getDbStatCode() { + return CollectionStatEnum.COLLECTION.getCode(); + } + }, + CANCEL_PRAISE(4, "取消点赞") { + @Override + public int getDbStatCode() { + return PraiseStatEnum.CANCEL_PRAISE.getCode(); + } + }, + CANCEL_COLLECTION(5, "取消收藏") { + @Override + public int getDbStatCode() { + return CollectionStatEnum.CANCEL_COLLECTION.getCode(); + } + }, + COMMENT(6, "评论") { + @Override + public int getDbStatCode() { + return CommentStatEnum.COMMENT.getCode(); + } + }, + DELETE_COMMENT(7, "删除评论") { + @Override + public int getDbStatCode() { + return CommentStatEnum.DELETE_COMMENT.getCode(); + } + }, + ; + + OperateTypeEnum(Integer code, String desc) { + this.code = code; + this.desc = desc; + } + + private final Integer code; + private final String desc; + + public static OperateTypeEnum fromCode(Integer code) { + for (OperateTypeEnum value : OperateTypeEnum.values()) { + if (value.getCode().equals(code)) { + return value; + } + } + return OperateTypeEnum.EMPTY; + } + + public abstract int getDbStatCode(); + + /** + * 判断操作的是否是文章 + * + * @param type + * @return true 表示文章的相关操作 false 表示评论的相关文章 + */ + public static DocumentTypeEnum getOperateDocumentType(OperateTypeEnum type) { + return (type == COMMENT || type == DELETE_COMMENT) ? DocumentTypeEnum.COMMENT : DocumentTypeEnum.ARTICLE; + } + + public static NotifyTypeEnum getNotifyType(OperateTypeEnum type) { + switch (type) { + case PRAISE: + return NotifyTypeEnum.PRAISE; + case CANCEL_PRAISE: + return NotifyTypeEnum.CANCEL_PRAISE; + case COLLECTION: + return NotifyTypeEnum.COLLECT; + case CANCEL_COLLECTION: + return NotifyTypeEnum.CANCEL_COLLECT; + default: + return null; + } + } +} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/PraiseStatEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/PraiseStatEnum.java similarity index 92% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/PraiseStatEnum.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/PraiseStatEnum.java index f32377cf3..b84e657d0 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/PraiseStatEnum.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/PraiseStatEnum.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.enums; +package com.github.paicoding.forum.api.model.enums; import lombok.Getter; diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/PushStatusEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/PushStatusEnum.java similarity index 85% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/PushStatusEnum.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/PushStatusEnum.java index 417e18017..e10c15250 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/PushStatusEnum.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/PushStatusEnum.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.enums; +package com.github.paicoding.forum.api.model.enums; import lombok.Getter; @@ -12,7 +12,8 @@ public enum PushStatusEnum { OFFLINE(0, "未发布"), - ONLINE(1,"已发布"); + ONLINE(1,"已发布"), + REVIEW(2, "审核"); PushStatusEnum(int code, String desc) { this.code = code; diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/ReadStatEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ReadStatEnum.java similarity index 92% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/ReadStatEnum.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ReadStatEnum.java index d8214568e..a029920e8 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/ReadStatEnum.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ReadStatEnum.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.enums; +package com.github.paicoding.forum.api.model.enums; import lombok.Getter; diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/RoleEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/RoleEnum.java new file mode 100644 index 000000000..394b4c26c --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/RoleEnum.java @@ -0,0 +1,33 @@ +package com.github.paicoding.forum.api.model.enums; + +import lombok.Getter; + +import java.util.Objects; + +/** + * @author YiHui + * @date 2023/1/31 + */ +public enum RoleEnum { + NORMAL(0, "普通用户"), + ADMIN(1, "超级用户"), + ; + + @Getter + private int role; + @Getter + private String desc; + + RoleEnum(int role, String desc) { + this.role = role; + this.desc = desc; + } + + public static String role(Integer roleId) { + if (Objects.equals(roleId, 1)) { + return ADMIN.name(); + } else { + return NORMAL.name(); + } + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/SidebarStyleEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/SidebarStyleEnum.java new file mode 100644 index 000000000..ae205b9c3 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/SidebarStyleEnum.java @@ -0,0 +1,29 @@ +package com.github.paicoding.forum.api.model.enums; + +import lombok.Getter; + +/** + * @author YiHui + * @date 2022/9/6 + */ +@Getter +public enum SidebarStyleEnum { + + NOTICE(1), + ARTICLES(2), + RECOMMEND(3), + ABOUT(4), + COLUMN(5), + PDF(6), + SUBSCRIBE(7), + /** + * 活跃排行榜 + */ + ACTIVITY_RANK(8); + + private int style; + + SidebarStyleEnum(int style) { + this.style = style; + } +} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/SourceTypeEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/SourceTypeEnum.java similarity index 92% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/SourceTypeEnum.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/SourceTypeEnum.java index 6edfd5e77..db6112482 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/SourceTypeEnum.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/SourceTypeEnum.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.enums; +package com.github.paicoding.forum.api.model.enums; import lombok.Getter; diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/TagTypeEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/TagTypeEnum.java similarity index 92% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/TagTypeEnum.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/TagTypeEnum.java index bd0e78a39..7f2d40549 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/TagTypeEnum.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/TagTypeEnum.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.enums; +package com.github.paicoding.forum.api.model.enums; import lombok.Getter; diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ToppingStatEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ToppingStatEnum.java new file mode 100644 index 000000000..343a758ef --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ToppingStatEnum.java @@ -0,0 +1,33 @@ +package com.github.paicoding.forum.api.model.enums; + +import lombok.Getter; + +/** + * 置顶状态枚举 + * + * @author louzai + * @since 2022/7/19 + */ +@Getter +public enum ToppingStatEnum { + + NOT_TOPPING(0, "不置顶"), + TOPPING(1, "置顶"); + + ToppingStatEnum(Integer code, String desc) { + this.code = code; + this.desc = desc; + } + + private final Integer code; + private final String desc; + + public static ToppingStatEnum formCode(Integer code) { + for (ToppingStatEnum value : ToppingStatEnum.values()) { + if (value.getCode().equals(code)) { + return value; + } + } + return ToppingStatEnum.NOT_TOPPING; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/WsConnectStateEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/WsConnectStateEnum.java new file mode 100644 index 000000000..5063e026e --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/WsConnectStateEnum.java @@ -0,0 +1,21 @@ +package com.github.paicoding.forum.api.model.enums; + +/** + * websocket 连接 状态 + * + * @author YiHui + * @date 2023/6/12 + */ +public enum WsConnectStateEnum { + // 初始化 + INIT, + // 连接中 + CONNECTING, + // 已连接 + CONNECTED, + // 连接失败 + FAILED, + // 已关闭 + CLOSED, + ; +} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/YesOrNoEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/YesOrNoEnum.java similarity index 96% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/YesOrNoEnum.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/YesOrNoEnum.java index c600f2d0b..876405a6f 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/enums/YesOrNoEnum.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/YesOrNoEnum.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.enums; +package com.github.paicoding.forum.api.model.enums; import lombok.Getter; diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ai/AISourceEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ai/AISourceEnum.java new file mode 100644 index 000000000..aa4fd9d81 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ai/AISourceEnum.java @@ -0,0 +1,82 @@ +package com.github.paicoding.forum.api.model.enums.ai; + +import lombok.Getter; + +/** + * @author YiHui + * @date 2023/6/9 + */ +@Getter +public enum AISourceEnum { + /** + * chatgpt 3.5 + */ + CHAT_GPT_3_5(0, "chatGpt3.5"), + /** + * chatgpt 4 + */ + CHAT_GPT_4(1, "chatGpt4"), + /** + * 技术派的模拟AI + */ + PAI_AI(2, "技术派"), + /** + * 讯飞 + */ + XUN_FEI_AI(3,"讯飞") { + @Override + public boolean syncSupport() { + return false; + } + }, + /** + * 智谱 AI + */ + ZHI_PU_AI(4, "智谱") { + @Override + public boolean asyncSupport() { + return true; + } + }, + /** + * 智谱 AI + */ + ALI_AI(5, "阿里"), + + /** + * 深度求索 AI + */ + DEEP_SEEK(6, "DeepSeek"), + /** + * 豆包 AI + */ + DOU_BAO_AI(7, "豆包") + ; + + + private String name; + private Integer code; + + AISourceEnum(Integer code, String name) { + this.code = code; + this.name = name; + } + + /** + * 是否支持同步 + * + * @return + */ + public boolean syncSupport() { + return true; + } + + /** + * 是否支持异步 + * + * @return + */ + public boolean asyncSupport() { + return true; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ai/AiBotEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ai/AiBotEnum.java new file mode 100644 index 000000000..cf983a7b7 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ai/AiBotEnum.java @@ -0,0 +1,34 @@ +package com.github.paicoding.forum.api.model.enums.ai; + +import lombok.Getter; + +/** + * @author YiHui + * @date 2025/2/24 + */ +@Getter +public enum AiBotEnum { + HATER_BOT("haterBot", "杠精机器人", "你现在是一个名叫\"杠精机器人\"的专业杠精,接下来我给你一个一段文本,你来回复我,回复内容限制在800字符内"), + ; + + /** + * 机器人名,全局唯一,对应 user 表中的 userName + */ + private String userName; + + /** + * 机器人昵称,对应 userInfo 中的 userName + */ + private String nickName; + + /** + * 机器人的提示词 + */ + private String prompt; + + AiBotEnum(String userName, String nickName, String prompt) { + this.userName = userName; + this.nickName = nickName; + this.prompt = prompt; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ai/AiChatStatEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ai/AiChatStatEnum.java new file mode 100644 index 000000000..9794040a6 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/ai/AiChatStatEnum.java @@ -0,0 +1,41 @@ +package com.github.paicoding.forum.api.model.enums.ai; + +/** + * @author YiHui + * @date 2023/6/15 + */ +public enum AiChatStatEnum { + IGNORE(-2) { + @Override + public boolean needResponse() { + return false; + } + }, + /** + * 会话异常 + */ + ERROR(-1), + /** + * 一次问答中,第一次返回 + */ + FIRST(0), + /** + * 一次问答中,中间的返回 + */ + MID(1), + /** + * 一次问答中,最后一次的回复 + */ + END(2), + ; + + private int state; + + AiChatStatEnum(int state) { + this.state = state; + } + + public boolean needResponse() { + return true; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/column/ColumnArticleReadEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/column/ColumnArticleReadEnum.java new file mode 100644 index 000000000..e1b9294b6 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/column/ColumnArticleReadEnum.java @@ -0,0 +1,40 @@ +package com.github.paicoding.forum.api.model.enums.column; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * 专栏文章的阅读类型 + * + * @author YiHui + * @date 2023/8/20 + */ +@AllArgsConstructor +@Getter +public enum ColumnArticleReadEnum { + COLUMN_TYPE(0, "沿用专栏的类型"), + LOGIN(1, "登录阅读"), + TIME_FREE(2, "免费"), + STAR_READ(3, "星球阅读"), + ; + + private int read; + + private String desc; + + private static Map cache; + + static { + cache = new HashMap<>(); + for (ColumnArticleReadEnum r : values()) { + cache.put(r.read, r); + } + } + + public static ColumnArticleReadEnum valueOf(int val) { + return cache.getOrDefault(val, COLUMN_TYPE); + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/column/ColumnStatusEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/column/ColumnStatusEnum.java new file mode 100644 index 000000000..4603d803a --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/column/ColumnStatusEnum.java @@ -0,0 +1,34 @@ +package com.github.paicoding.forum.api.model.enums.column; + +import lombok.Getter; + +/** + * 发布状态枚举 + * + * @author louzai + * @since 2022/7/19 + */ +@Getter +public enum ColumnStatusEnum { + + OFFLINE(0, "未发布"), + CONTINUE(1, "连载"), + OVER(2, "已完结"); + + ColumnStatusEnum(int code, String desc) { + this.code = code; + this.desc = desc; + } + + private final int code; + private final String desc; + + public static ColumnStatusEnum formCode(int code) { + for (ColumnStatusEnum status : values()) { + if (status.getCode() == code) { + return status; + } + } + return ColumnStatusEnum.OFFLINE; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/column/ColumnTypeEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/column/ColumnTypeEnum.java new file mode 100644 index 000000000..9281a285b --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/column/ColumnTypeEnum.java @@ -0,0 +1,36 @@ +package com.github.paicoding.forum.api.model.enums.column; + +import lombok.Getter; + +/** + * 发布状态枚举 + * + * @author louzai + * @since 2022/7/19 + */ +@Getter +public enum ColumnTypeEnum { + + FREE(0, "免费"), + LOGIN(1, "登录阅读"), + TIME_FREE(2, "限时免费"), + STAR_READ(3, "星球阅读"), + ; + + ColumnTypeEnum(int code, String desc) { + this.type = code; + this.desc = desc; + } + + private final int type; + private final String desc; + + public static ColumnTypeEnum formCode(int code) { + for (ColumnTypeEnum status : values()) { + if (status.getType() == code) { + return status; + } + } + return ColumnTypeEnum.FREE; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/pay/PayStatusEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/pay/PayStatusEnum.java new file mode 100644 index 000000000..6638d6392 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/pay/PayStatusEnum.java @@ -0,0 +1,41 @@ +package com.github.paicoding.forum.api.model.enums.pay; + +import lombok.Getter; + +import java.util.Objects; + +/** + * 支付状态 + * + * @author YiHui + * @date 2024/10/29 + */ +@Getter +public enum PayStatusEnum { + + NOT_PAY(0, "未支付"), + + PAYING(1, "支付中"), + + SUCCEED(2, "支付成功"), + + FAIL(3, "支付失败"), + ; + + private Integer status; + private String msg; + + PayStatusEnum(Integer status, String msg) { + this.status = status; + this.msg = msg; + } + + public static PayStatusEnum statusOf(Integer status) { + for (PayStatusEnum p : values()) { + if (Objects.equals(status, p.status)) { + return p; + } + } + return null; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/pay/ThirdPayWayEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/pay/ThirdPayWayEnum.java new file mode 100644 index 000000000..b971f99b0 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/pay/ThirdPayWayEnum.java @@ -0,0 +1,74 @@ +package com.github.paicoding.forum.api.model.enums.pay; + +import lombok.Getter; + +import java.util.Objects; + +/** + * 三方平台支付方式 + * + * @author YiHui + * @date 2024/12/3 + */ +public enum ThirdPayWayEnum { + // // 官方说明有效期五分钟,我们这里设置一下有效期为四分之后,避免正好卡在失效的时间点 + WX_H5("wx_h5", "H5", 250_000) { + @Override + public boolean wxPay() { + return true; + } + }, + // 官方说明有效期为两小时,我们设置为1.8小时之后失效 + WX_JSAPI("wx_jsapi", "JS", 18 * 360_000) { + @Override + public boolean wxPay() { + return true; + } + }, + // 官方说明有效期为两小时,我们设置为1.8小时之后失效 + WX_NATIVE("wx_native", "NA", 18 * 360_000) { + @Override + public boolean wxPay() { + return true; + } + }, + /** + * 个人收款码,基于邮件进行确认的模式,设置30天的有效期 + */ + EMAIL("email", "EM", 30 * 3600_000), + ; + + @Getter + private String pay; + + /** + * 外部支付编号的前缀 + */ + @Getter + private String prefix; + + /** + * prePay有效时间间隔,单位毫秒 + */ + @Getter + private Integer expireTimePeriod; + + ThirdPayWayEnum(String pay, String prefix, Integer expireTimePeriod) { + this.pay = pay; + this.prefix = prefix; + this.expireTimePeriod = expireTimePeriod; + } + + public static ThirdPayWayEnum ofPay(String pay) { + for (ThirdPayWayEnum value : values()) { + if (Objects.equals(value.pay, pay)) { + return value; + } + } + return null; + } + + public boolean wxPay() { + return false; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/rank/ActivityRankTimeEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/rank/ActivityRankTimeEnum.java new file mode 100644 index 000000000..1b34621dd --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/rank/ActivityRankTimeEnum.java @@ -0,0 +1,30 @@ +package com.github.paicoding.forum.api.model.enums.rank; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 活跃排行榜时间周期 + * + * @author YiHui + * @date 2023/8/19 + */ +@AllArgsConstructor +@Getter +public enum ActivityRankTimeEnum { + DAY(1, "day"), + MONTH(2, "month"), + ; + + private int type; + private String desc; + + public static ActivityRankTimeEnum nameOf(String name) { + if (DAY.desc.equalsIgnoreCase(name)) { + return DAY; + } else if (MONTH.desc.equalsIgnoreCase(name)) { + return MONTH; + } + return null; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/site/SiteVisitStatisticsEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/site/SiteVisitStatisticsEnum.java new file mode 100644 index 000000000..d01b44d10 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/site/SiteVisitStatisticsEnum.java @@ -0,0 +1,22 @@ +package com.github.paicoding.forum.api.model.enums.site; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 站点统计类型枚举 + * + * @author YiHui + * @date 2023/8/22 + */ +@AllArgsConstructor +@Getter +public enum SiteVisitStatisticsEnum { + PV(1, "浏览量"), + UV(2, "独立访客"), + VV(3, "访问次数"), + ; + + private int type; + private String desc; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/user/LoginTypeEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/user/LoginTypeEnum.java new file mode 100644 index 000000000..9cf1bdd9d --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/user/LoginTypeEnum.java @@ -0,0 +1,23 @@ +package com.github.paicoding.forum.api.model.enums.user; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author YiHui + * @date 2023/6/26 + */ +@Getter +@AllArgsConstructor +public enum LoginTypeEnum { + /** + * 微信登录 + */ + WECHAT(0), + /** + * 用户名+密码登录 + */ + USER_PWD(1), + ; + private int type; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/user/StarSourceEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/user/StarSourceEnum.java new file mode 100644 index 000000000..0910c7739 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/user/StarSourceEnum.java @@ -0,0 +1,25 @@ +package com.github.paicoding.forum.api.model.enums.user; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 星球来源枚举 + * + * @author YiHui + * @date 2023/6/26 + */ +@Getter +@AllArgsConstructor +public enum StarSourceEnum { + /** + * java进阶 + */ + JAVA_GUIDE(1), + /** + * 技术派 + */ + TECH_PAI(2), + ; + private int source; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/user/UserAIStatEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/user/UserAIStatEnum.java new file mode 100644 index 000000000..eb74e7623 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/user/UserAIStatEnum.java @@ -0,0 +1,36 @@ +package com.github.paicoding.forum.api.model.enums.user; + +import lombok.Getter; + +/** + * 派聪明用户状态枚举 + */ +@Getter +public enum UserAIStatEnum { + IGNORE(-1, "忽略"), + // 审核中 + AUDITING(0, "审核中"), + // 试用中 + TRYING(1, "试用中"), + // 正式用户 + FORMAL(2, "正式用户"), + // 未通过 + NOT_PASS(3, "未通过"); + + UserAIStatEnum(Integer code, String desc) { + this.code = code; + this.desc = desc; + } + + private final Integer code; + private final String desc; + + public static UserAIStatEnum fromCode(Integer code) { + for (UserAIStatEnum value : UserAIStatEnum.values()) { + if (value.getCode().equals(code)) { + return value; + } + } + return UserAIStatEnum.AUDITING; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/user/UserAiStrategyEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/user/UserAiStrategyEnum.java new file mode 100644 index 000000000..39842dc61 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/enums/user/UserAiStrategyEnum.java @@ -0,0 +1,40 @@ +package com.github.paicoding.forum.api.model.enums.user; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * ai可用次数的条件策略 + * + * @author YiHui + * @date 2023/6/26 + */ +@Getter +@AllArgsConstructor +public enum UserAiStrategyEnum { + WECHAT(1), + INVITE_USER(2), + STAR_JAVA_GUIDE(4), + STAR_TECH_PAI(8), + ; + + /** + * 二进制使用姿势 + * 第0位: = 1 表示已绑定微信公众号 + * 第1位: = 1 表示绑定了邀请用户 + * 第2位: = 1 表示绑定了java星球 + * 第3位: = 1 表示绑定了技术派星球 + */ + private Integer condition; + + public Integer updateCondition(Integer input) { + if (input == null) { + input = 0; + } + return input | condition; + } + + public boolean match(Integer strategy) { + return strategy != null && (strategy & condition) == condition.intValue(); + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/event/ArticleMsgEvent.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/event/ArticleMsgEvent.java new file mode 100644 index 000000000..225ce2bb2 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/event/ArticleMsgEvent.java @@ -0,0 +1,30 @@ +package com.github.paicoding.forum.api.model.event; + +import com.github.paicoding.forum.api.model.enums.ArticleEventEnum; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.context.ApplicationEvent; + +/** + * @author YiHui + * @date 2023/2/22 + */ +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = true) +public class ArticleMsgEvent extends ApplicationEvent { + + private ArticleEventEnum type; + + private T content; + + + public ArticleMsgEvent(Object source, ArticleEventEnum type, T content) { + super(source); + this.type = type; + this.content = content; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/event/ConfigRefreshEvent.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/event/ConfigRefreshEvent.java new file mode 100644 index 000000000..983240355 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/event/ConfigRefreshEvent.java @@ -0,0 +1,29 @@ +package com.github.paicoding.forum.api.model.event; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.context.ApplicationEvent; + +/** + * 配置变更消息事件 + * + * @author YiHui + * @date 2023/8/10 + */ +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = true) +public class ConfigRefreshEvent extends ApplicationEvent { + private String key; + private String val; + + + public ConfigRefreshEvent(Object source, String key, String value) { + super(source); + this.key = key; + this.val = value; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/exception/ExceptionUtil.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/exception/ExceptionUtil.java new file mode 100644 index 000000000..18fec1622 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/exception/ExceptionUtil.java @@ -0,0 +1,15 @@ +package com.github.paicoding.forum.api.model.exception; + +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; + +/** + * @author YiHui + * @date 2022/9/2 + */ +public class ExceptionUtil { + + public static ForumException of(StatusEnum status, Object... args) { + return new ForumException(status, args); + } + +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/exception/ForumAdviceException.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/exception/ForumAdviceException.java new file mode 100644 index 000000000..c6ce8c952 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/exception/ForumAdviceException.java @@ -0,0 +1,29 @@ +package com.github.paicoding.forum.api.model.exception; + +import com.github.paicoding.forum.api.model.vo.Status; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import lombok.Getter; + +/** + * 业务异常 + * + * @author YiHui + * @date 2022/9/2 + */ +public class ForumAdviceException extends RuntimeException { + @Getter + private Status status; + + public ForumAdviceException(Status status) { + this.status = status; + } + + public ForumAdviceException(int code, String msg) { + this.status = Status.newStatus(code, msg); + } + + public ForumAdviceException(StatusEnum statusEnum, Object... args) { + this.status = Status.newStatus(statusEnum, args); + } + +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/exception/ForumException.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/exception/ForumException.java new file mode 100644 index 000000000..59c46e2e8 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/exception/ForumException.java @@ -0,0 +1,29 @@ +package com.github.paicoding.forum.api.model.exception; + +import com.github.paicoding.forum.api.model.vo.Status; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import lombok.Getter; + +/** + * 业务异常 + * + * @author YiHui + * @date 2022/9/2 + */ +public class ForumException extends RuntimeException { + @Getter + private Status status; + + public ForumException(Status status) { + this.status = status; + } + + public ForumException(int code, String msg) { + this.status = Status.newStatus(code, msg); + } + + public ForumException(StatusEnum statusEnum, Object... args) { + this.status = Status.newStatus(statusEnum, args); + } + +} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/exception/NoVlaInGuavaException.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/exception/NoVlaInGuavaException.java similarity index 84% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/exception/NoVlaInGuavaException.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/exception/NoVlaInGuavaException.java index cb9444950..e4aa82b91 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/exception/NoVlaInGuavaException.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/exception/NoVlaInGuavaException.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.exception; +package com.github.paicoding.forum.api.model.exception; /** * 未命中异常 diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/package-info.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/package-info.java new file mode 100644 index 000000000..45af1f65c --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/package-info.java @@ -0,0 +1,5 @@ +/** + * @author YiHui + * @date 2022/7/6 + */ +package com.github.paicoding.forum.api.model; \ No newline at end of file diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/NextPageHtmlVo.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/NextPageHtmlVo.java new file mode 100644 index 000000000..20610ba7b --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/NextPageHtmlVo.java @@ -0,0 +1,19 @@ +package com.github.paicoding.forum.api.model.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @author YiHui + * @date 2022/7/6 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class NextPageHtmlVo implements Serializable { + private String html; + private Boolean hasMore; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/PageListVo.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/PageListVo.java new file mode 100644 index 000000000..b00576144 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/PageListVo.java @@ -0,0 +1,39 @@ +package com.github.paicoding.forum.api.model.vo; + +import lombok.Data; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * @author YiHui + * @date 2022/9/4 + */ +@Data +public class PageListVo { + + /** + * 用户列表 + */ + List list; + + /** + * 是否有更多 + */ + private Boolean hasMore; + + public static PageListVo emptyVo() { + PageListVo vo = new PageListVo<>(); + vo.setList(Collections.emptyList()); + vo.setHasMore(false); + return vo; + } + + public static PageListVo newVo(List list, long pageSize) { + PageListVo vo = new PageListVo<>(); + vo.setList(Optional.ofNullable(list).orElse(Collections.emptyList())); + vo.setHasMore(vo.getList().size() == pageSize); + return vo; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/PageParam.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/PageParam.java new file mode 100644 index 000000000..52d773671 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/PageParam.java @@ -0,0 +1,56 @@ +package com.github.paicoding.forum.api.model.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * 数据库分页参数 + * + * @author louzai + * @date 2022-07-120 + */ +@Data +public class PageParam { + + public static final Long DEFAULT_PAGE_NUM = 1L; + public static final Long DEFAULT_PAGE_SIZE = 10L; + + public static final Long TOP_PAGE_SIZE = 4L; + + + @ApiModelProperty("请求页数,从1开始计数") + private long pageNum; + + @ApiModelProperty("请求页大小,默认为 10") + private long pageSize; + private long offset; + private long limit; + + public static PageParam newPageInstance() { + return newPageInstance(DEFAULT_PAGE_NUM, DEFAULT_PAGE_SIZE); + } + + public static PageParam newPageInstance(Integer pageNum, Integer pageSize) { + return newPageInstance(pageNum.longValue(), pageSize.longValue()); + } + + public static PageParam newPageInstance(Long pageNum, Long pageSize) { + if (pageNum == null || pageSize == null) { + return null; + } + + final PageParam pageParam = new PageParam(); + pageParam.pageNum = pageNum; + pageParam.pageSize = pageSize; + + pageParam.offset = (pageNum - 1) * pageSize; + pageParam.limit = pageSize; + + return pageParam; + } + + public static String getLimitSql(PageParam pageParam) { + return String.format("limit %s,%s", pageParam.offset, pageParam.limit); + } + +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/PageVo.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/PageVo.java new file mode 100644 index 000000000..38839150d --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/PageVo.java @@ -0,0 +1,89 @@ +package com.github.paicoding.forum.api.model.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @author LouZai + * @date 2022/9/17 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PageVo { + + private List list; + + private long pageSize; + + private long pageNum; + + private long pageTotal; + + private long total; + + /** + * 构造方法,int参数,需去除 + * + * @param list + * @param pageSize + * @param pageNum + * @param total + * @return + */ + @Deprecated + public PageVo(List list, int pageSize, int pageNum, int total) { + this.list = list; + this.total = total; + this.pageSize = pageSize; + this.pageNum = pageNum; + this.pageTotal = (int) Math.ceil((double) total / pageSize); + } + + /** + * 构造PageVO + * + * @param list + * @param pageSize + * @param pageNum + * @param total + * @return + */ + public PageVo(List list, long pageSize, long pageNum, long total) { + this.list = list; + this.total = total; + this.pageSize = pageSize; + this.pageNum = pageNum; + this.pageTotal = (long) Math.ceil((double) total / pageSize); + } + + /** + * 创建PageVO + * + * @param list + * @param pageSize + * @param pageNum + * @param total + * @return PageVo + */ + @Deprecated + public static PageVo build(List list, int pageSize, int pageNum, int total) { + return new PageVo<>(list, pageSize, pageNum, total); + } + + /** + * 创建PageVO + * + * @param list + * @param pageSize + * @param pageNum + * @param total + * @return PageVo + */ + public static PageVo build(List list, long pageSize, long pageNum, long total) { + return new PageVo<>(list, pageSize, pageNum, total); + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/ResVo.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/ResVo.java new file mode 100644 index 000000000..2ac7971d9 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/ResVo.java @@ -0,0 +1,52 @@ +package com.github.paicoding.forum.api.model.vo; + +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author YiHui + * @date 2022/7/6 + */ +@Data +public class ResVo implements Serializable { + private static final long serialVersionUID = -510306209659393854L; + @ApiModelProperty(value = "返回结果说明", required = true) + private Status status; + + @ApiModelProperty(value = "返回的实体结果", required = true) + private T result; + + + public ResVo() { + } + + public ResVo(Status status) { + this.status = status; + } + + public ResVo(T t) { + status = Status.newStatus(StatusEnum.SUCCESS); + this.result = t; + } + + public static ResVo ok(T t) { + return new ResVo<>(t); + } + + private static final String OK_DEFAULT_MESSAGE = "ok"; + + public static ResVo ok() { + return ok(OK_DEFAULT_MESSAGE); + } + + public static ResVo fail(StatusEnum status, Object... args) { + return new ResVo<>(Status.newStatus(status, args)); + } + + public static ResVo fail(Status status) { + return new ResVo<>(status); + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/Status.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/Status.java new file mode 100644 index 000000000..a08212f1f --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/Status.java @@ -0,0 +1,43 @@ +package com.github.paicoding.forum.api.model.vo; + +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author YiHui + * @date 2022/7/6 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Status { + + /** + * 业务状态码 + */ + @ApiModelProperty(value = "状态码, 0表示成功返回,其他异常返回", required = true, example = "0") + private int code; + + /** + * 描述信息 + */ + @ApiModelProperty(value = "正确返回时为ok,异常时为描述文案", required = true, example = "ok") + private String msg; + + public static Status newStatus(int code, String msg) { + return new Status(code, msg); + } + + public static Status newStatus(StatusEnum status, Object... msgs) { + String msg; + if (msgs.length > 0) { + msg = String.format(status.getMsg(), msgs); + } else { + msg = status.getMsg(); + } + return newStatus(status.getCode(), msg); + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/ArticlePostReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/ArticlePostReq.java new file mode 100644 index 000000000..609220c92 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/ArticlePostReq.java @@ -0,0 +1,124 @@ +package com.github.paicoding.forum.api.model.vo.article; + +import com.github.paicoding.forum.api.model.enums.ArticleReadTypeEnum; +import com.github.paicoding.forum.api.model.enums.ArticleTypeEnum; +import com.github.paicoding.forum.api.model.enums.PushStatusEnum; +import com.github.paicoding.forum.api.model.enums.SourceTypeEnum; +import lombok.Data; + +import java.io.Serializable; +import java.util.Set; + +/** + * 发布文章请求参数 + * + * @author YiHui + * @date 2022/7/24 + */ +@Data +public class ArticlePostReq implements Serializable { + /** + * 文章ID, 当存在时,表示更新文章 + */ + private Long articleId; + /** + * 文章标题 + */ + private String title; + + /** + * 文章短标题 + */ + private String shortTitle; + + /** + * 分类 + */ + private Long categoryId; + + /** + * 标签 + */ + private Set tagIds; + + /** + * 简介 + */ + private String summary; + + /** + * 正文内容 + */ + private String content; + + /** + * 封面 + */ + private String cover; + + /** + * 文本类型 + * + * @see ArticleTypeEnum + */ + private String articleType; + + + /** + * 来源:1-转载,2-原创,3-翻译 + * + * @see SourceTypeEnum + */ + private Integer source; + + /** + * 状态:0-未发布,1-已发布 + * + * @see com.github.paicoding.forum.api.model.enums.PushStatusEnum + */ + private Integer status; + + /** + * 原文地址 + */ + private String sourceUrl; + + /** + * POST 发表, SAVE 暂存 DELETE 删除 + */ + private String actionType; + + /** + * 专栏序号 + */ + private Long columnId; + + /** + * 文章阅读类型 + * + * @see ArticleReadTypeEnum#getType() + */ + private Integer readType; + + /** + * 当 ArticleReadTypeEnum 为 付费阅读时,这里记录具体的收款方式 + */ + private String payWay; + + /** + * 付费解锁价格 + */ + private Integer payAmount; + + public PushStatusEnum pushStatus() { + if ("post".equalsIgnoreCase(actionType)) { + return PushStatusEnum.ONLINE; + } else { + return PushStatusEnum.OFFLINE; + } + } + + public boolean deleted() { + return "delete".equalsIgnoreCase(actionType); + } +} \ No newline at end of file diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/CategoryReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/CategoryReq.java new file mode 100644 index 000000000..8399c7046 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/CategoryReq.java @@ -0,0 +1,30 @@ +package com.github.paicoding.forum.api.model.vo.article; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 保存Category请求参数 + * + * @author LouZai + * @date 2022/9/17 + */ +@Data +public class CategoryReq implements Serializable { + + /** + * ID + */ + private Long categoryId; + + /** + * 类目名称 + */ + private String category; + + /** + * 排序 + */ + private Integer rank; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/ColumnArticleReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/ColumnArticleReq.java new file mode 100644 index 000000000..19cfdbcd2 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/ColumnArticleReq.java @@ -0,0 +1,46 @@ +package com.github.paicoding.forum.api.model.vo.article; + +import com.github.paicoding.forum.api.model.enums.column.ColumnArticleReadEnum; +import lombok.Data; + +import java.io.Serializable; + +/** + * 保存Column文章请求参数 + * + * @author LouZai + * @date 2022/9/26 + */ +@Data +public class ColumnArticleReq implements Serializable { + + /** + * 主键ID + */ + private Long id; + + /** + * 专栏ID + */ + private Long columnId; + + /** + * 文章ID + */ + private Long articleId; + + /** + * 文章排序 + */ + private Integer sort; + + /** + * 教程标题 + */ + private String shortTitle; + + /** + * 阅读方式 + */ + private Integer read; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/ColumnReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/ColumnReq.java new file mode 100644 index 000000000..df2677bd0 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/ColumnReq.java @@ -0,0 +1,70 @@ +package com.github.paicoding.forum.api.model.vo.article; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 保存Column请求参数 + * + * @author LouZai + * @date 2022/9/26 + */ +@Data +public class ColumnReq implements Serializable { + + /** + * ID + */ + private Long columnId; + + /** + * 专栏名 + */ + private String column; + + /** + * 作者 + */ + private Long author; + + /** + * 简介 + */ + private String introduction; + + /** + * 封面 + */ + private String cover; + + /** + * 状态 + */ + private Integer state; + + /** + * 排序 + */ + private Integer section; + + /** + * 专栏预计的文章数 + */ + private Integer nums; + + /** + * 专栏类型 + */ + private Integer type; + + /** + * 限时免费开始时间 + */ + private Long freeStartTime; + + /** + * 限时免费结束时间 + */ + private Long freeEndTime; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/ContentPostReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/ContentPostReq.java new file mode 100644 index 000000000..43affaf95 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/ContentPostReq.java @@ -0,0 +1,19 @@ +package com.github.paicoding.forum.api.model.vo.article; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 发布文章请求参数 + * + * @author YiHui + * @date 2022/7/24 + */ +@Data +public class ContentPostReq implements Serializable { + /** + * 正文内容 + */ + private String content; +} \ No newline at end of file diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SearchArticleReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SearchArticleReq.java new file mode 100644 index 000000000..01de39ae3 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SearchArticleReq.java @@ -0,0 +1,38 @@ +package com.github.paicoding.forum.api.model.vo.article; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel("文章查询") +public class SearchArticleReq { + + // 文章标题 + @ApiModelProperty("文章标题") + private String title; + + @ApiModelProperty("文章ID") + private Long articleId; + + @ApiModelProperty("作者ID") + private Long userId; + + @ApiModelProperty("作者名称") + private String userName; + + @ApiModelProperty("文章状态: 0-未发布,1-已发布,2-审核") + private Integer status; + + @ApiModelProperty("是否官方: 0-非官方,1-官方") + private Integer officalStat; + + @ApiModelProperty("是否置顶: 0-不置顶,1-置顶") + private Integer toppingStat; + + @ApiModelProperty("请求页数,从1开始计数") + private long pageNumber; + + @ApiModelProperty("请求页大小,默认为 10") + private long pageSize; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SearchCategoryReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SearchCategoryReq.java new file mode 100644 index 000000000..a75a6b3a3 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SearchCategoryReq.java @@ -0,0 +1,19 @@ +package com.github.paicoding.forum.api.model.vo.article; + +import lombok.Data; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 5/27/23 + */ +@Data +public class SearchCategoryReq { + // 类目名称 + private String category; + // 分页 + private Long pageNumber; + private Long pageSize; + +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SearchColumnArticleReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SearchColumnArticleReq.java new file mode 100644 index 000000000..26002e762 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SearchColumnArticleReq.java @@ -0,0 +1,28 @@ +package com.github.paicoding.forum.api.model.vo.article; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel("教程配套文章查询") +public class SearchColumnArticleReq { + + // 教程名称 + @ApiModelProperty("教程名称") + private String column; + + // 教程 ID + @ApiModelProperty("教程 ID") + private Long columnId; + + // 文章标题 + @ApiModelProperty("文章标题") + private String articleTitle; + + @ApiModelProperty("请求页数,从1开始计数") + private long pageNumber; + + @ApiModelProperty("请求页大小,默认为 10") + private long pageSize; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SearchColumnReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SearchColumnReq.java new file mode 100644 index 000000000..03dc8d19e --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SearchColumnReq.java @@ -0,0 +1,20 @@ +package com.github.paicoding.forum.api.model.vo.article; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel("教程查询") +public class SearchColumnReq { + + // 教程名称 + @ApiModelProperty("教程名称") + private String column; + + @ApiModelProperty("请求页数,从1开始计数") + private long pageNumber; + + @ApiModelProperty("请求页大小,默认为 10") + private long pageSize; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SearchTagReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SearchTagReq.java new file mode 100644 index 000000000..6a2654ac1 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SearchTagReq.java @@ -0,0 +1,18 @@ +package com.github.paicoding.forum.api.model.vo.article; + +import lombok.Data; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 5/29/23 + */ +@Data +public class SearchTagReq { + // 标签名称 + private String tag; + // 分页 + private Long pageNumber; + private Long pageSize; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SortColumnArticleByIDReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SortColumnArticleByIDReq.java new file mode 100644 index 000000000..304a1c31d --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SortColumnArticleByIDReq.java @@ -0,0 +1,24 @@ +package com.github.paicoding.forum.api.model.vo.article; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 11/25/23 + */ +@Data +@ApiModel("教程排序,根据 ID 和新填的排序") +public class SortColumnArticleByIDReq implements Serializable { + // 要排序的 id + @ApiModelProperty("要排序的 id") + private Long id; + // 新的排序 + @ApiModelProperty("新的排序") + private Integer sort; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SortColumnArticleReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SortColumnArticleReq.java new file mode 100644 index 000000000..6313f8ea5 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/SortColumnArticleReq.java @@ -0,0 +1,26 @@ +package com.github.paicoding.forum.api.model.vo.article; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 11/23/23 + */ +@Data +@ApiModel("教程排序") +public class SortColumnArticleReq implements Serializable { + // 排序前的文章 ID + @ApiModelProperty("排序前的文章 ID") + private Long activeId; + + // 排序后的文章 ID + @ApiModelProperty("排序后的文章 ID") + private Long overId; + +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/TagReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/TagReq.java new file mode 100644 index 000000000..acc70146e --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/TagReq.java @@ -0,0 +1,30 @@ +package com.github.paicoding.forum.api.model.vo.article; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 保存Tag请求参数 + * + * @author LouZai + * @date 2022/9/17 + */ +@Data +public class TagReq implements Serializable { + + /** + * ID + */ + private Long tagId; + + /** + * 标签名称 + */ + private String tag; + + /** + * 类目ID + */ + private Long categoryId; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ArticleAdminDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ArticleAdminDTO.java new file mode 100644 index 000000000..d3f2bb2da --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ArticleAdminDTO.java @@ -0,0 +1,75 @@ +package com.github.paicoding.forum.api.model.vo.article.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 文章信息 + *

+ * DTO 定义返回给 admin 后端的实体类 (VO) + * + * @author 沉默王二 + * @date 2023年05月23日 + */ +@Data +public class ArticleAdminDTO implements Serializable { + private static final long serialVersionUID = -793906904770296838L; + + private Long articleId; + + /** + * 作者uid + */ + private Long author; + + /** + * 作者名 + */ + private String authorName; + + /** + * 作者头像 + */ + private String authorAvatar; + + /** + * 文章标题 + */ + private String title; + + /** + * 短标题 + */ + private String shortTitle; + + /** + * 封面 + */ + private String cover; + + /** + * 0 未发布 1 已发布 + */ + private Integer status; + + /** + * 是否官方 + */ + private Integer officalStat; + + /** + * 是否置顶 + */ + private Integer toppingStat; + + /** + * 是否加精 + */ + private Integer creamStat; + + // 更新时间 + private Date updateTime; + +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ArticleDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ArticleDTO.java new file mode 100644 index 000000000..092cfb46d --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ArticleDTO.java @@ -0,0 +1,172 @@ +package com.github.paicoding.forum.api.model.vo.article.dto; + +import com.github.paicoding.forum.api.model.enums.ArticleReadTypeEnum; +import com.github.paicoding.forum.api.model.enums.SourceTypeEnum; +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import com.github.paicoding.forum.api.model.vo.user.dto.ArticleFootCountDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.SimpleUserInfoDTO; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 文章信息 + *

+ * DTO 定义返回给web前端的实体类 (VO) + * + * @author YiHui + * @date 2022/7/24 + */ +@Data +public class ArticleDTO implements Serializable { + private static final long serialVersionUID = -793906904770296838L; + + private Long articleId; + + /** + * 文章类型:1-博文,2-问答 + */ + private Integer articleType; + + /** + * 作者uid + */ + private Long author; + + /** + * 作者名 + */ + private String authorName; + + /** + * 作者头像 + */ + private String authorAvatar; + + /** + * 文章标题 + */ + private String title; + + /** + * 短标题 + */ + private String shortTitle; + + /** + * 简介 + */ + private String summary; + + /** + * 封面 + */ + private String cover; + + /** + * 正文 + */ + private String content; + + /** + * 文章来源 + * + * @see SourceTypeEnum + */ + private String sourceType; + + /** + * 原文地址 + */ + private String sourceUrl; + + /** + * 0 未发布 1 已发布 + */ + private Integer status; + + /** + * 阅读类型 + * + * @see ArticleReadTypeEnum#getType() + */ + private Integer readType; + + /** + * ture 表示可以阅读 false 表示无法阅读全文 + */ + private Boolean canRead; + + /** + * 是否官方 + */ + private Integer officalStat; + + /** + * 是否置顶 + */ + private Integer toppingStat; + + /** + * 是否加精 + */ + private Integer creamStat; + + /** + * 创建时间 + */ + private Long createTime; + + /** + * 最后更新时间 + */ + private Long lastUpdateTime; + + /** + * 分类 + */ + private CategoryDTO category; + + /** + * 标签 + */ + private List tags; + + /** + * 表示当前查看的用户是否已经点赞过 + */ + private Boolean praised; + + /** + * 表示当用户是否评论过 + */ + private Boolean commented; + + /** + * 表示当前用户是否收藏过 + */ + private Boolean collected; + + /** + * 文章对应的统计计数 + */ + private ArticleFootCountDTO count; + + /** + * 点赞用户信息 + */ + private List praisedUsers; + + /** + * 支付金额,单位(元), 为了防止精度问题,返回String格式 + */ + private String payAmount; + + /** + * 付款方式 + * + * @see ThirdPayWayEnum#wxPay() + */ + private String payWay; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ArticleOtherDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ArticleOtherDTO.java new file mode 100644 index 000000000..5b6cde1b4 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ArticleOtherDTO.java @@ -0,0 +1,17 @@ +package com.github.paicoding.forum.api.model.vo.article.dto; + +import lombok.Data; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 12/8/23 + */ +@Data +public class ArticleOtherDTO { + // 文章的阅读类型 + private Integer readType; + // 教程的翻页 + private ColumnArticleFlipDTO flip; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ArticlePayInfoDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ArticlePayInfoDTO.java new file mode 100644 index 000000000..3c338fc5c --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ArticlePayInfoDTO.java @@ -0,0 +1,65 @@ +package com.github.paicoding.forum.api.model.vo.article.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Map; + +/** + * 文章支付信息 + * + * @author YiHui + * @date 2024/10/29 + */ +@Data +public class ArticlePayInfoDTO implements Serializable { + /** + * 支付id + */ + private Long payId; + + /** + * 文章id + */ + private Long articleId; + + /** + * 支付用户 + */ + private Long payUserId; + + /** + * 支付状态 + */ + private Integer payStatus; + + /** + * 收款用户 + */ + private Long receiveUserId; + + /** + * 收款用户对应的各渠道的收款码 + */ + private Map payQrCodeMap; + + /** + * 支付方式 + */ + private String payWay; + + /** + * 支付金额 + */ + private String payAmount; + + /** + * 支付信息 + */ + private String prePayId; + + /** + * 失效时间 + */ + private Long prePayExpireTime; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/CategoryDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/CategoryDTO.java new file mode 100644 index 000000000..53e6fa20e --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/CategoryDTO.java @@ -0,0 +1,45 @@ +package com.github.paicoding.forum.api.model.vo.article.dto; + +import com.github.paicoding.forum.api.model.enums.PushStatusEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @author YiHui + * @date 2022/7/24 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CategoryDTO implements Serializable { + public static final String DEFAULT_TOTAL_CATEGORY = "全部"; + public static final CategoryDTO DEFAULT_CATEGORY = new CategoryDTO(0L, "全部"); + + private static final long serialVersionUID = 8272116638231812207L; + public static CategoryDTO EMPTY = new CategoryDTO(-1L, "illegal"); + + private Long categoryId; + + private String category; + + private Integer rank; + + private Integer status; + + private Boolean selected; + + public CategoryDTO(Long categoryId, String category) { + this(categoryId, category, 0); + } + + public CategoryDTO(Long categoryId, String category, Integer rank) { + this.categoryId = categoryId; + this.category = category; + this.status = PushStatusEnum.ONLINE.getCode(); + this.rank = rank; + this.selected = false; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ColumnArticleDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ColumnArticleDTO.java new file mode 100644 index 000000000..7ea0ebd67 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ColumnArticleDTO.java @@ -0,0 +1,64 @@ +package com.github.paicoding.forum.api.model.vo.article.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * 文章推荐 + * + * @author YiHui + * @date 2022/9/6 + */ +@Data +@Accessors(chain = true) +public class ColumnArticleDTO implements Serializable { + private static final long serialVersionUID = 3646376715620165839L; + + /** + * 唯一ID + */ + private Long id; + + /** + * 文章ID + */ + private Long articleId; + + /** + * 文章标题 + */ + private String title; + + /** + * 教程名称 + */ + private String shortTitle; + + /** + * 教程ID + */ + private Long columnId; + + /** + * 教程标题 + */ + private String column; + + /** + * 教程封面 + */ + private String columnCover; + + /** + * 文章排序 + */ + private Integer sort; + + /** + * 创建时间 + */ + private Timestamp createTime; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ColumnArticleFlipDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ColumnArticleFlipDTO.java new file mode 100644 index 000000000..df097c928 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ColumnArticleFlipDTO.java @@ -0,0 +1,17 @@ +package com.github.paicoding.forum.api.model.vo.article.dto; + +import lombok.Data; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 12/8/23 + */ +@Data +public class ColumnArticleFlipDTO { + String prevHref; + Boolean prevShow; + String nextHref; + Boolean nextShow; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ColumnArticlesDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ColumnArticlesDTO.java new file mode 100644 index 000000000..b148d9f1c --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ColumnArticlesDTO.java @@ -0,0 +1,60 @@ +package com.github.paicoding.forum.api.model.vo.article.dto; + +import com.github.paicoding.forum.api.model.enums.column.ColumnTypeEnum; +import com.github.paicoding.forum.api.model.vo.comment.dto.TopCommentDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.SimpleUserInfoDTO; +import lombok.Data; + +import java.util.List; + +/** + * @author YiHui + * @date 2022/9/14 + */ +@Data +public class ColumnArticlesDTO { + /** + * 专栏详情 + */ + private Long column; + + /** + * 当前查看的文章 + */ + private Integer section; + + /** + * 文章详情 + */ + private ArticleDTO article; + + /** + * 0 免费阅读 + * 1 要求登录阅读 + * 2 限时免费,若当前时间超过限时免费期,则调整为登录阅读 + * + * @see ColumnTypeEnum#getType() + */ + private Integer readType; + + /** + * 文章评论 + */ + private List comments; + + /** + * 热门评论 + */ + private TopCommentDTO hotComment; + + /** + * 文章目录列表 + */ + private List articleList; + + // 翻页 + private ArticleOtherDTO other; + + // 赞赏用户列表 + private List payUsers; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ColumnDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ColumnDTO.java new file mode 100644 index 000000000..c33350633 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/ColumnDTO.java @@ -0,0 +1,98 @@ +package com.github.paicoding.forum.api.model.vo.article.dto; + +import com.github.paicoding.forum.api.model.enums.column.ColumnStatusEnum; +import com.github.paicoding.forum.api.model.enums.column.ColumnTypeEnum; +import com.github.paicoding.forum.api.model.vo.user.dto.ColumnFootCountDTO; +import lombok.Data; + +/** + * @author YiHui + * @date 2022/9/14 + */ +@Data +public class ColumnDTO { + + /** + * 专栏id + */ + private Long columnId; + + /** + * 专栏名 + */ + private String column; + + /** + * 说明 + */ + private String introduction; + + /** + * 封面 + */ + private String cover; + + /** + * 发布时间 + */ + private Long publishTime; + + /** + * 排序 + */ + private Integer section; + + /** + * 0 未发布 1 连载 2 完结 + * + * @see ColumnStatusEnum#getCode() + */ + private Integer state; + + /** + * 专栏预计的文章数 + */ + private Integer nums; + + /** + * 专栏类型 + * + * @see ColumnTypeEnum#getType() + */ + private Integer type; + + /** + * 限时免费开始时间 + */ + private Long freeStartTime; + + /** + * 限时免费结束时间 + */ + private Long freeEndTime; + + /** + * 作者 + */ + private Long author; + + /** + * 作者名 + */ + private String authorName; + + /** + * 作者头像 + */ + private String authorAvatar; + + /** + * 个人简介 + */ + private String authorProfile; + + /** + * 统计计数相关信息 + */ + private ColumnFootCountDTO count; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/DictCommonDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/DictCommonDTO.java new file mode 100644 index 000000000..a5193d040 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/DictCommonDTO.java @@ -0,0 +1,22 @@ +package com.github.paicoding.forum.api.model.vo.article.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @author YiHui + * @date 2022/7/24 + */ +@Data +public class DictCommonDTO implements Serializable { + private static final long serialVersionUID = -8614833588325787479L; + + private String typeCode; + + private String dictCode; + + private String dictDesc; + + private Integer sortNo; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/PayConfirmDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/PayConfirmDTO.java new file mode 100644 index 000000000..45de4dccd --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/PayConfirmDTO.java @@ -0,0 +1,65 @@ +package com.github.paicoding.forum.api.model.vo.article.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @author YiHui + * @date 2024/10/31 + */ +@Data +public class PayConfirmDTO implements Serializable { + private static final long serialVersionUID = 5470985727304836957L; + + /** + * 文章标题 + */ + private String title; + + /** + * 访问地址 + */ + private String articleUrl; + + /** + * 支付用户 + */ + private String payUser; + + /** + * 打赏时间 + */ + private String payTime; + + /** + * 支付金额 + */ + private String payAmount; + + /** + * 支付方式 + */ + private String payWay; + + /** + * 通知次数 + */ + private Integer notifyCnt; + + /** + * 备注文案 + */ + private String mark; + + /** + * 回调地址 + */ + private String callback; + + /** + * 确认用户 + */ + private Long receiveUserId; + +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/SimpleArticleDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/SimpleArticleDTO.java new file mode 100644 index 000000000..126dde4e5 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/SimpleArticleDTO.java @@ -0,0 +1,45 @@ +package com.github.paicoding.forum.api.model.vo.article.dto; + +import com.github.paicoding.forum.api.model.enums.column.ColumnArticleReadEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * 文章推荐 + * + * @author YiHui + * @date 2022/9/6 + */ +@Data +@Accessors(chain = true) +public class SimpleArticleDTO implements Serializable { + private static final long serialVersionUID = 3646376715620165839L; + + @ApiModelProperty("文章ID") + private Long id; + + @ApiModelProperty("文章标题") + private String title; + + @ApiModelProperty("专栏ID") + private Long columnId; + + @ApiModelProperty("专栏标题") + private String column; + + @ApiModelProperty("文章排序") + private Integer sort; + + @ApiModelProperty("创建时间") + private Timestamp createTime; + + /** + * @see ColumnArticleReadEnum#getRead() + */ + @ApiModelProperty("阅读模式") + private Integer readType; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/SimpleColumnDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/SimpleColumnDTO.java new file mode 100644 index 000000000..d83e164a3 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/SimpleColumnDTO.java @@ -0,0 +1,24 @@ +package com.github.paicoding.forum.api.model.vo.article.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +@Data +@Accessors(chain = true) +public class SimpleColumnDTO implements Serializable { + + private static final long serialVersionUID = 3646376715620165839L; + + @ApiModelProperty("专栏id") + private Long columnId; + + @ApiModelProperty("专栏名") + private String column; + + // 封面 + @ApiModelProperty("封面") + private String cover; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/TagDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/TagDTO.java new file mode 100644 index 000000000..522c2af12 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/TagDTO.java @@ -0,0 +1,22 @@ +package com.github.paicoding.forum.api.model.vo.article.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @author YiHui + * @date 2022/7/24 + */ +@Data +public class TagDTO implements Serializable { + private static final long serialVersionUID = -8614833588325787479L; + + private Long tagId; + + private String tag; + + private Integer status; + + private Boolean selected; +} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/dto/TagSelectDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/TagSelectDTO.java similarity index 87% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/dto/TagSelectDTO.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/TagSelectDTO.java index 81998bd8c..c820bfb45 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/article/dto/TagSelectDTO.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/TagSelectDTO.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.vo.article.dto; +package com.github.paicoding.forum.api.model.vo.article.dto; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/YearArticleDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/YearArticleDTO.java new file mode 100644 index 000000000..a5bd8e3a1 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/article/dto/YearArticleDTO.java @@ -0,0 +1,25 @@ +package com.github.paicoding.forum.api.model.vo.article.dto; + +import lombok.Data; +import lombok.ToString; + +/** + * 创作历程 + * + * @author louzai + * @since 2022/7/19 + */ +@Data +@ToString(callSuper = true) +public class YearArticleDTO { + + /** + * 年份 + */ + private String year; + + /** + * 文章数量 + */ + private Integer articleCount; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/banner/ConfigReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/banner/ConfigReq.java new file mode 100644 index 000000000..1e71c32a5 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/banner/ConfigReq.java @@ -0,0 +1,55 @@ +package com.github.paicoding.forum.api.model.vo.banner; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 保存Banner请求参数 + * + * @author LouZai + * @date 2022/9/17 + */ +@Data +public class ConfigReq implements Serializable { + + /** + * ID + */ + private Long configId; + + /** + * 类型 + */ + private Integer type; + + /** + * 名称 + */ + private String name; + + /** + * 图片链接 + */ + private String bannerUrl; + + /** + * 跳转链接 + */ + private String jumpUrl; + + /** + * 内容 + */ + private String content; + + /** + * 排序 + */ + private Integer rank; + + /** + * 标签 + */ + private String tags; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/banner/SearchConfigReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/banner/SearchConfigReq.java new file mode 100644 index 000000000..be2f3115c --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/banner/SearchConfigReq.java @@ -0,0 +1,23 @@ +package com.github.paicoding.forum.api.model.vo.banner; + +import lombok.Data; + +@Data +public class SearchConfigReq { + /** + * 类型 + */ + private Integer type; + + /** + * 名称 + */ + private String name; + + /** + * 分页 + */ + private Long pageNumber; + private Long pageSize; + +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/banner/dto/ConfigDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/banner/dto/ConfigDTO.java new file mode 100644 index 000000000..0021aeb3a --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/banner/dto/ConfigDTO.java @@ -0,0 +1,62 @@ +package com.github.paicoding.forum.api.model.vo.banner.dto; + +import com.github.paicoding.forum.api.model.entity.BaseDTO; +import com.github.paicoding.forum.api.model.enums.ConfigTagEnum; +import lombok.Data; + +/** + * Banner + * + * @author louzai + * @date 2022-09-17 + */ +@Data +public class ConfigDTO extends BaseDTO { + + /** + * 类型 + */ + private Integer type; + + /** + * 名称 + */ + private String name; + + /** + * 图片链接 + */ + private String bannerUrl; + + /** + * 跳转链接 + */ + private String jumpUrl; + + /** + * 内容 + */ + private String content; + + /** + * 排序 + */ + private Integer rank; + + /** + * 状态:0-未发布,1-已发布 + */ + private Integer status; + + /** + * json格式扩展信息 + */ + private String extra; + + /** + * 配置相关的标签:如 火,推荐,精选 等等,英文逗号分隔 + * + * @see ConfigTagEnum#getCode() + */ + private String tags; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/chat/ChatItemVo.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/chat/ChatItemVo.java new file mode 100644 index 000000000..a93942c6e --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/chat/ChatItemVo.java @@ -0,0 +1,116 @@ +package com.github.paicoding.forum.api.model.vo.chat; + +import com.github.paicoding.forum.api.model.enums.ChatAnswerTypeEnum; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +/** + * 一次qa的聊天记录 + * + * @author YiHui + * @date 2023/6/9 + */ +@Data +@Accessors(chain = true) +public class ChatItemVo implements Serializable, Cloneable { + private static final long serialVersionUID = 7230339040247758226L; + /** + * 唯一的聊天id,不要求存在,主要用于简化流式输出时,前端对返回结果的处理 + */ + private String chatUid; + + /** + * 提问的内容 + */ + private String question; + + /** + * 提问的时间点 + */ + private String questionTime; + + /** + * 回答内容 + */ + private String answer; + + /** + * 回答的时间点 + */ + private String answerTime; + + /** + * 回答的内容类型,文本、JSON 字符串 + */ + private ChatAnswerTypeEnum answerType; + + /** + * 记录问题及记录时间 + * + * @param question + * @return + */ + public ChatItemVo initQuestion(String question) { + this.question = question; + this.questionTime = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss").format(LocalDateTime.now()); + return this; + } + + /** + * 记录返回结果及回答时间 + * + * @param answer + * @return + */ + public ChatItemVo initAnswer(String answer) { + this.answer = answer; + this.answerType = ChatAnswerTypeEnum.TEXT; + setAnswerTime(); + return this; + } + + public ChatItemVo initAnswer(String answer, ChatAnswerTypeEnum answerType) { + this.answer = answer; + this.answerType = answerType; + setAnswerTime(); + return this; + } + + /** + * 流式的追加返回 + * + * @param answer + * @return + */ + public ChatItemVo appendAnswer(String answer) { + if (this.answer == null || this.answer.isEmpty()) { + this.answer = answer; + this.chatUid = UUID.randomUUID().toString().replaceAll("-", ""); + } else { + this.answer += answer; + } + this.answerType = ChatAnswerTypeEnum.STREAM; + setAnswerTime(); + return this; + } + + public ChatItemVo setAnswerTime() { + this.answerTime = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss").format(LocalDateTime.now()); + return this; + } + + @Override + public ChatItemVo clone() { + ChatItemVo item = new ChatItemVo(); + item.question = question; + item.questionTime = questionTime; + item.answer = answer; + item.answerTime = answerTime; + return item; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/chat/ChatRecordsVo.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/chat/ChatRecordsVo.java new file mode 100644 index 000000000..e187b2227 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/chat/ChatRecordsVo.java @@ -0,0 +1,61 @@ +package com.github.paicoding.forum.api.model.vo.chat; + +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 聊天记录 + * + * @author YiHui + * @date 2023/6/9 + */ +@Data +@Accessors(chain = true) +public class ChatRecordsVo implements Serializable, Cloneable { + private static final long serialVersionUID = -2666259615985932920L; + /** + * AI来源 + */ + private AISourceEnum source; + + /** + * 当前用户最多可问答的次数 + */ + private int maxCnt; + + /** + * 使用的次数 + */ + private int usedCnt; + + /** + * 聊天记录,最新的在前面;最多返回50条 + */ + private List records; + + @Override + public ChatRecordsVo clone() { + ChatRecordsVo vo = new ChatRecordsVo(); + vo.source = source; + vo.maxCnt = maxCnt; + vo.usedCnt = usedCnt; + if (records != null) { + vo.setRecords(records.stream().map(ChatItemVo::clone).collect(Collectors.toList())); + } + return vo; + } + + /** + * 判断是否拥有提问次数 + * + * @return true 表示拥有提问次数 + */ + public boolean hasQaCnt() { + return maxCnt > usedCnt; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/chat/ChatSessionItemVo.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/chat/ChatSessionItemVo.java new file mode 100644 index 000000000..f274ac270 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/chat/ChatSessionItemVo.java @@ -0,0 +1,41 @@ +package com.github.paicoding.forum.api.model.vo.chat; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 对话 + * + * @author YiHui + * @date 2025/2/7 + */ +@Data +public class ChatSessionItemVo implements Serializable { + private static final long serialVersionUID = 4083274108548272765L; + /** + * 对话主题 + */ + private String title; + + /** + * 对话id,用于确认聊天历史 + */ + private String chatId; + + /** + * 首次提问时间 + */ + private Long creatTime; + + /** + * 最后一次提问应答时间 + */ + private Long updateTime; + + /** + * 问答次数 + */ + private int qasCnt; + +} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/CommentSaveReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/comment/CommentSaveReq.java similarity index 90% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/CommentSaveReq.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/comment/CommentSaveReq.java index 38c5f6ff1..390482ba6 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/CommentSaveReq.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/comment/CommentSaveReq.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.vo.comment; +package com.github.paicoding.forum.api.model.vo.comment; import lombok.Data; diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/dto/BaseCommentDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/comment/dto/BaseCommentDTO.java similarity index 85% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/dto/BaseCommentDTO.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/comment/dto/BaseCommentDTO.java index 34e1da81b..faaae4e7b 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/dto/BaseCommentDTO.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/comment/dto/BaseCommentDTO.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.vo.comment.dto; +package com.github.paicoding.forum.api.model.vo.comment.dto; import lombok.Data; import org.jetbrains.annotations.NotNull; @@ -47,6 +47,11 @@ public class BaseCommentDTO implements Comparable { */ private Integer praiseCount; + /** + * true 表示已经点赞 + */ + private Boolean praised; + @Override public int compareTo(@NotNull BaseCommentDTO o) { return Long.compare(o.getCommentTime(), this.commentTime); diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/dto/SubCommentDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/comment/dto/SubCommentDTO.java similarity index 88% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/dto/SubCommentDTO.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/comment/dto/SubCommentDTO.java index 49c80e7cb..3bd5fd188 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/dto/SubCommentDTO.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/comment/dto/SubCommentDTO.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.vo.comment.dto; +package com.github.paicoding.forum.api.model.vo.comment.dto; import lombok.Data; import lombok.ToString; diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/dto/TopCommentDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/comment/dto/TopCommentDTO.java similarity index 92% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/dto/TopCommentDTO.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/comment/dto/TopCommentDTO.java index 981ffd1c8..890ee3754 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/comment/dto/TopCommentDTO.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/comment/dto/TopCommentDTO.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.vo.comment.dto; +package com.github.paicoding.forum.api.model.vo.comment.dto; import lombok.Data; import org.jetbrains.annotations.NotNull; diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/config/GlobalConfigReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/config/GlobalConfigReq.java new file mode 100644 index 000000000..536b04927 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/config/GlobalConfigReq.java @@ -0,0 +1,21 @@ +package com.github.paicoding.forum.api.model.vo.config; + +import lombok.Data; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 6/30/23 + */ +@Data +public class GlobalConfigReq { + // 配置项名称 + private String keywords; + // 配置项值 + private String value; + // 备注 + private String comment; + // id + private Long id; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/config/SearchGlobalConfigReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/config/SearchGlobalConfigReq.java new file mode 100644 index 000000000..4bf6f2d71 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/config/SearchGlobalConfigReq.java @@ -0,0 +1,22 @@ +package com.github.paicoding.forum.api.model.vo.config; + +import lombok.Data; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 6/30/23 + */ +@Data +public class SearchGlobalConfigReq { + // 配置项名称 + private String keywords; + // 配置项值 + private String value; + // 备注 + private String comment; + // 分页 + private Long pageNumber; + private Long pageSize; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/config/dto/GlobalConfigDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/config/dto/GlobalConfigDTO.java new file mode 100644 index 000000000..ef5d792ee --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/config/dto/GlobalConfigDTO.java @@ -0,0 +1,26 @@ +package com.github.paicoding.forum.api.model.vo.config.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 6/30/23 + */ +@Data +public class GlobalConfigDTO implements Serializable { + // uid + private static final long serialVersionUID = 1L; + + // id + private Long id; + // 配置项名称 + private String keywords; + // 配置项值 + private String value; + // 备注 + private String comment; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/constants/StatusEnum.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/constants/StatusEnum.java new file mode 100644 index 000000000..9724e9af4 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/constants/StatusEnum.java @@ -0,0 +1,102 @@ +package com.github.paicoding.forum.api.model.vo.constants; + +import lombok.Getter; + +/** + * 异常码规范: + * xxx - xxx - xxx + * 业务 - 状态 - code + *

+ * 业务取值 + * - 100 全局 + * - 200 文章相关 + * - 300 评论相关 + * - 400 用户相关 + *

+ * 状态:基于http status的含义 + * - 4xx 调用方使用姿势问题 + * - 5xx 服务内部问题 + *

+ * code: 具体的业务code + * + * @author YiHui + * @date 2022/7/27 + */ +@Getter +public enum StatusEnum { + SUCCESS(0, "OK"), + + // -------------------------------- 通用 + + // 全局传参异常 + ILLEGAL_ARGUMENTS(100_400_001, "参数异常"), + ILLEGAL_ARGUMENTS_MIXED(100_400_002, "参数异常:%s"), + + // 全局权限相关 + FORBID_ERROR(100_403_001, "无权限"), + + FORBID_ERROR_MIXED(100_403_002, "无权限:%s"), + FORBID_NOTLOGIN(100_403_003, "未登录"), + + // 全局,数据不存在 + RECORDS_NOT_EXISTS(100_404_001, "记录不存在:%s"), + + // 系统异常 + UNEXPECT_ERROR(100_500_001, "非预期异常:%s"), + + // 图片相关异常类型 + UPLOAD_PIC_FAILED(100_500_002, "图片上传失败!"), + + // -------------------------------- + + // 文章相关异常类型,前缀为200 + ARTICLE_NOT_EXISTS(200_404_001, "文章不存在:%s"), + COLUMN_NOT_EXISTS(200_404_002, "教程不存在:%s"), + COLUMN_QUERY_ERROR(200_500_003, "教程查询异常:%s"), + // 教程文章已存在 + COLUMN_ARTICLE_EXISTS(200_500_004, "专栏教程已存在:%s"), + ARTICLE_RELATION_TUTORIAL(200_500_006, "文章已被添加为教程:%s"), + + // -------------------------------- + + // 评论相关异常类型 + COMMENT_NOT_EXISTS(300_404_001, "评论不存在:%s"), + + + // -------------------------------- + + // 用户相关异常 + LOGIN_FAILED_MIXED(400_403_001, "登录失败:%s"), + USER_NOT_EXISTS(400_404_001, "用户不存在:%s"), + USER_EXISTS(400_404_002, "用户已存在:%s"), + // 用户登录名重复 + USER_LOGIN_NAME_REPEAT(400_404_003, "用户登录名重复:%s"), + // 待审核 + USER_NOT_AUDIT(400_500_001, "用户未审核:%s"), + // 星球编号不存在 + USER_STAR_NOT_EXISTS(400_404_002, "星球编号不存在:%s"), + // 星球编号重复 + USER_STAR_REPEAT(400_404_002, "星球编号重复:%s,你已经绑定过了,直接用户名密码/扫码登录吧"), + USER_PWD_ERROR(400_500_002, "用户名or密码错误"); + + private int code; + + private String msg; + + StatusEnum(int code, String msg) { + this.code = code; + this.msg = msg; + } + + public static boolean is5xx(int code) { + return code % 1000_000 / 1000 >= 500; + } + + public static boolean is403(int code) { + return code % 1000_000 / 1000 == 403; + } + + public static boolean is4xx(int code) { + return code % 1000_000 / 1000 < 500; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/notify/NotifyMsgEvent.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/notify/NotifyMsgEvent.java new file mode 100644 index 000000000..4db98e7f2 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/notify/NotifyMsgEvent.java @@ -0,0 +1,32 @@ +package com.github.paicoding.forum.api.model.vo.notify; + +import com.github.paicoding.forum.api.model.enums.NotifyTypeEnum; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.context.ApplicationEvent; + +/** + * @author YiHui + * @date 2022/9/3 + */ +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = true) +public class NotifyMsgEvent extends ApplicationEvent { + + private NotifyTypeEnum notifyType; + + private T content; + + + public NotifyMsgEvent(Object source, NotifyTypeEnum notifyType, T content) { + super(source); + this.notifyType = notifyType; + this.content = content; + } + + +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/notify/dto/NotifyMsgDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/notify/dto/NotifyMsgDTO.java new file mode 100644 index 000000000..9a6e66b80 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/notify/dto/NotifyMsgDTO.java @@ -0,0 +1,62 @@ +package com.github.paicoding.forum.api.model.vo.notify.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @author YiHui + * @date 2022/9/4 + */ +@Data +public class NotifyMsgDTO implements Serializable { + private static final long serialVersionUID = 3833777672628522348L; + + private Long msgId; + + /** + * 消息关联的主体,如文章、评论 + */ + private String relatedId; + + /** + * 关联信息 + */ + private String relatedInfo; + + /** + * 发起消息的用户id + */ + private Long operateUserId; + + /** + * 发起消息的用户名 + */ + private String operateUserName; + + /** + * 发起消息的用户头像 + */ + private String operateUserPhoto; + + /** + * 消息类型 + */ + private Integer type; + + /** + * 消息正文 + */ + private String msg; + + /** + * 1 已读/ 0 未读 + */ + private Integer state; + + /** + * 消息产生时间 + */ + private Timestamp createTime; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/pay/dto/PayInfoDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/pay/dto/PayInfoDTO.java new file mode 100644 index 000000000..b35fedc69 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/pay/dto/PayInfoDTO.java @@ -0,0 +1,40 @@ +package com.github.paicoding.forum.api.model.vo.pay.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Map; + +/** + * 用于支付的相关信息 + * + * @author YiHui + * @date 2024/12/9 + */ +@Data +public class PayInfoDTO implements Serializable { + /** + * 收款用户对应的各渠道的收款码 + */ + private Map payQrCodeMap; + + /** + * 支付方式 + */ + private String payWay; + + /** + * 支付金额 + */ + private String payAmount; + + /** + * 支付信息 + */ + private String prePayId; + + /** + * 失效时间 + */ + private Long prePayExpireTime; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/rank/RankItemReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/rank/RankItemReq.java new file mode 100644 index 000000000..d51986ee5 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/rank/RankItemReq.java @@ -0,0 +1,9 @@ +package com.github.paicoding.forum.api.model.vo.rank; + +/** + * @author YiHui + * @date 2023/8/19 + */ +public class RankItemReq { + private String time; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/rank/dto/RankInfoDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/rank/dto/RankInfoDTO.java new file mode 100644 index 000000000..906836176 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/rank/dto/RankInfoDTO.java @@ -0,0 +1,18 @@ +package com.github.paicoding.forum.api.model.vo.rank.dto; + +import com.github.paicoding.forum.api.model.enums.rank.ActivityRankTimeEnum; +import lombok.Data; + +import java.util.List; + +/** + * 排行榜信息 + * + * @author YiHui + * @date 2023/8/19 + */ +@Data +public class RankInfoDTO { + private ActivityRankTimeEnum time; + private List items; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/rank/dto/RankItemDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/rank/dto/RankItemDTO.java new file mode 100644 index 000000000..af7bb602c --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/rank/dto/RankItemDTO.java @@ -0,0 +1,31 @@ +package com.github.paicoding.forum.api.model.vo.rank.dto; + +import com.github.paicoding.forum.api.model.vo.user.dto.SimpleUserInfoDTO; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 排行榜信息 + * + * @author YiHui + * @date 2023/8/19 + */ +@Data +@Accessors(chain = true) +public class RankItemDTO { + + /** + * 排名 + */ + private Integer rank; + + /** + * 评分 + */ + private Integer score; + + /** + * 用户 + */ + private SimpleUserInfoDTO user; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/recommend/CarouseDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/recommend/CarouseDTO.java new file mode 100644 index 000000000..2bc78ff15 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/recommend/CarouseDTO.java @@ -0,0 +1,29 @@ +package com.github.paicoding.forum.api.model.vo.recommend; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @author YiHui + * @date 2022/9/7 + */ +@Data +@Accessors(chain = true) +public class CarouseDTO implements Serializable { + + private static final long serialVersionUID = 1048555496974144842L; + /** + * 说明 + */ + private String name; + /** + * 图片地址 + */ + private String imgUrl; + /** + * 跳转地址 + */ + private String actionUrl; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/recommend/RateVisitDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/recommend/RateVisitDTO.java new file mode 100644 index 000000000..04ec9fd5d --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/recommend/RateVisitDTO.java @@ -0,0 +1,42 @@ +package com.github.paicoding.forum.api.model.vo.recommend; + +import lombok.Data; + +/** + * 资源的访问、评分信息 + * + * @author YiHui + * @date 2023/1/3 + */ +@Data +public class RateVisitDTO { + + /** + * 查看次数 + */ + private Integer visit; + + /** + * 下载次数 + */ + private Integer download; + + /** + * 评分, 浮点数,string方式返回,避免精度问题 + */ + private String rate; + + public RateVisitDTO() { + visit = 0; + download = 0; + rate = "8"; + } + + public void incrVisit() { + visit += 1; + } + + public void incrDownload() { + download += 1; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/recommend/SideBarDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/recommend/SideBarDTO.java new file mode 100644 index 000000000..ff1e485f2 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/recommend/SideBarDTO.java @@ -0,0 +1,39 @@ +package com.github.paicoding.forum.api.model.vo.recommend; + +import com.github.paicoding.forum.api.model.enums.SidebarStyleEnum; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 侧边推广信息 + * + * @author YiHui + * @date 2022/9/6 + */ +@Data +@Accessors(chain = true) +public class SideBarDTO { + + private String title; + + private String subTitle; + + private String icon; + + private String img; + + private String url; + + private String content; + + private List items; + + /** + * 侧边栏样式 + * + * @see SidebarStyleEnum#getStyle() + */ + private Integer style; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/recommend/SideBarItemDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/recommend/SideBarItemDTO.java new file mode 100644 index 000000000..7b81fa6b6 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/recommend/SideBarItemDTO.java @@ -0,0 +1,37 @@ +package com.github.paicoding.forum.api.model.vo.recommend; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 侧边推广信息 + * + * @author YiHui + * @date 2022/9/6 + */ +@Data +@Accessors(chain = true) +public class SideBarItemDTO { + + private String title; + + private String name; + + private String url; + + private String img; + + private Long time; + + /** + * tag列表 + */ + private List tags; + + /** + * 评分信息 + */ + private RateVisitDTO visit; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/seo/Seo.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/seo/Seo.java new file mode 100644 index 000000000..c0ef7c3b9 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/seo/Seo.java @@ -0,0 +1,14 @@ +package com.github.paicoding.forum.api.model.vo.seo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +@Builder +public class Seo { + private List ogp; + private Map jsonLd; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/seo/SeoTagVo.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/seo/SeoTagVo.java new file mode 100644 index 000000000..917d5b51d --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/seo/SeoTagVo.java @@ -0,0 +1,19 @@ +package com.github.paicoding.forum.api.model.vo.seo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author YiHui + * @date 2023/2/13 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SeoTagVo { + + private String key; + + private String val; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/shortlink/ShortLinkReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/shortlink/ShortLinkReq.java new file mode 100644 index 000000000..8b0bebe97 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/shortlink/ShortLinkReq.java @@ -0,0 +1,21 @@ +package com.github.paicoding.forum.api.model.vo.shortlink; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 短链接请求对象 + * + * @author betasecond + * @date 2025-02-13 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ShortLinkReq { + /** + * 原始URL + */ + private String originalUrl; +} \ No newline at end of file diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/shortlink/ShortLinkVO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/shortlink/ShortLinkVO.java new file mode 100644 index 000000000..c2c26146f --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/shortlink/ShortLinkVO.java @@ -0,0 +1,26 @@ +package com.github.paicoding.forum.api.model.vo.shortlink; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 短链接返回对象 + * + * @author betasecond + * @date 2025-02-13 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ShortLinkVO { + /** + * 短链接URL + */ + private String shortUrl; + + /** + * 原始URL + */ + private String originalUrl; +} \ No newline at end of file diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/shortlink/dto/ShortLinkDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/shortlink/dto/ShortLinkDTO.java new file mode 100644 index 000000000..0e6962005 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/shortlink/dto/ShortLinkDTO.java @@ -0,0 +1,31 @@ +package com.github.paicoding.forum.api.model.vo.shortlink.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 短链接传输对象 + * + * @author betasecond + * @date 2025-02-13 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ShortLinkDTO { + /** + * 原始URL + */ + private String originalUrl; + + /** + * 用户ID + */ + private String userId; + + /** + * 短链接代码 + */ + private String shortCode; +} \ No newline at end of file diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/statistics/dto/StatisticsCountDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/statistics/dto/StatisticsCountDTO.java new file mode 100644 index 000000000..cfeb91340 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/statistics/dto/StatisticsCountDTO.java @@ -0,0 +1,60 @@ +package com.github.paicoding.forum.api.model.vo.statistics.dto; + +import lombok.Builder; +import lombok.Data; + +/** + * 统计计数 + * + * @author louzai + * @date 2022-10-1 + */ +@Data +@Builder +public class StatisticsCountDTO { + + /** + * PV 数量 + */ + private Long pvCount; + + /** + * 总用户数 + */ + private Long userCount; + + /** + * 总评论数 + */ + private Long commentCount; + + /** + * 总阅读数 + */ + private Long readCount; + + /** + * 总点赞数 + */ + private Long likeCount; + + /** + * 总收藏数 + */ + private Long collectCount; + + /** + * 文章数量 + */ + private Long articleCount; + + /** + * 教程数量 + */ + private Long tutorialCount; + + /** + * 星球付费人数 + */ + private Integer starPayCount; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/statistics/dto/StatisticsDayDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/statistics/dto/StatisticsDayDTO.java new file mode 100644 index 000000000..15a74b73e --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/statistics/dto/StatisticsDayDTO.java @@ -0,0 +1,28 @@ +package com.github.paicoding.forum.api.model.vo.statistics.dto; + +import lombok.Data; + +/** + * 每天的统计计数 + * + * @author louzai + * @date 2022-10-1 + */ +@Data +public class StatisticsDayDTO { + + /** + * 日期 + */ + private String date; + + /** + * 数量 + */ + private Long pvCount; + + /** + * UV数量 + */ + private Long uvCount; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/SearchZsxqUserReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/SearchZsxqUserReq.java new file mode 100644 index 000000000..914fc67b2 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/SearchZsxqUserReq.java @@ -0,0 +1,24 @@ +package com.github.paicoding.forum.api.model.vo.user; + +import lombok.Data; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 6/29/23 + */ +@Data +public class SearchZsxqUserReq { + // 用户昵称 + private String name; + // 星球编号 + private String starNumber; + // 用户登录名 + private String userCode; + + private Integer state; + // 分页 + private Long pageNumber; + private Long pageSize; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/UserInfoSaveReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/UserInfoSaveReq.java new file mode 100644 index 000000000..c5589e470 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/UserInfoSaveReq.java @@ -0,0 +1,57 @@ +package com.github.paicoding.forum.api.model.vo.user; + +import lombok.Data; + +import java.util.Map; + +/** + * 用户信息入参 + * + * @author louzai + * @date 2022-07-24 + */ +@Data +public class UserInfoSaveReq { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String userName; + + /** + * 用户图像 + */ + private String photo; + + /** + * 职位 + */ + private String position; + + /** + * 公司 + */ + private String company; + + /** + * 个人简介 + */ + private String profile; + + /** + * 用户的邮件地址 + */ + private String email; + + /** + * 收款码 + * key: qq|wx|ali --> 收款渠道 + * value: 收款二维码内容 + */ + private Map payCode; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/UserPwdLoginReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/UserPwdLoginReq.java new file mode 100644 index 000000000..1f34d0147 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/UserPwdLoginReq.java @@ -0,0 +1,41 @@ +package com.github.paicoding.forum.api.model.vo.user; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * 基于用户名密码登录的相关请求信息 + * + * @author YiHui + * @date 2023/11/13 + */ +@Data +@Accessors(chain = true) +public class UserPwdLoginReq implements Serializable { + private static final long serialVersionUID = 2139742660700910738L; + /** + * 用户id + */ + private Long userId; + /** + * 登录用户名 + */ + private String username; + + /** + * 登录密码 + */ + private String password; + + /** + * 星球编号 + */ + private String starNumber; + + /** + * 邀请码 + */ + private String invitationCode; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/UserRelationReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/UserRelationReq.java new file mode 100644 index 000000000..2a558e21b --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/UserRelationReq.java @@ -0,0 +1,28 @@ +package com.github.paicoding.forum.api.model.vo.user; + +import lombok.Data; + +/** + * 用户关系入参 + * + * @author louzai + * @date 2022-07-24 + */ +@Data +public class UserRelationReq { + + /** + * 用户ID + */ + private Long userId; + + /** + * 粉丝用户ID + */ + private Long followUserId; + + /** + * 是否关注当前用户 + */ + private Boolean followed; +} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/UserSaveReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/UserSaveReq.java similarity index 88% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/UserSaveReq.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/UserSaveReq.java index 864484280..7e4e37632 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/UserSaveReq.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/UserSaveReq.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.vo.user; +package com.github.paicoding.forum.api.model.vo.user; import lombok.Data; import lombok.experimental.Accessors; diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/ZsxqUserBatchOperateReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/ZsxqUserBatchOperateReq.java new file mode 100644 index 000000000..2c5a6e727 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/ZsxqUserBatchOperateReq.java @@ -0,0 +1,20 @@ +package com.github.paicoding.forum.api.model.vo.user; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 6/29/23 + */ +@Data +public class ZsxqUserBatchOperateReq implements Serializable { + // ids + private List ids; + // 状态 + private Integer status; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/ZsxqUserPostReq.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/ZsxqUserPostReq.java new file mode 100644 index 000000000..f80d5f970 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/ZsxqUserPostReq.java @@ -0,0 +1,25 @@ +package com.github.paicoding.forum.api.model.vo.user; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 6/29/23 + */ +@Data +public class ZsxqUserPostReq implements Serializable { + // id + private Long id; + // 用户名 + private String userCode; + // 用户昵称 + private String name; + // 星球编号 + private String starNumber; + // AI 策略 + private Integer strategy; +} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/dto/ArticleFootCountDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/ArticleFootCountDTO.java similarity index 90% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/dto/ArticleFootCountDTO.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/ArticleFootCountDTO.java index f6d655434..c0bbcd58b 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/dto/ArticleFootCountDTO.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/ArticleFootCountDTO.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.vo.user.dto; +package com.github.paicoding.forum.api.model.vo.user.dto; import lombok.Data; diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/BaseUserInfoDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/BaseUserInfoDTO.java new file mode 100644 index 000000000..fc384b0c2 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/BaseUserInfoDTO.java @@ -0,0 +1,92 @@ +package com.github.paicoding.forum.api.model.vo.user.dto; + +import com.github.paicoding.forum.api.model.entity.BaseDTO; +import com.github.paicoding.forum.api.model.enums.user.UserAIStatEnum; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @author YiHui + * @date 2022/8/15 + */ +@Data +@ApiModel("用户基础实体对象") +@Accessors(chain = true) +public class BaseUserInfoDTO extends BaseDTO { + /** + * 用户id + */ + @ApiModelProperty(value = "用户id", required = true) + private Long userId; + + /** + * 用户名 + */ + @ApiModelProperty(value = "用户名", required = true) + private String userName; + + /** + * 用户角色 admin, normal + */ + @ApiModelProperty(value = "角色", example = "ADMIN|NORMAL") + private String role; + + /** + * 用户图像 + */ + @ApiModelProperty(value = "用户头像") + private String photo; + /** + * 个人简介 + */ + @ApiModelProperty(value = "用户简介") + private String profile; + /** + * 职位 + */ + @ApiModelProperty(value = "个人职位") + private String position; + + /** + * 公司 + */ + @ApiModelProperty(value = "公司") + private String company; + + /** + * 扩展字段 + */ + @ApiModelProperty(hidden = true) + private String extend; + + /** + * 是否删除 + */ + @ApiModelProperty(hidden = true, value = "用户是否被删除") + private Integer deleted; + + /** + * 用户最后登录区域 + */ + @ApiModelProperty(value = "用户最后登录的地理位置", example = "湖北·武汉") + private String region; + + /** + * 星球状态 + */ + private UserAIStatEnum starStatus; + + /** + * 用户的邮箱 + */ + @ApiModelProperty(value = "用户邮箱", example = "paicoding@126.com") + private String email; + + /** + * 收款码信息 + */ + @ApiModelProperty(value = "用户的收款码", example = "{\"wx\":\"wxp://f2f0YUXuGn6X2dI6FS2GrMjuG0Lw2plZqwjO4keoZaRr320\"}") + private String payCode; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/ColumnFootCountDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/ColumnFootCountDTO.java new file mode 100644 index 000000000..860389dfe --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/ColumnFootCountDTO.java @@ -0,0 +1,52 @@ +package com.github.paicoding.forum.api.model.vo.user.dto; + +import lombok.Data; + +/** + * 专栏统计计数 + * + * @author louzai + * @date 2022-07-18 + */ +@Data +public class ColumnFootCountDTO { + + /** + * 专栏点赞数 + */ + private Integer praiseCount; + + /** + * 专栏被阅读数 + */ + private Integer readCount; + + /** + * 专栏被收藏数 + */ + private Integer collectionCount; + + /** + * 专栏评论数 + */ + private Integer commentCount; + + /** + * 专栏已更新的文章数 + */ + private Integer articleCount; + + /** + * 专栏的文章总数 + */ + private Integer totalNums; + + public ColumnFootCountDTO() { + praiseCount = 0; + readCount = 0; + collectionCount = 0; + commentCount = 0; + articleCount = 0; + totalNums = 0; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/FollowUserInfoDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/FollowUserInfoDTO.java new file mode 100644 index 000000000..7cc7732c2 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/FollowUserInfoDTO.java @@ -0,0 +1,41 @@ +package com.github.paicoding.forum.api.model.vo.user.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 关注者用户信息 + * + * @author YiHui + * @date 2022/11/2 + */ +@Data +public class FollowUserInfoDTO implements Serializable { + private static final long serialVersionUID = 7169636386013658631L; + /** + * 当前登录的用户与这个用户之间的关联关系id + */ + private Long relationId; + + /** + * true 表示当前登录用户关注了这个用户 + * false 标识当前登录用户没有关注这个用户 + */ + private Boolean followed; + + /** + * 用户id + */ + private Long userId; + + /** + * 用户名 + */ + private String userName; + + /** + * 用户头像 + */ + private String avatar; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/SimpleUserInfoDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/SimpleUserInfoDTO.java new file mode 100644 index 000000000..11358d697 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/SimpleUserInfoDTO.java @@ -0,0 +1,31 @@ +package com.github.paicoding.forum.api.model.vo.user.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * 基本用户信息 + * + * @author YiHui + * @date 2022/9/26 + */ +@Data +@Accessors(chain = true) +public class SimpleUserInfoDTO implements Serializable { + private static final long serialVersionUID = 4802653694786272120L; + + @ApiModelProperty("作者ID") + private Long userId; + + @ApiModelProperty("作者名") + private String name; + + @ApiModelProperty("作者头像") + private String avatar; + + @ApiModelProperty("作者简介") + private String profile; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/UserFootStatisticDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/UserFootStatisticDTO.java new file mode 100644 index 000000000..99a2c12f2 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/UserFootStatisticDTO.java @@ -0,0 +1,42 @@ +package com.github.paicoding.forum.api.model.vo.user.dto; + +import lombok.Data; +import lombok.ToString; + +/** + * 用户主页信息 + * + * @author 沉默王二 + * @since 2023年05月25日 + */ +@Data +@ToString(callSuper = true) +public class UserFootStatisticDTO { + + /** + * 文章点赞数 + */ + private Long praiseCount; + + /** + * 文章被阅读数 + */ + private Long readCount; + + /** + * 文章被收藏数 + */ + private Long collectionCount; + + /** + * 文章被评论数 + */ + private Long commentCount; + + public UserFootStatisticDTO() { + praiseCount = 0L; + readCount = 0L; + collectionCount = 0L; + commentCount = 0L; + } +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/UserPayCodeDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/UserPayCodeDTO.java new file mode 100644 index 000000000..3ec0042e0 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/UserPayCodeDTO.java @@ -0,0 +1,30 @@ +package com.github.paicoding.forum.api.model.vo.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 用户收款码 + * + * @author YiHui + * @date 2024/10/30 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserPayCodeDTO implements Serializable { + private static final long serialVersionUID = -2601714252107169062L; + + /** + * base64格式的收款二维码图片 + */ + private String qrCode; + + /** + * 内容 + */ + private String qrMsg; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/UserStatisticInfoDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/UserStatisticInfoDTO.java new file mode 100644 index 000000000..de24a2aea --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/UserStatisticInfoDTO.java @@ -0,0 +1,74 @@ +package com.github.paicoding.forum.api.model.vo.user.dto; + +import com.github.paicoding.forum.api.model.vo.article.dto.YearArticleDTO; +import lombok.Data; +import lombok.ToString; + +import java.util.List; +import java.util.Map; + +/** + * 用户主页信息 + * + * @author louzai + * @since 2022/7/19 + */ +@Data +@ToString(callSuper = true) +public class UserStatisticInfoDTO extends BaseUserInfoDTO { + + /** + * 关注数 + */ + private Integer followCount; + + /** + * 粉丝数 + */ + private Integer fansCount; + + /** + * 加入天数 + */ + private Integer joinDayCount; + + /** + * 已发布文章数 + */ + private Integer articleCount; + + /** + * 文章点赞数 + */ + private Integer praiseCount; + + /** + * 文章被阅读数 + */ + private Integer readCount; + + /** + * 文章被收藏数 + */ + private Integer collectionCount; + + /** + * 是否关注当前用户 + */ + private Boolean followed; + + /** + * 身份信息完整度百分比 + */ + private Integer infoPercent; + + /** + * 创造历程 + */ + private List yearArticleList; + + /** + * 作者的收款码信息 + */ + private Map payQrCodes; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/ZsxqUserInfoDTO.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/ZsxqUserInfoDTO.java new file mode 100644 index 000000000..2e6261a9b --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/dto/ZsxqUserInfoDTO.java @@ -0,0 +1,59 @@ +package com.github.paicoding.forum.api.model.vo.user.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * 基本用户信息 + * + * @author YiHui + * @date 2022/9/26 + */ +@Data +@Accessors(chain = true) +public class ZsxqUserInfoDTO implements Serializable { + private static final long serialVersionUID = 4802653694786272120L; + + private Long id; + + @ApiModelProperty("用户ID") + private Long userId; + + // 这个是 userinfo 表中的 username + @ApiModelProperty("用户名") + private String name; + + @ApiModelProperty("用户头像") + private String avatar; + + // 这个是 user 表中的 username + @ApiModelProperty("用户编号") + private String userCode; + + // 星球编号 + @ApiModelProperty("星球编号") + private String starNumber; + + // 邀请码 + @ApiModelProperty("邀请码") + private String inviteCode; + + // 邀请人数 + @ApiModelProperty("邀请人数") + private Integer inviteNum; + + // 状态 + @ApiModelProperty("状态") + private Integer state; + + // login_type + @ApiModelProperty("登录类型") + private Integer loginType; + + // strategy + @ApiModelProperty("AI策略") + private Integer strategy; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/wx/BaseWxMsgResVo.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/wx/BaseWxMsgResVo.java new file mode 100644 index 000000000..9c3b9fe2b --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/wx/BaseWxMsgResVo.java @@ -0,0 +1,27 @@ +package com.github.paicoding.forum.api.model.vo.user.wx; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import lombok.Data; + +/** + * 返回的数据结构体 + *

+ * + * @author yihui + * @link + * @date 2022/6/20 + */ +@Data +@JacksonXmlRootElement(localName = "xml") +public class BaseWxMsgResVo { + + @JacksonXmlProperty(localName = "ToUserName") + private String toUserName; + @JacksonXmlProperty(localName = "FromUserName") + private String fromUserName; + @JacksonXmlProperty(localName = "CreateTime") + private Long createTime; + @JacksonXmlProperty(localName = "MsgType") + private String msgType; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/wx/WxImgTxtItemVo.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/wx/WxImgTxtItemVo.java new file mode 100644 index 000000000..ad880f9d6 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/wx/WxImgTxtItemVo.java @@ -0,0 +1,27 @@ +package com.github.paicoding.forum.api.model.vo.user.wx; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import lombok.Data; + +/** + * 返回的数据结构体 + *

+ * + * @author yihui + * @link + * @date 2022/6/20 + */ +@Data +@JacksonXmlRootElement(localName = "item") +public class WxImgTxtItemVo { + + @JacksonXmlProperty(localName = "Title") + private String title; + @JacksonXmlProperty(localName = "Description") + private String description; + @JacksonXmlProperty(localName = "PicUrl") + private String picUrl; + @JacksonXmlProperty(localName = "Url") + private String url; +} diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/wx/WxImgTxtMsgResVo.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/wx/WxImgTxtMsgResVo.java new file mode 100644 index 000000000..8580c1221 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/wx/WxImgTxtMsgResVo.java @@ -0,0 +1,32 @@ +package com.github.paicoding.forum.api.model.vo.user.wx; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import lombok.Data; +import lombok.ToString; + +import java.util.List; + +/** + * 返回的数据结构体 + *

+ * + * @author yihui + * @link + * @date 2022/6/20 + */ +@Data +@ToString(callSuper = true) +@JacksonXmlRootElement(localName = "xml") +public class WxImgTxtMsgResVo extends BaseWxMsgResVo { + @JacksonXmlProperty(localName = "ArticleCount") + private Integer articleCount; + @JacksonXmlElementWrapper(localName = "Articles") + @JacksonXmlProperty(localName = "item") + private List articles; + + public WxImgTxtMsgResVo() { + setMsgType("news"); + } +} diff --git a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/wx/WxTxtMsgReqVo.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/wx/WxTxtMsgReqVo.java similarity index 79% rename from form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/wx/WxTxtMsgReqVo.java rename to paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/wx/WxTxtMsgReqVo.java index 2a8f968cd..a87a78ab2 100644 --- a/form-api/src/main/java/com/github/liueyueyi/forum/api/model/vo/user/wx/WxTxtMsgReqVo.java +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/wx/WxTxtMsgReqVo.java @@ -1,4 +1,4 @@ -package com.github.liueyueyi.forum.api.model.vo.user.wx; +package com.github.paicoding.forum.api.model.vo.user.wx; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; @@ -22,6 +22,12 @@ public class WxTxtMsgReqVo { private Long createTime; @JacksonXmlProperty(localName = "MsgType") private String msgType; + @JacksonXmlProperty(localName = "Event") + private String event; + @JacksonXmlProperty(localName = "EventKey") + private String eventKey; + @JacksonXmlProperty(localName = "Ticket") + private String ticket; @JacksonXmlProperty(localName = "Content") private String content; @JacksonXmlProperty(localName = "MsgId") diff --git a/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/wx/WxTxtMsgResVo.java b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/wx/WxTxtMsgResVo.java new file mode 100644 index 000000000..3e2e20ee6 --- /dev/null +++ b/paicoding-api/src/main/java/com/github/paicoding/forum/api/model/vo/user/wx/WxTxtMsgResVo.java @@ -0,0 +1,26 @@ +package com.github.paicoding.forum.api.model.vo.user.wx; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import lombok.Data; +import lombok.ToString; + +/** + * 返回的数据结构体 + *

+ * + * @author yihui + * @link + * @date 2022/6/20 + */ +@Data +@ToString(callSuper = true) +@JacksonXmlRootElement(localName = "xml") +public class WxTxtMsgResVo extends BaseWxMsgResVo { + @JacksonXmlProperty(localName = "Content") + private String content; + + public WxTxtMsgResVo() { + setMsgType("text"); + } +} diff --git a/paicoding-core/pom.xml b/paicoding-core/pom.xml new file mode 100644 index 000000000..1f2904554 --- /dev/null +++ b/paicoding-core/pom.xml @@ -0,0 +1,204 @@ + + + + paicoding-forum + com.github.paicoding.forum + 0.0.1-SNAPSHOT + + 4.0.0 + + paicoding-core + + + 8 + 8 + + + + + com.github.paicoding.forum + paicoding-api + + + com.github.liuyueyi.media + qrcode-plugin + + + org.springframework + spring-context + + + javax.servlet + javax.servlet-api + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.apache.commons + commons-lang3 + + + + com.google.guava + guava + + + com.fasterxml.jackson.core + jackson-databind + + + org.springframework + spring-web + + + org.springframework.boot + spring-boot + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.lionsoul + ip2region + 2.6.6 + + + + + com.github.ben-manes.caffeine + caffeine + + + org.springframework.boot + spring-boot-starter-cache + + + + + com.github.xiaoymin + knife4j-openapi2-spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-mail + + + + + com.rabbitmq + amqp-client + 5.5.1 + + + + + com.vladsch.flexmark + flexmark-all + 0.62.2 + + + org.springframework + spring-jdbc + + + org.aspectj + aspectjweaver + + + org.mybatis + mybatis + 3.5.10 + compile + + + org.mybatis + mybatis-spring + 2.0.7 + compile + + + com.zaxxer + HikariCP + + + com.baomidou + mybatis-plus-core + 3.4.3.4 + compile + + + + com.alibaba + druid-spring-boot-starter + 1.2.16 + provided + + + mysql + mysql-connector-java + + + + com.github.plexpt + chatgpt + 4.4.0 + + + + com.github.houbb + sensitive-word + ${sensitive.version} + + + + com.alibaba + transmittable-thread-local + 2.14.5 + + + io.github.classgraph + classgraph + 4.8.83 + compile + + + + cn.bigmodel.openapi + oapi-java-sdk + release-V4-2.1.0 + + + + org.springframework.security + spring-security-core + 6.3.0 + + + + com.alibaba + dashscope-sdk-java + 2.16.2 + + + org.springframework + spring-messaging + provided + + + + cn.idev.excel + fastexcel + + + + \ No newline at end of file diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/ForumCoreAutoConfig.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/ForumCoreAutoConfig.java new file mode 100644 index 000000000..d03e342c8 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/ForumCoreAutoConfig.java @@ -0,0 +1,58 @@ +package com.github.paicoding.forum.core; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.paicoding.forum.core.cache.RedisClient; +import com.github.paicoding.forum.core.config.ProxyProperties; +import com.github.paicoding.forum.core.net.ProxyCenter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.CacheManager; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; + +import javax.annotation.PostConstruct; +import java.util.concurrent.TimeUnit; + +/** + * @author YiHui + * @date 2022/9/4 + */ +@Configuration +@EnableConfigurationProperties(ProxyProperties.class) +@ComponentScan(basePackages = "com.github.paicoding.forum.core") +public class ForumCoreAutoConfig { + @Autowired + private ProxyProperties proxyProperties; + + public ForumCoreAutoConfig(RedisTemplate redisTemplate) { + RedisClient.register(redisTemplate); + } + + /** + * 定义缓存管理器,配合Spring的 @Cache 来使用 + * + * @return + */ + @Bean("caffeineCacheManager") + public CacheManager cacheManager() { + CaffeineCacheManager cacheManager = new CaffeineCacheManager(); + cacheManager.setCaffeine(Caffeine.newBuilder(). + // 设置过期时间,写入后五分钟国企 + expireAfterWrite(5, TimeUnit.MINUTES) + // 初始化缓存空间大小 + .initialCapacity(100) + // 最大的缓存条数 + .maximumSize(200) + ); + return cacheManager; + } + + @PostConstruct + public void init() { + // 这里借助手动解析配置信息,并实例化为Java POJO对象,来实现代理池的初始化 + ProxyCenter.initProxyPool(proxyProperties.getProxy()); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/async/AsyncExecute.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/async/AsyncExecute.java new file mode 100644 index 000000000..cafd9e910 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/async/AsyncExecute.java @@ -0,0 +1,48 @@ +package com.github.paicoding.forum.core.async; + +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; +import java.util.concurrent.TimeUnit; + +/** + * 异步执行 + * + * @author YiHui + * @date 2023/11/10 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface AsyncExecute { + /** + * 是否开启异步执行 + * + * @return + */ + boolean value() default true; + + /** + * 超时时间,默认3s + * + * @return + */ + int timeOut() default 3; + + /** + * 超时时间单位,默认秒,配合上面的 timeOut 使用 + * + * @return + */ + TimeUnit unit() default TimeUnit.SECONDS; + + /** + * 当出现超时返回的兜底逻辑,支持SpEL + * 如果返回的是空字符串,则表示抛出异常 + * + * @return + */ + String timeOutRsp() default ""; +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/async/AsyncExecuteAspect.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/async/AsyncExecuteAspect.java new file mode 100644 index 000000000..eb12a892c --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/async/AsyncExecuteAspect.java @@ -0,0 +1,91 @@ +package com.github.paicoding.forum.core.async; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +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.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.stereotype.Component; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +/** + * 异步执行 + * + * @author YiHui + * @date 2023/11/10 + */ +@Slf4j +@Aspect +@Component +public class AsyncExecuteAspect implements ApplicationContextAware { + + /** + * 超时执行的切面 + * + * @param joinPoint + * @param asyncExecute + * @return + * @throws Throwable + */ + @Around("@annotation(asyncExecute)") + public Object handle(ProceedingJoinPoint joinPoint, AsyncExecute asyncExecute) throws Throwable { + if (!asyncExecute.value()) { + // 不支持异步执行时,直接返回 + return joinPoint.proceed(); + } + + try { + // 携带超时时间的执行调用 + return AsyncUtil.callWithTimeLimit(asyncExecute.timeOut(), asyncExecute.unit(), () -> { + try { + return joinPoint.proceed(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + }); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + if (StringUtils.isNotBlank(asyncExecute.timeOutRsp())) { + return defaultRespWhenTimeOut(joinPoint, asyncExecute); + } else { + throw e; + } + } catch (Exception e) { + throw e; + } + } + + private Object defaultRespWhenTimeOut(ProceedingJoinPoint joinPoint, AsyncExecute asyncExecute) { + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setBeanResolver(new BeanFactoryResolver(this.applicationContext)); + + // 超时,使用自定义的返回策略进行返回 + MethodSignature methodSignature = ((MethodSignature) joinPoint.getSignature()); + String[] parameterNames = methodSignature.getParameterNames(); + Object[] args = joinPoint.getArgs(); + for (int i = 0; i < parameterNames.length; i++) { + context.setVariable(parameterNames[i], args[i]); + } + log.info("{} 执行超时,返回兜底结果!", methodSignature.getMethod().getName()); + return parser.parseExpression(asyncExecute.timeOutRsp()).getValue(context); + } + + + private ExpressionParser parser; + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.parser = new SpelExpressionParser(); + this.applicationContext = applicationContext; + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/async/AsyncUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/async/AsyncUtil.java new file mode 100644 index 000000000..1b1852d93 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/async/AsyncUtil.java @@ -0,0 +1,333 @@ +package com.github.paicoding.forum.core.async; + +import cn.hutool.core.thread.ExecutorBuilder; +import cn.hutool.core.util.ArrayUtil; +import com.alibaba.ttl.TransmittableThreadLocal; +import com.alibaba.ttl.threadpool.TtlExecutors; +import com.github.paicoding.forum.core.util.EnvUtil; +import com.google.common.util.concurrent.SimpleTimeLimiter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.CollectionUtils; + +import java.io.Closeable; +import java.text.NumberFormat; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +/** + * 异步工具类 + * + * @author YiHui + * @date 2023/6/12 + */ +@Slf4j +public class AsyncUtil { + private static final TransmittableThreadLocal THREAD_LOCAL = new TransmittableThreadLocal<>(); + private static final ThreadFactory THREAD_FACTORY = new ThreadFactory() { + private final ThreadFactory defaultFactory = Executors.defaultThreadFactory(); + private final AtomicInteger threadNumber = new AtomicInteger(1); + + public Thread newThread(Runnable r) { + Thread thread = this.defaultFactory.newThread(r); + if (!thread.isDaemon()) { + thread.setDaemon(true); + } + + thread.setName("paicoding-" + this.threadNumber.getAndIncrement()); + return thread; + } + }; + private static ExecutorService executorService; + private static SimpleTimeLimiter simpleTimeLimiter; + + static { + initExecutorService(Runtime.getRuntime().availableProcessors() * 2, 50); + } + + public static void initExecutorService(int core, int max) { + // 异步工具类的默认线程池构建, 参数选择原则: + // 1. 技术派不存在cpu密集型任务,大部分操作都设计到 redis/mysql 等io操作 + // 2. 统一的异步封装工具,这里的线程池是一个公共的执行仓库,不希望被其他的线程执行影响,因此队列长度为0, 核心线程数满就创建线程执行,超过最大线程,就直接当前线程执行 + // 3. 同样因为属于通用工具类,再加上技术派的异步使用的情况实际上并不是非常饱和的,因此空闲线程直接回收掉即可;大部分场景下,cpu * 2的线程数即可满足要求了 + max = Math.max(core, max); + executorService = new ExecutorBuilder() + .setCorePoolSize(core) + .setMaxPoolSize(max) + .setKeepAliveTime(0) + .setKeepAliveTime(0, TimeUnit.SECONDS) + .setWorkQueue(new SynchronousQueue()) + .setHandler(new ThreadPoolExecutor.CallerRunsPolicy()) + .setThreadFactory(THREAD_FACTORY) + .buildFinalizable(); + // 包装一下线程池,避免出现上下文复用场景 + executorService = TtlExecutors.getTtlExecutorService(executorService); + simpleTimeLimiter = SimpleTimeLimiter.create(executorService); + } + + + /** + * 带超时时间的方法调用执行,当执行时间超过给定的时间,则返回一个超时异常,内部的任务还是正常执行 + * 若超时时间内执行完毕,则直接返回 + * + * @param time + * @param unit + * @param call + * @param + * @return + */ + public static T callWithTimeLimit(long time, TimeUnit unit, Callable call) throws ExecutionException, InterruptedException, TimeoutException { + return simpleTimeLimiter.callWithTimeout(call, time, unit); + } + + + public static void execute(Runnable call) { + executorService.execute(call); + } + + public static Future submit(Callable t) { + return executorService.submit(t); + } + + + public static boolean sleep(Number timeout, TimeUnit timeUnit) { + try { + timeUnit.sleep(timeout.longValue()); + return true; + } catch (InterruptedException var3) { + return false; + } + } + + public static boolean sleep(Number millis) { + return millis == null ? true : sleep(millis.longValue()); + } + + public static boolean sleep(long millis) { + if (millis > 0L) { + try { + Thread.sleep(millis); + } catch (InterruptedException var3) { + return false; + } + } + + return true; + } + + + public static class CompletableFutureBridge implements Closeable { + private List list; + private Map cost; + private String taskName; + private boolean markOver; + private ExecutorService executorService; + + public CompletableFutureBridge() { + this(AsyncUtil.executorService, "CompletableFutureExecute"); + } + + public CompletableFutureBridge(ExecutorService executorService, String task) { + this.taskName = task; + list = new CopyOnWriteArrayList<>(); + // 支持排序的耗时记录 + cost = new ConcurrentSkipListMap<>(); + cost.put(task, System.currentTimeMillis()); + this.executorService = TtlExecutors.getTtlExecutorService(executorService); + this.markOver = false; + } + + /** + * 异步执行,带返回结果 + * + * @param supplier 执行任务 + * @param name 耗时标识 + * @return + */ + public CompletableFutureBridge async(Supplier supplier, String name) { + list.add(CompletableFuture.supplyAsync(supplyWithTime(supplier, name), this.executorService)); + return this; + } + + /** + * 同步执行,待返回结果 + * + * @param supplier 执行任务 + * @param name 耗时标识 + * @param 返回类型 + * @return 任务的执行返回结果 + */ + public T sync(Supplier supplier, String name) { + return supplyWithTime(supplier, name).get(); + } + + /** + * 异步执行,无返回结果 + * + * @param run 执行任务 + * @param name 耗时标识 + * @return + */ + public CompletableFutureBridge async(Runnable run, String name) { + list.add(CompletableFuture.runAsync(runWithTime(run, name), this.executorService)); + return this; + } + + /** + * 同步执行,无返回结果 + * + * @param run 执行任务 + * @param name 耗时标识 + * @return + */ + public CompletableFutureBridge sync(Runnable run, String name) { + runWithTime(run, name).run(); + return this; + } + + private Runnable runWithTime(Runnable run, String name) { + return () -> { + startRecord(name); + try { + run.run(); + } finally { + endRecord(name); + } + }; + } + + private Supplier supplyWithTime(Supplier call, String name) { + return () -> { + startRecord(name); + try { + return call.get(); + } finally { + endRecord(name); + } + }; + } + + public CompletableFutureBridge allExecuted() { + if (!CollectionUtils.isEmpty(list)) { + CompletableFuture.allOf(ArrayUtil.toArray(list, CompletableFuture.class)).join(); + } + this.markOver = true; + endRecord(this.taskName); + return this; + } + + private void startRecord(String name) { + cost.put(name, System.currentTimeMillis()); + } + + private void endRecord(String name) { + long now = System.currentTimeMillis(); + long last = cost.getOrDefault(name, now); + if (last >= now / 1000) { + // 之前存储的是时间戳,因此我们需要更新成执行耗时 ms单位 + cost.put(name, now - last); + } + } + + public void prettyPrint() { + if (EnvUtil.isPro()) { + // 生产环境默认不打印执行耗时日志 + return; + } + + if (!this.markOver) { + // 在格式化输出时,要求所有任务执行完毕 + this.allExecuted(); + } + + StringBuilder sb = new StringBuilder(); + sb.append('\n'); + long totalCost = cost.remove(taskName); + sb.append("StopWatch '").append(taskName).append("': running time = ").append(totalCost).append(" ms"); + sb.append('\n'); + if (cost.size() <= 1) { + sb.append("No task info kept"); + } else { + sb.append("---------------------------------------------\n"); + sb.append("ms % Task name\n"); + sb.append("---------------------------------------------\n"); + NumberFormat pf = NumberFormat.getPercentInstance(); + pf.setMinimumIntegerDigits(2); + pf.setMinimumFractionDigits(2); + pf.setGroupingUsed(false); + for (Map.Entry entry : cost.entrySet()) { + sb.append(entry.getValue()).append("\t\t"); + sb.append(pf.format(entry.getValue() / (double) totalCost)).append("\t\t"); + sb.append(entry.getKey()).append("\n"); + } + } + + log.info("\n---------------------\n{}\n--------------------\n", sb); + } + + @Override + public void close() { + try { + if (!this.markOver) { + // 做一个兜底,避免业务侧没有手动结束,导致异步任务没有执行完就提前返回结果 + this.allExecuted(); + } + + AsyncUtil.release(); + prettyPrint(); + } catch (Exception e) { + log.error("释放耗时上下文异常! {}", taskName, e); + } + } + } + + public static CompletableFutureBridge concurrentExecutor(String... name) { + if (name.length > 0) { + return new CompletableFutureBridge(AsyncUtil.executorService, name[0]); + } + return new CompletableFutureBridge(); + } + + /** + * 开始桥接类 + * + * @param executorService 线程池 + * @param name 标记名 + * @return 桥接类 + */ + public static CompletableFutureBridge startBridge(ExecutorService executorService, String name) { + CompletableFutureBridge bridge = new CompletableFutureBridge(executorService, name); + THREAD_LOCAL.set(bridge); + return bridge; + } + + /** + * 获取计时桥接类 + * + * @return 桥接类 + */ + public static CompletableFutureBridge getBridge() { + return THREAD_LOCAL.get(); + } + + /** + * 释放统计 + */ + public static void release() { + THREAD_LOCAL.remove(); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/ConfigRefreshEventListener.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/ConfigRefreshEventListener.java new file mode 100644 index 000000000..ab7caef22 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/ConfigRefreshEventListener.java @@ -0,0 +1,30 @@ +package com.github.paicoding.forum.core.autoconf; + +import com.github.paicoding.forum.api.model.event.ConfigRefreshEvent; +import com.github.paicoding.forum.core.autoconf.property.SpringValueRegistry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Service; + +/** + * 配置刷新事件监听 + * + * @author YiHui + * @date 2023/09/14 + */ +@Service +public class ConfigRefreshEventListener implements ApplicationListener { + @Autowired + private DynamicConfigContainer dynamicConfigContainer; + + /** + * 监听配置变更事件 + * + * @param event + */ + @Override + public void onApplicationEvent(ConfigRefreshEvent event) { + dynamicConfigContainer.reloadConfig(); + SpringValueRegistry.updateValue(event.getKey()); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/DynamicConfigBinder.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/DynamicConfigBinder.java new file mode 100644 index 000000000..cafea2acd --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/DynamicConfigBinder.java @@ -0,0 +1,106 @@ +package com.github.paicoding.forum.core.autoconf; + +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver; +import org.springframework.boot.context.properties.bind.handler.IgnoreErrorsBindHandler; +import org.springframework.boot.context.properties.bind.handler.IgnoreTopLevelConverterNotFoundBindHandler; +import org.springframework.boot.context.properties.bind.handler.NoUnboundElementsBindHandler; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; +import org.springframework.boot.context.properties.source.UnboundElementsSourceFilter; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.env.PropertySources; + +import java.util.function.Consumer; + +/** + * 自定义动态配置绑定 + * + * @author YiHui + * @date 2023/6/20 + */ +public class DynamicConfigBinder { + private final ApplicationContext applicationContext; + private PropertySources propertySource; + + private volatile Binder binder; + + public DynamicConfigBinder(ApplicationContext applicationContext, PropertySources propertySource) { + this.applicationContext = applicationContext; + this.propertySource = propertySource; + } + + public void bind(Bindable bindable) { + ConfigurationProperties propertiesAno = bindable.getAnnotation(ConfigurationProperties.class); + if (propertiesAno != null) { + BindHandler bindHandler = getBindHandler(propertiesAno); + getBinder().bind(propertiesAno.prefix(), bindable, bindHandler); + } + } + + public void bind(String prefix, Bindable bindable, BindHandler bindHandler) { + getBinder().bind(prefix, bindable, bindHandler); + } + + private BindHandler getBindHandler(ConfigurationProperties annotation) { + BindHandler handler = new IgnoreTopLevelConverterNotFoundBindHandler(); + if (annotation.ignoreInvalidFields()) { + handler = new IgnoreErrorsBindHandler(handler); + } + if (!annotation.ignoreUnknownFields()) { + UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter(); + handler = new NoUnboundElementsBindHandler(handler, filter); + } + return handler; + } + + private Binder getBinder() { + if (this.binder == null) { + synchronized (this) { + if (this.binder == null) { + this.binder = new Binder(getConfigurationPropertySources(), + getPropertySourcesPlaceholdersResolver(), getConversionService(), + getPropertyEditorInitializer()); + } + } + } + return this.binder; + } + + private Iterable getConfigurationPropertySources() { + return ConfigurationPropertySources.from(this.propertySource); + } + + /** + * 指定占位符的前缀、后缀、默认值分隔符、未解析忽略、环境变量容器 + * + * @return + */ + private PropertySourcesPlaceholdersResolver getPropertySourcesPlaceholdersResolver() { + return new PropertySourcesPlaceholdersResolver(this.propertySource); + } + + /** + * 类型转换 + * + * @return + */ + private ConversionService getConversionService() { + return new DefaultConversionService(); + } + + private Consumer getPropertyEditorInitializer() { + if (this.applicationContext instanceof ConfigurableApplicationContext) { + return ((ConfigurableApplicationContext) this.applicationContext) + .getBeanFactory()::copyRegisteredEditorsTo; + } + return null; + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/DynamicConfigContainer.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/DynamicConfigContainer.java new file mode 100644 index 000000000..99af08c1f --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/DynamicConfigContainer.java @@ -0,0 +1,186 @@ +package com.github.paicoding.forum.core.autoconf; + +import com.github.paicoding.forum.core.autoconf.property.SpringValueRegistry; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.google.common.collect.Maps; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * 自定义的配置工厂类,专门用于 ConfDot 属性配置文件的配置加载,支持从自定义的配置源获取 + * + * @author YiHui + * @date 2023/6/20 + */ +@Slf4j +@Component +public class DynamicConfigContainer implements EnvironmentAware, ApplicationContextAware, CommandLineRunner { + private ConfigurableEnvironment environment; + private ApplicationContext applicationContext; + /** + * 存储db中的全局配置,优先级最高 + */ + @Getter + public Map cache; + + private DynamicConfigBinder binder; + + /** + * 配置变更的回调任务 + */ + @Getter + private Map refreshCallback = Maps.newHashMap(); + + @Override + public void setEnvironment(Environment environment) { + this.environment = (ConfigurableEnvironment) environment; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @PostConstruct + public void init() { + cache = Maps.newHashMap(); + bindBeansFromLocalCache("dbConfig", cache); + } + + /** + * 从db中获取全量的配置信息 + * + * @return true 表示有信息变更; false 表示无信息变更 + */ + private boolean loadAllConfigFromDb() { + List> list = SpringUtil.getBean(JdbcTemplate.class).queryForList("select `key`, `value` from global_conf where deleted = 0"); + Map val = Maps.newHashMapWithExpectedSize(list.size()); + for (Map conf : list) { + val.put(conf.get("key").toString(), conf.get("value").toString()); + } + if (val.equals(cache)) { + return false; + } + cache.clear(); + cache.putAll(val); + return true; + } + + private void bindBeansFromLocalCache(String namespace, Map cache) { + // 将内存的配置信息设置为最高优先级 + MapPropertySource propertySource = new MapPropertySource(namespace, cache); + environment.getPropertySources().addFirst(propertySource); + this.binder = new DynamicConfigBinder(this.applicationContext, environment.getPropertySources()); + } + + /** + * 配置绑定 + * + * @param bindable + */ + public void bind(Bindable bindable) { + binder.bind(bindable); + } + + + /** + * 监听配置的变更 + */ + public void reloadConfig() { + String before = JsonUtil.toStr(cache); + boolean toRefresh = loadAllConfigFromDb(); + if (toRefresh) { + refreshConfig(); + log.info("配置刷新! 旧:{}, 新:{}", before, JsonUtil.toStr(cache)); + } + } + + /** + * 强制刷新缓存配置 + */ + public void forceRefresh() { + loadAllConfigFromDb(); + refreshConfig(); + log.info("db配置强制刷新! {}", JsonUtil.toStr(cache)); + } + + /** + * 支持配置的动态刷新 + */ + private void refreshConfig() { + applicationContext.getBeansWithAnnotation(ConfigurationProperties.class).values().forEach(bean -> { + Bindable target = Bindable.ofInstance(bean).withAnnotations(AnnotationUtils.findAnnotation(bean.getClass(), ConfigurationProperties.class)); + bind(target); + if (refreshCallback.containsKey(bean.getClass())) { + refreshCallback.get(bean.getClass()).run(); + } + }); + } + + /** + * 注册db的动态配置变更 + */ + private void registerConfRefreshTask() { + Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> { + try { + reloadConfig(); + } catch (Exception e) { + log.warn("自动更新db配置信息异常!", e); + } + }, 5, 5, TimeUnit.MINUTES); + } + + /** + * 注册配置变更的回调任务 + * + * @param bean + * @param run + */ + public void registerRefreshCallback(Object bean, Runnable run) { + refreshCallback.put(bean.getClass(), run); + } + + + /** + * bean先加载,此时@Value对应的成员属性直接从默认的配置中读取了;这就导致无法获取db中的真实配置信息,只有这个配置再db中发生变更,才会生效 + * 因此,我们再自定义的配置加载完毕之后,重刷一下bean中的@Value属性,保证他们都获取的是最新的配置信息 + */ + private void autoUpdateSpringValueConfig() { + Set keys = SpringValueRegistry.registry.keySet(); + keys.forEach(SpringValueRegistry::updateValue); + } + + /** + * 应用启动之后,执行的动态配置初始化 + * + * @param args + * @throws Exception + */ + @Override + public void run(String... args) throws Exception { + reloadConfig(); + registerConfRefreshTask(); + autoUpdateSpringValueConfig(); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/property/PlaceholderHelper.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/property/PlaceholderHelper.java new file mode 100644 index 000000000..5145d545d --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/property/PlaceholderHelper.java @@ -0,0 +1,163 @@ +package com.github.paicoding.forum.core.autoconf.property; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.Scope; +import org.springframework.util.StringUtils; + +import java.util.HashSet; +import java.util.Set; +import java.util.Stack; + +/** + * 来自apollo-client + * Placeholder helper functions. + */ +public class PlaceholderHelper { + + private static final String PLACEHOLDER_PREFIX = "${"; + private static final String PLACEHOLDER_SUFFIX = "}"; + private static final String VALUE_SEPARATOR = ":"; + private static final String SIMPLE_PLACEHOLDER_PREFIX = "{"; + private static final String EXPRESSION_PREFIX = "#{"; + private static final String EXPRESSION_SUFFIX = "}"; + + /** + * Resolve placeholder property values, e.g. + *
+ *
+ * "${somePropertyValue}" -> "the actual property value" + */ + public Object resolvePropertyValue(ConfigurableBeanFactory beanFactory, String beanName, String placeholder) { + // resolve string value + String strVal = beanFactory.resolveEmbeddedValue(placeholder); + + BeanDefinition bd = (beanFactory.containsBean(beanName) ? beanFactory + .getMergedBeanDefinition(beanName) : null); + + // resolve expressions like "#{systemProperties.myProp}" + return evaluateBeanDefinitionString(beanFactory, strVal, bd); + } + + private Object evaluateBeanDefinitionString(ConfigurableBeanFactory beanFactory, String value, + BeanDefinition beanDefinition) { + if (beanFactory.getBeanExpressionResolver() == null) { + return value; + } + Scope scope = (beanDefinition != null ? beanFactory + .getRegisteredScope(beanDefinition.getScope()) : null); + return beanFactory.getBeanExpressionResolver() + .evaluate(value, new BeanExpressionContext(beanFactory, scope)); + } + + /** + * Extract keys from placeholder, e.g. + *

+ */ + public Set extractPlaceholderKeys(String propertyString) { + Set placeholderKeys = new HashSet<>(); + + if (!isNormalizedPlaceholder(propertyString) && !isExpressionWithPlaceholder(propertyString)) { + return placeholderKeys; + } + + Stack stack = new Stack<>(); + stack.push(propertyString); + + while (!stack.isEmpty()) { + String strVal = stack.pop(); + int startIndex = strVal.indexOf(PLACEHOLDER_PREFIX); + if (startIndex == -1) { + placeholderKeys.add(strVal); + continue; + } + int endIndex = findPlaceholderEndIndex(strVal, startIndex); + if (endIndex == -1) { + // invalid placeholder? + continue; + } + + String placeholderCandidate = strVal.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex); + + // ${some.key:other.key} + if (placeholderCandidate.startsWith(PLACEHOLDER_PREFIX)) { + stack.push(placeholderCandidate); + } else { + // some.key:${some.other.key:100} + int separatorIndex = placeholderCandidate.indexOf(VALUE_SEPARATOR); + + if (separatorIndex == -1) { + stack.push(placeholderCandidate); + } else { + stack.push(placeholderCandidate.substring(0, separatorIndex)); + String defaultValuePart = + normalizeToPlaceholder(placeholderCandidate.substring(separatorIndex + VALUE_SEPARATOR.length())); + if (!StringUtils.isEmpty(defaultValuePart)) { + stack.push(defaultValuePart); + } + } + } + + // has remaining part, e.g. ${a}.${b} + if (endIndex + PLACEHOLDER_SUFFIX.length() < strVal.length() - 1) { + String remainingPart = normalizeToPlaceholder(strVal.substring(endIndex + PLACEHOLDER_SUFFIX.length())); + if (!StringUtils.isEmpty(remainingPart)) { + stack.push(remainingPart); + } + } + } + + return placeholderKeys; + } + + private boolean isNormalizedPlaceholder(String propertyString) { + return propertyString.startsWith(PLACEHOLDER_PREFIX) && propertyString.endsWith(PLACEHOLDER_SUFFIX); + } + + private boolean isExpressionWithPlaceholder(String propertyString) { + return propertyString.startsWith(EXPRESSION_PREFIX) && propertyString.endsWith(EXPRESSION_SUFFIX) + && propertyString.contains(PLACEHOLDER_PREFIX); + } + + private String normalizeToPlaceholder(String strVal) { + int startIndex = strVal.indexOf(PLACEHOLDER_PREFIX); + if (startIndex == -1) { + return null; + } + int endIndex = strVal.lastIndexOf(PLACEHOLDER_SUFFIX); + if (endIndex == -1) { + return null; + } + + return strVal.substring(startIndex, endIndex + PLACEHOLDER_SUFFIX.length()); + } + + private int findPlaceholderEndIndex(CharSequence buf, int startIndex) { + int index = startIndex + PLACEHOLDER_PREFIX.length(); + int withinNestedPlaceholder = 0; + while (index < buf.length()) { + if (StringUtils.substringMatch(buf, index, PLACEHOLDER_SUFFIX)) { + if (withinNestedPlaceholder > 0) { + withinNestedPlaceholder--; + index = index + PLACEHOLDER_SUFFIX.length(); + } else { + return index; + } + } else if (StringUtils.substringMatch(buf, index, SIMPLE_PLACEHOLDER_PREFIX)) { + withinNestedPlaceholder++; + index = index + SIMPLE_PLACEHOLDER_PREFIX.length(); + } else { + index++; + } + } + return -1; + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/property/SpringValueProcessor.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/property/SpringValueProcessor.java new file mode 100644 index 000000000..e46c9b79b --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/property/SpringValueProcessor.java @@ -0,0 +1,116 @@ +package com.github.paicoding.forum.core.autoconf.property; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * 配置变更注册, 找到 @Value 注解修饰的配置,注册到 SpringValueRegistry,实现统一的配置变更自动刷新管理 + * + * @author YiHui + * @date 2023/6/26 + */ +@Slf4j +@Component +public class SpringValueProcessor implements BeanPostProcessor { + private final PlaceholderHelper placeholderHelper; + + public SpringValueProcessor() { + this.placeholderHelper = new PlaceholderHelper(); + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + Class clazz = bean.getClass(); + for (Field field : findAllField(clazz)) { + processField(bean, beanName, field); + } + for (Method method : findAllMethod(clazz)) { + processMethod(bean, beanName, method); + } + return bean; + } + + private List findAllField(Class clazz) { + final List res = new LinkedList<>(); + ReflectionUtils.doWithFields(clazz, res::add); + return res; + } + + private List findAllMethod(Class clazz) { + final List res = new LinkedList<>(); + ReflectionUtils.doWithMethods(clazz, res::add); + return res; + } + + /** + * 成员变量上添加 @Value 方式绑定的配置 + * + * @param bean + * @param beanName + * @param field + */ + protected void processField(Object bean, String beanName, Field field) { + // register @Value on field + Value value = field.getAnnotation(Value.class); + if (value == null) { + return; + } + Set keys = placeholderHelper.extractPlaceholderKeys(value.value()); + + if (keys.isEmpty()) { + return; + } + + for (String key : keys) { + SpringValueRegistry.SpringValue springValue = new SpringValueRegistry.SpringValue(key, value.value(), bean, beanName, field); + SpringValueRegistry.register(key, springValue); + log.debug("Monitoring {}", springValue); + } + } + + /** + * 通过 @Value 修饰方法的方式,通过一个传参进行实现的配置绑定 + * + * @param bean + * @param beanName + * @param method + */ + protected void processMethod(Object bean, String beanName, Method method) { + //register @Value on method + Value value = method.getAnnotation(Value.class); + if (value == null) { + return; + } + //skip Configuration bean methods + if (method.getAnnotation(Bean.class) != null) { + return; + } + if (method.getParameterTypes().length != 1) { + log.error("Ignore @Value setter {}.{}, expecting 1 parameter, actual {} parameters", bean.getClass().getName(), method.getName(), method.getParameterTypes().length); + return; + } + + Set keys = placeholderHelper.extractPlaceholderKeys(value.value()); + + if (keys.isEmpty()) { + return; + } + + for (String key : keys) { + SpringValueRegistry.SpringValue springValue = new SpringValueRegistry.SpringValue(key, value.value(), bean, beanName, method); + SpringValueRegistry.register(key, springValue); + log.debug("Monitoring {}", springValue); + } + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/property/SpringValueRegistry.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/property/SpringValueRegistry.java new file mode 100644 index 000000000..28dcff3b2 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/autoconf/property/SpringValueRegistry.java @@ -0,0 +1,170 @@ +package com.github.paicoding.forum.core.autoconf.property; + +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.core.util.StrUtil; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; + +/** + * 配置变更注册 + * + * @author YiHui + * @date 2023/6/26 + */ +@Slf4j +public class SpringValueRegistry { + @Data + public static class SpringValue { + /** + * 适合用于:配置是通过set类方法实现注入绑定的方式,只有一个传参,为对应的配置key + */ + private MethodParameter methodParameter; + /** + * 成员变量 + */ + private Field field; + /** + * bean示例的弱引用 + */ + private WeakReference beanRef; + /** + * Spring Bean Name + */ + private String beanName; + /** + * 配置对应的key: 如 config.user + */ + private String key; + /** + * 配置引用,如 ${config.user} + */ + private String placeholder; + /** + * 配置绑定的目标类型 + */ + private Class targetType; + + public SpringValue(String key, String placeholder, Object bean, String beanName, Field field) { + this.beanRef = new WeakReference<>(bean); + this.beanName = beanName; + this.field = field; + this.placeholder = placeholder; + this.targetType = field.getType(); + this.formatKey(key); + } + + public SpringValue(String key, String placeholder, Object bean, String beanName, Method method) { + this.beanRef = new WeakReference<>(bean); + this.beanName = beanName; + this.methodParameter = new MethodParameter(method, 0); + this.placeholder = placeholder; + Class[] paramTps = method.getParameterTypes(); + this.targetType = paramTps[0]; + this.formatKey(key); + } + + private void formatKey(String key) { + this.key = StrUtil.formatSpringConfigKey(key); + if (!Objects.equals(key, this.key)) { + log.info("配置key格式化输出: {} -> {}", key, this.key); + } + } + + /** + * 配置基于反射的动态变更 + * + * @param newVal String: 配置对应的key Class: 配置绑定的成员/方法参数类型, Object 新的配置值 + * @throws Exception + */ + public void update(BiFunction newVal) throws Exception { + if (isField()) { + injectField(newVal); + } else { + injectMethod(newVal); + } + } + + private void injectField(BiFunction newVal) throws Exception { + Object bean = beanRef.get(); + if (bean == null) { + return; + } + boolean accessible = field.isAccessible(); + field.setAccessible(true); + field.set(bean, newVal.apply(key, field.getType())); + field.setAccessible(accessible); + log.info("更新value: {}#{} = {}", beanName, field.getName(), field.get(bean)); + } + + private void injectMethod(BiFunction newVal) + throws Exception { + Object bean = beanRef.get(); + if (bean == null) { + return; + } + Object va = newVal.apply(key, methodParameter.getParameterType()); + methodParameter.getMethod().invoke(bean, va); + log.info("更新method: {}#{} = {}", beanName, methodParameter.getMethod().getName(), va); + } + + public boolean isField() { + return this.field != null; + } + } + + + public static Map> registry = new ConcurrentHashMap<>(); + + /** + * 像registry中注册配置key绑定的对象W + * + * @param key + * @param val + */ + public static void register(String key, SpringValue val) { + if (!registry.containsKey(key)) { + synchronized (SpringValueRegistry.class) { + if (!registry.containsKey(key)) { + registry.put(key, new HashSet<>()); + } + } + } + + Set set = registry.getOrDefault(key, new HashSet<>()); + set.add(val); + } + + /** + * key对应的配置发生了变更,找到绑定这个配置的属性,进行反射刷新 + * + * @param key + */ + public static void updateValue(String key) { + // 项目启动时,有一个配置,没有再配置文件中初始化,而是直接再应用代码中写上了默认值,此时若直接走下面的更新流程,会导致配置绑定异常,项目启动失败 + // 因此我们再执行更新时,先判断下配置上下文中是否有这个配置 + // fixme: 那么问题来了,如果是删除了一个动态配置,那应该怎么将应用中的配置刷新为默认值呢? + if (!SpringUtil.hasConfig(key)) { + return; + } + + Set set = registry.getOrDefault(key, new HashSet<>()); + set.forEach(s -> { + try { + s.update((s1, aClass) -> SpringUtil.getBinder().bindOrCreate(s1, aClass)); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/cache/RedisClient.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/cache/RedisClient.java new file mode 100644 index 000000000..342940d99 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/cache/RedisClient.java @@ -0,0 +1,475 @@ +package com.github.paicoding.forum.core.cache; + +import com.github.paicoding.forum.core.util.JsonUtil; +import com.google.common.collect.Maps; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisZSetCommands; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.util.CollectionUtils; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * @author YiHui + * @date 2023/2/7 + */ +public class RedisClient { + private static final Charset CODE = StandardCharsets.UTF_8; + private static final String KEY_PREFIX = "pai_"; + private static RedisTemplate template; + + public static void register(RedisTemplate template) { + RedisClient.template = template; + } + + public static void nullCheck(Object... args) { + for (Object obj : args) { + if (obj == null) { + throw new IllegalArgumentException("redis argument can not be null!"); + } + } + } + + /** + * 技术派的缓存值序列化处理 + * + * @param val + * @param + * @return + */ + public static byte[] valBytes(T val) { + + if (val instanceof String) { + return ((String) val).getBytes(CODE); + } else { + return JsonUtil.toStr(val).getBytes(CODE); + } + } + + /** + * 生成技术派的缓存key + * + * @param key + * @return + */ + public static byte[] keyBytes(String key) { + nullCheck(key); + key = KEY_PREFIX + key; + return key.getBytes(CODE); + } + + public static byte[][] keyBytes(List keys) { + byte[][] bytes = new byte[keys.size()][]; + int index = 0; + for (String key : keys) { + bytes[index++] = keyBytes(key); + } + return bytes; + } + + /** + * 返回key的有效期 + * + * @param key + * @return + */ + public static Long ttl(String key) { + return template.execute((RedisCallback) con -> con.ttl(keyBytes(key))); + } + + /** + * 查询缓存 + * + * @param key + * @return + */ + public static String getStr(String key) { + return template.execute((RedisCallback) con -> { + byte[] val = con.get(keyBytes(key)); + return val == null ? null : new String(val); + }); + } + + /** + * 设置缓存 + * + * @param key + * @param value + */ + public static void setStr(String key, String value) { + template.execute((RedisCallback) con -> { + con.set(keyBytes(key), valBytes(value)); + return null; + }); + } + + /** + * 删除缓存 + * + * @param key + */ + public static void del(String key) { + template.execute((RedisCallback) con -> con.del(keyBytes(key))); + } + + /** + * 设置缓存有效期 + * + * @param key + * @param expire 有效期,s为单位 + */ + public static void expire(String key, Long expire) { + template.execute((RedisCallback) connection -> { + connection.expire(keyBytes(key), expire); + return null; + }); + } + + /** + * 带过期时间的缓存写入 + * + * @param key + * @param value + * @param expire s为单位 + * @return + */ + public static Boolean setStrWithExpire(String key, String value, Long expire) { + return template.execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException { + return redisConnection.setEx(keyBytes(key), expire, valBytes(value)); + } + }); + } + + public static Map hGetAll(String key, Class clz) { + Map records = template.execute((RedisCallback>) con -> con.hGetAll(keyBytes(key))); + if (records == null) { + return Collections.emptyMap(); + } + + Map result = Maps.newHashMapWithExpectedSize(records.size()); + for (Map.Entry entry : records.entrySet()) { + if (entry.getKey() == null) { + continue; + } + + result.put(new String(entry.getKey()), toObj(entry.getValue(), clz)); + } + return result; + } + + public static T hGet(String key, String field, Class clz) { + return template.execute((RedisCallback) con -> { + byte[] records = con.hGet(keyBytes(key), valBytes(field)); + if (records == null) { + return null; + } + + return toObj(records, clz); + }); + } + + /** + * 自增 + * + * @param key + * @param filed + * @param cnt + * @return + */ + public static Long hIncr(String key, String filed, Integer cnt) { + return template.execute((RedisCallback) con -> con.hIncrBy(keyBytes(key), valBytes(filed), cnt)); + } + + public static Boolean hDel(String key, String field) { + return template.execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) throws DataAccessException { + return connection.hDel(keyBytes(key), valBytes(field)) > 0; + } + }); + } + + public static Boolean hSet(String key, String field, T ans) { + return template.execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException { + return redisConnection.hSet(keyBytes(key), valBytes(field), valBytes(ans)); + } + }); + } + + public static void hMSet(String key, Map fields) { + Map val = Maps.newHashMapWithExpectedSize(fields.size()); + for (Map.Entry entry : fields.entrySet()) { + val.put(valBytes(entry.getKey()), valBytes(entry.getValue())); + } + template.execute((RedisCallback) connection -> { + connection.hMSet(keyBytes(key), val); + return null; + }); + } + + public static Map hMGet(String key, final List fields, Class clz) { + return template.execute(new RedisCallback>() { + @Override + public Map doInRedis(RedisConnection connection) throws DataAccessException { + byte[][] f = new byte[fields.size()][]; + IntStream.range(0, fields.size()).forEach(i -> f[i] = valBytes(fields.get(i))); + List ans = connection.hMGet(keyBytes(key), f); + Map result = Maps.newHashMapWithExpectedSize(fields.size()); + IntStream.range(0, fields.size()).forEach(i -> { + result.put(fields.get(i), toObj(ans.get(i), clz)); + }); + return result; + } + }); + } + + /** + * 判断value是否再set中 + * + * @param key + * @param value + * @return + */ + public static Boolean sIsMember(String key, T value) { + return template.execute(new RedisCallback() { + @Override + public Boolean doInRedis(RedisConnection connection) throws DataAccessException { + return connection.sIsMember(keyBytes(key), valBytes(value)); + } + }); + } + + /** + * 获取set中的所有内容 + * + * @param key + * @param clz + * @param + * @return + */ + public static Set sGetAll(String key, Class clz) { + return template.execute(new RedisCallback>() { + @Override + public Set doInRedis(RedisConnection connection) throws DataAccessException { + Set set = connection.sMembers(keyBytes(key)); + if (CollectionUtils.isEmpty(set)) { + return Collections.emptySet(); + } + return set.stream().map(s -> toObj(s, clz)).collect(Collectors.toSet()); + } + }); + } + + /** + * 往set中添加内容 + * + * @param key + * @param val + * @param + * @return + */ + public static boolean sPut(String key, T val) { + return template.execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) throws DataAccessException { + return connection.sAdd(keyBytes(key), valBytes(val)); + } + }) > 0; + } + + /** + * 移除set中的内容 + * + * @param key + * @param val + * @param + */ + public static void sDel(String key, T val) { + template.execute(new RedisCallback() { + @Override + public Void doInRedis(RedisConnection connection) throws DataAccessException { + connection.sRem(keyBytes(key), valBytes(val)); + return null; + } + }); + } + + + /** + * 分数更新 + * + * @param key + * @param value + * @param score + * @return + */ + public static Double zIncrBy(String key, String value, Integer score) { + return template.execute(new RedisCallback() { + @Override + public Double doInRedis(RedisConnection connection) throws DataAccessException { + return connection.zIncrBy(keyBytes(key), score, valBytes(value)); + } + }); + } + + public static ImmutablePair zRankInfo(String key, String value) { + double score = zScore(key, value); + int rank = zRank(key, value); + return ImmutablePair.of(rank, score); + } + + /** + * 获取分数 + * + * @param key + * @param value + * @return + */ + public static Double zScore(String key, String value) { + return template.execute(new RedisCallback() { + @Override + public Double doInRedis(RedisConnection connection) throws DataAccessException { + return connection.zScore(keyBytes(key), valBytes(value)); + } + }); + } + + public static Integer zRank(String key, String value) { + return template.execute(new RedisCallback() { + @Override + public Integer doInRedis(RedisConnection connection) throws DataAccessException { + return connection.zRank(keyBytes(key), valBytes(value)).intValue(); + } + }); + } + + /** + * 找出排名靠前的n个 + * + * @param key + * @param n + * @return + */ + public static List> zTopNScore(String key, int n) { + return template.execute(new RedisCallback>>() { + @Override + public List> doInRedis(RedisConnection connection) throws DataAccessException { + Set set = connection.zRangeWithScores(keyBytes(key), -n, -1); + if (set == null) { + return Collections.emptyList(); + } + return set.stream() + .map(tuple -> ImmutablePair.of(toObj(tuple.getValue(), String.class), tuple.getScore())) + .sorted((o1, o2) -> Double.compare(o2.getRight(), o1.getRight())).collect(Collectors.toList()); + } + }); + } + + + public static Long lPush(String key, T val) { + return template.execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) throws DataAccessException { + return connection.lPush(keyBytes(key), valBytes(val)); + } + }); + } + + public static Long rPush(String key, T val) { + return template.execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) throws DataAccessException { + return connection.rPush(keyBytes(key), valBytes(val)); + } + }); + } + + public static List lRange(String key, int start, int size, Class clz) { + return template.execute(new RedisCallback>() { + + @Override + public List doInRedis(RedisConnection connection) throws DataAccessException { + List list = connection.lRange(keyBytes(key), start, size); + if (CollectionUtils.isEmpty(list)) { + return new ArrayList<>(); + } + return list.stream().map(k -> toObj(k, clz)).collect(Collectors.toList()); + } + }); + } + + public static void lTrim(String key, int start, int size) { + template.execute(new RedisCallback() { + @Override + public Void doInRedis(RedisConnection connection) throws DataAccessException { + connection.lTrim(keyBytes(key), start, size); + return null; + } + }); + } + + private static T toObj(byte[] ans, Class clz) { + if (ans == null) { + return null; + } + + if (clz == String.class) { + return (T) new String(ans, CODE); + } + + return JsonUtil.toObj(new String(ans, CODE), clz); + } + + + public static PipelineAction pipelineAction() { + return new PipelineAction(); + } + + /** + * redis 管道执行的封装链路 + */ + public static class PipelineAction { + private List run = new ArrayList<>(); + + private RedisConnection connection; + + public PipelineAction add(String key, BiConsumer conn) { + run.add(() -> conn.accept(connection, RedisClient.keyBytes(key))); + return this; + } + + public PipelineAction add(String key, String field, ThreeConsumer conn) { + run.add(() -> conn.accept(connection, RedisClient.keyBytes(key), valBytes(field))); + return this; + } + + public void execute() { + template.executePipelined((RedisCallback) connection -> { + PipelineAction.this.connection = connection; + run.forEach(Runnable::run); + return null; + }); + } + } + + @FunctionalInterface + public interface ThreeConsumer { + void accept(T t, U u, P p); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/common/CommonConstants.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/common/CommonConstants.java new file mode 100644 index 000000000..9226d0b8b --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/common/CommonConstants.java @@ -0,0 +1,124 @@ +package com.github.paicoding.forum.core.common; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 通用常量 + * + * @author Louzai + * @date 2022/11/1 + */ +public class CommonConstants { + + /** + * 消息队列 + */ + public static String EXCHANGE_NAME_DIRECT = "direct.exchange"; + public static String QUERE_KEY_PRAISE = "praise"; + public static String QUERE_NAME_PRAISE = "quere.praise"; + + /** + * 分类类型 + */ + public static final String CATEGORY_ALL = "全部"; + public static final String CATEGORY_BACK_EMD = "后端"; + public static final String CATEGORY_FORNT_END = "前端"; + public static final String CATEGORY_ANDROID = "Android"; + public static final String CATEGORY_IOS = "IOS"; + public static final String CATEGORY_BIG_DATA = "大数据"; + public static final String CATEGORY_INTELLIGENCE = "人工智能"; + public static final String CATEGORY_CODE_LIFE = "代码人生"; + public static final String CATEGORY_TOOL = "开发工具"; + public static final String CATEGORY_READ = "阅读"; + + /** + * 首页图片 + */ + public static final Map> HOMEPAGE_TOP_PIC_MAP = new HashMap>() { + { + put(CATEGORY_ALL, new ArrayList() { + { + add("https://cdn.tobebetterjavaer.com/paicoding/8b5865e9461948aed4aacffc62adbae7.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/71505c62fb6375cbbd62af63964b2ad4.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/147915cdddea55ce37c2c5ecfc7c089e.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/dee73c8810cb699ae1ec774a54612080.jpg"); + } + }); + put(CATEGORY_BACK_EMD, new ArrayList() { + { + add("https://cdn.tobebetterjavaer.com/paicoding/b2af54aefab4fbf8f001065961a8118b.gif"); + add("https://cdn.tobebetterjavaer.com/paicoding/99ca3b142d901d9ca63946efca0122d8.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/cfbdbd36b2194dd4fd9da2ea18e8a56a.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/f693ed4c1969724a44004a96fcce4263.jpg"); + } + }); + put(CATEGORY_FORNT_END, new ArrayList() { + { + add("https://cdn.tobebetterjavaer.com/paicoding/7c591a44a9f83eec9606d16f89040632.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/4eb9f0bad37ba5903ca9e249743e3ecb.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/6a810a33480fc3b42740713a6687f3fd.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/6ed5aa120b7a06a5f4b5fe351adfda94.jpg"); + } + }); + put(CATEGORY_ANDROID, new ArrayList() { + { + add("https://cdn.tobebetterjavaer.com/paicoding/f266aabeb976b2b9c4bf24a107a78c5d.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/dee27ae91078714cc9f6b1774161c1ef.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/a8cfe8140b683809a68205da76e77fb1.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/b964b76b111cf36602d9b2dc30bee9ee.jpg"); + } + }); + put(CATEGORY_IOS, new ArrayList() { + { + add("https://cdn.tobebetterjavaer.com/paicoding/7edcb3dd19d4d517be34a30bc082338d.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/ee2a3fec62d85df3b1c27908d53698c5.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/f476fbc0cf90ed81802ef6a1d51fcf16.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/bfc7cbdeb928e03d034be1f9d73f4a9e.jpg"); + } + }); + put(CATEGORY_BIG_DATA, new ArrayList() { + { + add("https://cdn.tobebetterjavaer.com/paicoding/989d3c1f73ee953a05347c0b99cba46d.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/a96fe34a09d9fd9cafd64eae90410428.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/1217d3dd677d91cb65b0cc85769f7f3d.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/5de37f5c879543bd17f031f4243fae7d.jpg"); + } + }); + put(CATEGORY_INTELLIGENCE, new ArrayList() { + { + add("https://cdn.tobebetterjavaer.com/paicoding/077b7d8891e69701e8d3d4302392dab5.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/be98a2779d2dde96c40092bbae958864.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/d368d4bed7daf51116f4defbb4afcb6d.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/c6bfe6bea326a64a267520ba7cada539.jpg"); + } + }); + put(CATEGORY_CODE_LIFE, new ArrayList() { + { + add("https://cdn.tobebetterjavaer.com/paicoding/077b7d8891e69701e8d3d4302392dab5.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/be98a2779d2dde96c40092bbae958864.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/d368d4bed7daf51116f4defbb4afcb6d.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/c6bfe6bea326a64a267520ba7cada539.jpg"); + } + }); + put(CATEGORY_TOOL, new ArrayList() { + { + add("https://cdn.tobebetterjavaer.com/paicoding/53de05a01c7246feadffb6ba24120416.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/e3e1f7a729d5cfbde0e5373c2d61377a.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/326585eab30c33cdfa1cc058b269bf5a.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/870997d1442e4d67186bf0dbc52e2096.jpg"); + } + }); + put(CATEGORY_READ, new ArrayList() { + { + add("https://cdn.tobebetterjavaer.com/paicoding/dd3f3e90b666cfe65f4ca5e56ebfc9f8.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/7c591a44a9f83eec9606d16f89040632.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/42dbb88fed8caa2d95860dbdb359c18f.jpg"); + add("https://cdn.tobebetterjavaer.com/paicoding/b99cab7999d5bdf3f5926dc0a98d02da.jpg"); + } + }); + } + }; +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/config/ImageProperties.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/config/ImageProperties.java new file mode 100644 index 000000000..d17916896 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/config/ImageProperties.java @@ -0,0 +1,48 @@ +package com.github.paicoding.forum.core.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 图片配置文件 + * + * @author LouZai + * @since 2022/9/7 + */ +@Setter +@Getter +@Component +@ConfigurationProperties(prefix = "image") +public class ImageProperties { + + /** + * 存储绝对路径 + */ + private String absTmpPath; + + /** + * 存储相对路径 + */ + private String webImgPath; + + /** + * 上传文件的临时存储目录 + */ + private String tmpUploadPath; + + /** + * 访问图片的host + */ + private String cdnHost; + + private OssProperties oss; + + public String buildImgUrl(String url) { + if (!url.startsWith(cdnHost)) { + return cdnHost + url; + } + return url; + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/config/OssProperties.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/config/OssProperties.java new file mode 100644 index 000000000..2198b8ab6 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/config/OssProperties.java @@ -0,0 +1,27 @@ +package com.github.paicoding.forum.core.config; + +import lombok.Data; + +/** + * @author YiHui + * @date 2023/1/12 + */ +@Data +public class OssProperties { + /** + * 上传文件前缀路径 + */ + private String prefix; + /** + * oss类型 + */ + private String type; + /** + * 下面几个是oss的配置参数 + */ + private String endpoint; + private String ak; + private String sk; + private String bucket; + private String host; +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/config/ProxyProperties.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/config/ProxyProperties.java new file mode 100644 index 000000000..dc81ea117 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/config/ProxyProperties.java @@ -0,0 +1,35 @@ +package com.github.paicoding.forum.core.config; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.net.Proxy; +import java.util.List; + +/** + * @author YiHui + * @date 2023/1/12 + */ +@Data +@ConfigurationProperties(prefix = "net") +public class ProxyProperties { + private List proxy; + + @Data + @Accessors(chain = true) + public static class ProxyType { + /** + * 代理类型 + */ + private Proxy.Type type; + /** + * 代理ip + */ + private String ip; + /** + * 代理端口 + */ + private Integer port; + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/config/RabbitmqProperties.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/config/RabbitmqProperties.java new file mode 100644 index 000000000..24236ea58 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/config/RabbitmqProperties.java @@ -0,0 +1,53 @@ +package com.github.paicoding.forum.core.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * RabbitMQ配置文件 + * + * @author LouZai + * @since 2023/5/10 + */ +@Setter +@Getter +@ConfigurationProperties(prefix = "rabbitmq") +public class RabbitmqProperties { + + /** + * 主机 + */ + private String host; + + /** + * 端口 + */ + private Integer port; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String passport; + + /** + * 路径 + */ + private String virtualhost; + + /** + * 连接池大小 + */ + private Integer poolSize; + + /** + * 开关 false-关闭,true-打开 + */ + private Boolean switchFlag; +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DS.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DS.java new file mode 100644 index 000000000..ce03ab040 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DS.java @@ -0,0 +1,14 @@ +package com.github.paicoding.forum.core.dal; + +/** + * @author YiHui + * @date 2023/4/30 + */ +public interface DS { + /** + * 使用的数据源名 + * + * @return + */ + String name(); +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DataSourceConfig.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DataSourceConfig.java new file mode 100644 index 000000000..c786829c2 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DataSourceConfig.java @@ -0,0 +1,147 @@ +package com.github.paicoding.forum.core.dal; + +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.support.http.StatViewServlet; +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.Environment; +import org.springframework.util.CollectionUtils; + +import javax.sql.DataSource; +import java.util.Map; + +/** + * 当配置了多数据源时,启用 + * + * @author YiHui + * @date 2023/4/30 + */ +@Slf4j +@Configuration +@ConditionalOnProperty(prefix = "spring.dynamic", name = "primary") +@EnableConfigurationProperties(DsProperties.class) +public class DataSourceConfig { + + private Environment environment; + + public DataSourceConfig(Environment environment) { + this.environment = environment; + log.info("动态数据源初始化!"); + } + + @Bean + public DsAspect dsAspect() { + return new DsAspect(); + } + + @Bean + public SqlStateInterceptor sqlStateInterceptor() { + return new SqlStateInterceptor(); + } + + /** + * 整合主从数据源 + * + * @param dsProperties + * @return 1 + */ + @Bean + @Primary + public DataSource dataSource(DsProperties dsProperties) { + Map targetDataSources = Maps.newHashMapWithExpectedSize(dsProperties.getDatasource().size()); + dsProperties.getDatasource().forEach((k, v) -> targetDataSources.put(k.toUpperCase(), initDataSource(k, v))); + + if (CollectionUtils.isEmpty(targetDataSources)) { + throw new IllegalStateException("多数据源配置,请以 spring.dynamic 开头"); + } + + MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource(); + Object key = dsProperties.getPrimary().toUpperCase(); + if (!targetDataSources.containsKey(key)) { + if (targetDataSources.containsKey(MasterSlaveDsEnum.MASTER.name())) { + // 当们没有配置primary对应的数据源时,存在MASTER数据源,则将主库作为默认的数据源 + key = MasterSlaveDsEnum.MASTER.name(); + } else { + key = targetDataSources.keySet().iterator().next(); + } + } + + log.info("动态数据源,默认启用为: " + key); + myRoutingDataSource.setDefaultTargetDataSource(targetDataSources.get(key)); + myRoutingDataSource.setTargetDataSources(targetDataSources); + return myRoutingDataSource; + } + + + public DataSource initDataSource(String prefix, DataSourceProperties properties) { + if (!DruidCheckUtil.hasDuridPkg()) { + log.info("实例化HikarDataSource: {}", prefix); + return properties.initializeDataSourceBuilder().build(); + } + + if (properties.getType() == null || !properties.getType().isAssignableFrom(DruidDataSource.class)) { + log.info("实例化HikarDataSource: {}", prefix); + return properties.initializeDataSourceBuilder().build(); + } + + log.info("实例化DruidDataSource: {}", prefix); + // fixme 知识点:手动将配置赋值到实例中的方式 + return Binder.get(environment).bindOrCreate(DsProperties.DS_PREFIX + ".datasource." + prefix, DruidDataSource.class); + } + + /** + * 在数据源实例化之后进行创建 + * + * @return + */ + @Bean + @ConditionalOnExpression(value = "T(com.github.paicoding.forum.core.dal.DruidCheckUtil).hasDuridPkg()") + public ServletRegistrationBean druidStatViewServlet() { + //先配置管理后台的servLet,访问的入口为/druid/ + ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean<>( + new StatViewServlet(), "/druid/*"); + // IP白名单 (没有配置或者为空,则允许所有访问) + servletRegistrationBean.addInitParameter("allow", "127.0.0.1"); + // IP黑名单 (存在共同时,deny优先于allow) + servletRegistrationBean.addInitParameter("deny", ""); + servletRegistrationBean.addInitParameter("loginUsername", "admin"); + servletRegistrationBean.addInitParameter("loginPassword", "admin"); + servletRegistrationBean.addInitParameter("resetEnable", "false"); + log.info("开启druid数据源监控面板"); + return servletRegistrationBean; + } + +// @Bean +// public JdbcTemplate notifyFullJdbcTemplate(DataSource myRoutingDataSource) { +// return new JdbcTemplate(myRoutingDataSource); +// } +// +// @Bean(name = "SqlSessionFactory") +// public SqlSessionFactory test1SqlSessionFactory(DataSource dynamicDataSource, GlobalConfig globalConfig) +// throws Exception { +// MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean(); +// bean.setDataSource(dynamicDataSource); +// /**当使用多数据源时,mybatisPlus默认配置将会失效,需要单独将其注入数据源中 */ +//// bean.setPlugins(plugins); +// /** 设置全局配置 */ +// bean.setGlobalConfig(globalConfig); +// return bean.getObject(); +// } +// +// /** 全局自定义配置 */ +// @Bean(name = "globalConfig") +// @ConfigurationProperties(prefix = "mybatis-plus.global-config") +// public GlobalConfig globalConfig(){ +// return new GlobalConfig(); +// } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DruidCheckUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DruidCheckUtil.java new file mode 100644 index 000000000..7fab8275f --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DruidCheckUtil.java @@ -0,0 +1,20 @@ +package com.github.paicoding.forum.core.dal; + +import com.github.hui.quick.plugin.qrcode.util.ClassUtils; + +/** + * @author YiHui + * @date 2023/5/28 + */ +public class DruidCheckUtil { + + /** + * 判断是否包含durid相关的数据包 + * + * @return + */ + public static boolean hasDuridPkg() { + return ClassUtils.isPresent("com.alibaba.druid.pool.DruidDataSource", DataSourceConfig.class.getClassLoader()); + } + +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DsAno.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DsAno.java new file mode 100644 index 000000000..773f72558 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DsAno.java @@ -0,0 +1,29 @@ +package com.github.paicoding.forum.core.dal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author YiHui + * @date 2023/4/30 + */ + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface DsAno { + /** + * 启用的数据源,默认主库 + * + * @return + */ + MasterSlaveDsEnum value() default MasterSlaveDsEnum.MASTER; + + /** + * 启用的数据源,如果存在,则优先使用它来替换默认的value + * + * @return + */ + String ds() default ""; +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DsAspect.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DsAspect.java new file mode 100644 index 000000000..ce8bac558 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DsAspect.java @@ -0,0 +1,52 @@ +package com.github.paicoding.forum.core.dal; + +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; + +import java.lang.reflect.Method; + +/** + * @author YiHui + * @date 2023/4/30 + */ +@Aspect +public class DsAspect { + /** + * 切入点, 拦截类上、方法上有注解的方法,用于切换数据源 + */ + @Pointcut("@annotation(com.github.paicoding.forum.core.dal.DsAno) || @within(com.github.paicoding.forum.core.dal.DsAno)") + public void pointcut() { + } + + @Around("pointcut()") + public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { + DsAno ds = getDsAno(proceedingJoinPoint); + try { + if (ds != null && (StringUtils.isNotBlank(ds.ds()) || ds.value() != null)) { + // 当上下文中没有时,则写入线程上下文,应该用哪个DB + DsContextHolder.set(StringUtils.isNoneBlank(ds.ds()) ? ds.ds() : ds.value().name()); + } + return proceedingJoinPoint.proceed(); + } finally { + // 清空上下文信息 + if (ds != null) { + DsContextHolder.reset(); + } + } + } + + private DsAno getDsAno(ProceedingJoinPoint proceedingJoinPoint) { + MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature(); + Method method = signature.getMethod(); + DsAno ds = method.getAnnotation(DsAno.class); + if (ds == null) { + // 获取类上的注解 + ds = (DsAno) proceedingJoinPoint.getSignature().getDeclaringType().getAnnotation(DsAno.class); + } + return ds; + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DsContextHolder.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DsContextHolder.java new file mode 100644 index 000000000..c36ba0b65 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DsContextHolder.java @@ -0,0 +1,77 @@ +package com.github.paicoding.forum.core.dal; + +/** + * 数据源选择上下持有类,用于存储当前选中的是哪个数据源 + * + * @author YiHui + * @date 2023/4/30 + */ +public class DsContextHolder { + /** + * 使用继承的线程上下文,支持异步时选择传递 + * 使用DsNode,支持链式的数据源切换,如最外层使用master数据源,内部某个方法使用slave数据源;但是请注意,对于事务的场景,不要交叉 + */ + private static final ThreadLocal CONTEXT_HOLDER = new InheritableThreadLocal<>(); + + private DsContextHolder() { + } + + + public static void set(String dbType) { + DsNode current = CONTEXT_HOLDER.get(); + CONTEXT_HOLDER.set(new DsNode(current, dbType)); + } + + public static String get() { + DsNode ds = CONTEXT_HOLDER.get(); + return ds == null ? null : ds.ds; + } + + + public static void set(DS ds) { + set(ds.name().toUpperCase()); + } + + + /** + * 移除上下文 + */ + public static void reset() { + DsNode ds = CONTEXT_HOLDER.get(); + if (ds == null) { + return; + } + + if (ds.pre != null) { + // 退出当前的数据源选择,切回去走上一次的数据源配置 + CONTEXT_HOLDER.set(ds.pre); + } else { + CONTEXT_HOLDER.remove(); + } + } + + /** + * 使用主数据源类型 + */ + public static void master() { + set(MasterSlaveDsEnum.MASTER.name()); + } + + /** + * 使用从数据源类型 + */ + public static void slave() { + set(MasterSlaveDsEnum.SLAVE.name()); + } + + public static class DsNode { + DsNode pre; + String ds; + + public DsNode(DsNode parent, String ds) { + pre = parent; + this.ds = ds; + } + } + +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DsProperties.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DsProperties.java new file mode 100644 index 000000000..e877d1e49 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DsProperties.java @@ -0,0 +1,28 @@ +package com.github.paicoding.forum.core.dal; + +import lombok.Data; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Map; + +/** + * 多数据源的配置加载 + * + * @author YiHui + * @date 2023/4/30 + */ +@Data +@ConfigurationProperties(prefix = DsProperties.DS_PREFIX) +public class DsProperties { + public static final String DS_PREFIX = "spring.dynamic"; + /** + * 默认数据源 + */ + private String primary; + + /** + * 多数据源配置 + */ + private Map datasource; +} \ No newline at end of file diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DsSelectExecutor.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DsSelectExecutor.java new file mode 100644 index 000000000..50b6f2b58 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/DsSelectExecutor.java @@ -0,0 +1,44 @@ +package com.github.paicoding.forum.core.dal; + +import java.util.function.Supplier; + +/** + * 手动指定数据源的用法 + * + * @author YiHui + * @date 2023/4/30 + */ +public class DsSelectExecutor { + + /** + * 有返回结果 + * + * @param ds + * @param supplier + * @param + * @return + */ + public static T submit(DS ds, Supplier supplier) { + DsContextHolder.set(ds); + try { + return supplier.get(); + } finally { + DsContextHolder.reset(); + } + } + + /** + * 无返回结果 + * + * @param ds + * @param call + */ + public static void execute(DS ds, Runnable call) { + DsContextHolder.set(ds); + try { + call.run(); + } finally { + DsContextHolder.reset(); + } + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/MasterSlaveDsEnum.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/MasterSlaveDsEnum.java new file mode 100644 index 000000000..a5caec65f --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/MasterSlaveDsEnum.java @@ -0,0 +1,18 @@ +package com.github.paicoding.forum.core.dal; + +/** + * 主从数据源的枚举 + * + * @author YiHui + * @date 2023/4/30 + */ +public enum MasterSlaveDsEnum implements DS { + /** + * master主数据源类型 + */ + MASTER, + /** + * slave从数据源类型 + */ + SLAVE; +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/MyRoutingDataSource.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/MyRoutingDataSource.java new file mode 100644 index 000000000..147272170 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/MyRoutingDataSource.java @@ -0,0 +1,17 @@ +package com.github.paicoding.forum.core.dal; + +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.lang.Nullable; + +/** + * @author YiHui + * @date 2023/4/30 + */ +public class MyRoutingDataSource extends AbstractRoutingDataSource { + @Nullable + @Override + protected Object determineCurrentLookupKey() { + return DsContextHolder.get(); + } + +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/SqlStateInterceptor.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/SqlStateInterceptor.java new file mode 100644 index 000000000..a06353cca --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/dal/SqlStateInterceptor.java @@ -0,0 +1,194 @@ +package com.github.paicoding.forum.core.dal; + +import com.alibaba.druid.pool.DruidPooledPreparedStatement; +import com.baomidou.mybatisplus.core.MybatisParameterHandler; +import com.github.paicoding.forum.core.util.DateUtil; +import com.mysql.cj.MysqlConnection; +import com.zaxxer.hikari.pool.HikariProxyConnection; +import com.zaxxer.hikari.pool.HikariProxyPreparedStatement; +import lombok.extern.slf4j.Slf4j; +import nonapi.io.github.classgraph.utils.ReflectionUtils; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.ParameterMapping; +import org.apache.ibatis.mapping.ParameterMode; +import org.apache.ibatis.plugin.*; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.scripting.defaults.DefaultParameterHandler; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.ResultHandler; +import org.springframework.util.CollectionUtils; + +import java.sql.Connection; +import java.sql.Date; +import java.sql.Statement; +import java.util.List; +import java.util.Properties; +import java.util.regex.Matcher; + +/** + * mybatis拦截器。输出sql执行情况 + * + * @author YiHui + * @date 2023/5/01 + */ +@Slf4j +@Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class, method = "update", args = {Statement.class})}) +public class SqlStateInterceptor implements Interceptor { + @Override + public Object intercept(Invocation invocation) throws Throwable { + long time = System.currentTimeMillis(); + StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); + String sql = buildSql(statementHandler); + Object[] args = invocation.getArgs(); + String uname = ""; + if (args[0] instanceof HikariProxyPreparedStatement) { + HikariProxyConnection connection = (HikariProxyConnection) ((HikariProxyPreparedStatement) invocation.getArgs()[0]).getConnection(); + uname = connection.getMetaData().getUserName(); + } else if (DruidCheckUtil.hasDuridPkg()) { + if (args[0] instanceof DruidPooledPreparedStatement) { + Connection connection = ((DruidPooledPreparedStatement) args[0]).getStatement().getConnection(); + if (connection instanceof MysqlConnection) { + Properties properties = ((MysqlConnection) connection).getProperties(); + uname = properties.getProperty("user"); + } + } + } + + Object rs; + try { + rs = invocation.proceed(); + } catch (Throwable e) { + log.error("error sql: " + sql, e); + throw e; + } finally { + long cost = System.currentTimeMillis() - time; + sql = this.replaceContinueSpace(sql); + // 这个方法的总耗时 + log.info("\n\n ============= \nsql ----> {}\nuser ----> {}\ncost ----> {}\n ============= \n", sql, uname, cost); + } + + return rs; + } + + /** + * 拼接sql + * + * @param statementHandler + * @return + */ + private String buildSql(StatementHandler statementHandler) { + BoundSql boundSql = statementHandler.getBoundSql(); + Configuration configuration = null; + if (statementHandler.getParameterHandler() instanceof DefaultParameterHandler) { + DefaultParameterHandler handler = (DefaultParameterHandler) statementHandler.getParameterHandler(); + configuration = (Configuration) ReflectionUtils.getFieldVal(handler, "configuration", false); + } else if (statementHandler.getParameterHandler() instanceof MybatisParameterHandler) { + MybatisParameterHandler paramHandler = (MybatisParameterHandler) statementHandler.getParameterHandler(); + configuration = ((MappedStatement) ReflectionUtils.getFieldVal(paramHandler, "mappedStatement", false)).getConfiguration(); + } + + if (configuration == null) { + return boundSql.getSql(); + } + + return getSql(boundSql, configuration); + } + + + /** + * 生成要执行的SQL命令 + * + * @param boundSql + * @param configuration + * @return + */ + private String getSql(BoundSql boundSql, Configuration configuration) { + String sql = boundSql.getSql(); + Object parameterObject = boundSql.getParameterObject(); + List parameterMappings = boundSql.getParameterMappings(); + if (CollectionUtils.isEmpty(parameterMappings) || parameterObject == null) { + return sql; + } + + MetaObject mo = configuration.newMetaObject(boundSql.getParameterObject()); + for (ParameterMapping parameterMapping : parameterMappings) { + if (parameterMapping.getMode() == ParameterMode.OUT) { + continue; + } + + //参数值 + Object value; + //获取参数名称 + String propertyName = parameterMapping.getProperty(); + if (boundSql.hasAdditionalParameter(propertyName)) { + //获取参数值 + value = boundSql.getAdditionalParameter(propertyName); + } else if (configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass())) { + //如果是单个值则直接赋值 + value = parameterObject; + } else { + value = mo.getValue(propertyName); + } + String param = Matcher.quoteReplacement(getParameter(value)); + sql = sql.replaceFirst("\\?", param); + } + sql += ";"; + return sql; + } + + public String getParameter(Object parameter) { + if (parameter instanceof String) { + return "'" + parameter + "'"; + } else if (parameter instanceof Date) { + // 日期格式化 + return "'" + DateUtil.format(DateUtil.DB_FORMAT, ((Date) parameter).getTime()) + "'"; + } else if (parameter instanceof java.util.Date) { + // 日期格式化 + return "'" + DateUtil.format(DateUtil.DB_FORMAT, ((java.util.Date) parameter).getTime()) + "'"; + } + return parameter.toString(); + } + + /** + * 替换连续的空白 + * + * @param str + * @return + */ + private String replaceContinueSpace(String str) { + StringBuilder builder = new StringBuilder(str.length()); + boolean preSpace = false; + for (int i = 0, len = str.length(); i < len; i++) { + char ch = str.charAt(i); + boolean isSpace = Character.isWhitespace(ch); + if (preSpace && isSpace) { + continue; + } + + if (preSpace) { + // 前面的是空白字符,当前的不是空白字符 + preSpace = false; + builder.append(ch); + } else if (isSpace) { + // 当前字符为空白字符,前面的那个不是的 + preSpace = true; + builder.append(" "); + } else { + // 前一个和当前字符都非空白字符 + builder.append(ch); + } + } + return builder.toString(); + } + + @Override + public Object plugin(Object o) { + return Plugin.wrap(o, this); + } + + @Override + public void setProperties(Properties properties) { + } +} \ No newline at end of file diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/markdown/CustomAdmonitionBlockParser.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/markdown/CustomAdmonitionBlockParser.java new file mode 100644 index 000000000..30f75c8d7 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/markdown/CustomAdmonitionBlockParser.java @@ -0,0 +1,224 @@ +package com.github.paicoding.forum.core.markdown; + +import com.vladsch.flexmark.ast.ListItem; +import com.vladsch.flexmark.ast.util.Parsing; +import com.vladsch.flexmark.ext.admonition.AdmonitionBlock; +import com.vladsch.flexmark.ext.admonition.internal.AdmonitionOptions; +import com.vladsch.flexmark.parser.block.*; +import com.vladsch.flexmark.util.ast.Block; +import com.vladsch.flexmark.util.data.DataHolder; +import com.vladsch.flexmark.util.sequence.BasedSequence; +import com.vladsch.flexmark.util.sequence.mappers.SpecialLeadInHandler; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CustomAdmonitionBlockParser extends AbstractBlockParser { + final private static String ADMONITION_START_FORMAT = "^(\\?{3}\\+|\\?{3}|!{3}|:{3})\\s*(%s)(?:\\s+(%s))?\\s*$"; + + final AdmonitionBlock block; + //private BlockContent content = new BlockContent(); + final private AdmonitionOptions options; + final private int contentIndent; + private boolean hadBlankLine; + private boolean isOver; + + CustomAdmonitionBlockParser(AdmonitionOptions options, int contentIndent) { + this.options = options; + this.contentIndent = contentIndent; + this.block = new AdmonitionBlock(); + } + + private int getContentIndent() { + return contentIndent; + } + + @Override + public Block getBlock() { + return block; + } + + @Override + public boolean isContainer() { + return true; + } + + @Override + public boolean canContain(ParserState state, BlockParser blockParser, final Block block) { + return true; + } + + @Override + public BlockContinue tryContinue(ParserState state) { + // 获取当前行内容 + BasedSequence line = state.getLine(); + final int nonSpaceIndex = state.getNextNonSpaceIndex(); + + // 判断是否是终止符 "!!!" + if (isOver) { + return BlockContinue.none(); + } + + if (line.startsWith("!!!") || line.startsWith("???") || line.startsWith(":::")) { + isOver = true;// 停止解析 + } + + // 如果当前行是空行,则继续解析,同时标记块中出现过空行 + if (state.isBlank()) { + hadBlankLine = true; + return BlockContinue.atIndex(nonSpaceIndex); + } + + // 如果允许懒惰继续(lazy continuation),且未遇到空行 + if (!hadBlankLine && options.allowLazyContinuation) { + return BlockContinue.atIndex(nonSpaceIndex); + } + + // 如果缩进足够,则继续解析当前行 + if (state.getIndent() >= options.contentIndent) { + int contentIndent = state.getColumn() + options.contentIndent; + return BlockContinue.atColumn(contentIndent); + } + + // 默认情况,继续解析当前行 + return BlockContinue.atIndex(nonSpaceIndex); + } + + @Override + public void closeBlock(ParserState state) { + block.setCharsFromContent(); + } + + public static class Factory implements CustomBlockParserFactory { + @Nullable + @Override + public Set> getAfterDependents() { + return null; + } + + @Nullable + @Override + public Set> getBeforeDependents() { + return null; + } + + @Override + public @Nullable SpecialLeadInHandler getLeadInHandler(@NotNull DataHolder options) { + return CustomAdmonitionBlockParser.AdmonitionLeadInHandler.HANDLER; + } + + @Override + public boolean affectsGlobalScope() { + return false; + } + + @NotNull + @Override + public BlockParserFactory apply(@NotNull DataHolder options) { + return new CustomAdmonitionBlockParser + .BlockFactory(options); + } + } + + static class AdmonitionLeadInHandler implements SpecialLeadInHandler { + final static SpecialLeadInHandler HANDLER = new CustomAdmonitionBlockParser + .AdmonitionLeadInHandler(); + + @Override + public boolean escape(@NotNull BasedSequence sequence, @Nullable DataHolder options, @NotNull Consumer consumer) { + if ((sequence.length() == 3 || sequence.length() == 4 && sequence.charAt(3) == '+') && (sequence.startsWith("???") || sequence.startsWith("!!!") || sequence.startsWith(":::"))) { + consumer.accept("\\"); + consumer.accept(sequence); + return true; + } + return false; + } + + @Override + public boolean unEscape(@NotNull BasedSequence sequence, @Nullable DataHolder options, @NotNull Consumer consumer) { + if ((sequence.length() == 4 || sequence.length() == 5 && sequence.charAt(4) == '+') && (sequence.startsWith("\\???") || sequence.startsWith("\\!!!") || sequence.startsWith("\\:::"))) { + consumer.accept(sequence.subSequence(1)); + return true; + } + return false; + } + } + + static boolean isMarker( + final ParserState state, + final int index, + final boolean inParagraph, + final boolean inParagraphListItem, + final AdmonitionOptions options + ) { + final boolean allowLeadingSpace = options.allowLeadingSpace; + final boolean interruptsParagraph = options.interruptsParagraph; + final boolean interruptsItemParagraph = options.interruptsItemParagraph; + final boolean withLeadSpacesInterruptsItemParagraph = options.withSpacesInterruptsItemParagraph; + CharSequence line = state.getLine(); + if (!inParagraph || interruptsParagraph) { + if ((allowLeadingSpace || state.getIndent() == 0) && (!inParagraphListItem || interruptsItemParagraph)) { + if (inParagraphListItem && !withLeadSpacesInterruptsItemParagraph) { + return state.getIndent() == 0; + } else { + return state.getIndent() < state.getParsing().CODE_BLOCK_INDENT; + } + } + } + return false; + } + + private static class BlockFactory extends AbstractBlockParserFactory { + final private AdmonitionOptions options; + + BlockFactory(DataHolder options) { + super(options); + this.options = new AdmonitionOptions(options); + } + + @Override + public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { + if (state.getIndent() >= 4) { + return BlockStart.none(); + } + + int nextNonSpace = state.getNextNonSpaceIndex(); + BlockParser matched = matchedBlockParser.getBlockParser(); + boolean inParagraph = matched.isParagraphParser(); + boolean inParagraphListItem = inParagraph && matched.getBlock().getParent() instanceof ListItem && matched.getBlock() == matched.getBlock().getParent().getFirstChild(); + + if (isMarker(state, nextNonSpace, inParagraph, inParagraphListItem, options)) { + BasedSequence line = state.getLine(); + BasedSequence trySequence = line.subSequence(nextNonSpace, line.length()); + Parsing parsing = state.getParsing(); + Pattern startPattern = Pattern.compile(String.format(ADMONITION_START_FORMAT, parsing.ATTRIBUTENAME, parsing.LINK_TITLE_STRING)); + Matcher matcher = startPattern.matcher(trySequence); + + if (matcher.find()) { + // admonition block + BasedSequence openingMarker = line.subSequence(nextNonSpace + matcher.start(1), nextNonSpace + matcher.end(1)); + BasedSequence info = line.subSequence(nextNonSpace + matcher.start(2), nextNonSpace + matcher.end(2)); + BasedSequence titleChars = matcher.group(3) == null ? BasedSequence.NULL : line.subSequence(nextNonSpace + matcher.start(3), nextNonSpace + matcher.end(3)); + + int contentOffset = options.contentIndent; + + CustomAdmonitionBlockParser admonitionBlockParser = new CustomAdmonitionBlockParser (options, contentOffset); + admonitionBlockParser.block.setOpeningMarker(openingMarker); + admonitionBlockParser.block.setInfo(info); + admonitionBlockParser.block.setTitleChars(titleChars); + + return BlockStart.of(admonitionBlockParser) + .atIndex(line.length()); + } else { + return BlockStart.none(); + } + } else { + return BlockStart.none(); + } + } + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/markdown/CustomAdmonitionExtension.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/markdown/CustomAdmonitionExtension.java new file mode 100644 index 000000000..d34528109 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/markdown/CustomAdmonitionExtension.java @@ -0,0 +1,215 @@ +package com.github.paicoding.forum.core.markdown; + +import com.vladsch.flexmark.ext.admonition.AdmonitionBlock; + +import com.vladsch.flexmark.ext.admonition.internal.AdmonitionNodeFormatter; +import com.vladsch.flexmark.ext.admonition.internal.AdmonitionNodeRenderer; +import com.vladsch.flexmark.formatter.Formatter; +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.data.DataKey; +import com.vladsch.flexmark.util.data.MutableDataHolder; +import org.jetbrains.annotations.NotNull; + +import java.io.*; +import java.util.HashMap; +import java.util.Map; + +/** + * Extension for admonitions + *

+ * Create it with {@link #create()} and then configure it on the builders + *

+ * The parsed admonition text is turned into {@link AdmonitionBlock} nodes. + */ +public class CustomAdmonitionExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension, Formatter.FormatterExtension + // , Parser.ReferenceHoldingExtension +{ + final public static DataKey CONTENT_INDENT = new DataKey<>("ADMONITION.CONTENT_INDENT", 4); + final public static DataKey ALLOW_LEADING_SPACE = new DataKey<>("ADMONITION.ALLOW_LEADING_SPACE", true); + final public static DataKey INTERRUPTS_PARAGRAPH = new DataKey<>("ADMONITION.INTERRUPTS_PARAGRAPH", true); + final public static DataKey INTERRUPTS_ITEM_PARAGRAPH = new DataKey<>("ADMONITION.INTERRUPTS_ITEM_PARAGRAPH", true); + final public static DataKey WITH_SPACES_INTERRUPTS_ITEM_PARAGRAPH = new DataKey<>("ADMONITION.WITH_SPACES_INTERRUPTS_ITEM_PARAGRAPH", true); + final public static DataKey ALLOW_LAZY_CONTINUATION = new DataKey<>("ADMONITION.ALLOW_LAZY_CONTINUATION", true); + final public static DataKey UNRESOLVED_QUALIFIER = new DataKey<>("ADMONITION.UNRESOLVED_QUALIFIER", "note"); + final public static DataKey> QUALIFIER_TYPE_MAP = new DataKey<>("ADMONITION.QUALIFIER_TYPE_MAP", CustomAdmonitionExtension::getQualifierTypeMap); + final public static DataKey> QUALIFIER_TITLE_MAP = new DataKey<>("ADMONITION.QUALIFIER_TITLE_MAP", CustomAdmonitionExtension::getQualifierTitleMap); + final public static DataKey> TYPE_SVG_MAP = new DataKey<>("ADMONITION.TYPE_SVG_MAP", CustomAdmonitionExtension::getQualifierSvgValueMap); + + public static Map getQualifierTypeMap() { + HashMap infoSvgMap = new HashMap<>(); + // qualifier type map + infoSvgMap.put("abstract", "abstract"); + infoSvgMap.put("summary", "abstract"); + infoSvgMap.put("tldr", "abstract"); + + infoSvgMap.put("bug", "bug"); + + infoSvgMap.put("danger", "danger"); + infoSvgMap.put("error", "danger"); + + infoSvgMap.put("example", "example"); + infoSvgMap.put("snippet", "example"); + + infoSvgMap.put("fail", "fail"); + infoSvgMap.put("failure", "fail"); + infoSvgMap.put("missing", "fail"); + + infoSvgMap.put("faq", "faq"); + infoSvgMap.put("question", "faq"); + infoSvgMap.put("help", "faq"); + + infoSvgMap.put("info", "info"); + infoSvgMap.put("todo", "info"); + + infoSvgMap.put("note", "note"); + infoSvgMap.put("seealso", "note"); + + infoSvgMap.put("quote", "quote"); + infoSvgMap.put("cite", "quote"); + + infoSvgMap.put("success", "success"); + infoSvgMap.put("check", "success"); + infoSvgMap.put("done", "success"); + + infoSvgMap.put("tip", "tip"); + infoSvgMap.put("hint", "tip"); + infoSvgMap.put("important", "tip"); + + infoSvgMap.put("warning", "warning"); + infoSvgMap.put("caution", "warning"); + infoSvgMap.put("attention", "warning"); + + return infoSvgMap; + } + + public static Map getQualifierTitleMap() { + HashMap infoTitleMap = new HashMap<>(); + infoTitleMap.put("abstract", "Abstract"); + infoTitleMap.put("summary", "Summary"); + infoTitleMap.put("tldr", "TLDR"); + + infoTitleMap.put("bug", "Bug"); + + infoTitleMap.put("danger", "Danger"); + infoTitleMap.put("error", "Error"); + + infoTitleMap.put("example", "Example"); + infoTitleMap.put("snippet", "Snippet"); + + infoTitleMap.put("fail", "Fail"); + infoTitleMap.put("failure", "Failure"); + infoTitleMap.put("missing", "Missing"); + + infoTitleMap.put("faq", "Faq"); + infoTitleMap.put("question", "Question"); + infoTitleMap.put("help", "Help"); + + infoTitleMap.put("info", "Info"); + infoTitleMap.put("todo", "To Do"); + + infoTitleMap.put("note", "Note"); + infoTitleMap.put("seealso", "See Also"); + + infoTitleMap.put("quote", "Quote"); + infoTitleMap.put("cite", "Cite"); + + infoTitleMap.put("success", "Success"); + infoTitleMap.put("check", "Check"); + infoTitleMap.put("done", "Done"); + + infoTitleMap.put("tip", "Tip"); + infoTitleMap.put("hint", "Hint"); + infoTitleMap.put("important", "Important"); + + infoTitleMap.put("warning", "Warning"); + infoTitleMap.put("caution", "Caution"); + infoTitleMap.put("attention", "Attention"); + + return infoTitleMap; + } + + public static Map getQualifierSvgValueMap() { + HashMap typeSvgMap = new HashMap<>(); + typeSvgMap.put("abstract", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-abstract.svg"))); + typeSvgMap.put("bug", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-bug.svg"))); + typeSvgMap.put("danger", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-danger.svg"))); + typeSvgMap.put("example", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-example.svg"))); + typeSvgMap.put("fail", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-fail.svg"))); + typeSvgMap.put("faq", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-faq.svg"))); + typeSvgMap.put("info", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-info.svg"))); + typeSvgMap.put("note", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-note.svg"))); + typeSvgMap.put("quote", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-quote.svg"))); + typeSvgMap.put("success", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-success.svg"))); + typeSvgMap.put("tip", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-tip.svg"))); + typeSvgMap.put("warning", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-warning.svg"))); + return typeSvgMap; + } + + public static String getInputStreamContent(InputStream inputStream) { + try { + InputStreamReader streamReader = new InputStreamReader(inputStream); + StringWriter stringWriter = new StringWriter(); + copy(streamReader, stringWriter); + stringWriter.close(); + return stringWriter.toString(); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } + + public static String getDefaultCSS() { + return getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/admonition.css")); + } + + public static String getDefaultScript() { + return getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/admonition.js")); + } + + public static void copy(Reader reader, Writer writer) throws IOException { + char[] buffer = new char[4096]; + int n; + while (-1 != (n = reader.read(buffer))) { + writer.write(buffer, 0, n); + } + writer.flush(); + reader.close(); + } + + private CustomAdmonitionExtension() { + } + + public static CustomAdmonitionExtension create() { + return new CustomAdmonitionExtension(); + } + + @Override + public void extend(Formatter.Builder formatterBuilder) { + formatterBuilder.nodeFormatterFactory(new AdmonitionNodeFormatter.Factory()); + } + + @Override + public void rendererOptions(@NotNull MutableDataHolder options) { + + } + + @Override + public void parserOptions(MutableDataHolder options) { + + } + + @Override + public void extend(Parser.Builder parserBuilder) { + parserBuilder.customBlockParserFactory(new CustomAdmonitionBlockParser.Factory()); + } + + @Override + public void extend(@NotNull HtmlRenderer.Builder htmlRendererBuilder, @NotNull String rendererType) { + if (htmlRendererBuilder.isRendererType("HTML")) { + htmlRendererBuilder.nodeRendererFactory(new AdmonitionNodeRenderer.Factory()); + } else if (htmlRendererBuilder.isRendererType("JIRA")) { + + } + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/mdc/MdcAspect.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/mdc/MdcAspect.java new file mode 100644 index 000000000..433efd40d --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/mdc/MdcAspect.java @@ -0,0 +1,93 @@ +package com.github.paicoding.forum.core.mdc; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +/** + * @author YiHui + * @date 2023/5/26 + */ +@Slf4j +@Aspect +@Component +public class MdcAspect implements ApplicationContextAware { + private ExpressionParser parser = new SpelExpressionParser(); + private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + @Pointcut("@annotation(MdcDot) || @within(MdcDot)") + public void getLogAnnotation() { + } + + @Around("getLogAnnotation()") + public Object handle(ProceedingJoinPoint joinPoint) throws Throwable { + long start = System.currentTimeMillis(); + boolean hasTag = addMdcCode(joinPoint); + try { + Object ans = joinPoint.proceed(); + return ans; + } finally { + log.info("执行耗时: {}#{} = {}ms", + joinPoint.getSignature().getDeclaringType().getSimpleName(), + joinPoint.getSignature().getName(), + System.currentTimeMillis() - start); + if (hasTag) { + MdcUtil.reset(); + } + } + } + + private boolean addMdcCode(ProceedingJoinPoint joinPoint) { + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + MdcDot dot = method.getAnnotation(MdcDot.class); + if (dot == null) { + dot = (MdcDot) joinPoint.getSignature().getDeclaringType().getAnnotation(MdcDot.class); + } + + if (dot != null) { + MdcUtil.add("bizCode", loadBizCode(dot.bizCode(), joinPoint)); + return true; + } + return false; + } + + private String loadBizCode(String key, ProceedingJoinPoint joinPoint) { + if (StringUtils.isBlank(key)) { + return ""; + } + + StandardEvaluationContext context = new StandardEvaluationContext(); + + context.setBeanResolver(new BeanFactoryResolver(applicationContext)); + String[] params = parameterNameDiscoverer.getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod()); + Object[] args = joinPoint.getArgs(); + for (int i = 0; i < args.length; i++) { + context.setVariable(params[i], args[i]); + } + return parser.parseExpression(key).getValue(context, String.class); + } + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/mdc/MdcDot.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/mdc/MdcDot.java new file mode 100644 index 000000000..b1525635e --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/mdc/MdcDot.java @@ -0,0 +1,18 @@ +package com.github.paicoding.forum.core.mdc; + +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 YiHui + * @date 2023/5/26 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MdcDot { + String bizCode() default ""; +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/mdc/MdcUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/mdc/MdcUtil.java new file mode 100644 index 000000000..e56734ac3 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/mdc/MdcUtil.java @@ -0,0 +1,34 @@ +package com.github.paicoding.forum.core.mdc; + +import org.slf4j.MDC; + +/** + * @author YiHui + * @date 2023/5/29 + */ +public class MdcUtil { + public static final String TRACE_ID_KEY = "traceId"; + + public static void add(String key, String val) { + MDC.put(key, val); + } + + public static void addTraceId() { + // traceId的生成规则,技术派提供了两种生成策略,可以使用自定义的也可以使用SkyWalking; 实际项目中选择一种即可 + MDC.put(TRACE_ID_KEY, SelfTraceIdGenerator.generate()); + } + + public static String getTraceId() { + return MDC.get(TRACE_ID_KEY); + } + + public static void reset() { + String traceId = MDC.get(TRACE_ID_KEY); + MDC.clear(); + MDC.put(TRACE_ID_KEY, traceId); + } + + public static void clear() { + MDC.clear(); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/mdc/SelfTraceIdGenerator.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/mdc/SelfTraceIdGenerator.java new file mode 100644 index 000000000..bdfafdc04 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/mdc/SelfTraceIdGenerator.java @@ -0,0 +1,102 @@ +package com.github.paicoding.forum.core.mdc; + +import com.github.paicoding.forum.core.util.IpUtil; +import com.google.common.base.Splitter; +import lombok.extern.slf4j.Slf4j; + +import java.lang.management.ManagementFactory; +import java.lang.management.RuntimeMXBean; +import java.net.InetAddress; +import java.time.Instant; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 自定义的traceId生成器 + *

+ * 生成规则参考 + * + * @author YiHui + * @date 2023/5/29 + */ +@Slf4j +public class SelfTraceIdGenerator { + private final static Integer MIN_AUTO_NUMBER = 1000; + private final static Integer MAX_AUTO_NUMBER = 10000; + private static volatile Integer autoIncreaseNumber = MIN_AUTO_NUMBER; + + /** + *

+ * 生成32位traceId,规则是 服务器 IP + 产生ID时的时间 + 自增序列 + 当前进程号 + * IP 8位:39.105.208.175 -> 2769d0af + * 产生ID时的时间 13位: 毫秒时间戳 -> 1403169275002 + * 当前进程号 5位: PID + * 自增序列 4位: 1000-9999循环 + *

+ * w + * + * @return ac13e001.1685348263825.095001000 + */ + public static String generate() { + StringBuilder traceId = new StringBuilder(); + try { + // 1. IP - 8 + traceId.append(convertIp(IpUtil.getLocalIp4Address())).append("."); + // 2. 时间戳 - 13 + traceId.append(Instant.now().toEpochMilli()).append("."); + // 3. 当前进程号 - 5 + traceId.append(getProcessId()); + // 4. 自增序列 - 4 + traceId.append(getAutoIncreaseNumber()); + } catch (Exception e) { + log.error("generate trace id error!", e); + return UUID.randomUUID().toString().replaceAll("-", ""); + } + return traceId.toString(); + } + + /** + * IP转换为十六进制 - 8位 + * + * @param ip 39.105.208.175 + * @return 2769d0af + */ + private static String convertIp(String ip) { + return Splitter.on(".").splitToStream(ip) + .map(s -> String.format("%02x", Integer.valueOf(s))) + .collect(Collectors.joining()); + } + + /** + * 使得自增序列在1000-9999之间循环 - 4位 + * + * @return 自增序列号 + */ + private static int getAutoIncreaseNumber() { + if (autoIncreaseNumber >= MAX_AUTO_NUMBER) { + autoIncreaseNumber = MIN_AUTO_NUMBER; + return autoIncreaseNumber; + } else { + return autoIncreaseNumber++; + } + } + + /** + * @return 5位当前进程号 + */ + private static String getProcessId() { + RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean(); + String processId = runtime.getName().split("@")[0]; + return String.format("%05d", Integer.parseInt(processId)); + } + + public static void main(String[] args) { + String t = generate(); + System.out.println(t); + String t2 = generate(); + System.out.println(t2); + + String trace = SkyWalkingTraceIdGenerator.generate(); + System.out.println(trace); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/mdc/SkyWalkingTraceIdGenerator.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/mdc/SkyWalkingTraceIdGenerator.java new file mode 100644 index 000000000..31ce33fcd --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/mdc/SkyWalkingTraceIdGenerator.java @@ -0,0 +1,84 @@ +package com.github.paicoding.forum.core.mdc; + +import com.google.common.base.Joiner; + +import java.util.UUID; + +/** + * SkyWalking的traceId生成策略 + *

+ * 源码: + * + * @author YiHui + * @date 2023/5/29 + */ +public class SkyWalkingTraceIdGenerator { + private static final String PROCESS_ID = UUID.randomUUID().toString().replaceAll("-", ""); + private static final ThreadLocal THREAD_ID_SEQUENCE = ThreadLocal.withInitial( + () -> new IDContext(System.currentTimeMillis(), (short) 0)); + + private SkyWalkingTraceIdGenerator() { + } + + /** + * Generate a new id, combined by three parts. + *

+ * The first one represents application instance id. + *

+ * The second one represents thread id. + *

+ * The third one also has two parts, 1) a timestamp, measured in milliseconds 2) a seq, in current thread, between + * 0(included) and 9999(included) + * + * @return unique id to represent a trace or segment + */ + public static String generate() { + return Joiner.on(".").join( + PROCESS_ID, + String.valueOf(Thread.currentThread().getId()), + String.valueOf(THREAD_ID_SEQUENCE.get().nextSeq()) + ); + } + + private static class IDContext { + private static final int MAX_SEQ = 10_000; + private long lastTimestamp; + private short threadSeq; + + // Just for considering time-shift-back only. + private long lastShiftTimestamp; + private int lastShiftValue; + + private IDContext(long lastTimestamp, short threadSeq) { + this.lastTimestamp = lastTimestamp; + this.threadSeq = threadSeq; + } + + private long nextSeq() { + return timestamp() * 10000 + nextThreadSeq(); + } + + private long timestamp() { + long currentTimeMillis = System.currentTimeMillis(); + + if (currentTimeMillis < lastTimestamp) { + // Just for considering time-shift-back by Ops or OS. @hanahmily 's suggestion. + if (lastShiftTimestamp != currentTimeMillis) { + lastShiftValue++; + lastShiftTimestamp = currentTimeMillis; + } + return lastShiftValue; + } else { + lastTimestamp = currentTimeMillis; + return lastTimestamp; + } + } + + private short nextThreadSeq() { + if (threadSeq == MAX_SEQ) { + threadSeq = 0; + } + return threadSeq++; + } + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/net/HttpRequestHelper.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/net/HttpRequestHelper.java new file mode 100644 index 000000000..25d76ca65 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/net/HttpRequestHelper.java @@ -0,0 +1,287 @@ +package com.github.paicoding.forum.core.net; + +import com.alibaba.fastjson.JSONObject; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientResponseException; +import org.springframework.web.client.RestTemplate; + +import javax.servlet.http.HttpServletRequest; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * 请求工具类 + * + * @author YiHui + * @date 2023/04/23 + */ +@Slf4j +public class HttpRequestHelper { + public static final String CHROME_UA = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"; + + /** + * rest template + */ + private static LoadingCache restTemplateMap; + + static { + restTemplateMap = CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES) + .build(new CacheLoader() { + @Override + public RestTemplate load(String key) throws Exception { + return buildRestTemplate(); + } + }); + } + + /** + * build rest template + * + * @return + */ + private static RestTemplate buildRestTemplate() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(15000); + factory.setReadTimeout(15000); + return new RestTemplate(factory); + } + + @Scheduled(cron = "0 0 0/1 * * ?") + public static void refreshRestTemplate() { + restTemplateMap.cleanUp(); + } + + + /** + * 文件上传 + * + * @param url 上传url + * @param paramName 参数名 + * @param fileName 上传的文件名 + * @param bytes 上传文件流 + * @return + */ + public static String upload(String url, String paramName, String fileName, byte[] bytes) { + //设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + //设置请求体,注意是LinkedMultiValueMap + ByteArrayResource fileSystemResource = new ByteArrayResource(bytes) { + @Override + public String getFilename() { + return fileName; + } + }; + MultiValueMap form = new LinkedMultiValueMap<>(); + // post的文件 + form.add(paramName, fileSystemResource); + + //用HttpEntity封装整个请求报文 + HttpEntity> files = new HttpEntity<>(form, headers); + String threadName = Thread.currentThread().getName(); + RestTemplate restTemplate = restTemplateMap.getUnchecked(threadName); + HttpEntity res = restTemplate.postForEntity(url, files, String.class); + return res.getBody(); + } + + /** + * @param url + * @param method + * @param params + * @param headers + * @param responseClass + * @param + * @return + */ + public static R fetchContentWithProxy(String url, HttpMethod method, Map params, + HttpHeaders headers, Class responseClass) { + R result = fetchContent(url, method, params, headers, responseClass, true); + if (result == null) { + return fetchContent(url, method, params, headers, responseClass, false); + } + + return result; + } + + /** + * @param url + * @param method + * @param params + * @param headers + * @param responseClass + * @param + * @return + */ + public static R fetchContentWithoutProxy(String url, HttpMethod method, Map params, + HttpHeaders headers, Class responseClass) { + return fetchContent(url, method, params, headers, responseClass, false); + } + + /** + * fetch content + * + * @param url + * @param method + * @param params + * @param headers + * @param responseClass + * @param useProxy + * @param + * @return + */ + private static R fetchContent(String url, HttpMethod method, + Map params, + HttpHeaders headers, + Class responseClass, boolean useProxy) { + String threadName = Thread.currentThread().getName(); + RestTemplate restTemplate = restTemplateMap.getUnchecked(threadName); + + String host = ""; + try { + host = new URL(url).getHost(); + } catch (MalformedURLException e) { + log.error("Failed to parse url:{}", url); + } + + if (useProxy) { + ensureProxy(restTemplate, host); + } else { + ensureProxy(restTemplate, ""); + } + + return fetchContentInternal(restTemplate, url, method, params, headers, responseClass); + } + + /** + * ensure proxy + * + * @param restTemplate + * @param host + */ + private static void ensureProxy(RestTemplate restTemplate, String host) { + SimpleClientHttpRequestFactory factory = (SimpleClientHttpRequestFactory) restTemplate.getRequestFactory(); + if (StringUtils.isBlank(host)) { + factory.setProxy(null); + return; + } + + Optional.ofNullable(ProxyCenter.loadProxy(host)).ifPresent(factory::setProxy); + } + + /** + * fetch content + * + * @param restTemplate + * @param url + * @param method + * @param params + * @param headers + * @param responseClass + * @param + * @return + */ + @SuppressWarnings("unchecked") + private static R fetchContentInternal(RestTemplate restTemplate, String url, HttpMethod method, + Map params, HttpHeaders headers, Class responseClass) { + ResponseEntity responseEntity; + try { + SslUtils.ignoreSSL(); + if (method.equals(HttpMethod.GET)) { + HttpEntity entity = new HttpEntity<>(headers); + responseEntity = restTemplate.exchange(url, method, entity, responseClass, params); + } else { + MultiValueMap args = new LinkedMultiValueMap<>(); + args.setAll(params); + HttpEntity> entity = new HttpEntity<>(args, headers); + responseEntity = restTemplate.exchange(url, method, entity, responseClass); + } + } catch (RestClientResponseException e) { + String res = e.getResponseBodyAsString(); + if (String.class.isAssignableFrom(responseClass)) { + return (R) res; + } else if (JSONObject.class.isAssignableFrom(responseClass)) { + return (R) JSONObject.parseObject(res); + } + return null; + } catch (Exception e) { + log.warn("Failed to fetch content, url:{}, params:{}, exception:{}", url, params, e.getMessage()); + return null; + } + + return responseEntity.getBody(); + } + + public static R fetchByRequestBody(String url, Map params, HttpHeaders headers, + Class responseClass) { + ResponseEntity responseEntity; + try { + String threadName = Thread.currentThread().getName(); + RestTemplate restTemplate = restTemplateMap.getUnchecked(threadName); + HttpEntity> entity = new HttpEntity<>(params, headers); + responseEntity = restTemplate.exchange(url, HttpMethod.POST, entity, responseClass); + } catch (Exception e) { + log.warn("Failed to fetch content, url:{}, params:{}, exception:{}", url, params, e.getMessage()); + return null; + } + + if (responseEntity != null) { + return responseEntity.getBody(); + } + + return null; + } + + + /** + * readData + * + * @param request request + * @return result + */ + // CHECKSTYLE:OFF:InnerAssignment + public static String readReqData(HttpServletRequest request) { + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8)); + StringBuilder stringBuilder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); + } + return stringBuilder.toString(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + log.error("请求参数解析异常! {}", request.getRequestURI(), e); + } + } + } + } +} \ No newline at end of file diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/net/ProxyCenter.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/net/ProxyCenter.java new file mode 100644 index 000000000..548a5104e --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/net/ProxyCenter.java @@ -0,0 +1,58 @@ +package com.github.paicoding.forum.core.net; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.paicoding.forum.core.config.ProxyProperties; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.List; + +/** + * @author YiHui + * @date 2023/6/2 + */ +public class ProxyCenter { + + /** + * 记录每个source使用的proxy索引 + */ + private static final Cache HOST_PROXY_INDEX = Caffeine.newBuilder().maximumSize(16).build(); + /** + * proxy + */ + private static List PROXIES = new ArrayList<>(); + + + public static void initProxyPool(List proxyTypes) { + PROXIES = proxyTypes; + } + + /** + * get proxy + * + * @return + */ + static ProxyProperties.ProxyType getProxy(String host) { + Integer index = HOST_PROXY_INDEX.getIfPresent(host); + if (index == null) { + index = -1; + } + + ++index; + if (index >= PROXIES.size()) { + index = 0; + } + HOST_PROXY_INDEX.put(host, index); + return PROXIES.get(index); + } + + public static Proxy loadProxy(String host) { + ProxyProperties.ProxyType proxyType = getProxy(host); + if (proxyType == null) { + return null; + } + return new Proxy(proxyType.getType(), new InetSocketAddress(proxyType.getIp(), proxyType.getPort())); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/net/SslUtils.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/net/SslUtils.java new file mode 100644 index 000000000..26d92f7a0 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/net/SslUtils.java @@ -0,0 +1,58 @@ +package com.github.paicoding.forum.core.net; + +import javax.net.ssl.*; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * @author YiHui + * @date 2023/4/20 + */ +public class SslUtils { + private static void trustAllHttpsCertificates() throws Exception { + TrustManager[] trustAllCerts = new TrustManager[1]; + TrustManager tm = new miTM(); + trustAllCerts[0] = tm; + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, null); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + } + + static class miTM implements TrustManager, X509TrustManager { + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + public boolean isServerTrusted(X509Certificate[] certs) { + return true; + } + + public boolean isClientTrusted(X509Certificate[] certs) { + return true; + } + + @Override + public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException { + } + + @Override + public void checkClientTrusted(X509Certificate[] certs, String authType) throws CertificateException { + } + } + + /** + * 忽略HTTPS请求的SSL证书,必须在openConnection之前调用 + * + * @throws Exception + */ + public static void ignoreSSL() throws Exception { + HostnameVerifier hv = (urlHostName, session) -> { + System.out.println("Warning: URL Host: " + urlHostName + " vs. " + session.getPeerHost()); + return true; + }; + trustAllHttpsCertificates(); + HttpsURLConnection.setDefaultHostnameVerifier(hv); + } +} + diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/package-info.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/package-info.java new file mode 100644 index 000000000..cdc879783 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/package-info.java @@ -0,0 +1,7 @@ +/** + * 公共依赖的核心包模块 + * + * @author YiHui + * @date 2022/7/6 + */ +package com.github.paicoding.forum.core; \ No newline at end of file diff --git a/forum-core/src/main/java/com/github/liuyueyi/forum/core/permission/Permission.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/permission/Permission.java similarity index 85% rename from forum-core/src/main/java/com/github/liuyueyi/forum/core/permission/Permission.java rename to paicoding-core/src/main/java/com/github/paicoding/forum/core/permission/Permission.java index da57a382d..af7eac513 100644 --- a/forum-core/src/main/java/com/github/liuyueyi/forum/core/permission/Permission.java +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/permission/Permission.java @@ -1,4 +1,4 @@ -package com.github.liuyueyi.forum.core.permission; +package com.github.paicoding.forum.core.permission; import java.lang.annotation.*; diff --git a/forum-core/src/main/java/com/github/liuyueyi/forum/core/permission/UserRole.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/permission/UserRole.java similarity index 79% rename from forum-core/src/main/java/com/github/liuyueyi/forum/core/permission/UserRole.java rename to paicoding-core/src/main/java/com/github/paicoding/forum/core/permission/UserRole.java index 53649d52d..da667dc67 100644 --- a/forum-core/src/main/java/com/github/liuyueyi/forum/core/permission/UserRole.java +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/permission/UserRole.java @@ -1,4 +1,4 @@ -package com.github.liuyueyi.forum.core.permission; +package com.github.paicoding.forum.core.permission; /** * @author YiHui diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/rabbitmq/RabbitmqConnection.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/rabbitmq/RabbitmqConnection.java new file mode 100644 index 000000000..b9de2ff3a --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/rabbitmq/RabbitmqConnection.java @@ -0,0 +1,51 @@ +package com.github.paicoding.forum.core.rabbitmq; + +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + * @author Louzai + * @date 2023/5/10 + */ +public class RabbitmqConnection { + + private Connection connection; + + public RabbitmqConnection(String host, int port, String userName, String password, String virtualhost) { + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost(host); + connectionFactory.setPort(port); + connectionFactory.setUsername(userName); + connectionFactory.setPassword(password); + connectionFactory.setVirtualHost(virtualhost); + try { + connection = connectionFactory.newConnection(); + } catch (IOException | TimeoutException e) { + e.printStackTrace(); + } + } + + /** + * 获取链接 + * + * @return + */ + public Connection getConnection() { + return connection; + } + + /** + * 关闭链接 + * + */ + public void close() { + try { + connection.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/rabbitmq/RabbitmqConnectionPool.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/rabbitmq/RabbitmqConnectionPool.java new file mode 100644 index 000000000..7cccf0292 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/rabbitmq/RabbitmqConnectionPool.java @@ -0,0 +1,30 @@ +package com.github.paicoding.forum.core.rabbitmq; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +public class RabbitmqConnectionPool { + + private static BlockingQueue pool; + + public static void initRabbitmqConnectionPool(String host, int port, String userName, String password, + String virtualhost, + Integer poolSize) { + pool = new LinkedBlockingQueue<>(poolSize); + for (int i = 0; i < poolSize; i++) { + pool.add(new RabbitmqConnection(host, port, userName, password, virtualhost)); + } + } + + public static RabbitmqConnection getConnection() throws InterruptedException { + return pool.take(); + } + + public static void returnConnection(RabbitmqConnection connection) { + pool.add(connection); + } + + public static void close() { + pool.forEach(RabbitmqConnection::close); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/rabbitmq/RabbitmqUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/rabbitmq/RabbitmqUtil.java new file mode 100644 index 000000000..6e4efe986 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/rabbitmq/RabbitmqUtil.java @@ -0,0 +1,82 @@ +package com.github.paicoding.forum.core.rabbitmq; + +import com.rabbitmq.client.ConnectionFactory; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 说明:添加rabbitmq连接池后,这个就可以废弃掉 + * @author Louzai + * @date 2023/5/10 + */ +public class RabbitmqUtil { + + /** + * 每个 host 都有自己的工厂,便于后面改造成多机的方式 + */ + private static Map executors = new ConcurrentHashMap<>(); + + /** + * 初始化一个工厂 + * + * @param host + * @param port + * @param username + * @param passport + * @param virtualhost + * @return + */ + private static ConnectionFactory init(String host, + Integer port, + String username, + String passport, + String virtualhost) { + ConnectionFactory factory = new ConnectionFactory(); + factory.setHost(host); + factory.setPort(port); + factory.setUsername(username); + factory.setPassword(passport); + factory.setVirtualHost(virtualhost); + return factory; + } + + /** + * 工厂单例,每个host都有属于自己的工厂 + * + * @param host + * @param port + * @param username + * @param passport + * @param virtualhost + * @return + */ + public static ConnectionFactory getOrInitConnectionFactory(String host, + Integer port, + String username, + String passport, + String virtualhost) { + String key = getConnectionFactoryKey(host, port); + ConnectionFactory connectionFactory = executors.get(key); + if (null == connectionFactory) { + synchronized (RabbitmqUtil.class) { + connectionFactory = executors.get(key); + if (null == connectionFactory) { + connectionFactory = init(host, port, username, passport, virtualhost); + executors.put(key, connectionFactory); + } + } + } + return connectionFactory; + } + + /** + * 获取key + * @param host + * @param port + * @return + */ + private static String getConnectionFactoryKey(String host, Integer port) { + return host + ":" + port; + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/region/IpRegionInfo.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/region/IpRegionInfo.java new file mode 100644 index 000000000..39541df6c --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/region/IpRegionInfo.java @@ -0,0 +1,72 @@ +package com.github.paicoding.forum.core.region; + +import lombok.Data; +import org.apache.commons.lang3.StringUtils; + +import java.util.Objects; + +/** + * ip区域信息 + * + * @author YiHui + * @date 2023/01/03 + */ +@Data +public class IpRegionInfo { + /** + * 国家or地区 + */ + private String country; + /** + * 区域 + */ + private String region; + /** + * 省份 + */ + private String province; + /** + * 城市 + */ + private String city; + /** + * 网络运营商 + */ + private String isp; + + public IpRegionInfo(String info) { + String[] cells = StringUtils.split(info, "|"); + if (cells.length < 5) { + country = ""; + region = ""; + province = ""; + city = ""; + isp = ""; + return; + } + country = "0".equals(cells[0]) ? "" : cells[0]; + region = "0".equals(cells[1]) ? "" : cells[1]; + province = "0".equals(cells[2]) ? "" : cells[2]; + city = "0".equals(cells[3]) ? "" : cells[3]; + isp = "0".equals(cells[4]) ? "" : cells[4]; + } + + public String toRegionStr() { + if (Objects.equals(country, "中国")) { + // 大陆,返回省 + 城市 + if (StringUtils.isNotBlank(province) && StringUtils.isNotBlank(city)) { + return province + "·" + city; + } else if (StringUtils.isNotBlank(province)) { + return province; + } else { + return country; + } + } else { + if (StringUtils.isNotBlank(province)) { + // 非大陆,返回国家+省份 + return country + "·" + province; + } + return country; + } + } +} \ No newline at end of file diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/SensitiveProperty.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/SensitiveProperty.java new file mode 100644 index 000000000..58a15b412 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/SensitiveProperty.java @@ -0,0 +1,34 @@ +package com.github.paicoding.forum.core.senstive; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 敏感词相关配置,db配置表中的配置优先级更高,支持动态刷新 + * + * @author YiHui + * @date 2023/8/9 + */ +@Data +@Component +@ConfigurationProperties(prefix = SensitiveProperty.SENSITIVE_KEY_PREFIX) +public class SensitiveProperty { + public static final String SENSITIVE_KEY_PREFIX = "paicoding.sensitive"; + /** + * true 表示开启敏感词校验 + */ + private Boolean enable; + + /** + * 自定义的敏感词 + */ + private List deny; + + /** + * 自定义的非敏感词 + */ + private List allow; +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/SensitiveService.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/SensitiveService.java new file mode 100644 index 000000000..efc460764 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/SensitiveService.java @@ -0,0 +1,125 @@ +package com.github.paicoding.forum.core.senstive; + +import com.github.houbb.sensitive.word.api.IWordAllow; +import com.github.houbb.sensitive.word.api.IWordDeny; +import com.github.houbb.sensitive.word.bs.SensitiveWordBs; +import com.github.houbb.sensitive.word.support.allow.WordAllowSystem; +import com.github.houbb.sensitive.word.support.deny.WordDenySystem; +import com.github.paicoding.forum.core.autoconf.DynamicConfigContainer; +import com.github.paicoding.forum.core.cache.RedisClient; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.BooleanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import javax.annotation.PostConstruct; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * 敏感词服务类 + * + * @author YiHui + * @date 2023/8/9 + */ +@Slf4j +@Service +public class SensitiveService { + /** + * 敏感词命中计数统计 + */ + private static final String SENSITIVE_WORD_CNT_PREFIX = "sensitive_word"; + private volatile SensitiveWordBs sensitiveWordBs; + @Autowired + private SensitiveProperty sensitiveConfig; + @Autowired + private DynamicConfigContainer dynamicConfigContainer; + + @PostConstruct + public void refresh() { + dynamicConfigContainer.registerRefreshCallback(sensitiveConfig, this::refresh); + IWordDeny deny = () -> { + List sub = WordDenySystem.getInstance().deny(); + sub.addAll(sensitiveConfig.getDeny()); + return sub; + }; + + IWordAllow allow = () -> { + List sub = WordAllowSystem.getInstance().allow(); + sub.addAll(sensitiveConfig.getAllow()); + return sub; + }; + sensitiveWordBs = SensitiveWordBs.newInstance() + .wordDeny(deny) + .wordAllow(allow) + .init(); + log.info("敏感词初始化完成!"); + } + + /** + * 判断是否包含敏感词 + * + * @param txt 需要校验的文本 + * @return 返回命中的敏感词 + */ + public List contains(String txt) { + if (!BooleanUtils.isTrue(sensitiveConfig.getEnable())) { + return Collections.emptyList(); + } + + List ans = sensitiveWordBs.findAll(txt); + if (CollectionUtils.isEmpty(ans)) { + return ans; + } + + // 敏感词命中次数+1 + RedisClient.PipelineAction action = RedisClient.pipelineAction(); + ans.forEach(key -> action.add(SENSITIVE_WORD_CNT_PREFIX, key, (connection, k, v) -> connection.hIncrBy(k, v, 1))); + action.execute(); + return ans; + } + + + /** + * 返回已命中的敏感词 + * + * @return key: 敏感词, value:计数 + */ + public Map getHitSensitiveWords() { + return RedisClient.hGetAll(SENSITIVE_WORD_CNT_PREFIX, Integer.class); + } + + /** + * 移除敏感词 + * + * @param word + */ + public void removeSensitiveWord(String word) { + RedisClient.hDel(SENSITIVE_WORD_CNT_PREFIX, word); + } + + /** + * 敏感词替换 + * + * @param txt + * @return + */ + public String replace(String txt) { + if (BooleanUtils.isTrue(sensitiveConfig.getEnable())) { + return sensitiveWordBs.replace(txt); + } + return txt; + } + + /** + * 查询文本中所有命中的敏感词 + * + * @param txt 校验文本 + * @return 命中的敏感词 + */ + public List findAll(String txt) { + return sensitiveWordBs.findAll(txt); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/ano/SensitiveField.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/ano/SensitiveField.java new file mode 100644 index 000000000..579865d5e --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/ano/SensitiveField.java @@ -0,0 +1,22 @@ +package com.github.paicoding.forum.core.senstive.ano; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author YiHui + * @date 2023/8/9 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface SensitiveField { + /** + * 绑定的db中的哪个字段 + * + * @return + */ + String bind() default ""; + +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/ibatis/SensitiveMetaCache.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/ibatis/SensitiveMetaCache.java new file mode 100644 index 000000000..a7380f430 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/ibatis/SensitiveMetaCache.java @@ -0,0 +1,38 @@ +package com.github.paicoding.forum.core.senstive.ibatis; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * 敏感词缓存 + * + * @author YiHui + * @date 2023/8/9 + */ +public class SensitiveMetaCache { + private static ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); + + public static SensitiveObjectMeta get(String key) { + return CACHE.get(key); + } + + public static void put(String key, SensitiveObjectMeta meta) { + CACHE.put(key, meta); + } + + public static void remove(String key) { + CACHE.remove(key); + } + + public static boolean contains(String key) { + return CACHE.containsKey(key); + } + + public static SensitiveObjectMeta putIfAbsent(String key, SensitiveObjectMeta meta) { + return CACHE.putIfAbsent(key, meta); + } + + public static SensitiveObjectMeta computeIfAbsent(String key, Function function) { + return CACHE.computeIfAbsent(key, function); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/ibatis/SensitiveObjectMeta.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/ibatis/SensitiveObjectMeta.java new file mode 100644 index 000000000..2c7c0b365 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/ibatis/SensitiveObjectMeta.java @@ -0,0 +1,87 @@ +package com.github.paicoding.forum.core.senstive.ibatis; + +import com.github.paicoding.forum.core.senstive.ano.SensitiveField; +import lombok.Data; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Optional; + +import static com.google.common.collect.Lists.newArrayList; +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; + +/** + * 敏感词相关配置,db配置表中的配置优先级更高,支持动态刷新 + * + * @author YiHui + * @date 2023/8/9 + */ +@Data +public class SensitiveObjectMeta { + private static final String JAVA_LANG_OBJECT = "java.lang.object"; + /** + * 是否启用脱敏 + */ + private Boolean enabledSensitiveReplace; + + /** + * 类名 + */ + private String className; + + /** + * 标注 SensitiveField 的成员 + */ + private List sensitiveFieldMetaList; + + public static Optional buildSensitiveObjectMeta(Object param) { + if (isNull(param)) { + return Optional.empty(); + } + + Class clazz = param.getClass(); + SensitiveObjectMeta sensitiveObjectMeta = new SensitiveObjectMeta(); + sensitiveObjectMeta.setClassName(clazz.getName()); + + List sensitiveFieldMetaList = newArrayList(); + sensitiveObjectMeta.setSensitiveFieldMetaList(sensitiveFieldMetaList); + boolean sensitiveField = parseAllSensitiveFields(clazz, sensitiveFieldMetaList); + sensitiveObjectMeta.setEnabledSensitiveReplace(sensitiveField); + return Optional.of(sensitiveObjectMeta); + } + + + private static boolean parseAllSensitiveFields(Class clazz, List sensitiveFieldMetaList) { + Class tempClazz = clazz; + boolean hasSensitiveField = false; + while (nonNull(tempClazz) && !JAVA_LANG_OBJECT.equalsIgnoreCase(tempClazz.getName())) { + for (Field field : tempClazz.getDeclaredFields()) { + SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class); + if (nonNull(sensitiveField)) { + SensitiveFieldMeta sensitiveFieldMeta = new SensitiveFieldMeta(); + sensitiveFieldMeta.setName(field.getName()); + sensitiveFieldMeta.setBindField(sensitiveField.bind()); + sensitiveFieldMetaList.add(sensitiveFieldMeta); + hasSensitiveField = true; + } + } + tempClazz = tempClazz.getSuperclass(); + } + return hasSensitiveField; + } + + + @Data + public static class SensitiveFieldMeta { + /** + * 默认根据字段名,找db中同名的字段 + */ + private String name; + + /** + * 绑定的数据库字段别名 + */ + private String bindField; + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/ibatis/SensitiveReadInterceptor.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/ibatis/SensitiveReadInterceptor.java new file mode 100644 index 000000000..f1ddd5de4 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/senstive/ibatis/SensitiveReadInterceptor.java @@ -0,0 +1,150 @@ +package com.github.paicoding.forum.core.senstive.ibatis; + + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.github.paicoding.forum.core.senstive.SensitiveService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ClassUtils; +import org.apache.ibatis.executor.resultset.ResultSetHandler; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.plugin.Intercepts; +import org.apache.ibatis.plugin.Invocation; +import org.apache.ibatis.plugin.Plugin; +import org.apache.ibatis.plugin.Signature; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.SystemMetaObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Proxy; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; + +import static com.google.common.collect.Lists.newArrayList; + + +/** + * 敏感词替换拦截器,这里主要是针对从db中读取的数据进行敏感词处理 (如果需要在写入db时,进行脱敏如加密,也可以使用类似的方式来实现) + * + * @author YiHui + * @date 2023/8/9 + */ +@Intercepts({ + @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {java.sql.Statement.class}) +}) +@Component +@Slf4j +public class SensitiveReadInterceptor implements Interceptor { + + private static final String MAPPED_STATEMENT = "mappedStatement"; + + @Autowired + private SensitiveService sensitiveService; + + @SuppressWarnings("unchecked") + @Override + public Object intercept(Invocation invocation) throws Throwable { + final List results = (List) invocation.proceed(); + + if (results.isEmpty()) { + return results; + } + + final ResultSetHandler statementHandler = realTarget(invocation.getTarget()); + final MetaObject metaObject = SystemMetaObject.forObject(statementHandler); + final MappedStatement mappedStatement = (MappedStatement) metaObject.getValue(MAPPED_STATEMENT); + + Optional firstOpt = results.stream().filter(Objects::nonNull).findFirst(); + if (!firstOpt.isPresent()) { + return results; + } + Object firstObject = firstOpt.get(); + // 找到需要进行敏感词替换的数据库实体类的成员信息 + SensitiveObjectMeta sensitiveObjectMeta = findSensitiveObjectMeta(firstObject); + + // 执行替换的敏感词替换 + replaceSensitiveResults(results, mappedStatement, sensitiveObjectMeta); + return results; + } + + /** + * 执行具体的敏感词替换 + * + * @param results + * @param mappedStatement + * @param sensitiveObjectMeta + */ + private void replaceSensitiveResults(Collection results, MappedStatement mappedStatement, SensitiveObjectMeta sensitiveObjectMeta) { + for (Object obj : results) { + if (sensitiveObjectMeta.getSensitiveFieldMetaList() == null) { + continue; + } + + final MetaObject objMetaObject = mappedStatement.getConfiguration().newMetaObject(obj); + sensitiveObjectMeta.getSensitiveFieldMetaList().forEach(i -> { + Object value = objMetaObject.getValue(StringUtils.isBlank(i.getBindField()) ? i.getName() : i.getBindField()); + if (value == null) { + return; + } else if (value instanceof String) { + String strValue = (String) value; + String processVal = sensitiveService.replace(strValue); + objMetaObject.setValue(i.getName(), processVal); + } else if (value instanceof Collection) { + Collection listValue = (Collection) value; + if (CollectionUtils.isNotEmpty(listValue)) { + Optional firstValOpt = listValue.stream().filter(Objects::nonNull).findFirst(); + if (firstValOpt.isPresent()) { + SensitiveObjectMeta valSensitiveObjectMeta = findSensitiveObjectMeta(firstValOpt.get()); + if (Boolean.TRUE.equals(valSensitiveObjectMeta.getEnabledSensitiveReplace()) && CollectionUtils.isNotEmpty(valSensitiveObjectMeta.getSensitiveFieldMetaList())) { + replaceSensitiveResults(listValue, mappedStatement, valSensitiveObjectMeta); + } + } + } + } else if (!ClassUtils.isPrimitiveOrWrapper(value.getClass())) { + // 对于非基本类型的,需要对其内部进行敏感词替换 + SensitiveObjectMeta valSensitiveObjectMeta = findSensitiveObjectMeta(value); + if (Boolean.TRUE.equals(valSensitiveObjectMeta.getEnabledSensitiveReplace()) && CollectionUtils.isNotEmpty(valSensitiveObjectMeta.getSensitiveFieldMetaList())) { + replaceSensitiveResults(newArrayList(value), mappedStatement, valSensitiveObjectMeta); + } + } + }); + } + } + + /** + * 查询对象中,携带有 @SensitiveField 的成员,进行敏感词替换 + * + * @param firstObject 待查询的对象 + * @return 返回对象的敏感词元数据 + */ + private SensitiveObjectMeta findSensitiveObjectMeta(Object firstObject) { + SensitiveMetaCache.computeIfAbsent(firstObject.getClass().getName(), s -> { + Optional sensitiveObjectMetaOpt = SensitiveObjectMeta.buildSensitiveObjectMeta(firstObject); + return sensitiveObjectMetaOpt.orElse(null); + }); + + return SensitiveMetaCache.get(firstObject.getClass().getName()); + } + + @Override + public Object plugin(Object o) { + return Plugin.wrap(o, this); + } + + @Override + public void setProperties(Properties properties) { + } + + public static T realTarget(Object target) { + if (Proxy.isProxyClass(target.getClass())) { + MetaObject metaObject = SystemMetaObject.forObject(target); + return realTarget(metaObject.getValue("h.target")); + } + return (T) target; + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/AlarmUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/AlarmUtil.java new file mode 100644 index 000000000..e252f6f96 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/AlarmUtil.java @@ -0,0 +1,34 @@ +package com.github.paicoding.forum.core.util; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import com.github.paicoding.forum.core.async.AsyncUtil; + +/** + * @author YiHui + * @date 2023/3/19 + */ +public class AlarmUtil extends AppenderBase { + private static final long INTERVAL = 10 * 1000 * 60; + private long lastAlarmTime = 0; + + @Override + protected void append(ILoggingEvent iLoggingEvent) { + if (canAlarm()) { + EmailUtil.sendMail(iLoggingEvent.getLoggerName(), + SpringUtil.getConfig("alarm.user", "xhhuiblog@163.com"), + iLoggingEvent.getFormattedMessage()); + } + } + + private boolean canAlarm() { + // 做一个简单的频率过滤,一分钟内只允许发送一条报警 + long now = System.currentTimeMillis(); + if (now - lastAlarmTime >= INTERVAL) { + lastAlarmTime = now; + return true; + } else { + return false; + } + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/ArticleUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/ArticleUtil.java new file mode 100644 index 000000000..664af513e --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/ArticleUtil.java @@ -0,0 +1,44 @@ +package com.github.paicoding.forum.core.util; + + +import org.apache.commons.lang3.StringUtils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author YiHui + * @date 2022/12/23 + */ +public class ArticleUtil { + private static final Integer MAX_SUMMARY_CHECK_TXT_LEN = 2000; + private static final Integer SUMMARY_LEN = 256; + private static Pattern LINK_IMG_PATTERN = Pattern.compile("!?\\[(.*?)\\]\\((.*?)\\)"); + private static Pattern CONTENT_PATTERN = Pattern.compile("[0-9a-zA-Z\u4e00-\u9fa5:;\"'<>,.?/·~!:;“”‘’《》,。?、()]"); + + private static Pattern HTML_TAG_PATTERN = Pattern.compile("<[^>]+>"); + + public static String pickSummary(String summary) { + if (StringUtils.isBlank(summary)) { + return StringUtils.EMPTY; + } + + // 首先移除所有的图片,链接 + summary = summary.substring(0, Math.min(summary.length(), MAX_SUMMARY_CHECK_TXT_LEN)).trim(); + // 移除md的图片、超链 + summary = summary.replaceAll(LINK_IMG_PATTERN.pattern(), ""); + // 移除html标签 + summary = HTML_TAG_PATTERN.matcher(summary).replaceAll(""); + + // 匹配对应字符 + StringBuilder result = new StringBuilder(); + Matcher matcher = CONTENT_PATTERN.matcher(summary); + while (matcher.find()) { + result.append(summary, matcher.start(), matcher.end()); + if (result.length() >= SUMMARY_LEN) { + return result.substring(0, SUMMARY_LEN).trim(); + } + } + return result.toString().trim(); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/CodeGenerateUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/CodeGenerateUtil.java new file mode 100644 index 000000000..34b467d14 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/CodeGenerateUtil.java @@ -0,0 +1,45 @@ +package com.github.paicoding.forum.core.util; + +import org.apache.commons.lang3.math.NumberUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +/** + * @author YiHui + * @date 2022/8/15 + */ +public class CodeGenerateUtil { + public static final Integer CODE_LEN = 3; + + private static final Random random = new Random(); + + private static final List specialCodes = Arrays.asList( + "666", "888", "000", "999", "555", "222", "333", "777", + "520", "911", + "234", "345", "456", "567", "678", "789" + ); + + public static String genCode(int cnt) { + if (cnt >= specialCodes.size()) { + int num = random.nextInt(1000); + if (num >= 100 && num <= 200) { + // 100-200之间的数字作为关键词回复,不用于验证码 + return genCode(cnt); + } + return String.format("%0" + CODE_LEN + "d", num); + } else { + return specialCodes.get(cnt); + } + } + + public static boolean isVerifyCode(String content) { + if (!NumberUtils.isDigits(content) || content.length() != CodeGenerateUtil.CODE_LEN) { + return false; + } + + int num = Integer.parseInt(content); + return num < 100 || num > 200; + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/CompressUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/CompressUtil.java new file mode 100644 index 000000000..edb61eedb --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/CompressUtil.java @@ -0,0 +1,149 @@ +package com.github.paicoding.forum.core.util; + +import java.nio.ByteBuffer; + +/** + * 压缩工具类 + * + * @author YiHui + * @date 2023/10/17 + */ +public class CompressUtil { + /** + * 进制转换数组 + */ + private static char[] BINARY_ARRAY = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); + + public static String int2str(long num) { + return int2str(num, BINARY_ARRAY.length); + } + + /** + * 整数的进制转换 + * + * @param num 数字 + * @param size 进制长度 + * @return 返回String格式的数据 + */ + public static String int2str(long num, int size) { + if (size > BINARY_ARRAY.length) { + size = BINARY_ARRAY.length; + } + + StringBuilder builder = new StringBuilder(); + while (num > 0) { + builder.insert(0, BINARY_ARRAY[(int) (num % size)]); + num /= size; + } + return builder.toString(); + } + + private static long zigzag(long n) { + return (n << 1) ^ (n >> 57); + } + + private static long unZigzag(long n) { + return (n >>> 1) ^ (n & 1); + } + + /** + * Returns the encoding size in bytes of its input value. + * + * @param v the long to be measured + * @return the encoding size in bytes of a given long value. + */ + public static int varLongSize(long v) { + int result = 0; + do { + result++; + v >>>= 7; + } while (v != 0); + return result; + } + + /** + * Reads an up to 64 bit long varint from the current position of the + * given ByteBuffer and returns the decoded value as long. + * + *

The position of the buffer is advanced to the first byte after the + * decoded varint. + * + * @param src the ByteBuffer to get the var int from + * @return The integer value of the decoded long varint + */ + public static long getVarLong(ByteBuffer src) { + long tmp; + if ((tmp = src.get()) >= 0) { + return tmp; + } + long result = tmp & 0x7f; + if ((tmp = src.get()) >= 0) { + result |= tmp << 7; + } else { + result |= (tmp & 0x7f) << 7; + if ((tmp = src.get()) >= 0) { + result |= tmp << 14; + } else { + result |= (tmp & 0x7f) << 14; + if ((tmp = src.get()) >= 0) { + result |= tmp << 21; + } else { + result |= (tmp & 0x7f) << 21; + if ((tmp = src.get()) >= 0) { + result |= tmp << 28; + } else { + result |= (tmp & 0x7f) << 28; + if ((tmp = src.get()) >= 0) { + result |= tmp << 35; + } else { + result |= (tmp & 0x7f) << 35; + if ((tmp = src.get()) >= 0) { + result |= tmp << 42; + } else { + result |= (tmp & 0x7f) << 42; + if ((tmp = src.get()) >= 0) { + result |= tmp << 49; + } else { + result |= (tmp & 0x7f) << 49; + if ((tmp = src.get()) >= 0) { + result |= tmp << 56; + } else { + result |= (tmp & 0x7f) << 56; + result |= ((long) src.get()) << 63; + } + } + } + } + } + } + } + } + return result; + } + + /** + * Encodes a long integer in a variable-length encoding, 7 bits per byte, to a + * ByteBuffer sink. + * + * @param v the value to encode + * @param sink the ByteBuffer to add the encoded value + */ + public static void putVarLong(long v, ByteBuffer sink) { + while (true) { + int bits = ((int) v) & 0x7f; + v >>>= 7; + if (v == 0) { + sink.put((byte) bits); + return; + } + sink.put((byte) (bits | 0x80)); + } + } + + public static String putVarLong(long v) { + byte[] bytes = new byte[varLongSize(v)]; + ByteBuffer sink = ByteBuffer.wrap(bytes); + putVarLong(v, sink); + return new String(bytes); + } +} diff --git a/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/CrossUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/CrossUtil.java similarity index 85% rename from forum-core/src/main/java/com/github/liuyueyi/forum/core/util/CrossUtil.java rename to paicoding-core/src/main/java/com/github/paicoding/forum/core/util/CrossUtil.java index 315c82340..79c34a918 100644 --- a/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/CrossUtil.java +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/CrossUtil.java @@ -1,4 +1,4 @@ -package com.github.liuyueyi.forum.core.util; +package com.github.paicoding.forum.core.util; import org.apache.commons.lang3.StringUtils; @@ -21,11 +21,12 @@ public static void buildCors(HttpServletRequest request, HttpServletResponse res String origin = request.getHeader("Origin"); if (StringUtils.isBlank(origin)) { response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "false"); } else { response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader("Access-Control-Allow-Credentials", "true"); } response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin")); - response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, HEAD"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, X-Real-IP, X-Forwarded-For, d-uuid, User-Agent, x-zd-cs, Proxy-Client-IP, HTTP_CLIENT_IP, HTTP_X_FORWARDED_FOR"); diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/DateUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/DateUtil.java new file mode 100644 index 000000000..231eb2811 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/DateUtil.java @@ -0,0 +1,89 @@ +package com.github.paicoding.forum.core.util; + +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +/** + * @author YiHui + * @date 2022/8/25 + */ +public class DateUtil { + public static final DateTimeFormatter UTC_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + + public static final DateTimeFormatter DB_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + public static final DateTimeFormatter BLOG_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm"); + + public static final DateTimeFormatter BLOG_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy年MM月dd日"); + + + // 微信支付日期格式 + public static final DateTimeFormatter WX_PAY_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'+08:00'"); + + + /** + * 一天对应的毫秒数 + */ + public static final Long ONE_DAY_MILL = 86400_000L; + public static final Long ONE_DAY_SECONDS = 86400L; + public static final Long ONE_MONTH_SECONDS = 31 * 86400L; + + + public static final Long THREE_DAY_MILL = 3 * ONE_DAY_MILL; + + /** + * 毫秒转日期 + * + * @param timestamp + * @return + */ + public static String time2day(long timestamp) { + return format(BLOG_TIME_FORMAT, timestamp); + } + + public static String time2day(Timestamp timestamp) { + return time2day(timestamp.getTime()); + } + + public static LocalDateTime time2LocalTime(long timestamp) { + return LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()); + } + + public static String time2utc(long timestamp) { + return format(UTC_FORMAT, timestamp); + } + + public static String time2date(long timestamp) { + return format(BLOG_DATE_FORMAT, timestamp); + } + + public static String time2date(Timestamp timestamp) { + return time2date(timestamp.getTime()); + } + + + public static String format(DateTimeFormatter format, long timestamp) { + LocalDateTime time = time2LocalTime(timestamp); + return format.format(time); + } + + /** + * 微信的支付时间,转时间戳 "2018-06-08T10:34:56+08:00" + * + * @param day + * @return + */ + public static Long wxDayToTimestamp(String day) { + LocalDateTime parse = LocalDateTime.parse(day, WX_PAY_FORMATTER); + return LocalDateTime.from(parse).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } + + + public static boolean skipDay(long last, long now) { + last = last / ONE_DAY_MILL; + now = now / ONE_DAY_MILL; + return last != now; + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/EmailUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/EmailUtil.java new file mode 100644 index 000000000..3731f3bd7 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/EmailUtil.java @@ -0,0 +1,57 @@ +package com.github.paicoding.forum.core.util; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; + +import javax.mail.internet.MimeMessage; + +/** + * @author YiHui + * @date 2023/3/19 + */ +@Slf4j +public class EmailUtil { + private static volatile String from; + + public static String getFrom() { + if (from == null) { + synchronized (EmailUtil.class) { + if (from == null) { + from = SpringUtil.getConfig("spring.mail.from", "xhhuiblog@163.com"); + } + } + } + return from; + } + + /** + * springboot-email封装的发送邮件 + * + * @param title + * @param to + * @param content + * @return + */ + public static boolean sendMail(String title, String to, String content) { + try { + JavaMailSender javaMailSender = SpringUtil.getBean(JavaMailSender.class); + MimeMessage mimeMailMessage = javaMailSender.createMimeMessage(); + MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMailMessage, true); + mimeMessageHelper.setFrom(getFrom()); + mimeMessageHelper.setTo(to); + mimeMessageHelper.setSubject(title); + //邮件内容,第二个参数设置为true,支持html模板 + mimeMessageHelper.setText(content, true); + // 解决 JavaMailSender no object DCH for MIME type multipart/mixed 问题 + // 详情参考:[Email发送失败问题记录 - 一灰灰Blog](https://blog.hhui.top/hexblog/2021/10/28/211028-Email%E5%8F%91%E9%80%81%E5%A4%B1%E8%B4%A5%E9%97%AE%E9%A2%98%E8%AE%B0%E5%BD%95/) + Thread.currentThread().setContextClassLoader(EmailUtil.class.getClassLoader()); + javaMailSender.send(mimeMailMessage); + return true; + } catch (Exception e) { + log.warn("sendEmail error {}@{}", title, to, e); + return false; + } + } + +} diff --git a/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/EnvUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/EnvUtil.java similarity index 90% rename from forum-core/src/main/java/com/github/liuyueyi/forum/core/util/EnvUtil.java rename to paicoding-core/src/main/java/com/github/paicoding/forum/core/util/EnvUtil.java index 1e49c7862..3505f897f 100644 --- a/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/EnvUtil.java +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/EnvUtil.java @@ -1,4 +1,4 @@ -package com.github.liuyueyi.forum.core.util; +package com.github.paicoding.forum.core.util; import org.springframework.util.Assert; @@ -44,7 +44,7 @@ public static EnvEnum getEnv() { } } } - Assert.isTrue(env != null, "env.name环境配置必然存在!"); + Assert.isTrue(env != null, "env.name环境配置必须存在!"); return env; } } diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/IpUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/IpUtil.java new file mode 100644 index 000000000..76352d053 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/IpUtil.java @@ -0,0 +1,231 @@ +package com.github.paicoding.forum.core.util; + +import com.github.hui.quick.plugin.base.file.FileWriteUtil; +import com.github.paicoding.forum.core.region.IpRegionInfo; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.lionsoul.ip2region.xdb.Searcher; + +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.io.IOException; +import java.net.*; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Optional; + +/** + * @author YiHui + * @date 2022/7/6 + */ +@Slf4j +public class IpUtil { + private static final String UNKNOWN = "unKnown"; + + public static final String DEFAULT_IP = "127.0.0.1"; + + /** + * 获取本机所有网卡信息 得到所有IP信息 + * + * @return Inet4Address> + */ + private static List getLocalIp4AddressFromNetworkInterface() throws SocketException { + List addresses = new ArrayList<>(1); + + // 所有网络接口信息 + Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + if (ObjectUtils.isEmpty(networkInterfaces)) { + return addresses; + } + while (networkInterfaces.hasMoreElements()) { + NetworkInterface networkInterface = networkInterfaces.nextElement(); + //滤回环网卡、点对点网卡、非活动网卡、虚拟网卡并要求网卡名字是eth或ens开头 + if (!isValidInterface(networkInterface)) { + continue; + } + + // 所有网络接口的IP地址信息 + Enumeration inetAddresses = networkInterface.getInetAddresses(); + while (inetAddresses.hasMoreElements()) { + InetAddress inetAddress = inetAddresses.nextElement(); + // 判断是否是IPv4,并且内网地址并过滤回环地址. + if (isValidAddress(inetAddress)) { + addresses.add((Inet4Address) inetAddress); + } + } + } + return addresses; + } + + /** + * 过滤回环网卡、点对点网卡、非活动网卡、虚拟网卡并要求网卡名字是eth或ens开头 + * + * @param ni 网卡 + * @return 如果满足要求则true,否则false + */ + private static boolean isValidInterface(NetworkInterface ni) throws SocketException { + return !ni.isLoopback() && !ni.isPointToPoint() && ni.isUp() && !ni.isVirtual() + && (ni.getName().startsWith("eth") || ni.getName().startsWith("ens")); + } + + /** + * 判断是否是IPv4,并且内网地址并过滤回环地址. + */ + private static boolean isValidAddress(InetAddress address) { + return address instanceof Inet4Address && address.isSiteLocalAddress() && !address.isLoopbackAddress(); + } + + /** + * 通过Socket 唯一确定一个IP + * 当有多个网卡的时候,使用这种方式一般都可以得到想要的IP。甚至不要求外网地址8.8.8.8是可连通的 + * + * @return Inet4Address> + */ + private static Optional getIpBySocket() throws SocketException { + try (final DatagramSocket socket = new DatagramSocket()) { + socket.connect(InetAddress.getByName("8.8.8.8"), 10002); + if (socket.getLocalAddress() instanceof Inet4Address) { + return Optional.of((Inet4Address) socket.getLocalAddress()); + } + } catch (UnknownHostException networkInterfaces) { + throw new RuntimeException(networkInterfaces); + } + return Optional.empty(); + } + + private static String LOCAL_IP = null; + + /** + * 获取本地IPv4地址 + * + * @return Inet4Address> + */ + public static String getLocalIp4Address() throws SocketException { + if (LOCAL_IP != null) { + return LOCAL_IP; + } + + final List inet4Addresses = getLocalIp4AddressFromNetworkInterface(); + if (inet4Addresses.size() != 1) { + final Optional ipBySocketOpt = getIpBySocket(); + LOCAL_IP = ipBySocketOpt.map(Inet4Address::getHostAddress).orElseGet(() -> inet4Addresses.isEmpty() ? DEFAULT_IP : inet4Addresses.get(0).getHostAddress()); + return LOCAL_IP; + } + LOCAL_IP = inet4Addresses.get(0).getHostAddress(); + return LOCAL_IP; + } + + + /** + * 获取请求来源的ip地址 + * + * @param request + * @return + */ + public static String getClientIp(HttpServletRequest request) { + try { + String xIp = request.getHeader("X-Real-IP"); + String xFor = request.getHeader("X-Forwarded-For"); + if (StringUtils.isNotEmpty(xFor) && !UNKNOWN.equalsIgnoreCase(xFor)) { + //多次反向代理后会有多个ip值,第一个ip才是真实ip + int index = xFor.indexOf(","); + if (index != -1) { + return xFor.substring(0, index); + } else { + return xFor; + } + } + xFor = xIp; + if (StringUtils.isNotEmpty(xFor) && !UNKNOWN.equalsIgnoreCase(xFor)) { + return xFor; + } + if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) { + xFor = request.getHeader("Proxy-Client-IP"); + } + if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) { + xFor = request.getHeader("WL-Proxy-Client-IP"); + } + if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) { + xFor = request.getHeader("HTTP_CLIENT_IP"); + } + if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) { + xFor = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) { + xFor = request.getRemoteAddr(); + } + + if ("localhost".equalsIgnoreCase(xFor) || "127.0.0.1".equalsIgnoreCase(xFor) || "0:0:0:0:0:0:0:1".equalsIgnoreCase(xFor)) { + return getLocalIp4Address(); + } + return xFor; + } catch (Exception e) { + log.error("get remote ip error!", e); + return "x.0.0.1"; + } + } + + /** + * ip库路径 + * + */ + private static final String dbPath = "data/ip2region.xdb"; + private static String tmpPath = null; + private static volatile byte[] vIndex = null; + + private static void initVIndex() { + if (vIndex == null) { + synchronized (IpUtil.class) { + if (vIndex == null) { + try { + String file = IpUtil.class.getClassLoader().getResource(dbPath).getFile(); + if (file.contains(".jar!")) { + // RandomAccessFile 无法加载jar包内的文件,因此我们将资源拷贝到临时目录下 + FileWriteUtil.FileInfo tmpFile = new FileWriteUtil.FileInfo("/tmp/data", "ip2region", "xdb"); + tmpPath = tmpFile.getAbsFile(); + if (!new File(tmpPath).exists()) { + // fixme 如果已经存在,则无需继续拷贝,因此当ip库变更之后,需要手动去删除 临时目录下生成的文件,避免出现更新不生效;更好的方式则是比较两个文件的差异性;当不同时,也需要拷贝过去 + FileWriteUtil.saveFileByStream(IpUtil.class.getClassLoader().getResourceAsStream(dbPath), tmpFile); + } + } else { + tmpPath = file; + } + vIndex = Searcher.loadVectorIndexFromFile(tmpPath); + } catch (Exception e) { + log.error("failed to load vector index from {}\n", dbPath, e); + } + } + } + } + } + + /** + * 根据ip查询对应的地址: 国家|区域|省份|城市|ISP + * 若对应的位置不存在值,则为0 + * + * @param ip + * @return + */ + public static IpRegionInfo getLocationByIp(String ip) { + // 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。 + initVIndex(); + Searcher searcher = null; + try { + searcher = Searcher.newWithVectorIndex(tmpPath, vIndex); + return new IpRegionInfo(searcher.search(ip)); + } catch (Exception e) { + log.error("failed to create vectorIndex cached searcher with {}: {}\n", dbPath, e); + return new IpRegionInfo(""); + } finally { + if (searcher != null) { + try { + searcher.close(); + } catch (IOException e) { + log.error("failed to close file:{}\n", dbPath, e); + } + } + } + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/JsonUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/JsonUtil.java new file mode 100644 index 000000000..7ab34d545 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/JsonUtil.java @@ -0,0 +1,91 @@ +package com.github.paicoding.forum.core.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * @author YiHui + * @date 2022/9/5 + */ +public class JsonUtil { + + private static final ObjectMapper jsonMapper = new ObjectMapper(); + + public static JsonNode toNode(String str) { + try { + return jsonMapper.readTree(str); + } catch (Exception e) { + throw new UnsupportedOperationException(e); + } + } + + public static T toObj(String str, Class clz) { + try { + return jsonMapper.readValue(str, clz); + } catch (Exception e) { + throw new UnsupportedOperationException(e); + } + } + + public static String toStr(T t) { + try { + return jsonMapper.writeValueAsString(t); + } catch (Exception e) { + throw new UnsupportedOperationException(e); + } + } + + /** + * 序列换成json时,将所有的long变成string + * 因为js中得数字类型不能包含所有的java long值 + */ + public static SimpleModule bigIntToStrsimpleModule() { + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, newSerializer(s -> String.valueOf(s))); + simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance); + simpleModule.addSerializer(long[].class, newSerializer((Function) String::valueOf)); + simpleModule.addSerializer(Long[].class, newSerializer((Function) String::valueOf)); + simpleModule.addSerializer(BigDecimal.class, newSerializer(BigDecimal::toString)); + simpleModule.addSerializer(BigDecimal[].class, newSerializer(BigDecimal::toString)); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger[].class, newSerializer((Function) BigInteger::toString)); + return simpleModule; + } + + public static JsonSerializer newSerializer(Function func) { + return new JsonSerializer() { + @Override + public void serialize(T t, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + if (t == null) { + jsonGenerator.writeNull(); + return; + } + + if (t.getClass().isArray()) { + jsonGenerator.writeStartArray(); + Stream.of(t).forEach(s -> { + try { + jsonGenerator.writeString(func.apply((K) s)); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + jsonGenerator.writeEndArray(); + } else { + jsonGenerator.writeString(func.apply((K) t)); + } + } + }; + } +} diff --git a/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/MapUtils.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/MapUtils.java similarity index 94% rename from forum-core/src/main/java/com/github/liuyueyi/forum/core/util/MapUtils.java rename to paicoding-core/src/main/java/com/github/paicoding/forum/core/util/MapUtils.java index 00ec95a01..21437e943 100644 --- a/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/MapUtils.java +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/MapUtils.java @@ -1,4 +1,4 @@ -package com.github.liuyueyi.forum.core.util; +package com.github.paicoding.forum.core.util; import com.google.common.collect.Maps; import org.springframework.util.CollectionUtils; diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/MarkdownConverter.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/MarkdownConverter.java new file mode 100644 index 000000000..bbac82ee7 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/MarkdownConverter.java @@ -0,0 +1,49 @@ +package com.github.paicoding.forum.core.util; + +import com.github.paicoding.forum.core.markdown.CustomAdmonitionBlockParser; +import com.github.paicoding.forum.core.markdown.CustomAdmonitionExtension; +import com.vladsch.flexmark.ext.admonition.AdmonitionExtension; +import com.vladsch.flexmark.ext.autolink.AutolinkExtension; +import com.vladsch.flexmark.ext.emoji.EmojiExtension; +import com.vladsch.flexmark.ext.footnotes.FootnoteExtension; +import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension; +import com.vladsch.flexmark.ext.gitlab.GitLabExtension; +import com.vladsch.flexmark.ext.tables.TablesExtension; +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.data.MutableDataSet; + +import java.util.Arrays; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 4/15/23 + */ +public class MarkdownConverter { + // 定义一个静态方法,将 Markdown 文本转换为 HTML + public static String markdownToHtml(String markdown) { + // 创建一个 MutableDataSet 对象来配置 Markdown 解析器的选项 + MutableDataSet options = new MutableDataSet(); + + // 添加各种 Markdown 解析器的扩展 + options.set(Parser.EXTENSIONS, Arrays.asList( + AutolinkExtension.create(), // 自动链接扩展,将URL文本转换为链接 + EmojiExtension.create(), // 表情符号扩展,用于解析表情符号 + GitLabExtension.create(), // GitLab特有的Markdown扩展 + FootnoteExtension.create(), // 脚注扩展,用于添加和解析脚注 + TaskListExtension.create(), // 任务列表扩展,用于创建任务列表 + CustomAdmonitionExtension.create(), // 提示框扩展,用于创建提示框 + TablesExtension.create())); // 表格扩展,用于解析和渲染表格 + + + // 使用配置的选项构建一个 Markdown 解析器 + Parser parser = Parser.builder(options).build(); + // 使用相同的选项构建一个 HTML 渲染器 + HtmlRenderer renderer = HtmlRenderer.builder(options).build(); + + // 解析传入的 Markdown 文本并将其渲染为 HTML + return renderer.render(parser.parse(markdown)); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/Md5Util.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/Md5Util.java new file mode 100644 index 000000000..79f01409e --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/Md5Util.java @@ -0,0 +1,56 @@ +package com.github.paicoding.forum.core.util; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * @author YiHui + * @date 2022/10/13 + */ +public class Md5Util { + private Md5Util() { + } + + public static String encode(String data) { + byte[] bytes = data.getBytes(StandardCharsets.UTF_8); + return encode(bytes); + } + + public static String encode(byte[] bytes) { + return encode(bytes, 0, bytes.length); + } + + public static String encode(byte[] data, int offset, int len) { + MessageDigest md; + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException var5) { + throw new RuntimeException(var5); + } + + md.update(data, offset, len); + byte[] secretBytes = md.digest(); + return getFormattedText(secretBytes); + } + + private static String getFormattedText(byte[] src) { + if (src != null && src.length != 0) { + StringBuilder stringBuilder = new StringBuilder(32); + + for (int i = 0; i < src.length; ++i) { + int v = src[i] & 255; + String hv = Integer.toHexString(v); + if (hv.length() < 2) { + stringBuilder.append(0); + } + + stringBuilder.append(hv); + } + + return stringBuilder.toString(); + } else { + return ""; + } + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/MdImgLoader.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/MdImgLoader.java new file mode 100644 index 000000000..57fcb58bb --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/MdImgLoader.java @@ -0,0 +1,47 @@ +package com.github.paicoding.forum.core.util; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * markdown文本中的图片识别 + * + * @author YiHui + * @date 2022/11/24 + */ +public class MdImgLoader { + private static Pattern IMG_PATTERN = Pattern.compile("!\\[(.*?)\\]\\((.*?)\\)"); + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class MdImg { + /** + * 原始文本 + */ + private String origin; + /** + * 图片描述 + */ + private String desc; + /** + * 图片地址 + */ + private String url; + } + + public static List loadImgs(String content) { + Matcher matcher = IMG_PATTERN.matcher(content); + List list = new ArrayList<>(); + while (matcher.find()) { + list.add(new MdImg(matcher.group(0), matcher.group(1), matcher.group(2))); + } + return list; + } +} diff --git a/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/NumUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/NumUtil.java similarity index 90% rename from forum-core/src/main/java/com/github/liuyueyi/forum/core/util/NumUtil.java rename to paicoding-core/src/main/java/com/github/paicoding/forum/core/util/NumUtil.java index 8ac0f37c5..87ba21e41 100644 --- a/forum-core/src/main/java/com/github/liuyueyi/forum/core/util/NumUtil.java +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/NumUtil.java @@ -1,4 +1,4 @@ -package com.github.liuyueyi.forum.core.util; +package com.github.paicoding.forum.core.util; /** * @author YiHui diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/PriceUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/PriceUtil.java new file mode 100644 index 000000000..c5979d6e0 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/PriceUtil.java @@ -0,0 +1,65 @@ +package com.github.paicoding.forum.core.util; + +import org.apache.commons.lang3.StringUtils; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; + +/** + * @author YiHui + * @date 2024/12/04 + */ +public class PriceUtil { + + /** + * 价格转分 + * + * @param price + * @return + */ + public static Integer toCentPrice(String price) { + if (StringUtils.isBlank(price)) { + return null; + } + + BigDecimal ans = new BigDecimal(price).multiply(new BigDecimal("100.0")).setScale(0, RoundingMode.HALF_DOWN); + return ans.intValue(); + } + + /** + * 分的价格转元 + * + * @param price + * @return + */ + public static String toYuanPrice(Integer price) { + if (price == null) { + return null; + } + DecimalFormat df1 = new DecimalFormat("0.00"); + String ans = df1.format(price / 100f); + if (price % 100 == 0) { + // 整元时,移除后面的小数 + return ans.substring(0, ans.length() - 3); + } else if (price % 10 == 0) { + return ans.substring(0, ans.length() - 1); + } + return ans; + } + + /** + * 判断是否为合法的价格 + * + * @param price + * @return ture 合法价格 + */ + public static boolean legalPrice(String price) { + if (StringUtils.isBlank(price)) { + return false; + } + + Integer pp = toCentPrice(price); + return pp != null && pp > 0; + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/RandUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/RandUtil.java new file mode 100644 index 000000000..b779a7936 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/RandUtil.java @@ -0,0 +1,25 @@ +package com.github.paicoding.forum.core.util; + + +import java.util.Random; + +/** + * 随机工具类 + * + * @author YiHui + * @date 2024/9/7 + */ +public class RandUtil { + private static Random random = new Random(); + private static final String txt = "0123456789qwertyuiopasdfghjklzxcvbnm"; + + public static String random(int len) { + StringBuilder builder = new StringBuilder(); + int size = txt.length(); + for (int i = 0; i < len; i++) { + builder.append(txt.charAt(random.nextInt(size))); + } + return builder.toString(); + } + +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/SessionUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/SessionUtil.java new file mode 100644 index 000000000..ccd0d82c1 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/SessionUtil.java @@ -0,0 +1,77 @@ +package com.github.paicoding.forum.core.util; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.util.CollectionUtils; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.List; + +/** + * @author YiHui + * @date 2023/6/6 + */ +public class SessionUtil { + private static final int COOKIE_AGE = 30 * 86400; + + public static Cookie newCookie(String key, String session) { + return newCookie(key, session, "/", COOKIE_AGE); + } + + public static Cookie newCookie(String key, String session, String path, int maxAge) { + Cookie cookie = new Cookie(key, session); + cookie.setPath(path); + cookie.setMaxAge(maxAge); + return cookie; + } + + + public static Cookie delCookie(String key) { + return delCookie(key, "/"); + } + + public static Cookie delCookie(String key, String path) { + Cookie cookie = new Cookie(key, null); + cookie.setPath("/"); + cookie.setMaxAge(0); + return cookie; + } + + /** + * 根据key查询cookie + * + * @param request + * @param name + * @return + */ + public static Cookie findCookieByName(HttpServletRequest request, String name) { + Cookie[] cookies = request.getCookies(); + if (cookies == null || cookies.length == 0) { + return null; + } + + return Arrays.stream(cookies).filter(cookie -> StringUtils.equalsAnyIgnoreCase(cookie.getName(), name)) + .findFirst().orElse(null); + } + + + public static String findCookieByName(ServerHttpRequest request, String name) { + List list = request.getHeaders().get("cookie"); + if (CollectionUtils.isEmpty(list)) { + return null; + } + + for (String sub : list) { + String[] elements = StringUtils.split(sub, ";"); + for (String element : elements) { + String[] subs = StringUtils.split(element, "="); + if (subs.length == 2 && StringUtils.equalsAnyIgnoreCase(subs[0].trim(), name)) { + return subs[1].trim(); + } + } + } + return null; + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/SocketUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/SocketUtil.java new file mode 100644 index 000000000..54a37dc43 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/SocketUtil.java @@ -0,0 +1,66 @@ +package com.github.paicoding.forum.core.util; + +import javax.net.ServerSocketFactory; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.util.Random; + +/** + * @author YiHui + * @date 2022/11/26 + */ +public class SocketUtil { + + /** + * 判断端口是否可用 + * + * @param port + * @return + */ + public static boolean isPortAvailable(int port) { + try { + ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket(port, 1); + serverSocket.close(); + return true; + } catch (Exception var3) { + return false; + } + } + + private static Random random = new Random(); + + private static int findRandomPort(int minPort, int maxPort) { + int portRange = maxPort - minPort; + return minPort + random.nextInt(portRange + 1); + } + + /** + * 找一个可用的端口号 + * + * @param minPort + * @param maxPort + * @param defaultPort + * @return + */ + public static int findAvailableTcpPort(int minPort, int maxPort, int defaultPort) { + if (isPortAvailable(defaultPort)) { + return defaultPort; + } + + if (maxPort <= minPort) { + throw new IllegalArgumentException("maxPort should bigger than miPort!"); + } + int portRange = maxPort - minPort; + int searchCounter = 0; + + while (searchCounter <= portRange) { + int candidatePort = findRandomPort(minPort, maxPort); + ++searchCounter; + if (isPortAvailable(candidatePort)) { + return candidatePort; + } + } + + throw new IllegalStateException(String.format("Could not find an available %s port in the range [%d, %d] after %d attempts", SocketUtil.class.getName(), minPort, maxPort, searchCounter)); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/SpringUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/SpringUtil.java new file mode 100644 index 000000000..9defcf32a --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/SpringUtil.java @@ -0,0 +1,124 @@ +package com.github.paicoding.forum.core.util; + +import org.springframework.beans.BeansException; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +/** + * @author YiHui + * @date 2022/8/29 + */ +@Component +public class SpringUtil implements ApplicationContextAware, EnvironmentAware { + private volatile static ApplicationContext context; + private volatile static Environment environment; + + private static Binder binder; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + SpringUtil.context = applicationContext; + } + + @Override + public void setEnvironment(Environment environment) { + SpringUtil.environment = environment; + binder = Binder.get(environment); + } + + public static ApplicationContext getContext() { + return context; + } + + /** + * 获取bean + * + * @param bean + * @param + * @return + */ + public static T getBean(Class bean) { + if (context != null) { + return context.getBean(bean); + } else { + throw new IllegalStateException("Spring ApplicationContext is not active or has been closed."); + } + } + + public static T getBeanOrNull(Class bean) { + try { + return context.getBean(bean); + } catch (Exception e) { + return null; + } + } + + public static Object getBean(String beanName) { + return context.getBean(beanName); + } + + public static Object getBeanOrNull(String beanName) { + try { + return context.getBean(beanName); + } catch (Exception e) { + return null; + } + } + + public static boolean hasConfig(String key) { + return environment.containsProperty(key); + } + + /** + * 获取配置 + * + * @param key + * @return + */ + public static String getConfig(String key) { + return environment.getProperty(key); + } + + public static String getConfigOrElse(String mainKey, String slaveKey) { + String ans = environment.getProperty(mainKey); + if (ans == null) { + return environment.getProperty(slaveKey); + } + return ans; + } + + /** + * 获取配置 + * + * @param key + * @param val 配置不存在时的默认值 + * @return + */ + public static String getConfig(String key, String val) { + return environment.getProperty(key, val); + } + + /** + * 发布事件消息 + * + * @param event + */ + public static void publishEvent(ApplicationEvent event) { + context.publishEvent(event); + } + + + /** + * 配置绑定类 + * + * @return + */ + public static Binder getBinder() { + return binder; + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/StopWatchUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/StopWatchUtil.java new file mode 100644 index 000000000..67fa71d5c --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/StopWatchUtil.java @@ -0,0 +1,73 @@ +package com.github.paicoding.forum.core.util; + +import org.springframework.util.StopWatch; + +import java.util.concurrent.Callable; + +/** + * 统计耗时工具类, 这个只支持同步的耗时打印,不支持异步的场景 + * + * @author YiHui + * @date 2023/11/10 + */ +public class StopWatchUtil { + private StopWatch stopWatch; + + private StopWatchUtil(String task) { + stopWatch = task == null ? new StopWatch() : new StopWatch(task); + } + + /** + * 初始化 + * + * @param task + * @return + */ + public static StopWatchUtil init(String... task) { + return new StopWatchUtil(task.length > 0 ? task[0] : null); + } + + /** + * 同步耗时计时 + * + * @param task 任务名 + * @param call 执行业务逻辑 + * @param 返回类型 + * @return 返回结果 + */ + public T record(String task, Callable call) { + stopWatch.start(task); + try { + return call.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + stopWatch.stop(); + } + } + + /** + * 同步耗时计时 + * + * @param task 任务名 + * @param run 执行业务逻辑 + */ + public void record(String task, Runnable run) { + stopWatch.start(task); + try { + run.run(); + } finally { + stopWatch.stop(); + } + } + + + /** + * 计时信息输出 + * + * @return + */ + public String prettyPrint() { + return stopWatch.prettyPrint(); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/StrUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/StrUtil.java new file mode 100644 index 000000000..bdccf2135 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/StrUtil.java @@ -0,0 +1,83 @@ +package com.github.paicoding.forum.core.util; + +import org.apache.commons.lang3.CharUtils; +import org.apache.commons.lang3.StringUtils; + +/** + * @author YiHui + * @date 2024/12/5 + */ +public class StrUtil { + + /** + * 微信支付的提示信息,不支持表情包,因此我们只保留中文 + 数字 + 英文字母 + 符号 '《》【】-_.' + * + * @return + */ + public static String pickWxSupportTxt(String text) { + if (StringUtils.isBlank(text)) { + return text; + } + + StringBuilder str = new StringBuilder(); + for (char c : text.toCharArray()) { + if (c >= '\u4E00' && c <= '\u9FA5') { + str.append(c); + } else if (CharUtils.isAsciiAlphanumeric(c)) { + str.append(c); + } else if (c == '【' || c == '】' || c == '《' || c == '》' || c == '-' || c == '_' || c == '.') { + str.append(c); + } + } + return str.toString(); + } + + private static final char MID_LINE = '-'; + private static final char DOT = '.'; + + /** + * Spring的配置命名规则有要求, 若不满足时,可能出现启动异常 + *

+ * Reason: Canonical names should be kebab-case (’-’ separated), lowercase alpha-numeric characters, and must start with a letter。 + * + * @return + */ + public static String formatSpringConfigKey(String key) { + if (null == key || key.isEmpty()) { + return null; + } + + int len = key.length(); + StringBuilder res = new StringBuilder(len + 2); + char pre = 0; + for (int i = 0; i < len; i++) { + char ch = key.charAt(i); + if (Character.isUpperCase(ch)) { + // 当前为大写字母时,若前面一个是中划线/点号,则直接转为小写;否则插入一个中划线 + if (pre != MID_LINE && pre != DOT) { + res.append(MID_LINE); + } + res.append(Character.toLowerCase(ch)); + } else { + res.append(ch); + } + pre = ch; + } + return res.toString(); + } + + + public static void main(String[] args) { + String text = "这是一个有趣的表😄过滤- 123 143 d 哒哒"; + System.out.println(pickWxSupportTxt(text)); + + text = "view.site.Host"; + System.out.println(formatSpringConfigKey(text)); + + text = "view.site.webHost"; + System.out.println(formatSpringConfigKey(text)); + + text = "view.site.web-Host"; + System.out.println(formatSpringConfigKey(text)); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/TransactionUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/TransactionUtil.java new file mode 100644 index 000000000..eeab1de3d --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/TransactionUtil.java @@ -0,0 +1,85 @@ +package com.github.paicoding.forum.core.util; + +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * 事务辅助工具类 + * + * @author YiHui + * @date 2023/6/26 + */ +public class TransactionUtil { + /** + * 注册事务回调-事务提交前执行,如果没在事务中就立即执行 + * + * @param runnable + */ + public static void registryBeforeCommitOrImmediatelyRun(Runnable runnable) { + if (runnable == null) { + return; + } + // 处于事务中 + if (TransactionSynchronizationManager.isSynchronizationActive()) { + // 等事务提交前执行,发生错误会回滚事务 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void beforeCommit(boolean readOnly) { + runnable.run(); + } + }); + } else { + // 马上执行 + runnable.run(); + } + } + + /** + * 事务执行完/回滚完之后执行 + * + * @param runnable + */ + public static void registryAfterCompletionOrImmediatelyRun(Runnable runnable) { + if (runnable == null) { + return; + } + // 处于事务中 + if (TransactionSynchronizationManager.isSynchronizationActive()) { + // 等事务提交或者回滚之后执行 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCompletion(int status) { + runnable.run(); + } + }); + } else { + // 马上执行 + runnable.run(); + } + } + + + /** + * 事务正常提交之后执行 + * + * @param runnable + */ + public static void registryAfterCommitOrImmediatelyRun(Runnable runnable) { + if (runnable == null) { + return; + } + // 处于事务中 + if (TransactionSynchronizationManager.isSynchronizationActive()) { + // 等事务提交之后执行 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + runnable.run(); + } + }); + } else { + // 马上执行 + runnable.run(); + } + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/id/IdUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/id/IdUtil.java new file mode 100644 index 000000000..0f16a9426 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/id/IdUtil.java @@ -0,0 +1,91 @@ +package com.github.paicoding.forum.core.util.id; + +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import com.github.paicoding.forum.core.async.AsyncUtil; +import com.github.paicoding.forum.core.util.CompressUtil; +import com.github.paicoding.forum.core.util.DateUtil; +import com.github.paicoding.forum.core.util.id.snowflake.PaiSnowflakeIdGenerator; +import com.github.paicoding.forum.core.util.id.snowflake.SnowflakeProducer; +import org.apache.commons.lang3.StringUtils; + +import java.util.concurrent.atomic.AtomicLong; + +import static com.github.paicoding.forum.core.util.CompressUtil.int2str; + +/** + * @author YiHui + * @date 2023/8/30 + */ +public class IdUtil { + /** + * 默认的id生成器 + */ + public static SnowflakeProducer DEFAULT_ID_PRODUCER = new SnowflakeProducer(new PaiSnowflakeIdGenerator()); + + private static AtomicLong INCR = new AtomicLong((int) (Math.random() * 500)); + private static long lastTime = 0; + + + /** + * 生成全局id + * + * @return + */ + public static Long genId() { + return DEFAULT_ID_PRODUCER.genId(); + } + + /** + * 生成字符串格式全局id + * + * @return + */ + public static String genStrId() { + return CompressUtil.int2str(genId()); + } + + + /** + * 生成支付的唯一code + * 简化的规则:payWay前缀 + 年月日+时分秒 + * + * @return + */ + public static String genPayCode(ThirdPayWayEnum payWay, Long id) { + long now = System.currentTimeMillis(); + if (DateUtil.skipDay(lastTime, now)) { + lastTime = now; + INCR.set((int) (Math.random() * 500)); + } + return payWay.getPrefix() + String.format("%06d", INCR.addAndGet(1)) + "-" + id; + } + + /** + * 根据payCode 解析获取 payId + * + * @param code + * @return + */ + public static Long getPayIdFromPayCode(String code) { + String[] str = StringUtils.split(code, "-"); + return Long.valueOf(str[str.length - 1]); + } + + public static void main(String[] args) { + System.out.println(IdUtil.genStrId()); + Long id = IdUtil.genId(); + System.out.println(id + " = " + int2str(id)); + System.out.println(IdUtil.genId() + "->" + IdUtil.genStrId()); + AsyncUtil.sleep(2000); + System.out.println(IdUtil.genId() + "->" + IdUtil.genStrId()); + + System.out.println("-----"); + + SnowflakeProducer producer = new SnowflakeProducer(new PaiSnowflakeIdGenerator()); + id = producer.genId(); + System.out.println("id: " + id + " -> " + int2str(id)); + AsyncUtil.sleep(3000L); + id = producer.genId(); + System.out.println("id: " + id + " -> " + int2str(id)); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/id/snowflake/HuToolSnowflakeIdGenerator.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/id/snowflake/HuToolSnowflakeIdGenerator.java new file mode 100644 index 000000000..0388eb7d8 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/id/snowflake/HuToolSnowflakeIdGenerator.java @@ -0,0 +1,24 @@ +package com.github.paicoding.forum.core.util.id.snowflake; + +import cn.hutool.core.lang.Snowflake; + +import java.util.Date; + + +/** + * @author YiHui + * @date 2023/10/17 + */ +public class HuToolSnowflakeIdGenerator implements IdGenerator { + private static final Date EPOC = new Date(2023, 1, 1); + private Snowflake snowflake; + + public HuToolSnowflakeIdGenerator(int workId, int datacenter) { + snowflake = new Snowflake(EPOC, workId, datacenter, false); + } + + @Override + public Long nextId() { + return snowflake.nextId(); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/id/snowflake/IdGenerator.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/id/snowflake/IdGenerator.java new file mode 100644 index 000000000..b19d5d00c --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/id/snowflake/IdGenerator.java @@ -0,0 +1,14 @@ +package com.github.paicoding.forum.core.util.id.snowflake; + +/** + * @author YiHui + * @date 2023/10/17 + */ +public interface IdGenerator { + /** + * 生成分布式id + * + * @return + */ + Long nextId(); +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/id/snowflake/PaiSnowflakeIdGenerator.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/id/snowflake/PaiSnowflakeIdGenerator.java new file mode 100644 index 000000000..3a07bfc6d --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/id/snowflake/PaiSnowflakeIdGenerator.java @@ -0,0 +1,153 @@ +package com.github.paicoding.forum.core.util.id.snowflake; + +import com.github.paicoding.forum.core.async.AsyncUtil; +import com.github.paicoding.forum.core.util.DateUtil; +import com.github.paicoding.forum.core.util.IpUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.time.LocalDateTime; + +/** + * 自定义实现的雪花算法生成器 + *

+ * 时间 + 数据中心(3位) + 机器id(7位) + 序列号(12位) + * + * @author YiHui + * @date 2023/10/16 + */ +@Slf4j +public class PaiSnowflakeIdGenerator implements IdGenerator { + /** + * 自增序号位数 + */ + private static final long SEQUENCE_BITS = 10L; + + /** + * 机器位数 + */ + private static final long WORKER_ID_BITS = 7L; + private static final long DATA_CENTER_BITS = 3L; + + private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1; + + private static final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS; + private static final long DATACENTER_LEFT_SHIFT_BITS = SEQUENCE_BITS + WORKER_ID_BITS; + private static final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS + DATA_CENTER_BITS; + /** + * 机器id (7位) + */ + private long workId = 1; + /** + * 数据中心 (3位) + */ + private long dataCenter = 1; + + /** + * 上次的访问时间 + */ + private long lastTime; + /** + * 自增序号 + */ + private long sequence; + + private byte sequenceOffset; + + public PaiSnowflakeIdGenerator() { + try { + String ip = IpUtil.getLocalIp4Address(); + String[] cells = StringUtils.split(ip, "."); + this.dataCenter = Integer.parseInt(cells[0]) & ((1 << DATA_CENTER_BITS) - 1); + this.workId = Integer.parseInt(cells[3]) >> 16 & ((1 << WORKER_ID_BITS) - 1); + } catch (Exception e) { + this.dataCenter = 1; + this.workId = 1; + } + } + + public PaiSnowflakeIdGenerator(int workId, int dateCenter) { + this.workId = workId; + this.dataCenter = dateCenter; + } + + /** + * 生成趋势自增的id + * + * @return + */ + @Override + public synchronized Long nextId() { + long nowTime = waitToIncrDiffIfNeed(getNowTime()); + if (lastTime == nowTime) { + if (0L == (sequence = (sequence + 1) & SEQUENCE_MASK)) { + // 表示当前这一时刻的自增数被用完了;等待下一时间点 + nowTime = waitUntilNextTime(nowTime); + } + } else { + // 上一毫秒若以0作为序列号开始值,则这一秒以1为序列号开始值 + vibrateSequenceOffset(); + sequence = sequenceOffset; + } + lastTime = nowTime; + long ans = ((nowTime % DateUtil.ONE_DAY_SECONDS) << TIMESTAMP_LEFT_SHIFT_BITS) | (dataCenter << DATACENTER_LEFT_SHIFT_BITS) | (workId << WORKER_ID_LEFT_SHIFT_BITS) | sequence; + if (log.isDebugEnabled()) { + log.debug("seconds:{}, datacenter:{}, work:{}, seq:{}, ans={}", nowTime % DateUtil.ONE_DAY_SECONDS, dataCenter, workId, sequence, ans); + } + return Long.parseLong(String.format("%s%011d", getDaySegment(nowTime), ans)); + } + + /** + * 若当前时间比上次执行时间要小,则等待时间追上来,避免出现时钟回拨导致的数据重复 + * + * @param nowTime 当前时间戳 + * @return 返回新的时间戳 + */ + private long waitToIncrDiffIfNeed(final long nowTime) { + if (lastTime <= nowTime) { + return nowTime; + } + long diff = lastTime - nowTime; + AsyncUtil.sleep(diff); + return getNowTime(); + } + + /** + * 等待下一秒 + * + * @param lastTime + * @return + */ + private long waitUntilNextTime(final long lastTime) { + long result = getNowTime(); + while (result <= lastTime) { + result = getNowTime(); + } + return result; + } + + private void vibrateSequenceOffset() { + sequenceOffset = (byte) (~sequenceOffset & 1); + } + + + /** + * 获取当前时间 + * + * @return 秒为单位 + */ + private long getNowTime() { + return System.currentTimeMillis() / 1000; + } + + /** + * 基于年月日构建分区 + * + * @param time 时间戳 + * @return 时间分区 + */ + private static String getDaySegment(long time) { + LocalDateTime localDate = DateUtil.time2LocalTime(time * 1000L); + return String.format("%02d%03d", localDate.getYear() % 100, localDate.getDayOfYear()); + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/id/snowflake/SnowflakeProducer.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/id/snowflake/SnowflakeProducer.java new file mode 100644 index 000000000..74afa5a09 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/util/id/snowflake/SnowflakeProducer.java @@ -0,0 +1,71 @@ +package com.github.paicoding.forum.core.util.id.snowflake; + +import com.github.paicoding.forum.core.util.DateUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * 基于雪花算法计算的id生成器 + * + * @author YiHui + * @date 2023/8/30 + */ +@Slf4j +public class SnowflakeProducer { + private BlockingQueue queue; + + /** + * id失效的间隔时间 + */ + public static final Long ID_EXPIRE_TIME_INTER = DateUtil.ONE_DAY_MILL; + private static final int QUEUE_SIZE = 10; + private ExecutorService es = Executors.newSingleThreadExecutor((Runnable r) -> { + Thread t = new Thread(r); + t.setName("SnowflakeProducer-generate-thread"); + t.setDaemon(true); + return t; + }); + + public SnowflakeProducer(final IdGenerator generator) { + queue = new LinkedBlockingQueue<>(QUEUE_SIZE); + es.submit(() -> { + long lastTime = System.currentTimeMillis(); + while (true) { + try { + queue.offer(generator.nextId(), 1, TimeUnit.MINUTES); + } catch (InterruptedException e1) { + } catch (Exception e) { + log.info("gen id error! {}", e.getMessage()); + } + + // 当出现跨天时,自动重置业务id + try { + long now = System.currentTimeMillis(); + if (now / ID_EXPIRE_TIME_INTER - lastTime / ID_EXPIRE_TIME_INTER > 0) { + // 跨天,清空队列 + queue.clear(); + log.info("清空id队列,重新设置"); + } + lastTime = now; + + } catch (Exception e) { + log.info("auto remove illegal ids error! {}", e.getMessage()); + } + } + }); + } + + public Long genId() { + try { + return queue.take(); + } catch (InterruptedException e) { + log.error("雪花算法生成逻辑异常", e); + throw new RuntimeException("雪花算法生成id异常!", e); + } + } +} diff --git a/paicoding-core/src/main/java/com/github/paicoding/forum/core/ws/WebSocketResponseUtil.java b/paicoding-core/src/main/java/com/github/paicoding/forum/core/ws/WebSocketResponseUtil.java new file mode 100644 index 000000000..e4fc4d933 --- /dev/null +++ b/paicoding-core/src/main/java/com/github/paicoding/forum/core/ws/WebSocketResponseUtil.java @@ -0,0 +1,76 @@ +package com.github.paicoding.forum.core.ws; + +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.core.mdc.MdcUtil; +import com.github.paicoding.forum.core.util.SpringUtil; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.SimpMessagingTemplate; + +/** + * websocket消息响应封装工具类 + * + * @author YiHui + * @date 2024/11/27 + */ +public class WebSocketResponseUtil { + private static volatile SimpMessagingTemplate simpMessagingTemplate; + + /** + * 初始化 + */ + private static void initSimpMessageTemplate() { + if (simpMessagingTemplate == null) { + synchronized (WebSocketResponseUtil.class) { + if (simpMessagingTemplate == null) { + simpMessagingTemplate = SpringUtil.getBean(SimpMessagingTemplate.class); + } + } + } + } + + /** + * 给用户发送消息 + * + * @param user 用户 + * @param destination 用户订阅地址 + * @param data 消息实体 + */ + public static void sendMsgToUser(String user, String destination, Object data) { + initSimpMessageTemplate(); + simpMessagingTemplate.convertAndSendToUser(user, destination, data); + } + + /** + * 消息广播 + * + * @param destination 订阅地址 + * @param data 消息实体 + */ + public static void broadcastMsg(String destination, Object data) { + initSimpMessageTemplate(); + simpMessagingTemplate.convertAndSend(destination, data); + } + + /** + * 封装websocket的消息处理,主要是设置上下文,全链路traceId + * + * @param accessor 请求 + * @param func 执行体 + */ + public static void execute(SimpMessageHeaderAccessor accessor, Runnable func) { + try { + ReqInfoContext.ReqInfo reqInfo = (ReqInfoContext.ReqInfo) accessor.getUser(); + ReqInfoContext.addReqInfo(reqInfo); + String traceId = (String) accessor.getSessionAttributes().get(MdcUtil.TRACE_ID_KEY); + MdcUtil.add(MdcUtil.TRACE_ID_KEY, traceId); + + + // 执行具体的业务逻辑 + func.run(); + + } finally { + ReqInfoContext.clear(); + MdcUtil.clear(); + } + } +} diff --git a/paicoding-core/src/main/resources/META-INF/spring.factories b/paicoding-core/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..8ce866a20 --- /dev/null +++ b/paicoding-core/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configure +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.github.paicoding.forum.core.ForumCoreAutoConfig \ No newline at end of file diff --git a/paicoding-service/pom.xml b/paicoding-service/pom.xml new file mode 100644 index 000000000..c93b23db4 --- /dev/null +++ b/paicoding-service/pom.xml @@ -0,0 +1,122 @@ + + + + paicoding-forum + com.github.paicoding.forum + 0.0.1-SNAPSHOT + + 4.0.0 + + paicoding-service + + + + com.github.paicoding.forum + paicoding-core + + + org.springframework + spring-context + + + + com.fasterxml.jackson.core + jackson-databind + + + + com.baomidou + mybatis-plus-boot-starter + + + mysql + mysql-connector-java + + + + com.aliyun.oss + aliyun-sdk-oss + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + + + org.mapstruct + mapstruct + + + org.mapstruct + mapstruct-processor + compile + + + + + org.elasticsearch + elasticsearch + 6.8.2 + + + + org.elasticsearch.client + elasticsearch-rest-client + 6.8.2 + + + org.elasticsearch.client + elasticsearch-rest-high-level-client + 6.8.2 + + + + cn.hutool + hutool-all + + + + com.auth0 + java-jwt + ${jwt.version} + + + org.springframework.security + spring-security-crypto + + + org.thymeleaf + thymeleaf-spring5 + provided + + + + org.springframework + spring-messaging + + + + + com.github.wechatpay-apiv3 + wechatpay-java + 0.2.14 + + + + cn.idev.excel + fastexcel + + + + + com.volcengine + volcengine-java-sdk-ark-runtime + 0.1.150 + + + + + \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/ServiceAutoConfig.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/ServiceAutoConfig.java new file mode 100644 index 000000000..6953b48f9 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/ServiceAutoConfig.java @@ -0,0 +1,25 @@ +package com.github.paicoding.forum.service; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * @author YiHui + * @date 2022/7/6 + */ +@Configuration +@ComponentScan("com.github.paicoding.forum.service") +@MapperScan(basePackages = { + "com.github.paicoding.forum.service.article.repository.mapper", + "com.github.paicoding.forum.service.user.repository.mapper", + "com.github.paicoding.forum.service.comment.repository.mapper", + "com.github.paicoding.forum.service.config.repository.mapper", + "com.github.paicoding.forum.service.statistics.repository.mapper", + "com.github.paicoding.forum.service.notify.repository.mapper", + "com.github.paicoding.forum.service.shortlink.repository.mapper", +}) +public class ServiceAutoConfig { + + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/ArticleConverter.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/ArticleConverter.java new file mode 100644 index 000000000..b644a9edb --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/ArticleConverter.java @@ -0,0 +1,162 @@ +package com.github.paicoding.forum.service.article.conveter; + +import com.github.paicoding.forum.api.model.enums.ArticleReadTypeEnum; +import com.github.paicoding.forum.api.model.enums.ArticleTypeEnum; +import com.github.paicoding.forum.api.model.enums.SourceTypeEnum; +import com.github.paicoding.forum.api.model.enums.YesOrNoEnum; +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import com.github.paicoding.forum.api.model.vo.article.ArticlePostReq; +import com.github.paicoding.forum.api.model.vo.article.CategoryReq; +import com.github.paicoding.forum.api.model.vo.article.SearchArticleReq; +import com.github.paicoding.forum.api.model.vo.article.TagReq; +import com.github.paicoding.forum.api.model.vo.article.dto.ArticleDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.CategoryDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.TagDTO; +import com.github.paicoding.forum.core.util.PriceUtil; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDO; +import com.github.paicoding.forum.service.article.repository.entity.CategoryDO; +import com.github.paicoding.forum.service.article.repository.entity.TagDO; +import com.github.paicoding.forum.service.article.repository.params.SearchArticleParams; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 文章转换 + *

+ * + * @author louzai + * @date 2022-07-31 + */ +public class ArticleConverter { + + public static ArticleDO toArticleDo(ArticlePostReq req, Long author) { + ArticleDO article = new ArticleDO(); + // 设置作者ID + article.setUserId(author); + article.setId(req.getArticleId()); + article.setTitle(req.getTitle()); + article.setShortTitle(req.getShortTitle()); + article.setArticleType(ArticleTypeEnum.valueOf(req.getArticleType().toUpperCase()).getCode()); + article.setPicture(req.getCover() == null ? "" : req.getCover()); + article.setCategoryId(req.getCategoryId()); + article.setSource(req.getSource()); + article.setSourceUrl(req.getSourceUrl()); + article.setSummary(req.getSummary()); + article.setStatus(req.pushStatus().getCode()); + article.setDeleted(req.deleted() ? YesOrNoEnum.YES.getCode() : YesOrNoEnum.NO.getCode()); + article.setReadType(req.getReadType() == null ? ArticleReadTypeEnum.NORMAL.getType() : req.getReadType()); + if (article.getReadType().equals(ArticleReadTypeEnum.PAY_READ.getType())) { + // 不指定价格时,默认0.99元 + article.setPayAmount(req.getPayAmount() == null ? 99 : req.getPayAmount()); + // 当不指定具体的支付方式时,统一使用native的扫码支付方式 + article.setPayWay(StringUtils.isBlank(req.getPayWay()) ? ThirdPayWayEnum.WX_NATIVE.getPay() : req.getPayWay()); + } + return article; + } + + public static ArticleDTO toDto(ArticleDO articleDO) { + if (articleDO == null) { + return null; + } + ArticleDTO articleDTO = new ArticleDTO(); + articleDTO.setAuthor(articleDO.getUserId()); + articleDTO.setArticleId(articleDO.getId()); + articleDTO.setArticleType(articleDO.getArticleType()); + articleDTO.setTitle(articleDO.getTitle()); + articleDTO.setShortTitle(articleDO.getShortTitle()); + articleDTO.setSummary(articleDO.getSummary()); + articleDTO.setCover(articleDO.getPicture()); + articleDTO.setSourceType(SourceTypeEnum.formCode(articleDO.getSource()).getDesc()); + articleDTO.setSourceUrl(articleDO.getSourceUrl()); + articleDTO.setStatus(articleDO.getStatus()); + articleDTO.setCreateTime(articleDO.getCreateTime().getTime()); + articleDTO.setLastUpdateTime(articleDO.getUpdateTime().getTime()); + articleDTO.setOfficalStat(articleDO.getOfficalStat()); + articleDTO.setToppingStat(articleDO.getToppingStat()); + articleDTO.setCreamStat(articleDO.getCreamStat()); + articleDTO.setReadType(articleDO.getReadType()); + articleDTO.setPayAmount(PriceUtil.toYuanPrice(articleDO.getPayAmount())); + articleDTO.setPayWay(articleDO.getPayWay()); + + // 设置类目id + articleDTO.setCategory(new CategoryDTO(articleDO.getCategoryId(), null)); + return articleDTO; + } + + public static List toArticleDtoList(List articleDOS) { + return articleDOS.stream().map(ArticleConverter::toDto).collect(Collectors.toList()); + } + + /** + * do转换 + * + * @param tag + * @return + */ + public static TagDTO toDto(TagDO tag) { + if (tag == null) { + return null; + } + TagDTO dto = new TagDTO(); + dto.setTag(tag.getTagName()); + dto.setTagId(tag.getId()); + dto.setStatus(tag.getStatus()); + return dto; + } + + public static List toDtoList(List tags) { + return tags.stream().map(ArticleConverter::toDto).collect(Collectors.toList()); + } + + + public static CategoryDTO toDto(CategoryDO category) { + CategoryDTO dto = new CategoryDTO(); + dto.setCategory(category.getCategoryName()); + dto.setCategoryId(category.getId()); + dto.setRank(category.getRank()); + dto.setStatus(category.getStatus()); + dto.setSelected(false); + return dto; + } + + public static List toCategoryDtoList(List categorys) { + return categorys.stream().map(ArticleConverter::toDto).collect(Collectors.toList()); + } + + public static TagDO toDO(TagReq tagReq) { + if (tagReq == null) { + return null; + } + TagDO tagDO = new TagDO(); + tagDO.setTagName(tagReq.getTag()); + return tagDO; + } + + public static CategoryDO toDO(CategoryReq categoryReq) { + if (categoryReq == null) { + return null; + } + CategoryDO categoryDO = new CategoryDO(); + categoryDO.setCategoryName(categoryReq.getCategory()); + categoryDO.setRank(categoryReq.getRank()); + return categoryDO; + } + + public static SearchArticleParams toSearchParams(SearchArticleReq req) { + if (req == null) { + return null; + } + SearchArticleParams searchArticleParams = new SearchArticleParams(); + searchArticleParams.setTitle(req.getTitle()); + searchArticleParams.setArticleId(req.getArticleId()); + searchArticleParams.setUserId(req.getUserId()); + searchArticleParams.setStatus(req.getStatus()); + searchArticleParams.setOfficalStat(req.getOfficalStat()); + searchArticleParams.setToppingStat(req.getToppingStat()); + searchArticleParams.setPageNum(req.getPageNumber()); + searchArticleParams.setPageSize(req.getPageSize()); + return searchArticleParams; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/ArticleStructMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/ArticleStructMapper.java new file mode 100644 index 000000000..1baa72832 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/ArticleStructMapper.java @@ -0,0 +1,15 @@ +package com.github.paicoding.forum.service.article.conveter; + +import com.github.paicoding.forum.api.model.vo.article.SearchArticleReq; +import com.github.paicoding.forum.service.article.repository.params.SearchArticleParams; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface ArticleStructMapper { + ArticleStructMapper INSTANCE = Mappers.getMapper( ArticleStructMapper.class ); + + @Mapping(source = "pageNumber", target = "pageNum") + SearchArticleParams toSearchParams(SearchArticleReq req); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/CategoryStructMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/CategoryStructMapper.java new file mode 100644 index 000000000..31b6ec2db --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/CategoryStructMapper.java @@ -0,0 +1,39 @@ +package com.github.paicoding.forum.service.article.conveter; + +import com.github.paicoding.forum.api.model.vo.article.CategoryReq; +import com.github.paicoding.forum.api.model.vo.article.SearchCategoryReq; +import com.github.paicoding.forum.api.model.vo.article.dto.CategoryDTO; +import com.github.paicoding.forum.service.article.repository.entity.CategoryDO; +import com.github.paicoding.forum.service.article.repository.params.SearchCategoryParams; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 5/27/23 + */ +@Mapper +public interface CategoryStructMapper { + // instance + CategoryStructMapper INSTANCE = Mappers.getMapper( CategoryStructMapper.class ); + + // req to params + @Mapping(source = "pageNumber", target = "pageNum") + SearchCategoryParams toSearchParams(SearchCategoryReq req); + + // do to dto + @Mapping(source = "id", target = "categoryId") + @Mapping(source = "categoryName", target = "category") + CategoryDTO toDTO(CategoryDO categoryDO); + + List toDTOs(List list); + + // req to do + @Mapping(source = "category", target = "categoryName") + CategoryDO toDO(CategoryReq categoryReq); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/ColumnArticleStructMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/ColumnArticleStructMapper.java new file mode 100644 index 000000000..22b42d3c3 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/ColumnArticleStructMapper.java @@ -0,0 +1,20 @@ +package com.github.paicoding.forum.service.article.conveter; + +import com.github.paicoding.forum.api.model.vo.article.ColumnArticleReq; +import com.github.paicoding.forum.api.model.vo.article.SearchColumnArticleReq; +import com.github.paicoding.forum.service.article.repository.entity.ColumnArticleDO; +import com.github.paicoding.forum.service.article.repository.params.ColumnArticleParams; +import com.github.paicoding.forum.service.article.repository.params.SearchColumnArticleParams; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface ColumnArticleStructMapper { + ColumnArticleStructMapper INSTANCE = Mappers.getMapper( ColumnArticleStructMapper.class ); + + SearchColumnArticleParams toSearchParams(SearchColumnArticleReq req); + + ColumnArticleParams toParams(ColumnArticleReq req); + + ColumnArticleDO reqToDO(ColumnArticleReq req); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/ColumnConvert.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/ColumnConvert.java new file mode 100644 index 000000000..e28f890c4 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/ColumnConvert.java @@ -0,0 +1,71 @@ +package com.github.paicoding.forum.service.article.conveter; + +import com.github.paicoding.forum.api.model.vo.article.ColumnArticleReq; +import com.github.paicoding.forum.api.model.vo.article.ColumnReq; +import com.github.paicoding.forum.api.model.vo.article.dto.ColumnDTO; +import com.github.paicoding.forum.service.article.repository.entity.ColumnArticleDO; +import com.github.paicoding.forum.service.article.repository.entity.ColumnInfoDO; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * @author YiHui + * @date 2022/9/15 + */ +public class ColumnConvert { + + public static ColumnDTO toDto(ColumnInfoDO info) { + ColumnDTO dto = new ColumnDTO(); + dto.setColumnId(info.getId()); + dto.setColumn(info.getColumnName()); + dto.setCover(info.getCover()); + dto.setIntroduction(info.getIntroduction()); + dto.setState(info.getState()); + dto.setNums(info.getNums()); + dto.setAuthor(info.getUserId()); + dto.setSection(info.getSection()); + dto.setPublishTime(info.getPublishTime().getTime()); + dto.setType(info.getType()); + dto.setFreeStartTime(info.getFreeStartTime().getTime()); + dto.setFreeEndTime(info.getFreeEndTime().getTime()); + return dto; + } + + public static List toDtos(List columnInfoDOS) { + List columnDTOS = new ArrayList<>(); + columnInfoDOS.forEach(info -> columnDTOS.add(ColumnConvert.toDto(info))); + return columnDTOS; + } + + public static ColumnInfoDO toDo(ColumnReq columnReq) { + if (columnReq == null) { + return null; + } + ColumnInfoDO columnInfoDO = new ColumnInfoDO(); + columnInfoDO.setColumnName(columnReq.getColumn()); + columnInfoDO.setUserId(columnReq.getAuthor()); + columnInfoDO.setIntroduction(columnReq.getIntroduction()); + columnInfoDO.setCover(columnReq.getCover()); + columnInfoDO.setState(columnReq.getState()); + columnInfoDO.setSection(columnReq.getSection()); + columnInfoDO.setNums(columnReq.getNums()); + columnInfoDO.setType(columnReq.getType()); + columnInfoDO.setFreeStartTime(new Date(columnReq.getFreeStartTime())); + columnInfoDO.setFreeEndTime(new Date(columnReq.getFreeEndTime())); + return columnInfoDO; + } + + public static ColumnArticleDO toDo(ColumnArticleReq columnArticleReq) { + if (columnArticleReq == null) { + return null; + } + ColumnArticleDO columnArticleDO = new ColumnArticleDO(); + columnArticleDO.setColumnId(columnArticleReq.getColumnId()); + columnArticleDO.setArticleId(columnArticleReq.getArticleId()); + columnArticleDO.setSection(columnArticleReq.getSort()); + return columnArticleDO; + } + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/ColumnStructMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/ColumnStructMapper.java new file mode 100644 index 000000000..c42d193df --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/ColumnStructMapper.java @@ -0,0 +1,62 @@ +package com.github.paicoding.forum.service.article.conveter; + +import com.github.paicoding.forum.api.model.vo.article.ColumnReq; +import com.github.paicoding.forum.api.model.vo.article.SearchColumnReq; +import com.github.paicoding.forum.api.model.vo.article.dto.ColumnDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.SimpleColumnDTO; +import com.github.paicoding.forum.service.article.repository.entity.ColumnInfoDO; +import com.github.paicoding.forum.service.article.repository.params.SearchColumnParams; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface ColumnStructMapper { + ColumnStructMapper INSTANCE = Mappers.getMapper( ColumnStructMapper.class); + + /** + * SearchColumnReq to SearchColumnParams + * @param req + * @return + */ + SearchColumnParams reqToSearchParams(SearchColumnReq req); + + /** + * ColumnInfoDO to ColumnDTO + * @param columnInfoDO + * @return + */ + // sources 是参数,target 是目标 + @Mapping(source = "id", target = "columnId") + @Mapping(source = "columnName", target = "column") + @Mapping(source = "userId", target = "author") + // Date 转 Long + @Mapping(target = "publishTime", expression = "java(columnInfoDO.getPublishTime().getTime())") + @Mapping(target = "freeStartTime", expression = "java(columnInfoDO.getFreeStartTime().getTime())") + @Mapping(target = "freeEndTime", expression = "java(columnInfoDO.getFreeEndTime().getTime())") + ColumnDTO infotoDto(ColumnInfoDO columnInfoDO); + + List infoToDtos(List columnInfoDOs); + + + /** + * ColumnInfoDO to SimpleColumnDTO + * @param columnInfoDO + * @return + */ + // 专栏 ID 、专栏名、封面 + @Mapping(source = "id", target = "columnId") + @Mapping(source = "columnName", target = "column") + SimpleColumnDTO infoToSimpleDto(ColumnInfoDO columnInfoDO); + + List infoToSimpleDtos(List columnInfoDOs); + + @Mapping(source = "column", target = "columnName") + @Mapping(source = "author", target = "userId") + // Long 转 Date + @Mapping(target = "freeStartTime", expression = "java(new java.util.Date(req.getFreeStartTime()))") + @Mapping(target = "freeEndTime", expression = "java(new java.util.Date(req.getFreeEndTime()))") + ColumnInfoDO toDo(ColumnReq req); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/PayConverter.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/PayConverter.java new file mode 100644 index 000000000..6c52041a2 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/PayConverter.java @@ -0,0 +1,87 @@ +package com.github.paicoding.forum.service.article.conveter; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.hui.quick.plugin.base.Base64Util; +import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeGenV3; +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import com.github.paicoding.forum.api.model.vo.article.dto.ArticlePayInfoDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.UserPayCodeDTO; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.core.util.PriceUtil; +import com.github.paicoding.forum.service.article.repository.entity.ArticlePayRecordDO; +import org.apache.commons.lang3.StringUtils; + +import java.awt.image.BufferedImage; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * @author YiHui + * @date 2024/10/29 + */ +public class PayConverter { + + public static ArticlePayInfoDTO toPay(ArticlePayRecordDO record) { + ArticlePayInfoDTO info = new ArticlePayInfoDTO(); + info.setPayId(record.getId()); + info.setPayUserId(record.getPayUserId()); + info.setPayStatus(record.getPayStatus()); + info.setReceiveUserId(record.getReceiveUserId()); + info.setArticleId(record.getArticleId()); + info.setPayAmount(PriceUtil.toYuanPrice(record.getPayAmount())); + ThirdPayWayEnum payWay = ThirdPayWayEnum.ofPay(record.getPayWay()); + if (payWay != null) { + info.setPayWay(payWay.getPay()); + info.setPrePayExpireTime(record.getPrePayExpireTime() == null ? null : record.getPrePayExpireTime().getTime()); + info.setPrePayId(genQrCode(record.getPrePayId())); + } + return info; + } + + + /** + * 格式化收款码 + * + * @return key: 渠道 value: 收款二维码base64格式 + */ + public static Map formatPayCode(String dbCode) { + if (StringUtils.isBlank(dbCode)) { + return Collections.emptyMap(); + } + + JsonNode node = JsonUtil.toNode(dbCode); + Map result = new HashMap<>(); + node.fields().forEachRemaining(kv -> { + String key = kv.getKey(); + String value = kv.getValue().asText(); + result.put(key, genQrCode(value)); + }); + return result; + } + + public static Map formatPayCodeInfo(String dbCode) { + if (StringUtils.isBlank(dbCode)) { + return Collections.emptyMap(); + } + + JsonNode node = JsonUtil.toNode(dbCode); + Map result = new HashMap<>(); + node.fields().forEachRemaining(kv -> { + String key = kv.getKey(); + String value = kv.getValue().asText(); + result.put(key, new UserPayCodeDTO(genQrCode(value), value)); + }); + return result; + } + + + public static String genQrCode(String txt) { + try { + BufferedImage img = QrCodeGenV3.of(txt).setSize(500).asImg(); + return Base64Util.encode(img, "png"); + } catch (Exception e) { + return txt; + } + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/TagStructMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/TagStructMapper.java new file mode 100644 index 000000000..10478c832 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/conveter/TagStructMapper.java @@ -0,0 +1,38 @@ +package com.github.paicoding.forum.service.article.conveter; + +import com.github.paicoding.forum.api.model.vo.article.SearchTagReq; +import com.github.paicoding.forum.api.model.vo.article.TagReq; +import com.github.paicoding.forum.api.model.vo.article.dto.TagDTO; +import com.github.paicoding.forum.service.article.repository.entity.TagDO; +import com.github.paicoding.forum.service.article.repository.params.SearchTagParams; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 5/29/23 + */ +@Mapper +public interface TagStructMapper { + // instance + TagStructMapper INSTANCE = Mappers.getMapper( TagStructMapper.class ); + + // req to params + @Mapping(source = "pageNumber", target = "pageNum") + SearchTagParams toSearchParams(SearchTagReq req); + + // do to dto + @Mapping(source = "id", target = "tagId") + @Mapping(source = "tagName", target = "tag") + TagDTO toDTO(TagDO tagDO); + + List toDTOs(List list); + + @Mapping(source = "tag", target = "tagName") + TagDO toDO(TagReq tagReq); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/ArticleDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/ArticleDao.java new file mode 100644 index 000000000..d7df2bf0c --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/ArticleDao.java @@ -0,0 +1,392 @@ +package com.github.paicoding.forum.service.article.repository.dao; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.api.model.enums.DocumentTypeEnum; +import com.github.paicoding.forum.api.model.enums.OfficalStatEnum; +import com.github.paicoding.forum.api.model.enums.PushStatusEnum; +import com.github.paicoding.forum.api.model.enums.YesOrNoEnum; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.article.dto.ArticleAdminDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.ArticleDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.SimpleArticleDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.YearArticleDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; +import com.github.paicoding.forum.core.permission.UserRole; +import com.github.paicoding.forum.service.article.conveter.ArticleConverter; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDO; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDetailDO; +import com.github.paicoding.forum.service.article.repository.entity.ReadCountDO; +import com.github.paicoding.forum.service.article.repository.mapper.ArticleDetailMapper; +import com.github.paicoding.forum.service.article.repository.mapper.ArticleMapper; +import com.github.paicoding.forum.service.article.repository.mapper.ReadCountMapper; +import com.github.paicoding.forum.service.article.repository.params.SearchArticleParams; +import com.google.common.collect.Maps; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 文章相关DB操作 + *

+ * 多表结构的操作封装,只与DB操作相关 + * + * @author louzai + * @date 2022-07-18 + */ +@Repository +public class ArticleDao extends ServiceImpl { + @Resource + private ArticleDetailMapper articleDetailMapper; + @Resource + private ReadCountMapper readCountMapper; + @Resource + private ArticleMapper articleMapper; + + + /** + * 查询文章详情 + * + * @param articleId + * @return + */ + public ArticleDTO queryArticleDetail(Long articleId) { + // 查询文章记录 + ArticleDO article = baseMapper.selectById(articleId); + if (article == null || Objects.equals(article.getDeleted(), YesOrNoEnum.YES.getCode())) { + return null; + } + + // 查询文章正文 + ArticleDTO dto = ArticleConverter.toDto(article); + if (showReviewContent(article)) { + ArticleDetailDO detail = findLatestDetail(articleId); + dto.setContent(detail.getContent()); + } else { + // 对于审核中的文章,只有作者本人才能看到原文 + dto.setContent("### 文章审核中,请稍后再看"); + } + return dto; + } + + /** + * 判断展示审核中的字样,还是展示原文 + * + * @param article 文章实体 + * @return false 表示需要展示审核中的字样 | true 表示展示原文 + */ + private boolean showReviewContent(ArticleDO article) { + if (article.getStatus() != PushStatusEnum.REVIEW.getCode()) { + return true; + } + + BaseUserInfoDTO user = ReqInfoContext.getReqInfo().getUser(); + if (user == null) { + return false; + } + + // 作者本人和admin超管可以看到审核内容 + return user.getUserId().equals(article.getUserId()) || (user.getRole() != null && user.getRole().equalsIgnoreCase(UserRole.ADMIN.name())); + } + + + // ------------ article content ---------------- + + private ArticleDetailDO findLatestDetail(long articleId) { + // 查询文章内容 + LambdaQueryWrapper contentQuery = Wrappers.lambdaQuery(); + contentQuery.eq(ArticleDetailDO::getDeleted, YesOrNoEnum.NO.getCode()) + .eq(ArticleDetailDO::getArticleId, articleId) + .orderByDesc(ArticleDetailDO::getVersion); + return articleDetailMapper.selectList(contentQuery).get(0); + } + + /** + * 保存文章正文 + * + * @param articleId + * @param content + * @return + */ + public Long saveArticleContent(Long articleId, String content) { + ArticleDetailDO detail = new ArticleDetailDO(); + detail.setArticleId(articleId); + detail.setContent(content); + detail.setVersion(1L); + articleDetailMapper.insert(detail); + return detail.getId(); + } + + /** + * 更正文章正文 + * + * @param articleId + * @param content + * @param update true 表示更新最后一条记录; false 表示新插入一个新的记录 + */ + public void updateArticleContent(Long articleId, String content, boolean update) { + if (update) { + articleDetailMapper.updateContent(articleId, content); + } else { + ArticleDetailDO latest = findLatestDetail(articleId); + latest.setVersion(latest.getVersion() + 1); + latest.setId(null); + latest.setContent(content); + articleDetailMapper.insert(latest); + } + } + + // ------------- 文章列表查询 -------------- + + public List listArticlesByUserId(Long userId, PageParam pageParam) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(ArticleDO::getDeleted, YesOrNoEnum.NO.getCode()) + .eq(ArticleDO::getUserId, userId) + .last(PageParam.getLimitSql(pageParam)) + .orderByDesc(ArticleDO::getId); + if (!Objects.equals(ReqInfoContext.getReqInfo().getUserId(), userId)) { + // 作者本人,可以查看草稿、审核、上线文章;其他用户,只能查看上线的文章 + query.eq(ArticleDO::getStatus, PushStatusEnum.ONLINE.getCode()); + } + return baseMapper.selectList(query); + } + + + public List listArticlesByCategoryId(Long categoryId, PageParam pageParam) { + if (categoryId != null && categoryId <= 0) { + // 分类不存在时,表示查所有 + categoryId = null; + } + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(ArticleDO::getDeleted, YesOrNoEnum.NO.getCode()) + .eq(ArticleDO::getStatus, PushStatusEnum.ONLINE.getCode()); + + // 如果分页中置顶的四条数据,需要加上官方的查询条件 + // 说明是查询官方的文章,非置顶的文章,只限制全部分类 + if (categoryId == null && pageParam.getPageSize() == PageParam.TOP_PAGE_SIZE) { + query.eq(ArticleDO::getOfficalStat, OfficalStatEnum.OFFICAL.getCode()); + } + + Optional.ofNullable(categoryId).ifPresent(cid -> query.eq(ArticleDO::getCategoryId, cid)); + query.last(PageParam.getLimitSql(pageParam)) + .orderByDesc(ArticleDO::getToppingStat, ArticleDO::getCreateTime); + return baseMapper.selectList(query); + } + + public Long countArticleByCategoryId(Long categoryId) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(ArticleDO::getDeleted, YesOrNoEnum.NO.getCode()) + .eq(ArticleDO::getStatus, PushStatusEnum.ONLINE.getCode()) + .eq(ArticleDO::getCategoryId, categoryId); + return baseMapper.selectCount(query); + } + + /** + * 按照分类统计文章的数量 + * + * @return key: categoryId, value: count + */ + public Map countArticleByCategoryId() { + QueryWrapper query = Wrappers.query(); + query.select("category_id, count(*) as cnt") + .eq("deleted", YesOrNoEnum.NO.getCode()) + .eq("status", PushStatusEnum.ONLINE.getCode()).groupBy("category_id"); + List> mapList = baseMapper.selectMaps(query); + Map result = Maps.newHashMapWithExpectedSize(mapList.size()); + for (Map mp : mapList) { + Long cnt = (Long) mp.get("cnt"); + if (cnt != null && cnt > 0) { + result.put((Long) mp.get("category_id"), cnt); + } + } + return result; + } + + public List listArticlesByBySearchKey(String key, PageParam pageParam) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(ArticleDO::getDeleted, YesOrNoEnum.NO.getCode()) + .eq(ArticleDO::getStatus, PushStatusEnum.ONLINE.getCode()) + .and(!StringUtils.isEmpty(key), + v -> v.like(ArticleDO::getTitle, key) + .or() + .like(ArticleDO::getShortTitle, key) + .or() + .like(ArticleDO::getSummary, key)); + query.last(PageParam.getLimitSql(pageParam)) + .orderByDesc(ArticleDO::getId); + return baseMapper.selectList(query); + } + + /** + * 通过关键词,从标题中找出相似的进行推荐,只返回主键 + 标题 + * + * @param key + * @return + */ + public List listSimpleArticlesByBySearchKey(String key) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(ArticleDO::getDeleted, YesOrNoEnum.NO.getCode()) + .eq(ArticleDO::getStatus, PushStatusEnum.ONLINE.getCode()) + .and(!StringUtils.isEmpty(key), + v -> v.like(ArticleDO::getTitle, key) + .or() + .like(ArticleDO::getShortTitle, key) + ); + query.select(ArticleDO::getId, ArticleDO::getTitle, ArticleDO::getShortTitle) + .last("limit 10") + .orderByDesc(ArticleDO::getId); + return baseMapper.selectList(query); + } + + + /** + * 阅读计数 + * + * @param articleId + * @return + */ + public int incrReadCount(Long articleId) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(ReadCountDO::getDocumentId, articleId).eq(ReadCountDO::getDocumentType, DocumentTypeEnum.ARTICLE.getCode()); + ReadCountDO record = readCountMapper.selectOne(query); + if (record == null) { + record = new ReadCountDO().setDocumentId(articleId).setDocumentType(DocumentTypeEnum.ARTICLE.getCode()).setCnt(1); + readCountMapper.insert(record); + } else { + // fixme: 这里存在并发覆盖问题,推荐使用 update read_count set cnt = cnt + 1 where id = xxx + record.setCnt(record.getCnt() + 1); + readCountMapper.updateById(record); + } + return record.getCnt(); + } + + /** + * 统计用户的文章计数 + * + * @param userId + * @return + */ + public int countArticleByUser(Long userId) { + return lambdaQuery().eq(ArticleDO::getUserId, userId) + .eq(ArticleDO::getStatus, PushStatusEnum.ONLINE.getCode()) + .eq(ArticleDO::getDeleted, YesOrNoEnum.NO.getCode()) + .count().intValue(); + } + + + /** + * 热门文章推荐,适用于首页的侧边栏 + * + * @param pageParam + * @return + */ + public List listHotArticles(PageParam pageParam) { + return baseMapper.listArticlesByReadCounts(pageParam); + } + + /** + * 作者的热门文章推荐,适用于作者的详情页侧边栏 + * + * @param userId + * @param pageParam + * @return + */ + public List listAuthorHotArticles(long userId, PageParam pageParam) { + return baseMapper.listArticlesByUserIdOrderByReadCounts(userId, pageParam); + } + + /** + * 根据相同的类目 + 标签进行推荐 + * + * @param categoryId + * @param tagIds + * @return + */ + public List listRelatedArticlesOrderByReadCount(Long categoryId, List tagIds, PageParam pageParam) { + List list = baseMapper.listArticleByCategoryAndTags(categoryId, tagIds, pageParam); + if (CollectionUtils.isEmpty(list)) { + return new ArrayList<>(); + } + + List ids = list.stream().map(ReadCountDO::getDocumentId).collect(Collectors.toList()); + List result = baseMapper.selectBatchIds(ids); + result.sort((o1, o2) -> { + int i1 = ids.indexOf(o1.getId()); + int i2 = ids.indexOf(o2.getId()); + return Integer.compare(i1, i2); + }); + return result; + } + + + /** + * 根据用户ID获取创作历程 + * + * @param userId + * @return + */ + public List listYearArticleByUserId(Long userId) { + return baseMapper.listYearArticleByUserId(userId); + } + + /** + * 抽取样板代码 + */ + private LambdaQueryChainWrapper buildQuery(SearchArticleParams searchArticleParams) { + return lambdaQuery() + .like(StringUtils.isNotBlank(searchArticleParams.getTitle()), ArticleDO::getTitle, searchArticleParams.getTitle()) + // ID 不为空 + .eq(Objects.nonNull(searchArticleParams.getArticleId()), ArticleDO::getId, searchArticleParams.getArticleId()) + .eq(Objects.nonNull(searchArticleParams.getUserId()), ArticleDO::getUserId, searchArticleParams.getUserId()) + .eq(Objects.nonNull(searchArticleParams.getStatus()) && searchArticleParams.getStatus() != -1, ArticleDO::getStatus, searchArticleParams.getStatus()) + .eq(Objects.nonNull(searchArticleParams.getOfficalStat()) && searchArticleParams.getOfficalStat() != -1, ArticleDO::getOfficalStat, searchArticleParams.getOfficalStat()) + .eq(Objects.nonNull(searchArticleParams.getToppingStat()) && searchArticleParams.getToppingStat() != -1, ArticleDO::getToppingStat, searchArticleParams.getToppingStat()) + .eq(ArticleDO::getDeleted, YesOrNoEnum.NO.getCode()); + } + + + /** + * 文章列表(用于后台) + */ + public List listArticlesByParams(SearchArticleParams params) { + return articleMapper.listArticlesByParams(params, + PageParam.newPageInstance(params.getPageNum(), params.getPageSize())); + } + + /** + * 文章总数(用于后台) + */ + public Long countArticleByParams(SearchArticleParams searchArticleParams) { + return articleMapper.countArticlesByParams(searchArticleParams); + } + + /** + * 文章总数(用于后台) + * + * @return + */ + public Long countArticle() { + return lambdaQuery() + .eq(ArticleDO::getDeleted, YesOrNoEnum.NO.getCode()) + .count(); + } + + public List selectByIds(List ids) { + + List articleDOS = baseMapper.selectBatchIds(ids); + return articleDOS; + + } +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/ArticlePayDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/ArticlePayDao.java new file mode 100644 index 000000000..50a06ac30 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/ArticlePayDao.java @@ -0,0 +1,74 @@ +package com.github.paicoding.forum.service.article.repository.dao; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.paicoding.forum.api.model.enums.pay.PayStatusEnum; +import com.github.paicoding.forum.service.article.repository.entity.ArticlePayRecordDO; +import com.github.paicoding.forum.service.article.repository.mapper.ArticleMapper; +import com.github.paicoding.forum.service.article.repository.mapper.ArticlePayRecordMapper; +import org.springframework.stereotype.Repository; +import org.springframework.util.CollectionUtils; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 文章支付记录 + *

+ * + * @author YiHui + * @date 2024-10-29 + */ +@Repository +public class ArticlePayDao extends ServiceImpl { + + @Resource + private ArticleMapper articleMapper; + + /** + * 用户的文章支付记录 + * + * @param articleId 文章id + * @param payUserId 支付用户id + * @return 支付记录 + */ + public ArticlePayRecordDO queryRecordByArticleId(Long articleId, Long payUserId) { + List list = lambdaQuery() + .eq(ArticlePayRecordDO::getArticleId, articleId) + .eq(ArticlePayRecordDO::getPayUserId, payUserId).list(); + if (CollectionUtils.isEmpty(list)) { + return null; + } + return list.get(0); + } + + + /** + * 查询文章成功支付的用户id + * + * @param articleId 文章id + * @return + */ + public List querySucceedPayUsersByArticleId(Long articleId) { + List records = lambdaQuery().select(ArticlePayRecordDO::getPayUserId) + .eq(ArticlePayRecordDO::getArticleId, articleId) + .eq(ArticlePayRecordDO::getPayStatus, PayStatusEnum.SUCCEED.getStatus()) + .list(); + return records.stream().map(ArticlePayRecordDO::getPayUserId).collect(Collectors.toList()); + } + + + /** + * 加写锁 + * + * @param id + * @return + */ + public ArticlePayRecordDO selectForUpdate(Long id) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("id", id); + queryWrapper.last("for update"); + return baseMapper.selectOne(queryWrapper); + } +} \ No newline at end of file diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/dao/ArticleTagDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/ArticleTagDao.java similarity index 86% rename from forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/dao/ArticleTagDao.java rename to paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/ArticleTagDao.java index 43321d373..f5e280af2 100644 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/dao/ArticleTagDao.java +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/ArticleTagDao.java @@ -1,10 +1,10 @@ -package com.github.liuyueyi.forum.service.article.repository.dao; +package com.github.paicoding.forum.service.article.repository.dao; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.github.liueyueyi.forum.api.model.enums.YesOrNoEnum; -import com.github.liueyueyi.forum.api.model.vo.article.dto.TagDTO; -import com.github.liuyueyi.forum.service.article.repository.entity.ArticleTagDO; -import com.github.liuyueyi.forum.service.article.repository.mapper.ArticleTagMapper; +import com.github.paicoding.forum.api.model.enums.YesOrNoEnum; +import com.github.paicoding.forum.api.model.vo.article.dto.TagDTO; +import com.github.paicoding.forum.service.article.repository.entity.ArticleTagDO; +import com.github.paicoding.forum.service.article.repository.mapper.ArticleTagMapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/CategoryDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/CategoryDao.java new file mode 100644 index 000000000..69ee1f54f --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/CategoryDao.java @@ -0,0 +1,68 @@ +package com.github.paicoding.forum.service.article.repository.dao; + +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.paicoding.forum.api.model.enums.PushStatusEnum; +import com.github.paicoding.forum.api.model.enums.YesOrNoEnum; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.article.dto.CategoryDTO; +import com.github.paicoding.forum.service.article.conveter.CategoryStructMapper; +import com.github.paicoding.forum.service.article.repository.entity.CategoryDO; +import com.github.paicoding.forum.service.article.repository.mapper.CategoryMapper; +import com.github.paicoding.forum.service.article.repository.params.SearchCategoryParams; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 类目Service + * + * @author louzai + * @date 2022-07-20 + */ +@Repository +public class CategoryDao extends ServiceImpl { + /** + * @return + */ + public List listAllCategoriesFromDb() { + return lambdaQuery() + .eq(CategoryDO::getDeleted, YesOrNoEnum.NO.getCode()) + .eq(CategoryDO::getStatus, PushStatusEnum.ONLINE.getCode()) + .list(); + } + + // 抽一个私有方法,构造查询条件 + private LambdaQueryChainWrapper createCategoryQuery(SearchCategoryParams params) { + return lambdaQuery() + .eq(CategoryDO::getDeleted, YesOrNoEnum.NO.getCode()) + .like(StringUtils.isNotBlank(params.getCategory()), CategoryDO::getCategoryName, params.getCategory()); + } + + /** + * 获取所有 Categorys 列表(分页) + * + * @return + */ + public List listCategory(SearchCategoryParams params) { + List list = createCategoryQuery(params) + .orderByDesc(CategoryDO::getUpdateTime) + .orderByAsc(CategoryDO::getRank) + .last(PageParam.getLimitSql( + PageParam.newPageInstance(params.getPageNum(), params.getPageSize()) + )) + .list(); + return CategoryStructMapper.INSTANCE.toDTOs(list); + } + + /** + * 获取所有 Categorys 总数(分页) + * + * @return + */ + public Long countCategory(SearchCategoryParams params) { + return createCategoryQuery(params) + .count(); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/ColumnArticleDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/ColumnArticleDao.java new file mode 100644 index 000000000..c4cc7e506 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/ColumnArticleDao.java @@ -0,0 +1,56 @@ +package com.github.paicoding.forum.service.article.repository.dao; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.paicoding.forum.service.article.repository.entity.ColumnArticleDO; +import com.github.paicoding.forum.service.article.repository.mapper.ColumnArticleMapper; +import org.springframework.stereotype.Repository; +import org.springframework.util.CollectionUtils; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 5/30/23 + */ +@Repository +public class ColumnArticleDao extends ServiceImpl { + @Resource + private ColumnArticleMapper columnArticleMapper; + + /** + * 返回专栏最大更新章节数 + * + * @param columnId + * @return 专栏内无文章时,返回0;否则返回当前最大的章节数 + */ + public int selectMaxSection(Long columnId) { + return columnArticleMapper.selectMaxSection(columnId); + } + + /** + * 根据文章id,查询再所属的专栏信息 + * fixme: 如果一篇文章,在多个专栏内,就会有问题 + * + * @param articleId + * @return + */ + public ColumnArticleDO selectColumnArticleByArticleId(Long articleId) { + List list = lambdaQuery() + .eq(ColumnArticleDO::getArticleId, articleId) + .list(); + if (CollectionUtils.isEmpty(list)) { + return null; + } + return list.get(0); + } + + public ColumnArticleDO selectBySection(Long columnId, Integer sort) { + return lambdaQuery() + .eq(ColumnArticleDO::getColumnId, columnId) + .eq(ColumnArticleDO::getSection, sort) + .one(); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/ColumnDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/ColumnDao.java new file mode 100644 index 000000000..ba57d1c63 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/ColumnDao.java @@ -0,0 +1,145 @@ +package com.github.paicoding.forum.service.article.repository.dao; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.paicoding.forum.api.model.enums.column.ColumnStatusEnum; +import com.github.paicoding.forum.api.model.exception.ExceptionUtil; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.article.dto.ColumnArticleDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.SimpleArticleDTO; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import com.github.paicoding.forum.service.article.repository.entity.ColumnArticleDO; +import com.github.paicoding.forum.service.article.repository.entity.ColumnInfoDO; +import com.github.paicoding.forum.service.article.repository.mapper.ColumnArticleMapper; +import com.github.paicoding.forum.service.article.repository.mapper.ColumnInfoMapper; +import com.github.paicoding.forum.service.article.repository.params.SearchColumnArticleParams; +import com.github.paicoding.forum.service.article.repository.params.SearchColumnParams; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author YiHui + * @date 2022/9/14 + */ +@Repository +public class ColumnDao extends ServiceImpl { + + @Autowired + private ColumnArticleMapper columnArticleMapper; + + /** + * 分页查询专辑列表 + * + * @param pageParam + * @return + */ + public List listOnlineColumns(PageParam pageParam) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.gt(ColumnInfoDO::getState, ColumnStatusEnum.OFFLINE.getCode()) + .last(PageParam.getLimitSql(pageParam)) + .orderByAsc(ColumnInfoDO::getSection); + return baseMapper.selectList(query); + } + + /** + * 统计专栏的文章数 + * + * @return + */ + public int countColumnArticles(Long columnId) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(ColumnArticleDO::getColumnId, columnId); + return columnArticleMapper.selectCount(query).intValue(); + } + + public Long countColumnArticles() { + return columnArticleMapper.selectCount(Wrappers.emptyWrapper()); + } + + /** + * 统计专栏的阅读人数 + * @return + */ + public int countColumnReadPeoples(Long columnId) { + return columnArticleMapper.countColumnReadUserNums(columnId).intValue(); + } + + /** + * 根据教程ID查询文章信息列表 + * @return + */ + public List listColumnArticlesDetail(SearchColumnArticleParams params, + PageParam pageParam) { + return columnArticleMapper.listColumnArticlesByColumnIdArticleName(params.getColumnId(), + params.getArticleTitle(), + pageParam); + } + + public Integer countColumnArticles(SearchColumnArticleParams params) { + return columnArticleMapper.countColumnArticlesByColumnIdArticleName(params.getColumnId(), + params.getArticleTitle()).intValue(); + } + + /** + * 根据教程ID查询文章ID列表 + * + * @param columnId + * @return + */ + public List listColumnArticles(Long columnId) { + return columnArticleMapper.listColumnArticles(columnId); + } + + public ColumnArticleDO getColumnArticleId(long columnId, Integer section) { + return columnArticleMapper.getColumnArticle(columnId, section); + } + + /** + * 删除专栏 + * + * fixme 改为逻辑删除 + * + * @param columnId + */ + public void deleteColumn(Long columnId) { + ColumnInfoDO columnInfoDO = baseMapper.selectById(columnId); + if (columnInfoDO != null) { + // 如果专栏对应的文章不为空,则不允许删除 + // 统计专栏的文章数 + int count = countColumnArticles(columnId); + if (count > 0) { + throw ExceptionUtil.of(StatusEnum.COLUMN_ARTICLE_EXISTS,"请先删除教程"); + } + + // 删除专栏 + baseMapper.deleteById(columnId); + } + } + + /** + * 查询教程 + */ + public List listColumnsByParams(SearchColumnParams params, PageParam pageParam) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + // 加上判空条件 + query.like(StringUtils.isNotBlank(params.getColumn()), ColumnInfoDO::getColumnName, params.getColumn()); + query.last(PageParam.getLimitSql(pageParam)) + .orderByAsc(ColumnInfoDO::getSection) + .orderByDesc(ColumnInfoDO::getUpdateTime); + return baseMapper.selectList(query); + + } + + /** + * 查询教程总数 + */ + public Integer countColumnsByParams(SearchColumnParams params) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + lambdaQuery().like(StringUtils.isNotBlank(params.getColumn()), ColumnInfoDO::getColumnName, params.getColumn()); + return baseMapper.selectCount(query).intValue(); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/TagDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/TagDao.java new file mode 100644 index 000000000..ef055889f --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/dao/TagDao.java @@ -0,0 +1,118 @@ +package com.github.paicoding.forum.service.article.repository.dao; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.paicoding.forum.api.model.enums.PushStatusEnum; +import com.github.paicoding.forum.api.model.enums.YesOrNoEnum; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.article.dto.TagDTO; +import com.github.paicoding.forum.service.article.conveter.ArticleConverter; +import com.github.paicoding.forum.service.article.repository.entity.TagDO; +import com.github.paicoding.forum.service.article.repository.mapper.TagMapper; +import com.github.paicoding.forum.service.article.repository.params.SearchTagParams; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author YiHui + * @date 2022/9/2 + */ +@Repository +public class TagDao extends ServiceImpl { + + /** + * 获取已上线 Tags 列表(分页) + * + * @return + */ + public List listOnlineTag(String key, PageParam pageParam) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(TagDO::getStatus, PushStatusEnum.ONLINE.getCode()) + .eq(TagDO::getDeleted, YesOrNoEnum.NO.getCode()) + .and(StringUtils.isNotBlank(key), v -> v.like(TagDO::getTagName, key)) + .orderByDesc(TagDO::getId); + if (pageParam != null) { + query.last(PageParam.getLimitSql(pageParam)); + } + List list = baseMapper.selectList(query); + return ArticleConverter.toDtoList(list); + } + + /** + * 获取已上线 Tags 总数(分页) + * + * @return + */ + public Integer countOnlineTag(String key) { + return lambdaQuery() + .eq(TagDO::getStatus, PushStatusEnum.ONLINE.getCode()) + .eq(TagDO::getDeleted, YesOrNoEnum.NO.getCode()) + .and(!StringUtils.isEmpty(key), v -> v.like(TagDO::getTagName, key)) + .count() + .intValue(); + } + + private LambdaQueryChainWrapper createTagQuery(SearchTagParams params) { + return lambdaQuery() + .eq(TagDO::getDeleted, YesOrNoEnum.NO.getCode()) + .apply(StringUtils.isNotBlank(params.getTag()), + "LOWER(tag_name) LIKE {0}", + "%" + params.getTag().toLowerCase() + "%"); + } + + /** + * 获取所有 Tags 列表(分页) + * + * @return + */ + public List listTag(SearchTagParams params) { + List list = createTagQuery(params) + .orderByDesc(TagDO::getUpdateTime) + .last(PageParam.getLimitSql( + PageParam.newPageInstance(params.getPageNum(), params.getPageSize()) + )) + .list(); + return list; + } + + + + /** + * 获取所有 Tags 总数(分页) + * + * @return + */ + public Long countTag(SearchTagParams params) { + return createTagQuery(params) + .count(); + } + + /** + * 查询tagId + * + * @param tag + * @return + */ + public Long selectTagIdByTag(String tag) { + TagDO record = lambdaQuery().select(TagDO::getId) + .eq(TagDO::getDeleted, YesOrNoEnum.NO.getCode()) + .eq(TagDO::getTagName, tag) + .last("limit 1") + .one(); + return record != null ? record.getId() : null; + } + + /** + * 查询tag + * @param tagId + * @return + */ + public TagDTO selectById(Long tagId) { + TagDO tagDO = lambdaQuery().eq(TagDO::getId, tagId).one(); + return ArticleConverter.toDto(tagDO); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ArticleDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ArticleDO.java new file mode 100644 index 000000000..f636b31b3 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ArticleDO.java @@ -0,0 +1,109 @@ +package com.github.paicoding.forum.service.article.repository.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import com.github.paicoding.forum.api.model.enums.ArticleReadTypeEnum; +import com.github.paicoding.forum.api.model.enums.PushStatusEnum; +import com.github.paicoding.forum.api.model.enums.SourceTypeEnum; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 文章表 + * + * @author louzai + * @date 2022-07-18 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("article") +public class ArticleDO extends BaseDO { + private static final long serialVersionUID = 1L; + + /** + * 作者 + */ + private Long userId; + + /** + * 文章类型:1-博文,2-问答, 3-专栏文章 + */ + private Integer articleType; + + /** + * 文章标题 + */ + private String title; + + /** + * 短标题 + */ + private String shortTitle; + + /** + * 文章头图 + */ + private String picture; + + /** + * 文章摘要 + */ + private String summary; + + /** + * 类目ID + */ + private Long categoryId; + + /** + * 来源:1-转载,2-原创,3-翻译 + * + * @see SourceTypeEnum + */ + private Integer source; + + /** + * 原文地址 + */ + private String sourceUrl; + + /** + * 状态:0-未发布,1-已发布 + * + * @see PushStatusEnum + */ + private Integer status; + + /** + * 是否官方 + */ + private Integer officalStat; + + /** + * 是否置顶 + */ + private Integer toppingStat; + + /** + * 是否加精 + */ + private Integer creamStat; + + private Integer deleted; + + /** + * 阅读类型 + * @see ArticleReadTypeEnum#getType() + */ + private Integer readType; + + /** + * 支付解锁金额 + */ + private Integer payAmount; + + /** + * 支付方式 + */ + private String payWay; +} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/ArticleDetailDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ArticleDetailDO.java similarity index 82% rename from forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/ArticleDetailDO.java rename to paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ArticleDetailDO.java index 78541d82e..83c4c5fff 100644 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/ArticleDetailDO.java +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ArticleDetailDO.java @@ -1,7 +1,7 @@ -package com.github.liuyueyi.forum.service.article.repository.entity; +package com.github.paicoding.forum.service.article.repository.entity; import com.baomidou.mybatisplus.annotation.TableName; -import com.github.liueyueyi.forum.api.model.entity.BaseDO; +import com.github.paicoding.forum.api.model.entity.BaseDO; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ArticlePayRecordDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ArticlePayRecordDO.java new file mode 100644 index 000000000..293a37826 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ArticlePayRecordDO.java @@ -0,0 +1,101 @@ + +package com.github.paicoding.forum.service.article.repository.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * 文章支付记录 + * + * @author YiHui + * @date 2024-10-29 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("article_pay_record") +public class ArticlePayRecordDO extends BaseDO { + private static final long serialVersionUID = 1L; + + /** + * 支付用户 + */ + private Long payUserId; + + /** + * 收款用户 + */ + private Long receiveUserId; + + /** + * 文章 + */ + private Long articleId; + + /** + * 支付状态 + */ + private Integer payStatus; + + /** + * 邮件通知用户的时间 + */ + private Date notifyTime; + + /** + * 通知确认次数 + */ + private Integer notifyCnt; + + /** + * 备注信息 + */ + private String notes; + + /** + * - 个人收款码场景: 用于验证合法性的code + * - 微信支付场景: 这里是传递给第三方系统的唯一外部订单号 + */ + private String verifyCode; + + /** + * 支付金额 + * 说明:对人个人收款码场景,无法知道具体的收款金额 + */ + private Integer payAmount; + + /** + * 微信支付回传的关键参数 + * h5支付: 返回的是支付中间页地址 + * jspai支付:返回的是唤起支付的prePayId + * native支付:返回的是用于生成微信支付二维码的字符串 + */ + private String prePayId; + + /** + * prePayId的有效截止时间 + */ + private Date prePayExpireTime; + + /** + * 支付方式 + * + * @see ThirdPayWayEnum#getPay() + */ + private String payWay; + + /** + * 记录的是三方交易单号 + */ + private String thirdTransCode; + + /** + * 回调的支付成功/失败时间 + */ + private Date payCallbackTime; + +} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/ArticleTagDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ArticleTagDO.java similarity index 79% rename from forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/ArticleTagDO.java rename to paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ArticleTagDO.java index 28470376f..c949cebc2 100644 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/ArticleTagDO.java +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ArticleTagDO.java @@ -1,7 +1,7 @@ -package com.github.liuyueyi.forum.service.article.repository.entity; +package com.github.paicoding.forum.service.article.repository.entity; import com.baomidou.mybatisplus.annotation.TableName; -import com.github.liueyueyi.forum.api.model.entity.BaseDO; +import com.github.paicoding.forum.api.model.entity.BaseDO; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/CategoryDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/CategoryDO.java new file mode 100644 index 000000000..2b7638b09 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/CategoryDO.java @@ -0,0 +1,39 @@ +package com.github.paicoding.forum.service.article.repository.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 类目管理表 + * + * @author louzai + * @date 2022-07-18 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("category") +public class CategoryDO extends BaseDO { + + private static final long serialVersionUID = 1L; + + /** + * 类目名称 + */ + private String categoryName; + + /** + * 状态:0-未发布,1-已发布 + */ + private Integer status; + + /** + * 排序 + */ + @TableField("`rank`") + private Integer rank; + + private Integer deleted; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ColumnArticleDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ColumnArticleDO.java new file mode 100644 index 000000000..f3a4bde08 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ColumnArticleDO.java @@ -0,0 +1,36 @@ +package com.github.paicoding.forum.service.article.repository.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import com.github.paicoding.forum.api.model.enums.column.ColumnArticleReadEnum; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 专栏文章 + * + * @author YiHui + * @date 2022/9/14 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("column_article") +public class ColumnArticleDO extends BaseDO { + private static final long serialVersionUID = -2372103913090667453L; + + private Long columnId; + + private Long articleId; + + /** + * 顺序,越小越靠前 + */ + private Integer section; + + /** + * 专栏类型:免费、登录阅读、收费阅读等 + * + * @see ColumnArticleReadEnum#getRead() + */ + private Integer readType; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ColumnInfoDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ColumnInfoDO.java new file mode 100644 index 000000000..2ae383bf9 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ColumnInfoDO.java @@ -0,0 +1,79 @@ +package com.github.paicoding.forum.service.article.repository.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import com.github.paicoding.forum.api.model.enums.column.ColumnStatusEnum; +import com.github.paicoding.forum.api.model.enums.column.ColumnTypeEnum; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * @author YiHui + * @date 2022/9/14 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("column_info") +public class ColumnInfoDO extends BaseDO { + + private static final long serialVersionUID = 1920830534262012026L; + /** + * 专栏名 + */ + private String columnName; + + /** + * 作者 + */ + private Long userId; + + /** + * 简介 + */ + private String introduction; + + /** + * 封面 + */ + private String cover; + + /** + * 状态 + * + * @see ColumnStatusEnum#getCode() + */ + private Integer state; + + /** + * 排序 + */ + private Integer section; + + /** + * 上线时间 + */ + private Date publishTime; + + /** + * 专栏预计的文章数 + */ + private Integer nums; + + /** + * 专栏类型:免费、登录阅读、收费阅读等 + * @see ColumnTypeEnum#getType() + */ + private Integer type; + + /** + * 免费开始时间 + */ + private Date freeStartTime; + + /** + * 免费结束时间 + */ + private Date freeEndTime; +} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/ReadCountDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ReadCountDO.java similarity index 82% rename from forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/ReadCountDO.java rename to paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ReadCountDO.java index 6b47b1f79..97b1984e5 100644 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/ReadCountDO.java +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/ReadCountDO.java @@ -1,7 +1,7 @@ -package com.github.liuyueyi.forum.service.article.repository.entity; +package com.github.paicoding.forum.service.article.repository.entity; import com.baomidou.mybatisplus.annotation.TableName; -import com.github.liueyueyi.forum.api.model.entity.BaseDO; +import com.github.paicoding.forum.api.model.entity.BaseDO; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/TagDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/TagDO.java similarity index 78% rename from forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/TagDO.java rename to paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/TagDO.java index 46739a949..384bc62d7 100644 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/entity/TagDO.java +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/entity/TagDO.java @@ -1,7 +1,7 @@ -package com.github.liuyueyi.forum.service.article.repository.entity; +package com.github.paicoding.forum.service.article.repository.entity; import com.baomidou.mybatisplus.annotation.TableName; -import com.github.liueyueyi.forum.api.model.entity.BaseDO; +import com.github.paicoding.forum.api.model.entity.BaseDO; import lombok.Data; import lombok.EqualsAndHashCode; @@ -27,15 +27,13 @@ public class TagDO extends BaseDO { */ private Integer tagType; - /** - * 类目ID - */ - private Long categoryId; - /** * 状态:0-未发布,1-已发布 */ private Integer status; + /** + * 是否删除 + */ private Integer deleted; } diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/ArticleDetailMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ArticleDetailMapper.java similarity index 84% rename from forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/ArticleDetailMapper.java rename to paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ArticleDetailMapper.java index a0447f841..d23ea533e 100644 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/article/repository/mapper/ArticleDetailMapper.java +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ArticleDetailMapper.java @@ -1,7 +1,7 @@ -package com.github.liuyueyi.forum.service.article.repository.mapper; +package com.github.paicoding.forum.service.article.repository.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.github.liuyueyi.forum.service.article.repository.entity.ArticleDetailDO; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDetailDO; import org.apache.ibatis.annotations.Update; /** @@ -22,5 +22,4 @@ public interface ArticleDetailMapper extends BaseMapper { */ @Update("update article_detail set `content` = #{content}, `version` = `version` + 1 where article_id = #{articleId} and `deleted`=0 order by `version` desc limit 1") int updateContent(long articleId, String content); - } diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ArticleMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ArticleMapper.java new file mode 100644 index 000000000..35dbf0cb8 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ArticleMapper.java @@ -0,0 +1,73 @@ +package com.github.paicoding.forum.service.article.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.article.dto.ArticleAdminDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.SimpleArticleDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.YearArticleDTO; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDO; +import com.github.paicoding.forum.service.article.repository.entity.ReadCountDO; +import com.github.paicoding.forum.service.article.repository.params.SearchArticleParams; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 文章mapper接口 + * + * @author louzai + * @date 2022-07-18 + */ +public interface ArticleMapper extends BaseMapper { + + /** + * 通过id遍历文章, 用于生成sitemap.xml + * + * @param lastId + * @param size + * @return + */ + List listArticlesOrderById(@Param("lastId") Long lastId, @Param("size") int size); + + /** + * 根据阅读次数获取热门文章 + * + * @param pageParam + * @return + */ + List listArticlesByReadCounts(@Param("pageParam") PageParam pageParam); + + /** + * 查询作者的热门文章 + * + * @param userId + * @param pageParam + * @return + */ + List listArticlesByUserIdOrderByReadCounts(@Param("userId") Long userId, @Param("pageParam") PageParam pageParam); + + /** + * 根据类目 + 标签查询文章 + * + * @param category + * @param tagIds + * @param pageParam + * @return + */ + List listArticleByCategoryAndTags(@Param("categoryId") Long category, + @Param("tags") List tagIds, + @Param("pageParam") PageParam pageParam); + + /** + * 根据用户ID获取创作历程 + * + * @param userId + * @return + */ + List listYearArticleByUserId(@Param("userId") Long userId); + + List listArticlesByParams(@Param("searchParams") SearchArticleParams searchArticleParams, + @Param("pageParam") PageParam pageParam); + + Long countArticlesByParams(@Param("searchParams") SearchArticleParams searchArticleParams); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ArticlePayRecordMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ArticlePayRecordMapper.java new file mode 100644 index 000000000..5dfea9453 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ArticlePayRecordMapper.java @@ -0,0 +1,16 @@ +package com.github.paicoding.forum.service.article.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.service.article.repository.entity.ArticlePayRecordDO; + +/** + * 文章详情mapper接口 + * + * @author louzai + * @date 2022-07-18 + */ +public interface ArticlePayRecordMapper extends BaseMapper { + + + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ArticleTagMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ArticleTagMapper.java new file mode 100644 index 000000000..78bf82212 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ArticleTagMapper.java @@ -0,0 +1,28 @@ +package com.github.paicoding.forum.service.article.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.api.model.vo.article.dto.TagDTO; +import com.github.paicoding.forum.service.article.repository.entity.ArticleTagDO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 文章标签映mapper接口 + * + * @author louzai + * @date 2022-07-18 + */ +public interface ArticleTagMapper extends BaseMapper { + + /** + * 查询文章标签 + * + * @param articleId + * @return + */ + List listArticleTagDetails(@Param("articleId") Long articleId); + + + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/CategoryMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/CategoryMapper.java new file mode 100644 index 000000000..81c17d603 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/CategoryMapper.java @@ -0,0 +1,13 @@ +package com.github.paicoding.forum.service.article.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.service.article.repository.entity.CategoryDO; + +/** + * 类目管理mapper接口 + * + * @author louzai + * @date 2022-07-18 + */ +public interface CategoryMapper extends BaseMapper { +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ColumnArticleMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ColumnArticleMapper.java new file mode 100644 index 000000000..98a46322c --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ColumnArticleMapper.java @@ -0,0 +1,65 @@ +package com.github.paicoding.forum.service.article.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.article.dto.ColumnArticleDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.SimpleArticleDTO; +import com.github.paicoding.forum.service.article.repository.entity.ColumnArticleDO; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * @author YiHui + * @date 2022/9/14 + */ +public interface ColumnArticleMapper extends BaseMapper { + /** + * 查询文章列表 + * + * @param columnId + * @return + */ + List listColumnArticles(@Param("columnId") Long columnId); + + /** + * 查询文章 + * + * @param columnId + * @param section + * @return + */ + ColumnArticleDO getColumnArticle(@Param("columnId") Long columnId, @Param("section") Integer section); + + + /** + * 统计专栏的阅读人数 + * + * @param columnId + * @return + */ + Long countColumnReadUserNums(@Param("columnId") Long columnId); + + /** + * 根据教程 ID 文章名称查询文章列表 + * + * @param columnId + * @param articleTitle + * @return + */ + List listColumnArticlesByColumnIdArticleName(@Param("columnId") Long columnId, + @Param("articleTitle") String articleTitle, + @Param("pageParam") PageParam pageParam); + + Long countColumnArticlesByColumnIdArticleName(@Param("columnId") Long columnId, @Param("articleTitle") String articleTitle); + + /** + * 根据教程 ID 查询当前教程中最大的 section + * + * @param columnId + * @return 教程内无文章时,返回0 + */ + @Select("select ifnull(max(section), 0) from column_article where column_id = #{columnId}") + int selectMaxSection(@Param("columnId") Long columnId); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ColumnInfoMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ColumnInfoMapper.java new file mode 100644 index 000000000..f94d6fd36 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ColumnInfoMapper.java @@ -0,0 +1,11 @@ +package com.github.paicoding.forum.service.article.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.service.article.repository.entity.ColumnInfoDO; + +/** + * @author YiHui + * @date 2022/9/14 + */ +public interface ColumnInfoMapper extends BaseMapper { +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ReadCountMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ReadCountMapper.java new file mode 100644 index 000000000..835a1dbfa --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/ReadCountMapper.java @@ -0,0 +1,13 @@ +package com.github.paicoding.forum.service.article.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.service.article.repository.entity.ReadCountDO; + +/** + * 标签mapper接口 + * + * @author louzai + * @date 2022-07-18 + */ +public interface ReadCountMapper extends BaseMapper { +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/TagMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/TagMapper.java new file mode 100644 index 000000000..4c2254f79 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/mapper/TagMapper.java @@ -0,0 +1,13 @@ +package com.github.paicoding.forum.service.article.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.service.article.repository.entity.TagDO; + +/** + * 标签mapper接口 + * + * @author louzai + * @date 2022-07-18 + */ +public interface TagMapper extends BaseMapper { +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/ColumnArticleParams.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/ColumnArticleParams.java new file mode 100644 index 000000000..a652ebe77 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/ColumnArticleParams.java @@ -0,0 +1,19 @@ +package com.github.paicoding.forum.service.article.repository.params; + +import lombok.Data; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 5/30/23 + */ +@Data +public class ColumnArticleParams { + // 教程 ID + private Long columnId; + // 文章 ID + private Long articleId; + // section + private Integer section; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/SearchArticleParams.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/SearchArticleParams.java new file mode 100644 index 000000000..fb4a4c501 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/SearchArticleParams.java @@ -0,0 +1,48 @@ +package com.github.paicoding.forum.service.article.repository.params; + +import com.github.paicoding.forum.api.model.vo.PageParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 文章查询 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class SearchArticleParams extends PageParam { + + /** + * 文章标题 + */ + private String title; + + /** + * 文章ID + */ + private Long articleId; + + /** + * 作者ID + */ + private Long userId; + + /** + * 作者名称 + */ + private String userName; + + /** + * 文章状态: 0-未发布,1-已发布,2-审核 + */ + private Integer status; + + /** + * 是否官方: 0-非官方,1-官方 + */ + private Integer officalStat; + + /** + * 是否置顶: 0-不置顶,1-置顶 + */ + private Integer toppingStat; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/SearchCategoryParams.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/SearchCategoryParams.java new file mode 100644 index 000000000..c3a17ed09 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/SearchCategoryParams.java @@ -0,0 +1,18 @@ +package com.github.paicoding.forum.service.article.repository.params; + +import com.github.paicoding.forum.api.model.vo.PageParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 5/27/23 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class SearchCategoryParams extends PageParam { + // 类目名称 + private String category; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/SearchColumnArticleParams.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/SearchColumnArticleParams.java new file mode 100644 index 000000000..a64339828 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/SearchColumnArticleParams.java @@ -0,0 +1,28 @@ +package com.github.paicoding.forum.service.article.repository.params; + +import com.github.paicoding.forum.api.model.vo.PageParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 专栏查询 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class SearchColumnArticleParams extends PageParam { + + /** + * 专栏名称 + */ + private String column; + + /** + * 专栏id + */ + private Long columnId; + + /** + * 文章标题 + */ + private String articleTitle; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/SearchColumnParams.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/SearchColumnParams.java new file mode 100644 index 000000000..236cac470 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/SearchColumnParams.java @@ -0,0 +1,16 @@ +package com.github.paicoding.forum.service.article.repository.params; + +import com.github.paicoding.forum.api.model.vo.PageParam; +import lombok.Data; + +/** + * 专栏查询 + */ +@Data +public class SearchColumnParams extends PageParam { + + /** + * 专栏名称 + */ + private String column; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/SearchTagParams.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/SearchTagParams.java new file mode 100644 index 000000000..586b7da8f --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/repository/params/SearchTagParams.java @@ -0,0 +1,18 @@ +package com.github.paicoding.forum.service.article.repository.params; + +import com.github.paicoding.forum.api.model.vo.PageParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 5/29/23 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class SearchTagParams extends PageParam { + // 标签名称 + private String tag; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ArticlePayService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ArticlePayService.java new file mode 100644 index 000000000..b328b1345 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ArticlePayService.java @@ -0,0 +1,74 @@ +package com.github.paicoding.forum.service.article.service; + +import com.github.paicoding.forum.api.model.enums.pay.PayStatusEnum; +import com.github.paicoding.forum.api.model.vo.article.dto.ArticlePayInfoDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.PayConfirmDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.SimpleUserInfoDTO; +import com.github.paicoding.forum.service.article.repository.entity.ArticlePayRecordDO; + +import java.util.List; + +/** + * @author YiHui + * @date 2024/10/29 + */ +public interface ArticlePayService { + + /** + * 用户是否已经支付过了 + * + * @param article + * @param currentUerId + * @return + */ + boolean hasPayed(Long article, Long currentUerId); + + /** + * 唤起支付 + * + * @param articleId 文章 + * @param currentUserId 当前用户 + * @param notes 备注 + */ + ArticlePayInfoDTO toPay(Long articleId, Long currentUserId, String notes); + + /** + * 更新为支付中,由用户告诉后端,表明自己已经支付成功了 + * + * @param payId 支付id + * @param currentUserId 当前登录用户 + * @param notes 备注 + * @return true 表示更新成功 + */ + boolean updatePaying(Long payId, Long currentUserId, String notes); + + /** + * 支付状态更新 + * + * @param payId 支付id + * @param verifyCode 验证码 + * @param payStatus 支付状态 + * @param payTime 支付成功时间 + * @param transactionId 三方交易流水号 + * @return + */ + boolean updatePayStatus(Long payId, String verifyCode, PayStatusEnum payStatus, Long payTime, String transactionId); + + /** + * 构建支付结果回调的基础信息 + * + * @param payId 支付id + * @param record 支付记录 + * @return + */ + PayConfirmDTO buildPayConfirmInfo(Long payId, ArticlePayRecordDO record); + + + /** + * 查询文章的打赏用户 + * + * @param articleId 文章id + * @return + */ + List queryPayUsers(Long articleId); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ArticleReadService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ArticleReadService.java new file mode 100644 index 000000000..9d0bb6e48 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ArticleReadService.java @@ -0,0 +1,159 @@ +package com.github.paicoding.forum.service.article.service; + +import com.github.paicoding.forum.api.model.enums.HomeSelectEnum; +import com.github.paicoding.forum.api.model.vo.PageListVo; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.article.dto.ArticleDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.SimpleArticleDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.TagDTO; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDO; + +import java.util.List; +import java.util.Map; + +public interface ArticleReadService { + + /** + * 查询基础的文章信息 + * + * @param articleId + * @return + */ + ArticleDO queryBasicArticle(Long articleId); + + /** + * 提前文章摘要 + * + * @param content + * @return + */ + String generateSummary(String content); + + /** + * 查询文章标签列表 + * + * @param articleId + * @return + */ + PageVo queryTagsByArticleId(Long articleId); + + /** + * 查询文章详情,包括正文内容,分类、标签等信息 + * + * @param articleId + * @return + */ + ArticleDTO queryDetailArticleInfo(Long articleId); + + /** + * 查询文章所有的关联信息,正文,分类,标签,阅读计数+1,当前登录用户是否点赞、评论过 + * + * @param articleId 文章id + * @param currentUser 当前查看的用户ID + * @return + */ + ArticleDTO queryFullArticleInfo(Long articleId, Long currentUser); + + /** + * 查询某个分类下的文章,支持翻页 + * + * @param categoryId + * @param page + * @return + */ + PageListVo queryArticlesByCategory(Long categoryId, PageParam page); + + + /** + * 获取 Top 文章 + * + * @param categoryId + * @return + */ + List queryTopArticlesByCategory(Long categoryId); + + + /** + * 获取分类文章数 + * + * @param categoryId + * @return + */ + Long queryArticleCountByCategory(Long categoryId); + + /** + * 根据分类统计文章计数 + * + * @return + */ + Map queryArticleCountsByCategory(); + + /** + * 查询某个标签下的文章,支持翻页 + * + * @param tagId + * @param page + * @return + */ + PageListVo queryArticlesByTag(Long tagId, PageParam page); + + /** + * 根据关键词匹配标题,查询用于推荐的文章列表,只返回 articleId + title + * + * @param key + * @return + */ + List querySimpleArticleBySearchKey(String key); + + /** + * 根据查询条件查询文章列表,支持翻页 + * + * @param key + * @param page + * @return + */ + PageListVo queryArticlesBySearchKey(String key, PageParam page); + + /** + * 查询用户的文章列表 + * + * @param userId + * @param pageParam + * @param select + * @return + */ + PageListVo queryArticlesByUserAndType(Long userId, PageParam pageParam, HomeSelectEnum select); + + /** + * 文章实体补齐统计、作者、分类标签等信息 + * + * @param records + * @param pageSize + * @return + */ + PageListVo buildArticleListVo(List records, long pageSize); + + /** + * 查询热门文章 + * + * @param pageParam + * @return + */ + PageListVo queryHotArticlesForRecommend(PageParam pageParam); + + /** + * 查询作者的文章数 + * + * @param authorId + * @return + */ + int queryArticleCount(long authorId); + + /** + * 返回总的文章计数 + * + * @return + */ + Long getArticleCount(); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ArticleRecommendService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ArticleRecommendService.java new file mode 100644 index 000000000..abdca3656 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ArticleRecommendService.java @@ -0,0 +1,20 @@ +package com.github.paicoding.forum.service.article.service; + +import com.github.paicoding.forum.api.model.vo.PageListVo; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.article.dto.ArticleDTO; + +/** + * @author YiHui + * @date 2022/9/26 + */ +public interface ArticleRecommendService { + /** + * 文章关联推荐 + * + * @param article + * @param pageParam + * @return + */ + PageListVo relatedRecommend(Long article, PageParam pageParam); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ArticleSettingService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ArticleSettingService.java new file mode 100644 index 000000000..7296fd209 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ArticleSettingService.java @@ -0,0 +1,46 @@ +package com.github.paicoding.forum.service.article.service; + +import com.github.paicoding.forum.api.model.enums.OperateArticleEnum; +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.article.ArticlePostReq; +import com.github.paicoding.forum.api.model.vo.article.SearchArticleReq; +import com.github.paicoding.forum.api.model.vo.article.dto.ArticleAdminDTO; + +/** + * 文章后台接口 + * + * @author louzai + * @date 2022-09-19 + */ +public interface ArticleSettingService { + + /** + * 更新文章 + * + * @param req + */ + void updateArticle(ArticlePostReq req); + + /** + * 获取文章列表 + * + * @param req + * @return + */ + PageVo getArticleList(SearchArticleReq req); + + /** + * 删除文章 + * + * @param articleId + */ + void deleteArticle(Long articleId); + + /** + * 操作文章 + * + * @param articleId + * @param operate + */ + void operateArticle(Long articleId, OperateArticleEnum operate); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ArticleWriteService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ArticleWriteService.java new file mode 100644 index 000000000..7252cb2a6 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ArticleWriteService.java @@ -0,0 +1,23 @@ +package com.github.paicoding.forum.service.article.service; + +import com.github.paicoding.forum.api.model.vo.article.ArticlePostReq; + +public interface ArticleWriteService { + + /** + * 保存or更新文章 + * + * @param req 上传的文章体 + * @param author 作者 + * @return 返回文章主键 + */ + Long saveArticle(ArticlePostReq req, Long author); + + /** + * 删除文章 + * + * @param articleId 文章id + * @param loginUserId 执行操作的用户 + */ + void deleteArticle(Long articleId, Long loginUserId); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/CategoryService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/CategoryService.java new file mode 100644 index 000000000..8ad9a46a7 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/CategoryService.java @@ -0,0 +1,43 @@ +package com.github.paicoding.forum.service.article.service; + +import com.github.paicoding.forum.api.model.vo.article.dto.CategoryDTO; + +import java.util.List; + +/** + * 标签Service + * + * @author louzai + * @date 2022-07-20 + */ +public interface CategoryService { + /** + * 查询类目名 + * + * @param categoryId + * @return + */ + String queryCategoryName(Long categoryId); + + + /** + * 查询所有的分离 + * + * @return + */ + List loadAllCategories(); + + /** + * 查询类目id + * + * @param category + * @return + */ + Long queryCategoryId(String category); + + + /** + * 刷新缓存 + */ + public void refreshCache(); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/CategorySettingService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/CategorySettingService.java new file mode 100644 index 000000000..828ae6ea8 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/CategorySettingService.java @@ -0,0 +1,29 @@ +package com.github.paicoding.forum.service.article.service; + +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.article.CategoryReq; +import com.github.paicoding.forum.api.model.vo.article.SearchCategoryReq; +import com.github.paicoding.forum.api.model.vo.article.dto.CategoryDTO; + +/** + * 分类后台接口 + * + * @author louzai + * @date 2022-09-17 + */ +public interface CategorySettingService { + + void saveCategory(CategoryReq categoryReq); + + void deleteCategory(Integer categoryId); + + void operateCategory(Integer categoryId, Integer pushStatus); + + /** + * 获取category列表 + * + * @param pageParam + * @return + */ + PageVo getCategoryList(SearchCategoryReq params); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ColumnService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ColumnService.java new file mode 100644 index 000000000..a380509b2 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ColumnService.java @@ -0,0 +1,71 @@ +package com.github.paicoding.forum.service.article.service; + +import com.github.paicoding.forum.api.model.vo.PageListVo; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.article.dto.ColumnDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.SimpleArticleDTO; +import com.github.paicoding.forum.service.article.repository.entity.ColumnArticleDO; + +import java.util.List; + +/** + * @author YiHui + * @date 2022/9/14 + */ +public interface ColumnService { + /** + * 根据文章id,构建对应的专栏详情地址 + * + * @param articleId 文章主键 + * @return 专栏详情页 + */ + ColumnArticleDO getColumnArticleRelation(Long articleId); + + /** + * 专栏列表 + * + * @param pageParam + * @return + */ + PageListVo listColumn(PageParam pageParam); + + /** + * 获取专栏中的第N篇文章 + * + * @param columnId + * @param order + * @return + */ + ColumnArticleDO queryColumnArticle(long columnId, Integer order); + + /** + * 只查询基本的专栏信息,不需要统计、作者等信息 + * + * @param columnId + * @return + */ + ColumnDTO queryBasicColumnInfo(Long columnId); + + /** + * 专栏详情 + * + * @param columnId + * @return + */ + ColumnDTO queryColumnInfo(Long columnId); + + /** + * 专栏 + 文章列表详情 + * + * @param columnId + * @return + */ + List queryColumnArticles(long columnId); + + /** + * 返回教程数量 + * + * @return + */ + Long getTutorialCount(); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ColumnSettingService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ColumnSettingService.java new file mode 100644 index 000000000..2f963026f --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/ColumnSettingService.java @@ -0,0 +1,70 @@ +package com.github.paicoding.forum.service.article.service; + +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.article.*; +import com.github.paicoding.forum.api.model.vo.article.dto.ColumnArticleDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.ColumnDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.SimpleColumnDTO; + +import java.util.List; + +/** + * 专栏后台接口 + * + * @author louzai + * @date 2022-09-19 + */ +public interface ColumnSettingService { + + /** + * 将文章保存到对应的专栏中 + * + * @param articleId + * @param columnId + */ + void saveColumnArticle(Long articleId, Long columnId); + + /** + * 保存专栏 + * + * @param columnReq + */ + void saveColumn(ColumnReq columnReq); + + /** + * 保存专栏文章 + * + * @param req + */ + void saveColumnArticle(ColumnArticleReq req); + + /** + * 删除专栏 + * + * @param columnId + */ + void deleteColumn(Long columnId); + + /** + * 删除专栏文章 + * + * @param id + */ + void deleteColumnArticle(Long id); + + /** + * 通过关键词,从标题中找出相似的进行推荐,只返回主键 + 标题 + * + * @param key + * @return + */ + List listSimpleColumnBySearchKey(String key); + + PageVo getColumnList(SearchColumnReq req); + + PageVo getColumnArticleList(SearchColumnArticleReq req); + + void sortColumnArticleApi(SortColumnArticleReq req); + + void sortColumnArticleByIDApi(SortColumnArticleByIDReq req); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/TagService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/TagService.java new file mode 100644 index 000000000..452112087 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/TagService.java @@ -0,0 +1,18 @@ +package com.github.paicoding.forum.service.article.service; + +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.article.dto.TagDTO; + +/** + * 标签Service + * + * @author louzai + * @date 2022-07-20 + */ +public interface TagService { + + PageVo queryTags(String key, PageParam pageParam); + + Long queryTagId(String tag); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/TagSettingService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/TagSettingService.java new file mode 100644 index 000000000..6d8c3f074 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/TagSettingService.java @@ -0,0 +1,31 @@ +package com.github.paicoding.forum.service.article.service; + +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.article.SearchTagReq; +import com.github.paicoding.forum.api.model.vo.article.TagReq; +import com.github.paicoding.forum.api.model.vo.article.dto.TagDTO; + +/** + * 标签后台接口 + * + * @author louzai + * @date 2022-09-17 + */ +public interface TagSettingService { + + void saveTag(TagReq tagReq); + + void deleteTag(Integer tagId); + + void operateTag(Integer tagId, Integer pushStatus); + + /** + * 获取tag列表 + * + * @param pageParam + * @return + */ + PageVo getTagList(SearchTagReq req); + + TagDTO getTagById(Long tagId); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ArticlePayServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ArticlePayServiceImpl.java new file mode 100644 index 000000000..aa0f83609 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ArticlePayServiceImpl.java @@ -0,0 +1,321 @@ +package com.github.paicoding.forum.service.article.service.impl; + +import com.github.paicoding.forum.api.model.enums.NotifyTypeEnum; +import com.github.paicoding.forum.api.model.enums.pay.PayStatusEnum; +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import com.github.paicoding.forum.api.model.exception.ExceptionUtil; +import com.github.paicoding.forum.api.model.vo.article.dto.ArticlePayInfoDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.PayConfirmDTO; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import com.github.paicoding.forum.api.model.vo.pay.dto.PayInfoDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.SimpleUserInfoDTO; +import com.github.paicoding.forum.core.util.DateUtil; +import com.github.paicoding.forum.core.util.PriceUtil; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.core.util.id.IdUtil; +import com.github.paicoding.forum.service.article.conveter.PayConverter; +import com.github.paicoding.forum.service.article.repository.dao.ArticlePayDao; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDO; +import com.github.paicoding.forum.service.article.repository.entity.ArticlePayRecordDO; +import com.github.paicoding.forum.service.article.service.ArticlePayService; +import com.github.paicoding.forum.service.article.service.ArticleReadService; +import com.github.paicoding.forum.service.notify.help.MsgNotifyHelper; +import com.github.paicoding.forum.service.pay.PayServiceFactory; +import com.github.paicoding.forum.service.user.service.UserService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +/** + * @author YiHui + * @date 2024/10/29 + */ +@Slf4j +@Service +public class ArticlePayServiceImpl implements ArticlePayService { + @Autowired + private ArticleReadService articleReadService; + @Autowired + private UserService userService; + + @Autowired + private ArticlePayDao articlePayDao; + + @Value("${view.site.host:https://paicoding.com}") + private String host; + + @Autowired + private PayServiceFactory payServiceFactory; + + + @Override + public boolean hasPayed(Long article, Long currentUerId) { + ArticlePayRecordDO dbRecord = articlePayDao.queryRecordByArticleId(article, currentUerId); + if (dbRecord == null) { + return false; + } + + return PayStatusEnum.SUCCEED.getStatus().equals(dbRecord.getPayStatus()); + } + + /** + * 唤起支付 + * + * @param articleId 文章 + * @param currentUserId 当前用户 + */ + @Transactional(rollbackFor = Exception.class) + public ArticlePayInfoDTO toPay(Long articleId, Long currentUserId, String notes) { + ArticlePayRecordDO dbRecord = articlePayDao.queryRecordByArticleId(articleId, currentUserId); + boolean recordChanged = false; + if (dbRecord == null) { + // 不存在时,创建一个 + dbRecord = createPayRecord(articleId, currentUserId, notes); + recordChanged = true; + } + + // 加事务写锁,防止并发修改支付记录,出现的支付状态不一致的问题 + dbRecord = articlePayDao.selectForUpdate(dbRecord.getId()); + ThirdPayWayEnum payWay = ThirdPayWayEnum.ofPay(dbRecord.getPayWay()); + + // 已经支付成功 或者 已经是支付中,则直接返回 + if (Objects.equals(dbRecord.getPayStatus(), PayStatusEnum.SUCCEED.getStatus()) + || Objects.equals(dbRecord.getPayStatus(), PayStatusEnum.PAYING.getStatus())) { + recordChanged = false; + } else if (Objects.equals(dbRecord.getPayStatus(), PayStatusEnum.FAIL.getStatus())) { + // 支付失败,需要重置支付相关信息 + dbRecord.setVerifyCode(IdUtil.genPayCode(payWay, dbRecord.getId())); + dbRecord.setNotifyTime(null); + dbRecord.setPayStatus(PayStatusEnum.NOT_PAY.getStatus()); + recordChanged = true; + } else if (dbRecord.getPrePayExpireTime() == null + || System.currentTimeMillis() >= dbRecord.getPrePayExpireTime().getTime()) { + // 未支付、但是唤起支付的verifyCode已经过期的场景 + dbRecord.setVerifyCode(IdUtil.genPayCode(payWay, dbRecord.getId())); + recordChanged = true; + } else { + // 可以直接使用数据库中缓存的用于唤起支付的信息 + recordChanged = false; + } + + // 收款用户信息 + ArticlePayInfoDTO dto = PayConverter.toPay(dbRecord); + // 存在数据变更时,需要调用支付服务,重新获取支付相关信息 + PayInfoDTO payInfo = payServiceFactory.getPayService(payWay).toPay(dbRecord, recordChanged); + if (recordChanged) { + // 如果数据有变更,执行落库操作 + articlePayDao.updateById(dbRecord); + } + + // 补齐支付信息 + dto.setPrePayId(payInfo.getPrePayId()); + dto.setPrePayExpireTime(payInfo.getPrePayExpireTime()); + dto.setPayQrCodeMap(payInfo.getPayQrCodeMap()); + return dto; + } + + private ArticlePayRecordDO createPayRecord(Long articleId, Long currentUserId, String notes) { + ArticleDO articleDO = articleReadService.queryBasicArticle(articleId); + if (articleDO == null) { + throw ExceptionUtil.of(StatusEnum.RECORDS_NOT_EXISTS, articleId); + } + + ThirdPayWayEnum payWay = ThirdPayWayEnum.ofPay(articleDO.getPayWay()); + if (payWay == null) { + // 文章不需要付费阅读 + throw ExceptionUtil.of(StatusEnum.UNEXPECT_ERROR, "文章不需要付费阅读!"); + } + + if (payWay.wxPay() && Objects.equals(SpringUtil.getConfig("view.site.wxPayEnable"), "false")) { + // 微信支付未开启时,只能走个人收款码方式 + throw ExceptionUtil.of(StatusEnum.UNEXPECT_ERROR, "微信支付未开启,请联系作者换用个人收款码支付方式吧!"); + } + + ArticlePayRecordDO record = new ArticlePayRecordDO(); + record.setArticleId(articleId); + record.setReceiveUserId(articleDO.getUserId()); + record.setPayUserId(currentUserId); + record.setPayStatus(PayStatusEnum.NOT_PAY.getStatus()); + record.setNotifyTime(null); + record.setNotifyCnt(0); + String mark = String.format("支付解锁阅读《%s》-- %s", articleDO.getTitle(), notes == null ? "" : notes); + record.setNotes(mark); + record.setId(IdUtil.genId()); + record.setVerifyCode(IdUtil.genPayCode(payWay, record.getId())); + record.setPayWay(payWay.getPay()); + record.setPayAmount(articleDO.getPayAmount()); + articlePayDao.save(record); + return record; + } + + + /** + * 前端回调,告知后端已经支付成功了 + * - 首先做权限管控,不能修改别人的支付记录 + * - 已经知道是支付成功/支付失败(即回调的结果已经过来了),直接返回不做任何处理 + * - 未支付 -> 将支付状态设置为支付中 ; 有数据变更 -> 执行业务变更 ==> 调用支付服务,执行支付中的逻辑 + * - 发送消息变更事件 + * + * @param payId 支付id + * @param currentUserId 当前登录用户 + * @param notes 备注 + * @return + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updatePaying(Long payId, Long currentUserId, String notes) { + ArticlePayRecordDO record = articlePayDao.selectForUpdate(payId); + if (!record.getPayUserId().equals(currentUserId)) { + // 用户不一致,不支持更新 + throw ExceptionUtil.of(StatusEnum.FORBID_ERROR); + } + + if (Objects.equals(record.getPayStatus(), PayStatusEnum.SUCCEED.getStatus()) || + Objects.equals(record.getPayStatus(), PayStatusEnum.FAIL.getStatus())) { + // 支付状态幂等 + return true; + } + + // 更新为支付中 + ThirdPayWayEnum payWay = ThirdPayWayEnum.ofPay(record.getPayWay()); + if (PayStatusEnum.NOT_PAY.getStatus().equals(record.getPayStatus())) { + record.setPayStatus(PayStatusEnum.PAYING.getStatus()); + record.setUpdateTime(new Date()); + if (StringUtils.isNotBlank(notes)) { + // 更新备注信息 + record.setNotes(notes); + } + } else if (StringUtils.isNotBlank(notes) && Objects.equals(notes, record.getNotes())) { + // 备注信息不同时,更新并发送邮件通知 + record.setUpdateTime(new Date()); + record.setNotes(notes); + } + + // 调用具体的支付服务,执行支付中的逻辑;然后回写变更逻辑到db + boolean dbChanged = payServiceFactory.getPayService(payWay).paying(record); + if (!dbChanged) { + return true; + } + // 保存数据变更 + boolean ans = articlePayDao.updateById(record); + if (!ans) { + return false; + } + + // 发布支付状态变更消息 + this.publishPayStatusChangeNotify(record); + return true; + } + + + /** + * 根据支付状态发布对应的通知消息 + * + * @param record + */ + private void publishPayStatusChangeNotify(ArticlePayRecordDO record) { + // 支付状态变更的消息回调 + if (Objects.equals(record.getPayStatus(), PayStatusEnum.PAYING.getStatus())) { + // 更新支付状态为支付中 + MsgNotifyHelper.publish(NotifyTypeEnum.PAYING, record); + } else if (Objects.equals(record.getPayStatus(), PayStatusEnum.SUCCEED.getStatus()) + || Objects.equals(record.getPayStatus(), PayStatusEnum.FAIL.getStatus())) { + // 支付成功or失败 + MsgNotifyHelper.publish(NotifyTypeEnum.PAY, record); + } + } + + /** + * 更新支付状态 + * + * @param payId + * @param payStatus + * @return + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updatePayStatus(Long payId, String verifyCode, PayStatusEnum payStatus, + Long payTime, String transactionId) { + ArticlePayRecordDO dbRecord = articlePayDao.selectForUpdate(payId); + if (dbRecord == null || !Objects.equals(dbRecord.getVerifyCode(), verifyCode)) { + throw ExceptionUtil.of(StatusEnum.RECORDS_NOT_EXISTS, "支付记录:" + payId); + } + + if (Objects.equals(payStatus.getStatus(), dbRecord.getPayStatus()) + || PayStatusEnum.SUCCEED.getStatus().equals(dbRecord.getPayStatus())) { + // 幂等 or 已支付成功到了终态,不再进行后续的更新 + return true; + } + + // 更新原来的支付状态为最新的结果 + dbRecord.setPayStatus(payStatus.getStatus()); + dbRecord.setPayCallbackTime(new Date(payTime)); + dbRecord.setUpdateTime(new Date()); + dbRecord.setThirdTransCode(transactionId); + boolean ans = articlePayDao.updateById(dbRecord); + if (ans) { + publishPayStatusChangeNotify(dbRecord); + } + return ans; + } + + /** + * 给作者提供的一个支付确认中间页 + * + * @param payId 支付id + * @param record 支付记录 + * @return + */ + @Override + public PayConfirmDTO buildPayConfirmInfo(Long payId, ArticlePayRecordDO record) { + if (record == null) { + record = articlePayDao.getById(payId); + } + + // 文章 + ArticleDO article = articleReadService.queryBasicArticle(record.getArticleId()); + // 支付用户 + BaseUserInfoDTO pay = userService.queryBasicUserInfo(record.getPayUserId()); + + PayConfirmDTO confirm = new PayConfirmDTO(); + confirm.setTitle(article.getTitle()); + confirm.setArticleUrl(String.format("%s/article/detail/%s", host, article.getId())); + confirm.setNotifyCnt(record.getNotifyCnt()); + confirm.setPayTime(record.getNotifyTime() == null ? "-" : DateUtil.format(DateUtil.DB_FORMAT, record.getNotifyTime().getTime())); + confirm.setPayUser(pay.getUserName()); + confirm.setMark(record.getNotes()); + confirm.setReceiveUserId(record.getReceiveUserId()); + confirm.setPayWay(record.getPayWay()); + confirm.setPayAmount(Objects.equals(record.getPayWay(), ThirdPayWayEnum.EMAIL.getPay()) ? "" : PriceUtil.toYuanPrice(record.getPayAmount())); + confirm.setCallback(host + "/article/api/pay/callback?payId=" + record.getId() + "&verifyCode=" + record.getVerifyCode()); + return confirm; + } + + + /** + * 查询文章的打上用户列表 + * + * @param articleId + * @return + */ + public List queryPayUsers(Long articleId) { + List users = articlePayDao.querySucceedPayUsersByArticleId(articleId); + if (CollectionUtils.isEmpty(users)) { + return Collections.emptyList(); + } + + return userService.batchQuerySimpleUserInfo(users); + } + + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ArticleReadServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ArticleReadServiceImpl.java new file mode 100644 index 000000000..df7e52513 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ArticleReadServiceImpl.java @@ -0,0 +1,337 @@ +package com.github.paicoding.forum.service.article.service.impl; + +import com.github.paicoding.forum.api.model.enums.CollectionStatEnum; +import com.github.paicoding.forum.api.model.enums.CommentStatEnum; +import com.github.paicoding.forum.api.model.enums.DocumentTypeEnum; +import com.github.paicoding.forum.api.model.enums.HomeSelectEnum; +import com.github.paicoding.forum.api.model.enums.OperateTypeEnum; +import com.github.paicoding.forum.api.model.enums.PraiseStatEnum; +import com.github.paicoding.forum.api.model.exception.ExceptionUtil; +import com.github.paicoding.forum.api.model.vo.PageListVo; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.article.dto.ArticleDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.CategoryDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.SimpleArticleDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.TagDTO; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; +import com.github.paicoding.forum.core.util.ArticleUtil; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.service.article.conveter.ArticleConverter; +import com.github.paicoding.forum.service.article.repository.dao.ArticleDao; +import com.github.paicoding.forum.service.article.repository.dao.ArticleTagDao; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDO; +import com.github.paicoding.forum.service.article.service.ArticleReadService; +import com.github.paicoding.forum.service.article.service.CategoryService; +import com.github.paicoding.forum.service.constant.EsFieldConstant; +import com.github.paicoding.forum.service.constant.EsIndexConstant; +import com.github.paicoding.forum.service.statistics.service.CountService; +import com.github.paicoding.forum.service.user.repository.entity.UserFootDO; +import com.github.paicoding.forum.service.user.service.UserFootService; +import com.github.paicoding.forum.service.user.service.UserService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.index.query.MultiMatchQueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.SearchHits; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 文章查询相关服务类 + * + * @author louzai + * @date 2022-07-20 + */ +@Slf4j +@Service +public class ArticleReadServiceImpl implements ArticleReadService { + + @Autowired + private ArticleDao articleDao; + + @Autowired + private ArticleTagDao articleTagDao; + + @Autowired + private CategoryService categoryService; + /** + * 在一个项目中,UserFootService 就是内部服务调用 + * 拆微服务时,这个会作为远程服务访问 + */ + @Autowired + private UserFootService userFootService; + + @Autowired + private CountService countService; + + @Autowired + private UserService userService; + + // 是否开启ES + @Value("${elasticsearch.open:false}") + private Boolean openES; + + @Override + public ArticleDO queryBasicArticle(Long articleId) { + return articleDao.getById(articleId); + } + + @Override + public String generateSummary(String content) { + return ArticleUtil.pickSummary(content); + } + + @Override + public PageVo queryTagsByArticleId(Long articleId) { + List tagDTOS = articleTagDao.queryArticleTagDetails(articleId); + return PageVo.build(tagDTOS, 1, 10, tagDTOS.size()); + } + + @Override + public ArticleDTO queryDetailArticleInfo(Long articleId) { + ArticleDTO article = articleDao.queryArticleDetail(articleId); + if (article == null) { + throw ExceptionUtil.of(StatusEnum.ARTICLE_NOT_EXISTS, articleId); + } + // 更新分类相关信息 + CategoryDTO category = article.getCategory(); + category.setCategory(categoryService.queryCategoryName(category.getCategoryId())); + + // 更新标签信息 + article.setTags(articleTagDao.queryArticleTagDetails(articleId)); + return article; + } + + /** + * 查询文章所有的关联信息,正文,分类,标签,阅读计数,当前登录用户是否点赞、评论过 + * + * @param articleId + * @param readUser + * @return + */ + @Override + public ArticleDTO queryFullArticleInfo(Long articleId, Long readUser) { + ArticleDTO article = queryDetailArticleInfo(articleId); + + // 文章阅读计数+1 + countService.incrArticleReadCount(article.getAuthor(), articleId); + + // 文章的操作标记 + if (readUser != null) { + // 更新用于足迹,并判断是否点赞、评论、收藏 + UserFootDO foot = userFootService.saveOrUpdateUserFoot(DocumentTypeEnum.ARTICLE, articleId, + article.getAuthor(), readUser, OperateTypeEnum.READ); + article.setPraised(Objects.equals(foot.getPraiseStat(), PraiseStatEnum.PRAISE.getCode())); + article.setCommented(Objects.equals(foot.getCommentStat(), CommentStatEnum.COMMENT.getCode())); + article.setCollected(Objects.equals(foot.getCollectionStat(), CollectionStatEnum.COLLECTION.getCode())); + } else { + // 未登录,全部设置为未处理 + article.setPraised(false); + article.setCommented(false); + article.setCollected(false); + } + + // 更新文章统计计数 + article.setCount(countService.queryArticleStatisticInfo(articleId)); + + // 设置文章的点赞列表 + article.setPraisedUsers(userFootService.queryArticlePraisedUsers(articleId)); + return article; + } + + + /** + * 查询文章列表 + * + * @param categoryId + * @param page + * @return + */ + @Override + public PageListVo queryArticlesByCategory(Long categoryId, PageParam page) { + List records = articleDao.listArticlesByCategoryId(categoryId, page); + return buildArticleListVo(records, page.getPageSize()); + } + + /** + * 查询置顶的文章列表 + * + * @param categoryId + * @return + */ + @Override + public List queryTopArticlesByCategory(Long categoryId) { + PageParam page = PageParam.newPageInstance(PageParam.DEFAULT_PAGE_NUM, PageParam.TOP_PAGE_SIZE); + List articleDTOS = articleDao.listArticlesByCategoryId(categoryId, page); + return articleDTOS.stream().map(this::fillArticleRelatedInfo).collect(Collectors.toList()); + } + + @Override + public Long queryArticleCountByCategory(Long categoryId) { + return articleDao.countArticleByCategoryId(categoryId); + } + + @Override + public Map queryArticleCountsByCategory() { + return articleDao.countArticleByCategoryId(); + } + + @Override + public PageListVo queryArticlesByTag(Long tagId, PageParam page) { + List records = articleDao.listRelatedArticlesOrderByReadCount(null, Arrays.asList(tagId), page); + return buildArticleListVo(records, page.getPageSize()); + } + + @Override + public List querySimpleArticleBySearchKey(String key) { + // todo 当key为空时,返回热门推荐 + if (StringUtils.isBlank(key)) { + return Collections.emptyList(); + } + key = key.trim(); + if (!openES) { + List records = articleDao.listSimpleArticlesByBySearchKey(key); + return records.stream().map(s -> new SimpleArticleDTO().setId(s.getId()).setTitle(s.getTitle())) + .collect(Collectors.toList()); + } + // TODO ES整合 + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(key, + EsFieldConstant.ES_FIELD_TITLE, + EsFieldConstant.ES_FIELD_SHORT_TITLE); + searchSourceBuilder.query(multiMatchQueryBuilder); + + SearchRequest searchRequest = new SearchRequest(new String[]{EsIndexConstant.ES_INDEX_ARTICLE}, + searchSourceBuilder); + SearchResponse searchResponse = null; + try { + searchResponse = SpringUtil.getBean(RestHighLevelClient.class).search(searchRequest, RequestOptions.DEFAULT); + } catch (IOException e) { + log.error("failed to query from es: key", e); + } + SearchHits hits = searchResponse.getHits(); + SearchHit[] hitsList = hits.getHits(); + List ids = new ArrayList<>(); + for (SearchHit documentFields : hitsList) { + ids.add(Integer.parseInt(documentFields.getId())); + } + if (ObjectUtils.isEmpty(ids)) { + return null; + } + List records = articleDao.selectByIds(ids); + return records.stream().map(s -> new SimpleArticleDTO().setId(s.getId()).setTitle(s.getTitle())) + .collect(Collectors.toList()); + } + + @Override + public PageListVo queryArticlesBySearchKey(String key, PageParam page) { + List records = articleDao.listArticlesByBySearchKey(key, page); + return buildArticleListVo(records, page.getPageSize()); + } + + + @Override + public PageListVo queryArticlesByUserAndType(Long userId, PageParam pageParam, HomeSelectEnum select) { + List records = null; + if (select == HomeSelectEnum.ARTICLE) { + // 用户的文章列表 + records = articleDao.listArticlesByUserId(userId, pageParam); + } else if (select == HomeSelectEnum.READ) { + // 用户的阅读记录 + List articleIds = userFootService.queryUserReadArticleList(userId, pageParam); + records = CollectionUtils.isEmpty(articleIds) ? Collections.emptyList() : articleDao.listByIds(articleIds); + records = sortByIds(articleIds, records); + } else if (select == HomeSelectEnum.COLLECTION) { + // 用户的收藏列表 + List articleIds = userFootService.queryUserCollectionArticleList(userId, pageParam); + records = CollectionUtils.isEmpty(articleIds) ? Collections.emptyList() : articleDao.listByIds(articleIds); + records = sortByIds(articleIds, records); + } + + if (CollectionUtils.isEmpty(records)) { + return PageListVo.emptyVo(); + } + return buildArticleListVo(records, pageParam.getPageSize()); + } + + /** + * fixme @楼仔 这个排序逻辑看着像是有问题的样子 + * + * @param articleIds + * @param records + * @return + */ + private List sortByIds(List articleIds, List records) { + List articleDOS = new ArrayList<>(); + Map articleDOMap = records.stream().collect(Collectors.toMap(ArticleDO::getId, t -> t)); + articleIds.forEach(articleId -> { + if (articleDOMap.containsKey(articleId)) { + articleDOS.add(articleDOMap.get(articleId)); + } + }); + return articleDOS; + } + + @Override + public PageListVo buildArticleListVo(List records, long pageSize) { + List result = records.stream().map(this::fillArticleRelatedInfo).collect(Collectors.toList()); + return PageListVo.newVo(result, pageSize); + } + + /** + * 补全文章的阅读计数、作者、分类、标签等信息 + * + * @param record + * @return + */ + private ArticleDTO fillArticleRelatedInfo(ArticleDO record) { + ArticleDTO dto = ArticleConverter.toDto(record); + // 分类信息 + dto.getCategory().setCategory(categoryService.queryCategoryName(record.getCategoryId())); + // 标签列表 + dto.setTags(articleTagDao.queryArticleTagDetails(record.getId())); + // 阅读计数统计 + dto.setCount(countService.queryArticleStatisticInfo(record.getId())); + // 作者信息 + BaseUserInfoDTO author = userService.queryBasicUserInfo(dto.getAuthor()); + dto.setAuthorName(author.getUserName()); + dto.setAuthorAvatar(author.getPhoto()); + return dto; + } + + @Override + public PageListVo queryHotArticlesForRecommend(PageParam pageParam) { + List list = articleDao.listHotArticles(pageParam); + return PageListVo.newVo(list, pageParam.getPageSize()); + } + + @Override + public int queryArticleCount(long authorId) { + return articleDao.countArticleByUser(authorId); + } + + @Override + public Long getArticleCount() { + return articleDao.countArticle(); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ArticleRecommendServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ArticleRecommendServiceImpl.java new file mode 100644 index 000000000..11f823663 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ArticleRecommendServiceImpl.java @@ -0,0 +1,61 @@ +package com.github.paicoding.forum.service.article.service.impl; + +import com.github.paicoding.forum.api.model.vo.PageListVo; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.article.dto.ArticleDTO; +import com.github.paicoding.forum.service.article.repository.dao.ArticleDao; +import com.github.paicoding.forum.service.article.repository.dao.ArticleTagDao; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDO; +import com.github.paicoding.forum.service.article.repository.entity.ArticleTagDO; +import com.github.paicoding.forum.service.article.service.ArticleReadService; +import com.github.paicoding.forum.service.article.service.ArticleRecommendService; +import com.github.paicoding.forum.service.sidebar.service.SidebarService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author YiHui + * @date 2022/9/26 + */ +@Service +public class ArticleRecommendServiceImpl implements ArticleRecommendService { + @Autowired + private ArticleDao articleDao; + @Autowired + private ArticleTagDao articleTagDao; + @Autowired + private ArticleReadService articleReadService; + @Autowired + private SidebarService sidebarService; + + /** + * 查询文章关联推荐列表 + * + * @param articleId + * @param page + * @return + */ + @Override + public PageListVo relatedRecommend(Long articleId, PageParam page) { + ArticleDO article = articleDao.getById(articleId); + if (article == null) { + return PageListVo.emptyVo(); + } + List tagIds = articleTagDao.listArticleTags(articleId).stream() + .map(ArticleTagDO::getTagId).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(tagIds)) { + return PageListVo.emptyVo(); + } + + List recommendArticles = articleDao.listRelatedArticlesOrderByReadCount(article.getCategoryId(), tagIds, page); + if (recommendArticles.removeIf(s -> s.getId().equals(articleId))) { + // 移除推荐列表中的当前文章 + page.setPageSize(page.getPageSize() - 1); + } + return articleReadService.buildArticleListVo(recommendArticles, page.getPageSize()); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ArticleSettingServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ArticleSettingServiceImpl.java new file mode 100644 index 000000000..c98e9f0b2 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ArticleSettingServiceImpl.java @@ -0,0 +1,164 @@ +package com.github.paicoding.forum.service.article.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.github.paicoding.forum.api.model.enums.ArticleEventEnum; +import com.github.paicoding.forum.api.model.enums.OperateArticleEnum; +import com.github.paicoding.forum.api.model.enums.PushStatusEnum; +import com.github.paicoding.forum.api.model.enums.YesOrNoEnum; +import com.github.paicoding.forum.api.model.event.ArticleMsgEvent; +import com.github.paicoding.forum.api.model.exception.ExceptionUtil; +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.article.ArticlePostReq; +import com.github.paicoding.forum.api.model.vo.article.SearchArticleReq; +import com.github.paicoding.forum.api.model.vo.article.dto.ArticleAdminDTO; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.service.article.conveter.ArticleStructMapper; +import com.github.paicoding.forum.service.article.repository.dao.ArticleDao; +import com.github.paicoding.forum.service.article.repository.dao.ColumnArticleDao; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDO; +import com.github.paicoding.forum.service.article.repository.entity.ColumnArticleDO; +import com.github.paicoding.forum.service.article.repository.params.SearchArticleParams; +import com.github.paicoding.forum.service.article.service.ArticleSettingService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * 文章后台 + * + * @author louzai + * @date 2022-09-19 + */ +@Service +public class ArticleSettingServiceImpl implements ArticleSettingService { + + @Autowired + private ArticleDao articleDao; + + @Autowired + private ColumnArticleDao columnArticleDao; + + @Override + @CacheEvict(key = "'sideBar_' + #req.articleId", cacheManager = "caffeineCacheManager", cacheNames = "article") + public void updateArticle(ArticlePostReq req) { + if (req.getStatus() != PushStatusEnum.OFFLINE.getCode() + && req.getStatus() != PushStatusEnum.ONLINE.getCode() + && req.getStatus() != PushStatusEnum.REVIEW.getCode()) { + throw ExceptionUtil.of(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "发布状态不合法!"); + } + ArticleDO article = articleDao.getById(req.getArticleId()); + if (article == null) { + throw ExceptionUtil.of(StatusEnum.RECORDS_NOT_EXISTS, "文章不存在!"); + } + + if (StringUtils.isNotBlank(req.getTitle())) { + article.setTitle(req.getTitle()); + } + if (StringUtils.isNotBlank(req.getShortTitle())) { + article.setShortTitle(req.getShortTitle()); + } + + ArticleEventEnum operateEvent = null; + if (req.getStatus() != null) { + article.setStatus(req.getStatus()); + if (req.getStatus() == PushStatusEnum.OFFLINE.getCode()) { + operateEvent = ArticleEventEnum.OFFLINE; + } else if (req.getStatus() == PushStatusEnum.REVIEW.getCode()) { + operateEvent = ArticleEventEnum.REVIEW; + } else if (req.getStatus() == PushStatusEnum.ONLINE.getCode()) { + operateEvent = ArticleEventEnum.ONLINE; + } + } + articleDao.updateById(article); + + if (operateEvent != null) { + // 发布文章待审核、上线、下线事件 + SpringUtil.publishEvent(new ArticleMsgEvent<>(this, operateEvent, article)); + } + } + + @Override + public PageVo getArticleList(SearchArticleReq req) { + // 转换参数,从前端获取的参数转换为数据库查询参数 + SearchArticleParams searchArticleParams = ArticleStructMapper.INSTANCE.toSearchParams(req); + + // 查询文章列表,分页 + List articleDTOS = articleDao.listArticlesByParams(searchArticleParams); + + // 查询文章总数 + Long totalCount = articleDao.countArticleByParams(searchArticleParams); + return PageVo.build(articleDTOS, req.getPageSize(), req.getPageNumber(), totalCount); + } + + @Override + public void deleteArticle(Long articleId) { + ArticleDO dto = articleDao.getById(articleId); + if (dto != null && dto.getDeleted() != YesOrNoEnum.YES.getCode()) { + // 查询该文章是否关联了教程,如果已经关联了教程,则不能删除 + long count = columnArticleDao.count( + Wrappers.lambdaQuery().eq(ColumnArticleDO::getArticleId, articleId)); + + if (count > 0) { + throw ExceptionUtil.of(StatusEnum.ARTICLE_RELATION_TUTORIAL, articleId, "请先解除文章与教程的关联关系"); + } + + dto.setDeleted(YesOrNoEnum.YES.getCode()); + articleDao.updateById(dto); + + // 发布文章删除事件 + SpringUtil.publishEvent(new ArticleMsgEvent<>(this, ArticleEventEnum.DELETE, dto)); + } else { + throw ExceptionUtil.of(StatusEnum.ARTICLE_NOT_EXISTS, articleId); + } + } + + @Override + public void operateArticle(Long articleId, OperateArticleEnum operate) { + ArticleDO articleDO = articleDao.getById(articleId); + if (articleDO == null) { + throw ExceptionUtil.of(StatusEnum.ARTICLE_NOT_EXISTS, articleId); + } + setArticleStat(articleDO, operate); + articleDao.updateById(articleDO); + } + + private void setArticleStat(ArticleDO articleDO, OperateArticleEnum operate) { + switch (operate) { + case OFFICAL: + case CANCEL_OFFICAL: + compareAndUpdate(articleDO::getOfficalStat, articleDO::setOfficalStat, operate.getDbStatCode()); + return; + case TOPPING: + case CANCEL_TOPPING: + compareAndUpdate(articleDO::getToppingStat, articleDO::setToppingStat, operate.getDbStatCode()); + return; + case CREAM: + case CANCEL_CREAM: + compareAndUpdate(articleDO::getCreamStat, articleDO::setCreamStat, operate.getDbStatCode()); + return; + default: + } + } + + /** + * 相同则直接返回false不用更新;不同则更新,返回true + * + * @param + * @param supplier + * @param consumer + * @param input + */ + private void compareAndUpdate(Supplier supplier, Consumer consumer, T input) { + if (Objects.equals(supplier.get(), input)) { + return; + } + consumer.accept(input); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ArticleWriteServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ArticleWriteServiceImpl.java new file mode 100644 index 000000000..abb8f186c --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ArticleWriteServiceImpl.java @@ -0,0 +1,216 @@ +package com.github.paicoding.forum.service.article.service.impl; + +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.api.model.enums.*; +import com.github.paicoding.forum.api.model.event.ArticleMsgEvent; +import com.github.paicoding.forum.api.model.exception.ExceptionUtil; +import com.github.paicoding.forum.api.model.vo.article.ArticlePostReq; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; +import com.github.paicoding.forum.core.permission.UserRole; +import com.github.paicoding.forum.core.util.NumUtil; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.core.util.id.IdUtil; +import com.github.paicoding.forum.service.article.conveter.ArticleConverter; +import com.github.paicoding.forum.service.article.repository.dao.ArticleDao; +import com.github.paicoding.forum.service.article.repository.dao.ArticleTagDao; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDO; +import com.github.paicoding.forum.service.article.service.ArticleWriteService; +import com.github.paicoding.forum.service.article.service.ColumnSettingService; +import com.github.paicoding.forum.service.image.service.ImageService; +import com.github.paicoding.forum.service.user.service.AuthorWhiteListService; +import com.github.paicoding.forum.service.user.service.UserFootService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.Objects; +import java.util.Set; + +/** + * 文章操作相关服务类 + * + * @author louzai + * @date 2022-07-20 + */ +@Slf4j +@Service +public class ArticleWriteServiceImpl implements ArticleWriteService { + + private final ArticleDao articleDao; + + private final ArticleTagDao articleTagDao; + + @Autowired + private ColumnSettingService columnSettingService; + + @Autowired + private UserFootService userFootService; + + @Autowired + private ImageService imageService; + + @Resource + private TransactionTemplate transactionTemplate; + + @Autowired + private AuthorWhiteListService articleWhiteListService; + + public ArticleWriteServiceImpl(ArticleDao articleDao, ArticleTagDao articleTagDao) { + this.articleDao = articleDao; + this.articleTagDao = articleTagDao; + } + + /** + * 保存文章,当articleId存在时,表示更新记录; 不存在时,表示插入 + * + * @param req + * @return + */ + @Override + public Long saveArticle(ArticlePostReq req, Long author) { + ArticleDO article = ArticleConverter.toArticleDo(req, author); + String content = imageService.mdImgReplace(req.getContent()); + return transactionTemplate.execute(new TransactionCallback() { + @Override + public Long doInTransaction(TransactionStatus status) { + Long articleId; + if (NumUtil.nullOrZero(req.getArticleId())) { + articleId = insertArticle(article, content, req.getTagIds()); + log.info("文章发布成功! title={}", req.getTitle()); + } else { + articleId = updateArticle(article, content, req.getTagIds()); + log.info("文章更新成功! title={}", article.getTitle()); + } + if (req.getColumnId() != null) { + // 更新文章对应的专栏信息 + columnSettingService.saveColumnArticle(articleId, req.getColumnId()); + } + return articleId; + } + }); + } + + /** + * 新建文章 + * + * @param article + * @param content + * @param tags + * @return + */ + private Long insertArticle(ArticleDO article, String content, Set tags) { + // article + article_detail + tag 三张表的数据变更 + if (needToReview(article)) { + // 非白名单中的作者发布文章需要进行审核 + article.setStatus(PushStatusEnum.REVIEW.getCode()); + } + + // 1. 保存文章 + // 使用分布式id生成文章主键 + Long articleId = IdUtil.genId(); + article.setId(articleId); + articleDao.saveOrUpdate(article); + + // 2. 保存文章内容 + articleDao.saveArticleContent(articleId, content); + + // 3. 保存文章标签 + articleTagDao.batchSave(articleId, tags); + + // 发布文章,阅读计数+1 + userFootService.saveOrUpdateUserFoot(DocumentTypeEnum.ARTICLE, articleId, article.getUserId(), article.getUserId(), OperateTypeEnum.READ); + + // todo 事件发布这里可以进行优化,一次发送多个事件? 或者借助bit知识点来表示多种事件状态 + // 发布文章创建事件 + SpringUtil.publishEvent(new ArticleMsgEvent<>(this, ArticleEventEnum.CREATE, article)); + // 文章直接上线时,发布上线事件 + if (Objects.equals(article.getStatus(), PushStatusEnum.ONLINE.getCode())) { + SpringUtil.publishEvent(new ArticleMsgEvent<>(this, ArticleEventEnum.ONLINE, article)); + } else if (Objects.equals(article.getStatus(), PushStatusEnum.REVIEW.getCode())) { + SpringUtil.publishEvent(new ArticleMsgEvent<>(this, ArticleEventEnum.REVIEW, article)); + } + return articleId; + } + + /** + * 更新文章 + * + * @param article + * @param content + * @param tags + * @return + */ + private Long updateArticle(ArticleDO article, String content, Set tags) { + // fixme 待补充文章的历史版本支持:若文章处于审核状态,则直接更新上一条记录;否则新插入一条记录 + boolean review = article.getStatus().equals(PushStatusEnum.REVIEW.getCode()); + if (needToReview(article)) { + article.setStatus(PushStatusEnum.REVIEW.getCode()); + } + // 更新文章 + article.setUpdateTime(new Date()); + articleDao.updateById(article); + + // 更新内容 + articleDao.updateArticleContent(article.getId(), content, review); + + // 标签更新 + if (tags != null && !tags.isEmpty()) { + articleTagDao.updateTags(article.getId(), tags); + } + + // 发布文章待审核事件 + if (article.getStatus() == PushStatusEnum.ONLINE.getCode()) { + // 修改之后依然直接上线 (对于白名单作者而言) + SpringUtil.publishEvent(new ArticleMsgEvent<>(this, ArticleEventEnum.ONLINE, article)); + } else if (review) { + // 非白名单作者,修改再审核中的文章,依然是待审核状态 + SpringUtil.publishEvent(new ArticleMsgEvent<>(this, ArticleEventEnum.REVIEW, article)); + } + return article.getId(); + } + + + /** + * 删除文章 + * + * @param articleId + */ + @Override + public void deleteArticle(Long articleId, Long loginUserId) { + ArticleDO dto = articleDao.getById(articleId); + if (dto != null && !Objects.equals(dto.getUserId(), loginUserId)) { + // 没有权限 + throw ExceptionUtil.of(StatusEnum.FORBID_ERROR_MIXED, "请确认文章是否属于您!"); + } + + if (dto != null && dto.getDeleted() != YesOrNoEnum.YES.getCode()) { + dto.setDeleted(YesOrNoEnum.YES.getCode()); + articleDao.updateById(dto); + + // 发布文章删除事件 + SpringUtil.publishEvent(new ArticleMsgEvent<>(this, ArticleEventEnum.DELETE, dto)); + } + } + + + /** + * 非白名单的用户,发布的文章需要先进行审核 + * + * @param article + * @return + */ + private boolean needToReview(ArticleDO article) { + // 把 admin 用户加入白名单 + BaseUserInfoDTO user = ReqInfoContext.getReqInfo().getUser(); + if (user.getRole() != null && user.getRole().equalsIgnoreCase(UserRole.ADMIN.name())) { + return false; + } + return article.getStatus() == PushStatusEnum.ONLINE.getCode() && !articleWhiteListService.authorInArticleWhiteList(article.getUserId()); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/CategoryServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/CategoryServiceImpl.java new file mode 100644 index 000000000..67b4db389 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/CategoryServiceImpl.java @@ -0,0 +1,98 @@ +package com.github.paicoding.forum.service.article.service.impl; + +import com.github.paicoding.forum.api.model.enums.YesOrNoEnum; +import com.github.paicoding.forum.api.model.vo.article.dto.CategoryDTO; +import com.github.paicoding.forum.service.article.conveter.ArticleConverter; +import com.github.paicoding.forum.service.article.repository.dao.CategoryDao; +import com.github.paicoding.forum.service.article.repository.entity.CategoryDO; +import com.github.paicoding.forum.service.article.service.CategoryService; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * 类目Service + * + * @author louzai + * @date 2022-07-20 + */ +@Service +public class CategoryServiceImpl implements CategoryService { + /** + * 分类数一般不会特别多,如编程领域可以预期的分类将不会超过30,所以可以做一个全量的内存缓存 + * todo 后续可改为Guava -> Redis + */ + private LoadingCache categoryCaches; + + private CategoryDao categoryDao; + + public CategoryServiceImpl(CategoryDao categoryDao) { + this.categoryDao = categoryDao; + } + + @PostConstruct + public void init() { + categoryCaches = CacheBuilder.newBuilder().maximumSize(300).build(new CacheLoader() { + @Override + public CategoryDTO load(@NotNull Long categoryId) throws Exception { + CategoryDO category = categoryDao.getById(categoryId); + if (category == null || category.getDeleted() == YesOrNoEnum.YES.getCode()) { + return CategoryDTO.EMPTY; + } + return new CategoryDTO(categoryId, category.getCategoryName(), category.getRank()); + } + }); + } + + /** + * 查询类目名 + * + * @param categoryId + * @return + */ + @Override + public String queryCategoryName(Long categoryId) { + return categoryCaches.getUnchecked(categoryId).getCategory(); + } + + /** + * 查询所有的分类 + * + * @return + */ + @Override + public List loadAllCategories() { + if (categoryCaches.size() <= 5) { + refreshCache(); + } + List list = new ArrayList<>(categoryCaches.asMap().values()); + list.removeIf(s -> s.getCategoryId() <= 0); + list.sort(Comparator.comparingInt(CategoryDTO::getRank)); + return list; + } + + /** + * 刷新缓存 + */ + @Override + public void refreshCache() { + List list = categoryDao.listAllCategoriesFromDb(); + categoryCaches.invalidateAll(); + categoryCaches.cleanUp(); + list.forEach(s -> categoryCaches.put(s.getId(), ArticleConverter.toDto(s))); + } + + @Override + public Long queryCategoryId(String category) { + return categoryCaches.asMap().values().stream() + .filter(s -> s.getCategory().equalsIgnoreCase(category)) + .findFirst().map(CategoryDTO::getCategoryId).orElse(null); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/CategorySettingServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/CategorySettingServiceImpl.java new file mode 100644 index 000000000..8ec4bc0e6 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/CategorySettingServiceImpl.java @@ -0,0 +1,75 @@ +package com.github.paicoding.forum.service.article.service.impl; + +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.article.CategoryReq; +import com.github.paicoding.forum.api.model.vo.article.SearchCategoryReq; +import com.github.paicoding.forum.api.model.vo.article.dto.CategoryDTO; +import com.github.paicoding.forum.core.util.NumUtil; +import com.github.paicoding.forum.service.article.conveter.ArticleConverter; +import com.github.paicoding.forum.service.article.conveter.CategoryStructMapper; +import com.github.paicoding.forum.service.article.repository.dao.CategoryDao; +import com.github.paicoding.forum.service.article.repository.entity.CategoryDO; +import com.github.paicoding.forum.service.article.repository.params.SearchCategoryParams; +import com.github.paicoding.forum.service.article.service.CategoryService; +import com.github.paicoding.forum.service.article.service.CategorySettingService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 分类后台接口 + * + * @author louzai + * @date 2022-09-17 + */ +@Service +public class CategorySettingServiceImpl implements CategorySettingService { + + @Autowired + private CategoryDao categoryDao; + + @Autowired + private CategoryService categoryService; + + @Override + public void saveCategory(CategoryReq categoryReq) { + CategoryDO categoryDO = CategoryStructMapper.INSTANCE.toDO(categoryReq); + if (NumUtil.nullOrZero(categoryReq.getCategoryId())) { + categoryDao.save(categoryDO); + } else { + categoryDO.setId(categoryReq.getCategoryId()); + categoryDao.updateById(categoryDO); + } + categoryService.refreshCache(); + } + + @Override + public void deleteCategory(Integer categoryId) { + CategoryDO categoryDO = categoryDao.getById(categoryId); + if (categoryDO != null){ + categoryDao.removeById(categoryDO); + } + categoryService.refreshCache(); + } + + @Override + public void operateCategory(Integer categoryId, Integer pushStatus) { + CategoryDO categoryDO = categoryDao.getById(categoryId); + if (categoryDO != null){ + categoryDO.setStatus(pushStatus); + categoryDao.updateById(categoryDO); + } + categoryService.refreshCache(); + } + + @Override + public PageVo getCategoryList(SearchCategoryReq req) { + // 转换 + SearchCategoryParams params = CategoryStructMapper.INSTANCE.toSearchParams(req); + // 查询 + List categoryDTOS = categoryDao.listCategory(params); + Long totalCount = categoryDao.countCategory(params); + return PageVo.build(categoryDTOS, params.getPageSize(), params.getPageNum(), totalCount); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ColumnServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ColumnServiceImpl.java new file mode 100644 index 000000000..037e7fcf2 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ColumnServiceImpl.java @@ -0,0 +1,123 @@ +package com.github.paicoding.forum.service.article.service.impl; + +import com.github.paicoding.forum.api.model.exception.ExceptionUtil; +import com.github.paicoding.forum.api.model.vo.PageListVo; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.article.dto.ColumnDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.SimpleArticleDTO; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.ColumnFootCountDTO; +import com.github.paicoding.forum.service.article.conveter.ColumnConvert; +import com.github.paicoding.forum.service.article.repository.dao.ArticleDao; +import com.github.paicoding.forum.service.article.repository.dao.ColumnArticleDao; +import com.github.paicoding.forum.service.article.repository.dao.ColumnDao; +import com.github.paicoding.forum.service.article.repository.entity.ColumnArticleDO; +import com.github.paicoding.forum.service.article.repository.entity.ColumnInfoDO; +import com.github.paicoding.forum.service.article.service.ColumnService; +import com.github.paicoding.forum.service.user.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author YiHui + * @date 2022/9/14 + */ +@Service +public class ColumnServiceImpl implements ColumnService { + @Autowired + private ColumnDao columnDao; + @Autowired + private ArticleDao articleDao; + + @Autowired + private ColumnArticleDao columnArticleDao; + + @Autowired + private UserService userService; + + @Override + public ColumnArticleDO getColumnArticleRelation(Long articleId) { + return columnArticleDao.selectColumnArticleByArticleId(articleId); + } + + /** + * 专栏列表 + * + * @return + */ + @Override + public PageListVo listColumn(PageParam pageParam) { + List columnList = columnDao.listOnlineColumns(pageParam); + List result = columnList.stream().map(this::buildColumnInfo).collect(Collectors.toList()); + return PageListVo.newVo(result, pageParam.getPageSize()); + } + + @Override + public ColumnDTO queryBasicColumnInfo(Long columnId) { + // 查找专栏信息 + ColumnInfoDO column = columnDao.getById(columnId); + if (column == null) { + throw ExceptionUtil.of(StatusEnum.COLUMN_NOT_EXISTS, columnId); + } + return ColumnConvert.toDto(column); + } + + @Override + public ColumnDTO queryColumnInfo(Long columnId) { + return buildColumnInfo(queryBasicColumnInfo(columnId)); + } + + private ColumnDTO buildColumnInfo(ColumnInfoDO info) { + return buildColumnInfo(ColumnConvert.toDto(info)); + } + + /** + * 构建专栏详情信息 + * + * @param dto + * @return + */ + private ColumnDTO buildColumnInfo(ColumnDTO dto) { + // 补齐专栏对应的用户信息 + BaseUserInfoDTO user = userService.queryBasicUserInfo(dto.getAuthor()); + dto.setAuthorName(user.getUserName()); + dto.setAuthorAvatar(user.getPhoto()); + dto.setAuthorProfile(user.getProfile()); + + // 统计计数 + ColumnFootCountDTO countDTO = new ColumnFootCountDTO(); + // 更新文章数 + countDTO.setArticleCount(columnDao.countColumnArticles(dto.getColumnId())); + // 专栏阅读人数 + countDTO.setReadCount(columnDao.countColumnReadPeoples(dto.getColumnId())); + // 总的章节数 + countDTO.setTotalNums(dto.getNums()); + dto.setCount(countDTO); + return dto; + } + + + @Override + public ColumnArticleDO queryColumnArticle(long columnId, Integer section) { + ColumnArticleDO article = columnDao.getColumnArticleId(columnId, section); + if (article == null) { + throw ExceptionUtil.of(StatusEnum.ARTICLE_NOT_EXISTS, section); + } + return article; + } + + @Override + public List queryColumnArticles(long columnId) { + return columnDao.listColumnArticles(columnId); + } + + @Override + public Long getTutorialCount() { + return this.columnDao.countColumnArticles(); + } + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ColumnSettingServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ColumnSettingServiceImpl.java new file mode 100644 index 000000000..cebcbc3ed --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/ColumnSettingServiceImpl.java @@ -0,0 +1,307 @@ +package com.github.paicoding.forum.service.article.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.github.paicoding.forum.api.model.exception.ExceptionUtil; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.article.*; +import com.github.paicoding.forum.api.model.vo.article.dto.ColumnArticleDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.ColumnDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.SimpleColumnDTO; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; +import com.github.paicoding.forum.core.util.NumUtil; +import com.github.paicoding.forum.service.article.conveter.ColumnArticleStructMapper; +import com.github.paicoding.forum.service.article.conveter.ColumnStructMapper; +import com.github.paicoding.forum.service.article.repository.dao.ArticleDao; +import com.github.paicoding.forum.service.article.repository.dao.ColumnArticleDao; +import com.github.paicoding.forum.service.article.repository.dao.ColumnDao; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDO; +import com.github.paicoding.forum.service.article.repository.entity.ColumnArticleDO; +import com.github.paicoding.forum.service.article.repository.entity.ColumnInfoDO; +import com.github.paicoding.forum.service.article.repository.params.SearchColumnArticleParams; +import com.github.paicoding.forum.service.article.repository.params.SearchColumnParams; +import com.github.paicoding.forum.service.article.service.ColumnSettingService; +import com.github.paicoding.forum.service.user.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 专栏后台接口 + * + * @author louzai + * @date 2022-09-19 + */ +@Service +public class ColumnSettingServiceImpl implements ColumnSettingService { + + @Autowired + private UserService userService; + + @Autowired + private ColumnArticleDao columnArticleDao; + + @Autowired + private ColumnDao columnDao; + + @Autowired + private ArticleDao articleDao; + + @Autowired + private ColumnStructMapper columnStructMapper; + + @Override + public void saveColumn(ColumnReq req) { + ColumnInfoDO columnInfoDO = columnStructMapper.toDo(req); + if (NumUtil.nullOrZero(req.getColumnId())) { + columnDao.save(columnInfoDO); + } else { + columnInfoDO.setId(req.getColumnId()); + columnDao.updateById(columnInfoDO); + } + } + + + /** + * 将文章保存到对应的专栏中 + * + * @param articleId + * @param columnId + */ + public void saveColumnArticle(Long articleId, Long columnId) { + // 转换参数 + // 插入的时候,需要判断是否已经存在 + ColumnArticleDO exist = columnArticleDao.getOne(Wrappers.lambdaQuery() + .eq(ColumnArticleDO::getArticleId, articleId)); + if (exist != null) { + if (!Objects.equals(columnId, exist.getColumnId())) { + // 更新 + exist.setColumnId(columnId); + columnArticleDao.updateById(exist); + } + } else { + // 将文章保存到专栏中,章节序号+1 + ColumnArticleDO columnArticleDO = new ColumnArticleDO(); + columnArticleDO.setColumnId(columnId); + columnArticleDO.setArticleId(articleId); + // section 自增+1 + Integer maxSection = columnArticleDao.selectMaxSection(columnId); + columnArticleDO.setSection(maxSection + 1); + columnArticleDao.save(columnArticleDO); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveColumnArticle(ColumnArticleReq req) { + // 转换参数 + ColumnArticleDO columnArticleDO = ColumnArticleStructMapper.INSTANCE.reqToDO(req); + if (NumUtil.nullOrZero(columnArticleDO.getId())) { + // 插入的时候,需要判断是否已经存在 + ColumnArticleDO exist = columnArticleDao.getOne(Wrappers.lambdaQuery() + .eq(ColumnArticleDO::getColumnId, columnArticleDO.getColumnId()) + .eq(ColumnArticleDO::getArticleId, columnArticleDO.getArticleId())); + if (exist != null) { + throw ExceptionUtil.of(StatusEnum.COLUMN_ARTICLE_EXISTS, "请勿重复添加"); + } + + // section 自增+1 + Integer maxSection = columnArticleDao.selectMaxSection(columnArticleDO.getColumnId()); + columnArticleDO.setSection(maxSection + 1); + columnArticleDao.save(columnArticleDO); + } else { + columnArticleDao.updateById(columnArticleDO); + } + + // 同时,更新 article 的 shortTitle 短标题 + if (req.getShortTitle() != null) { + ArticleDO articleDO = new ArticleDO(); + articleDO.setShortTitle(req.getShortTitle()); + articleDO.setId(req.getArticleId()); + articleDao.updateById(articleDO); + } + } + + @Override + public void deleteColumn(Long columnId) { + columnDao.deleteColumn(columnId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteColumnArticle(Long id) { + ColumnArticleDO columnArticleDO = columnArticleDao.getById(id); + if (columnArticleDO != null) { + columnArticleDao.removeById(id); + // 删除的时候,批量更新 section,比如说原来是 1,2,3,4,5,6,7,8,9,10,删除 5,那么 6-10 的 section 都要减 1 + columnArticleDao.update(null, Wrappers.lambdaUpdate() + .setSql("section = section - 1") + .eq(ColumnArticleDO::getColumnId, columnArticleDO.getColumnId()) + // section 大于 1 + .gt(ColumnArticleDO::getSection, 1) + .gt(ColumnArticleDO::getSection, columnArticleDO.getSection())); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void sortColumnArticleApi(SortColumnArticleReq req) { + // 根据 req 的两个 ID 调换两篇文章的顺序 + ColumnArticleDO activeDO = columnArticleDao.getById(req.getActiveId()); + ColumnArticleDO overDO = columnArticleDao.getById(req.getOverId()); + if (activeDO != null && overDO != null && !activeDO.getId().equals(overDO.getId())) { + Integer activeSection = activeDO.getSection(); + Integer overSection = overDO.getSection(); + // 假如原始顺序为1、2、3、4 + // + //把 1 拖到 4 后面 2 变 1 3 变 2 4 变 3 1 变 4 + //把 1 拖到 3 后面 2 变 1 3 变 2 4 不变 1 变 3 + //把 1 拖到 2 后面 2 变 1 3 不变 4 不变 1 变 2 + //把 2 拖到 4 后面 1 不变 3 变 2 4 变 3 2 变 4 + //把 2 拖到 3 后面 1 不变 3 变 2 4 不变 2 变 3 + //把 3 拖到 4 后面 1 不变 2 不变 4 变 3 3 变 4 + //把 4 拖到 1 前面 1 变 2 2 变 3 3 变 4 + //把 4 拖到 2 前面 1 不变 2 变 3 3 变 4 4 变 1 + //把 4 拖到 3 前面 1 不变 2 不变 3 变 4 4 变 1 + //把 3 拖到 1 前面 1 变 2 2 变 3 3 变 4 4 变 1 + //依次类推 + // 1. 如果 activeSection > overSection,那么 activeSection - 1 到 overSection 的 section 都要 +1 + // 向上拖动 + if (activeSection > overSection) { + // 当 activeSection 大于 overSection 时,表示文章被向上拖拽。 + // 需要将 activeSection 到 overSection(不包括 activeSection 本身)之间的所有文章的 section 加 1, + // 并将 activeSection 设置为 overSection。 + columnArticleDao.update(null, Wrappers.lambdaUpdate() + .setSql("section = section + 1") // 将符合条件的记录的 section 字段的值增加 1 + .eq(ColumnArticleDO::getColumnId, overDO.getColumnId()) // 指定要更新记录的 columnId 条件 + .ge(ColumnArticleDO::getSection, overSection) // 指定 section 字段的下限(包含此值) + .lt(ColumnArticleDO::getSection, activeSection)); // 指定 section 字段的上限 + + // 将 activeDO 的 section 设置为 overSection + activeDO.setSection(overSection); + columnArticleDao.updateById(activeDO); + } else { + // 2. 如果 activeSection < overSection, + // 那么 activeSection + 1 到 overSection 的 section 都要 -1 + // 向下拖动 + // 需要将 activeSection 到 overSection(包括 overSection)之间的所有文章的 section 减 1 + columnArticleDao.update(null, Wrappers.lambdaUpdate() + .setSql("section = section - 1") // 将符合条件的记录的 section 字段的值减少 1 + .eq(ColumnArticleDO::getColumnId, overDO.getColumnId()) // 指定要更新记录的 columnId 条件 + .gt(ColumnArticleDO::getSection, activeSection) // 指定 section 字段的下限(不包含此值) + .le(ColumnArticleDO::getSection, overSection)); // 指定 section 字段的上限(包含此值) + + // 将 activeDO 的 section 设置为 overSection -1 + activeDO.setSection(overSection); + columnArticleDao.updateById(activeDO); + + } + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void sortColumnArticleByIDApi(SortColumnArticleByIDReq req) { + // 获取要重新排序的专栏文章 + ColumnArticleDO columnArticleDO = columnArticleDao.getById(req.getId()); + // 不等于空 + if (columnArticleDO == null) { + throw ExceptionUtil.of(StatusEnum.COLUMN_ARTICLE_EXISTS, "教程不存在"); + } + // 如果顺序没变 + if (req.getSort().equals(columnArticleDO.getSection())) { + return; + } + // 获取教程可以调整的最大顺序 + Integer maxSection = columnArticleDao.selectMaxSection(columnArticleDO.getColumnId()); + // 如果输入的顺序大于最大顺序,提示错误 + if (req.getSort() > maxSection) { + throw ExceptionUtil.of(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "顺序超出范围"); + } + // 查看输入的顺序是否存在 + ColumnArticleDO changeColumnArticleDO = columnArticleDao.selectBySection(columnArticleDO.getColumnId(), req.getSort()); + // 如果存在,交换顺序 + if (changeColumnArticleDO != null) { + // 交换顺序 + columnArticleDao.update(null, Wrappers.lambdaUpdate() + .set(ColumnArticleDO::getSection, columnArticleDO.getSection()) + .eq(ColumnArticleDO::getId, changeColumnArticleDO.getId())); + columnArticleDao.update(null, Wrappers.lambdaUpdate() + .set(ColumnArticleDO::getSection, changeColumnArticleDO.getSection()) + .eq(ColumnArticleDO::getId, columnArticleDO.getId())); + } else { + // 如果不存在,直接修改顺序 + throw ExceptionUtil.of(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "输入的顺序不存在,无法完成交换"); + } + } + + @Override + public PageVo getColumnList(SearchColumnReq req) { + // 转换参数 + ColumnStructMapper mapper = ColumnStructMapper.INSTANCE; + SearchColumnParams params = mapper.reqToSearchParams(req); + // 查询 + List columnList = columnDao.listColumnsByParams(params, PageParam.newPageInstance(req.getPageNumber(), req.getPageSize())); + // 转属性 + List columnDTOS = mapper.infoToDtos(columnList); + + // 进行优化,由原来的多次查询用户信息,改为一次查询用户信息 + // 获取所有需要的用户id + // 判断 columnDTOS 是否为空 + if (CollUtil.isNotEmpty(columnDTOS)) { + List userIds = columnDTOS.stream().map(ColumnDTO::getAuthor).collect(Collectors.toList()); + + // 查询所有的用户信息 + List users = userService.batchQueryBasicUserInfo(userIds); + + // 创建一个id到用户信息的映射 + Map userMap = users.stream().collect(Collectors.toMap(BaseUserInfoDTO::getId, Function.identity())); + + // 设置作者信息 + columnDTOS.forEach(columnDTO -> { + BaseUserInfoDTO user = userMap.get(columnDTO.getAuthor()); + columnDTO.setAuthorName(user.getUserName()); + columnDTO.setAuthorAvatar(user.getPhoto()); + columnDTO.setAuthorProfile(user.getProfile()); + }); + } + + Integer totalCount = columnDao.countColumnsByParams(params); + return PageVo.build(columnDTOS, req.getPageSize(), req.getPageNumber(), totalCount); + } + + @Override + public PageVo getColumnArticleList(SearchColumnArticleReq req) { + // 转换参数 + ColumnArticleStructMapper mapper = ColumnArticleStructMapper.INSTANCE; + SearchColumnArticleParams params = mapper.toSearchParams(req); + // 查询 + List simpleArticleDTOS = columnDao.listColumnArticlesDetail(params, PageParam.newPageInstance(req.getPageNumber(), req.getPageSize())); + int totalCount = columnDao.countColumnArticles(params); + return PageVo.build(simpleArticleDTOS, req.getPageSize(), req.getPageNumber(), totalCount); + } + + @Override + public List listSimpleColumnBySearchKey(String key) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.select(ColumnInfoDO::getId, ColumnInfoDO::getColumnName, ColumnInfoDO::getCover) + .and(!StringUtils.isEmpty(key), + v -> v.like(ColumnInfoDO::getColumnName, key) + ) + .orderByDesc(ColumnInfoDO::getId); + List articleDOS = columnDao.list(query); + return ColumnStructMapper.INSTANCE.infoToSimpleDtos(articleDOS); + } + + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/TagServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/TagServiceImpl.java new file mode 100644 index 000000000..5981cccb4 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/TagServiceImpl.java @@ -0,0 +1,37 @@ +package com.github.paicoding.forum.service.article.service.impl; + +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.article.dto.TagDTO; +import com.github.paicoding.forum.service.article.repository.dao.TagDao; +import com.github.paicoding.forum.service.article.service.TagService; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 标签Service + * + * @author louzai + * @date 2022-07-20 + */ +@Service +public class TagServiceImpl implements TagService { + private final TagDao tagDao; + + public TagServiceImpl(TagDao tagDao) { + this.tagDao = tagDao; + } + + @Override + public PageVo queryTags(String key, PageParam pageParam) { + List tagDTOS = tagDao.listOnlineTag(key, pageParam); + Integer totalCount = tagDao.countOnlineTag(key); + return PageVo.build(tagDTOS, pageParam.getPageSize(), pageParam.getPageNum(), totalCount); + } + + @Override + public Long queryTagId(String tag) { + return tagDao.selectTagIdByTag(tag); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/TagSettingServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/TagSettingServiceImpl.java new file mode 100644 index 000000000..7593c0c4b --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/article/service/impl/TagSettingServiceImpl.java @@ -0,0 +1,112 @@ +package com.github.paicoding.forum.service.article.service.impl; + +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.article.SearchTagReq; +import com.github.paicoding.forum.api.model.vo.article.TagReq; +import com.github.paicoding.forum.api.model.vo.article.dto.TagDTO; +import com.github.paicoding.forum.core.cache.RedisClient; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.core.util.NumUtil; +import com.github.paicoding.forum.service.article.conveter.TagStructMapper; +import com.github.paicoding.forum.service.article.repository.dao.TagDao; +import com.github.paicoding.forum.service.article.repository.entity.TagDO; +import com.github.paicoding.forum.service.article.repository.params.SearchTagParams; +import com.github.paicoding.forum.service.article.service.TagSettingService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 标签后台接口 + * + * @author louzai + * @date 2022-09-17 + */ +@Service +public class TagSettingServiceImpl implements TagSettingService { + + private static final String CACHE_TAG_PRE = "cache_tag_pre_"; + + private static final Long CACHE_TAG_EXPRIE_TIME = 100L; + + @Autowired + private TagDao tagDao; + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveTag(TagReq tagReq) { + TagDO tagDO = TagStructMapper.INSTANCE.toDO(tagReq); + + // 先写 MySQL + if (NumUtil.nullOrZero(tagReq.getTagId())) { + tagDao.save(tagDO); + } else { + tagDO.setId(tagReq.getTagId()); + tagDao.updateById(tagDO); + } + + // 再删除 Redis + String redisKey = CACHE_TAG_PRE + tagDO.getId(); + RedisClient.del(redisKey); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteTag(Integer tagId) { + TagDO tagDO = tagDao.getById(tagId); + if (tagDO != null){ + // 先写 MySQL + tagDao.removeById(tagId); + + // 再删除 Redis + String redisKey = CACHE_TAG_PRE + tagDO.getId(); + RedisClient.del(redisKey); + } + } + + @Override + public void operateTag(Integer tagId, Integer pushStatus) { + TagDO tagDO = tagDao.getById(tagId); + if (tagDO != null){ + + // 先写 MySQL + tagDO.setStatus(pushStatus); + tagDao.updateById(tagDO); + + // 再删除 Redis + String redisKey = CACHE_TAG_PRE + tagDO.getId(); + RedisClient.del(redisKey); + } + } + + @Override + public PageVo getTagList(SearchTagReq req) { + // 转换 + SearchTagParams params = TagStructMapper.INSTANCE.toSearchParams(req); + // 查询 + List tagDTOS = TagStructMapper.INSTANCE.toDTOs(tagDao.listTag(params)); + Long totalCount = tagDao.countTag(params); + return PageVo.build(tagDTOS, params.getPageSize(), params.getPageNum(), totalCount); + } + + @Override + public TagDTO getTagById(Long tagId) { + + String redisKey = CACHE_TAG_PRE + tagId; + + // 先查询缓存,如果有就直接返回 + String tagInfoStr = RedisClient.getStr(redisKey); + if (tagInfoStr != null && !tagInfoStr.isEmpty()) { + return JsonUtil.toObj(tagInfoStr, TagDTO.class); + } + + // 如果未查询到,需要先查询 DB ,再写入缓存 + TagDTO tagDTO = tagDao.selectById(tagId); + tagInfoStr = JsonUtil.toStr(tagDTO); + RedisClient.setStrWithExpire(redisKey, tagInfoStr, CACHE_TAG_EXPRIE_TIME); + + return tagDTO; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/ChatFacade.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/ChatFacade.java new file mode 100644 index 000000000..a952b6517 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/ChatFacade.java @@ -0,0 +1,185 @@ +package com.github.paicoding.forum.service.chatai; + +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatRecordsVo; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.service.chatai.service.ChatServiceFactory; +import com.github.paicoding.forum.service.chatai.service.impl.ali.AliIntegration; +import com.github.paicoding.forum.service.chatai.service.impl.chatgpt.ChatGptIntegration; +import com.github.paicoding.forum.service.chatai.service.impl.xunfei.XunFeiIntegration; +import com.github.paicoding.forum.service.chatai.service.impl.zhipu.ZhipuIntegration; +import com.github.paicoding.forum.service.user.service.conf.AiConfig; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.collect.Sets; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * 聊天的门面类 + * + * @author YiHui + * @date 2023/6/9 + */ +@Slf4j +@Service +public class ChatFacade { + + @Autowired + private AiConfig aiConfig; + @Autowired + private ChatServiceFactory chatServiceFactory; + + /** + * 基于Guava的单实例缓存 + */ + private Supplier aiSourceCache; + + /** + * 返回推荐的AI模型 + * + * @return + */ + public AISourceEnum getRecommendAiSource() { + if (aiSourceCache == null) { + refreshAiSourceCache(Collections.emptySet()); + } + AISourceEnum sourceEnum = aiSourceCache.get(); + if (sourceEnum == null) { + refreshAiSourceCache(getRecommendAiSource(Collections.emptySet())); + } + return aiSourceCache.get(); + } + + public void refreshAiSourceCache(AISourceEnum ai) { + aiSourceCache = Suppliers.memoizeWithExpiration(() -> ai, 10, TimeUnit.MINUTES); + } + + public void refreshAiSourceCache(Set except) { + refreshAiSourceCache(getRecommendAiSource(except)); + } + + /** + * 返回推荐的AI模型 + * + * @param except 不选择的AI模型 + * @return + */ + private AISourceEnum getRecommendAiSource(Set except) { + AISourceEnum source; + try { + ChatGptIntegration.ChatGptConfig config = SpringUtil.getBean(ChatGptIntegration.ChatGptConfig.class); + if (!except.contains(AISourceEnum.CHAT_GPT_3_5) && !CollectionUtils.isEmpty(config.getConf() + .get(config.getMain()).getKeys())) { + source = AISourceEnum.CHAT_GPT_3_5; + } else if (!except.contains(AISourceEnum.ZHI_PU_AI) && StringUtils.isNotBlank(SpringUtil.getBean(ZhipuIntegration.ZhipuConfig.class) + .getApiSecretKey())) { + source = AISourceEnum.ZHI_PU_AI; + } else if (!except.contains(AISourceEnum.XUN_FEI_AI) && StringUtils.isNotBlank(SpringUtil.getBean(XunFeiIntegration.XunFeiConfig.class) + .getApiKey())) { + source = AISourceEnum.XUN_FEI_AI; + } else if (!except.contains(AISourceEnum.ALI_AI)) { + source = AISourceEnum.ALI_AI; + } else if(!except.contains(AISourceEnum.DEEP_SEEK)) { + source = AISourceEnum.DEEP_SEEK; + } else if(!except.contains(AISourceEnum.DOU_BAO_AI)) { + source = AISourceEnum.DOU_BAO_AI; + } else { + source = AISourceEnum.PAI_AI; + } + } catch (Exception e) { + source = AISourceEnum.PAI_AI; + } + + if (source != AISourceEnum.PAI_AI && !aiConfig.getSource().contains(source)) { + Set totalExcepts = Sets.newHashSet(except); + totalExcepts.add(source); + return getRecommendAiSource(totalExcepts); + } + log.info("当前选中的AI模型:{}", source); + return source; + } + + /** + * 高度封装的AI聊天访问入口,对于使用这而言,只需要提问,定义接收返回结果的回调即可 + * + * @param question 提出的问题 + * @param callback 定义异步聊天接口返回时的回调策略 + * @return 表示同步直接返回的结果 + */ + public ChatRecordsVo autoChat(String question, Consumer callback) { + AISourceEnum source = getRecommendAiSource(); + return autoChat(source, question, callback); + } + + + /** + * 自动根据AI的支持方式,选择同步/异步的交互方式 + * + * @param source + * @param question + * @param callback + * @return + */ + public ChatRecordsVo autoChat(AISourceEnum source, String question, Consumer callback) { + if (source.asyncSupport() && chatServiceFactory.getChatService(source).asyncFirst()) { + // 支持异步且异步优先的场景下,自动选择异步方式进行聊天 + return asyncChat(source, question, callback); + } + return chat(source, question, callback); + } + + /** + * 开始聊天 + * + * @param question + * @param source + * @return + */ + public ChatRecordsVo chat(AISourceEnum source, String question) { + return chatServiceFactory.getChatService(source).chat(ReqInfoContext.getReqInfo().getUserId(), question); + } + + /** + * 开始聊天 + * + * @param question + * @param source + * @return + */ + public ChatRecordsVo chat(AISourceEnum source, String question, Consumer callback) { + return chatServiceFactory.getChatService(source) + .chat(ReqInfoContext.getReqInfo().getUserId(), question, callback); + } + + /** + * 异步聊天的方式 + * + * @param source + * @param question + */ + public ChatRecordsVo asyncChat(AISourceEnum source, String question, Consumer callback) { + return chatServiceFactory.getChatService(source) + .asyncChat(ReqInfoContext.getReqInfo().getUserId(), question, callback); + } + + /** + * 返回历史聊天记录 + * + * @param source + * @return + */ + public ChatRecordsVo history(AISourceEnum source) { + source = source == null ? getRecommendAiSource() : source; + return chatServiceFactory.getChatService(source).getChatHistory(ReqInfoContext.getReqInfo().getUserId(), source); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/bot/AiBots.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/bot/AiBots.java new file mode 100644 index 000000000..544900051 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/bot/AiBots.java @@ -0,0 +1,37 @@ +package com.github.paicoding.forum.service.chatai.bot; + +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Objects; + +/** + * @author YiHui + * @date 2025/2/24 + */ +@Service +public class AiBots { + @Autowired + private HaterBot haterBot; + + /** + * 判断目标用户是否为AI机器人 + * + * @param userId + * @return + */ + public boolean aiBots(Long userId) { + return Objects.equals(userId, haterBot.getBotUser().getUserId()); + } + + /** + * 自动补齐AI机器人的提示词 + * + * @param userId + * @return + */ + public ChatItemVo autoBuildPrompt(Long userId) { + return haterBot.addPrompt(userId); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/bot/HaterBot.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/bot/HaterBot.java new file mode 100644 index 000000000..10988e305 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/bot/HaterBot.java @@ -0,0 +1,105 @@ +package com.github.paicoding.forum.service.chatai.bot; + +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.api.model.enums.ChatAnswerTypeEnum; +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.enums.ai.AiBotEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; +import com.github.paicoding.forum.core.async.AsyncUtil; +import com.github.paicoding.forum.service.chatai.ChatFacade; +import com.github.paicoding.forum.service.chatai.constants.ChatConstants; +import com.github.paicoding.forum.service.user.service.RegisterService; +import com.github.paicoding.forum.service.user.service.UserService; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * 基于大模型的杠精机器人 + * + * @author YiHui + * @date 2025/2/24 + */ +@Component +public class HaterBot { + + @Autowired + private ChatFacade chatFacade; + + @Autowired + private UserService userService; + + @Autowired + private RegisterService registerService; + + private Supplier haterBotUser = Suppliers.memoizeWithExpiration(() -> { + BaseUserInfoDTO user = userService.queryUserByLoginName(AiBotEnum.HATER_BOT.getUserName()); + if (user == null) { + // 避免某些同学本地使用的版本,无法借助Liquid实现自动初始化AI机器人;我们这里加一个兜底的创建逻辑 + Long userId = registerService.registerSystemUser(AiBotEnum.HATER_BOT.getUserName(), AiBotEnum.HATER_BOT.getUserName(), "https://cdn.tobebetterjavaer.com/paicoding/e0f01d775d3f67b309b394bc04d4e091.jpg"); + user = userService.queryBasicUserInfo(userId); + } + return user; + }, 1, TimeUnit.HOURS); + + /** + * 触发AI机器人 + * + * @param question + * @return + */ + public void trigger(String question, String sourceBizId, Consumer consumer) { + BaseUserInfoDTO user = haterBotUser.get(); + AsyncUtil.execute(() -> { + // 设置AI机器人问答上下文 + ReqInfoContext.ReqInfo reqInfo = new ReqInfoContext.ReqInfo(); + reqInfo.setUser(user); + reqInfo.setUserId(user.getUserId()); + reqInfo.setChatId(sourceBizId); + ReqInfoContext.addReqInfo(reqInfo); + + chatFacade.autoChat(AISourceEnum.DEEP_SEEK, question, vo -> { + ChatItemVo item = vo.getRecords().get(0); + if (item.getAnswerType() == ChatAnswerTypeEnum.JSON + || item.getAnswerType() == ChatAnswerTypeEnum.TEXT + || item.getAnswerType() == ChatAnswerTypeEnum.STREAM_END) { + try { + consumer.accept(item.getAnswer()); + } finally { + // 清空上下文信息 + ReqInfoContext.clear(); + } + } + }); + }); + } + + /** + * 获取杠精机器人相关信息 + * + * @return + */ + public BaseUserInfoDTO getBotUser() { + return haterBotUser.get(); + } + + /** + * 添加机器人提示词 + * + * @param userId + * @return + */ + public ChatItemVo addPrompt(Long userId) { + if (Objects.equals(userId, getBotUser().getUserId())) { + return new ChatItemVo() + .setQuestion(ChatConstants.PROMPT_TAG + AiBotEnum.HATER_BOT.getPrompt()); + } + return null; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/constants/ChatConstants.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/constants/ChatConstants.java new file mode 100644 index 000000000..9f578ebb0 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/constants/ChatConstants.java @@ -0,0 +1,123 @@ +package com.github.paicoding.forum.service.chatai.constants; + +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.chat.ChatRecordsVo; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * @author YiHui + * @date 2023/6/2 + */ +public final class ChatConstants { + /** + * 记录每个用户的使用次数 + */ + public static String getAiRateKey(AISourceEnum ai) { + return "chat.rates." + ai.name().toLowerCase(); + } + + public static String getAiRateKeyPerDay(AISourceEnum ai) { + return "chat.rates." + ai.name().toLowerCase() + "-" + LocalDate.now(); + } + + /** + * 对话列表缓存 + * + * @param ai + * @param user + * @return + */ + public static String getAiChatListKey(AISourceEnum ai, Long user) { + return "chat.list." + ai.name().toLowerCase() + "." + user; + } + + /** + * 聊天历史记录 + * + * @param ai + * @param user + * @return + */ + public static String getAiHistoryRecordsKey(AISourceEnum ai, Long user) { + return "chat.history." + ai.name().toLowerCase() + "." + user; + } + + /** + * 聊天历史记录 + * + * @param ai + * @param user + * @return + */ + public static String getAiHistoryRecordsKey(AISourceEnum ai, String user) { + return "chat.history." + ai.name().toLowerCase() + "." + user; + } + + /** + * 聊天历史构建问答上下问 + * + * @param chatList 聊天记录,包含历史聊天内容,最新的提问在前面 + * @param function 实体转换方式 + * @param + * @return + */ + public static List toMsgList(List chatList, Function> function) { + int qaCnt = chatList.size(); + List ans = new ArrayList<>(qaCnt << 1); + for (int i = qaCnt - 1; i >= 0; i--) { + ans.addAll(function.apply(chatList.get(i))); + } + return ans; + } + + /** + * 每个用户的最大使用次数 + */ + public static final int MAX_CHATGPT_QAS_CNT = 10; + + /** + * 最多保存的历史聊天记录 + */ + public static final int MAX_HISTORY_RECORD_ITEMS = 500; + + /** + * 两次提问的间隔时间,要求20s + */ + public static final long QAS_TIME_INTERVAL = 20_000; + + + public static final String CHAT_REPLY_RECOMMEND = "请注册技术派之后再来体验吧,技术派官网: \n https://paicoding.com"; + public static final String CHAT_REPLY_BEGIN = "让我们开始体验ChatGPT的魅力吧~"; + public static final String CHAT_REPLY_OVER = "体验结束,让我们下次再见吧~"; + public static final String CHAT_REPLY_CNT_OVER = "次数使用完了哦,勾搭一下群主,多申请点使用次数吧~\n微信:itwanger"; + + + public static final String CHAT_REPLY_TIME_WAITING = "chatgpt还在努力回答中,请等待几秒之后再问一次吧...."; + public static final String CHAT_REPLY_QAS_TOO_FAST = "提问太频繁了,喝一杯咖啡,暂缓一下..."; + + + public static final String TOKEN_OVER = "您的免费次数已经使用完毕了!"; + + /** + * 异步聊天时返回得提示文案 + */ + public static final String ASYNC_CHAT_TIP = "小派正在努力回答中, 耐心等待一下吧..."; + + /** + * 请切换到其他大模型 + */ + public static final String SWITCH_TO_OTHER_MODEL = "当前模型还在开发当中,请右上角下拉框切换到其他模型"; + + + public static final String SENSITIVE_QUESTION = "提问中包含敏感词:%s,请微信联系二哥「itwanger」加入白名单!"; + + /** + * 提示词标识 + */ + public static final String PROMPT_TAG = "prompt-"; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/package-info.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/package-info.java new file mode 100644 index 000000000..c50aef44e --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/package-info.java @@ -0,0 +1,13 @@ +/** + * @author YiHui + * @date 2023/7/9 + */ +package com.github.paicoding.forum.service.chatai; + + +/* + 本包下,主要为派聪明相关的实现,强烈推荐配合相关的教程进行理解 + 1. https://www.yuque.com/itwanger/az7yww/oobmcdkym1232f6k?singleDoc# 《✅技术派实现自定义配置注入与动态刷新》 + 2. https://www.yuque.com/itwanger/az7yww/gegzgwh2t6zsutf3?singleDoc# 《✅技术派设计模式之策略模式在派聪明的实战演练》 + 3. https://www.yuque.com/itwanger/az7yww/dr4ga8zwraw9yopu?singleDoc# 《✅技术派设计模式之抽象设计模式在派聪明的实战演练 + */ \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/AbsChatService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/AbsChatService.java new file mode 100644 index 000000000..60bda70bb --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/AbsChatService.java @@ -0,0 +1,292 @@ +package com.github.paicoding.forum.service.chatai.service; + +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.enums.ai.AiChatStatEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.chat.ChatRecordsVo; +import com.github.paicoding.forum.core.cache.RedisClient; +import com.github.paicoding.forum.core.senstive.SensitiveService; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.service.chatai.ChatFacade; +import com.github.paicoding.forum.service.chatai.bot.AiBots; +import com.github.paicoding.forum.service.chatai.constants.ChatConstants; +import com.github.paicoding.forum.service.user.service.UserAiService; +import com.google.common.collect.Sets; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * 聊天的抽象模板类 + * + * @author YiHui + * @date 2023/6/9 + */ +@Slf4j +@Service +public abstract class AbsChatService implements ChatService { + @Autowired + private UserAiService userAiService; + @Autowired + private SensitiveService sensitiveService; + @Autowired + private ChatHistoryService chatHistoryService; + + @Value("${ai.maxNum.historyContextCnt:10}") + protected Integer chatHistoryContextNum; + + + /** + * 查询已经使用的次数 + * + * @param user + * @return + */ + protected int queryUserdCnt(Long user) { + Integer cnt = RedisClient.hGet(ChatConstants.getAiRateKeyPerDay(source()), String.valueOf(user), Integer.class); + if (cnt == null) { + cnt = 0; + } + return cnt; + } + + + /** + * 使用次数+1 + * + * @param user + * @return + */ + protected Long incrCnt(Long user) { + String key = ChatConstants.getAiRateKeyPerDay(source()); + Long cnt = RedisClient.hIncr(key, String.valueOf(user), 1); + if (cnt == 1L) { + // 做一个简单的判定,如果是某个用户的第一次提问,那就刷新一下这个缓存的有效期 + // fixme 这里有个不太优雅的地方:每新来一个用户,会导致这个有效期重新刷一边,可以通过再查一下hash的key个数,如果只有一个才进行重置有效期;这里出于简单考虑,省了这一步 + RedisClient.expire(key, 86400L); + } + return cnt; + } + + /** + * 保存聊天记录 + */ + protected void recordChatItem(Long user, ChatItemVo item) { + // 保存聊天记录 + chatHistoryService.saveRecord(source(), user, ReqInfoContext.getReqInfo().getChatId(), item); + } + + /** + * 查询用户的聊天历史 + * + * @return + */ + public ChatRecordsVo getChatHistory(Long user, AISourceEnum aiSource) { + if (aiSource == null) { + aiSource = source(); + } + List chats = chatHistoryService.listHistory(source(), user, ReqInfoContext.getReqInfo().getChatId(), null); + chats.add(0, new ChatItemVo().initAnswer(String.format("开始你和派聪明(%s-大模型)的AI之旅吧!", aiSource.getName()))); + ChatRecordsVo vo = new ChatRecordsVo(); + vo.setMaxCnt(getMaxQaCnt(user)); + vo.setUsedCnt(queryUserdCnt(user)); + vo.setSource(source()); + vo.setRecords(chats); + return vo; + } + + @Override + public ChatRecordsVo chat(Long user, String question) { + // 构建提问、返回的实体类,计算使用次数,最大次数 + ChatRecordsVo res = initResVo(user, question); + if (!res.hasQaCnt()) { + return res; + } + + // 执行提问 + answer(user, res); + // 返回AI应答结果 + return res; + } + + @Override + public ChatRecordsVo chat(Long user, String question, Consumer consumer) { + ChatRecordsVo res = initResVo(user, question); + if (!res.hasQaCnt()) { + return res; + } + + // 同步聊天时,直接返回结果 + answer(user, res); + consumer.accept(res); + return res; + } + + private ChatRecordsVo initResVo(Long user, String question) { + ChatRecordsVo res = new ChatRecordsVo(); + res.setSource(source()); + int maxCnt = getMaxQaCnt(user); + int usedCnt = queryUserdCnt(user); + res.setMaxCnt(maxCnt); + res.setUsedCnt(usedCnt); + + ChatItemVo item = new ChatItemVo().initQuestion(question); + if (!res.hasQaCnt()) { + // 次数已经使用完毕,不需要再与AI进行交互了;直接返回 + item.initAnswer(ChatConstants.TOKEN_OVER); + res.setRecords(Arrays.asList(item)); + return res; + } + + // 构建多轮对话的聊天上下文 + List history = buildChatContext(user); + history.add(0, item); + res.setRecords(history); + return res; + } + + /** + * 构建聊天上下文 + * 该方法旨在为用户构建一个聊天上下文,基于用户的聊天历史记录 + * 特别注意,对于多轮对话,我们仅取最近的十条记录作为上下文,如果聊天中存在提示词,则提示词之前的聊天全部丢掉 + * + * @param user 用户ID,用于识别和获取特定用户的聊天历史 + * @return 返回一个包含聊天上下文的ChatItemVo对象列表 + */ + private List buildChatContext(Long user) { + // 用于多轮对话,我们这里只取最近的十条作为上下文传参 + List history = chatHistoryService.listHistory(source(), user, ReqInfoContext.getReqInfo().getChatId(), chatHistoryContextNum); + if (CollectionUtils.isEmpty(history) || history.size() == 1) { + return history; + } + + // 过滤掉提示词之前的消息 + Iterator iterator = history.iterator(); + boolean toRemove = false; + while (iterator.hasNext()) { + ChatItemVo tmp = iterator.next(); + if (!toRemove) { + if (tmp.getQuestion().startsWith(ChatConstants.PROMPT_TAG)) { + // 找到提示词,之后的全部删除 + toRemove = true; + } + } else { + iterator.remove(); + } + } + return history; + } + + protected AiChatStatEnum answer(Long user, ChatRecordsVo res) { + ChatItemVo itemVo = res.getRecords().get(0); + AiChatStatEnum ans; + List sensitiveWords = sensitiveService.contains(itemVo.getQuestion()); + if (!CollectionUtils.isEmpty(sensitiveWords)) { + itemVo.initAnswer(String.format(ChatConstants.SENSITIVE_QUESTION, sensitiveWords)); + ans = AiChatStatEnum.ERROR; + } else { + ans = doAnswer(user, itemVo); + if (ans == AiChatStatEnum.END) { + processAfterSuccessedAnswered(user, res); + } + } + return ans; + } + + /** + * 提问,并将结果写入chat + * + * @param user + * @param chat + * @return true 表示正确回答了; false 表示回答出现异常 + */ + public abstract AiChatStatEnum doAnswer(Long user, ChatItemVo chat); + + /** + * 成功返回之后的后置操作 + * + * @param user + * @param response + */ + protected void processAfterSuccessedAnswered(Long user, ChatRecordsVo response) { + // 回答成功,保存聊天记录,剩余次数-1 + response.setUsedCnt(incrCnt(user).intValue()); + recordChatItem(user, response.getRecords().get(0)); + } + + /** + * 异步聊天,即提问并不要求直接得到接口;等后台准备完毕之后再写入对应的结果 + * + * @param user + * @param question + * @param consumer 执行成功之后,直接异步回调的通知 + * @return + */ + @Override + public ChatRecordsVo asyncChat(Long user, String question, Consumer consumer) { + ChatRecordsVo res = initResVo(user, question); + if (!res.hasQaCnt()) { + // 次数使用完毕 + consumer.accept(res); + return res; + } + + List sensitiveWord = sensitiveService.contains(res.getRecords().get(0).getQuestion()); + if (!CollectionUtils.isEmpty(sensitiveWord) && !SpringUtil.getBean(AiBots.class).aiBots(user)) { + // 机器人不进行敏感词校验 + // 包含敏感词的提问,直接返回异常 + res.getRecords().get(0).initAnswer(String.format(ChatConstants.SENSITIVE_QUESTION, sensitiveWord)); + consumer.accept(res); + } else { + final ChatRecordsVo newRes = res.clone(); + AiChatStatEnum needReturn = doAsyncAnswer(user, newRes, (ans, vo) -> { + if (ans == AiChatStatEnum.END) { + // 只有最后一个会话,即ai的回答结束,才需要进行持久化,并计数 + processAfterSuccessedAnswered(user, newRes); + } else if (ans == AiChatStatEnum.ERROR) { + // 执行异常,更新AI模型 + SpringUtil.getBean(ChatFacade.class).refreshAiSourceCache(Sets.newHashSet(source())); + } + // ai异步返回结果之后,我们将结果推送给前端用户 + consumer.accept(newRes); + }); + + if (needReturn.needResponse()) { + // 异步响应时,为了避免长时间的等待,这里直接响应用户的提问,返回一个稍等得提示文案 + ChatItemVo nowItem = res.getRecords().get(0); + nowItem.initAnswer(ChatConstants.ASYNC_CHAT_TIP); + consumer.accept(res); + } + } + return res; + } + + /** + * 异步返回结果 + * + * @param user + * @param response 保存提问 & 返回的结果,最终会返回给前端用户 + * @param consumer 具体将 response 写回前端的实现策略 + * @return 返回的会话状态,控制是否需要将结果直接返回给前端 + */ + public abstract AiChatStatEnum doAsyncAnswer(Long user, ChatRecordsVo response, BiConsumer consumer); + + /** + * 查询当前用户最多可提问的次数 + * + * @param user + * @return + */ + protected int getMaxQaCnt(Long user) { + return userAiService.getMaxChatCnt(user); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/ChatHistoryService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/ChatHistoryService.java new file mode 100644 index 000000000..bb8b9bcaa --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/ChatHistoryService.java @@ -0,0 +1,47 @@ +package com.github.paicoding.forum.service.chatai.service; + +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.chat.ChatSessionItemVo; + +import java.util.List; + +/** + * 对话会话记录服务 + * + * @author YiHui + * @date 2025/2/7 + */ +public interface ChatHistoryService { + /** + * 获取对话列表 + * + * @param source AI模型 + * @return + */ + List listChatSessions(AISourceEnum source, Long userId); + + /** + * 获取对话记录 + * + * @param source AI模型 + * @param chatId 对话id + * @param size 记录条数 + * @return 对话记录 + */ + List listHistory(AISourceEnum source, Long userId, String chatId, Integer size); + + /** + * 保存最新的一条对话内容 + * + * @param source AI模型 + * @param chatId 对话id + * @param item 对话内容 + */ + void saveRecord(AISourceEnum source, Long userId, String chatId, ChatItemVo item); + + + Boolean updateChatSessionName(AISourceEnum source, String chatId, String title, Long userId); + + Boolean removeChatSession(AISourceEnum source, String chatId, Long userId); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/ChatService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/ChatService.java new file mode 100644 index 000000000..1e171ac69 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/ChatService.java @@ -0,0 +1,69 @@ +package com.github.paicoding.forum.service.chatai.service; + +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatRecordsVo; + +import java.util.function.Consumer; + +/** + * @author YiHui + * @date 2023/6/9 + */ +public interface ChatService { + + /** + * 具体AI选择 + * + * @return + */ + AISourceEnum source(); + + /** + * 是否时异步优先 + * + * @return + */ + default boolean asyncFirst() { + return true; + } + + /** + * 开始进入聊天 + * + * @param user 提问人 + * @param question 聊天的问题 + * @return 返回的结果 + */ + ChatRecordsVo chat(Long user, String question); + + /** + * 开始进入聊天 + * + * @param user 提问人 + * @param question 聊天的问题 + * @param consumer 接收到AI返回之后可执行的回调 + * @return 同步直接返回的结果 + */ + ChatRecordsVo chat(Long user, String question, Consumer consumer); + + /** + * 异步聊天 + * + * @param user + * @param question + * @param consumer 执行成功之后,直接异步回调的通知 + * @return 同步直接返回的结果 + */ + ChatRecordsVo asyncChat(Long user, String question, Consumer consumer); + + + /** + * 查询聊天历史 + * + * @param user + * @param aiSource + * @return + */ + ChatRecordsVo getChatHistory(Long user, AISourceEnum aiSource); + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/ChatServiceFactory.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/ChatServiceFactory.java new file mode 100644 index 000000000..4a026857a --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/ChatServiceFactory.java @@ -0,0 +1,29 @@ +package com.github.paicoding.forum.service.chatai.service; + +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.google.common.collect.Maps; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @author YiHui + * @date 2023/7/2 + */ +@Component +public class ChatServiceFactory { + private final Map chatServiceMap; + + + public ChatServiceFactory(List chatServiceList) { + chatServiceMap = Maps.newHashMapWithExpectedSize(chatServiceList.size()); + for (ChatService chatService : chatServiceList) { + chatServiceMap.put(chatService.source(), chatService); + } + } + + public ChatService getChatService(AISourceEnum aiSource) { + return chatServiceMap.get(aiSource); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/ChatgptService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/ChatgptService.java new file mode 100644 index 000000000..79cf3c6e8 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/ChatgptService.java @@ -0,0 +1,25 @@ +package com.github.paicoding.forum.service.chatai.service; + +/** + * @author YiHui + * @date 2023/6/2 + */ +public interface ChatgptService { + + /** + * 判断是否在会话中 + * + * @param wxUuid + * @return + */ + boolean inChat(String wxUuid, String content); + + /** + * 开始进入聊天 + * + * @param content 输入的内容 + * @return chatgpt返回的结果 + */ + String chat(String wxUuid, String content); + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/history/ChatHistoryServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/history/ChatHistoryServiceImpl.java new file mode 100644 index 000000000..c59f69743 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/history/ChatHistoryServiceImpl.java @@ -0,0 +1,163 @@ +package com.github.paicoding.forum.service.chatai.service.history; + +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.chat.ChatSessionItemVo; +import com.github.paicoding.forum.core.cache.RedisClient; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.service.chatai.bot.AiBots; +import com.github.paicoding.forum.service.chatai.bot.HaterBot; +import com.github.paicoding.forum.service.chatai.constants.ChatConstants; +import com.github.paicoding.forum.service.chatai.service.ChatHistoryService; +import com.github.paicoding.forum.service.user.service.UserAiService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 对话历史记录 + * + * @author YiHui + * @date 2025/2/7 + */ +@Service +public class ChatHistoryServiceImpl implements ChatHistoryService { + @Autowired + private UserAiService userAiService; + @Autowired + private AiBots aiBots; + + /** + * 列出聊天会话 + *

+ * 根据用户ID和AI源枚举获取聊天会话列表从Redis中通过哈希结构存储的键值对获取所有会话项, + * 并按更新时间降序排序返回 + * + * @param source AI源枚举,用于区分不同的AI来源 + * @param userId 用户ID,用于获取特定用户的聊天会话 + * @return 返回一个ChatSessionItemVo对象列表,包含用户的聊天会话项 + */ + @Override + public List listChatSessions(AISourceEnum source, Long userId) { + // 构造Redis中哈希结构的键 + String key = ChatConstants.getAiChatListKey(source, userId); + // 从Redis中获取所有会话项,使用hGetAll方法获取哈希表中所有的字段和值 + Map map = RedisClient.hGetAll(key, ChatSessionItemVo.class); + // 将Map中的值转换为List + List list = new ArrayList<>(map.values()); + // 对列表按更新时间降序排序 + list.sort((o1, o2) -> o2.getUpdateTime().compareTo(o1.getUpdateTime())); + // 返回排序后的列表 + return list; + } + + @Override + public List listHistory(AISourceEnum source, Long userId, String chatId, Integer size) { + size = size == null ? 50 : size; + List list = RedisClient.lRange(getChatIdKey(source, userId, chatId), 0, size, ChatItemVo.class); + + // 对于特殊的交互机器人,自动补齐相关的提示词 + ChatItemVo prompt = aiBots.autoBuildPrompt(userId); + if (prompt != null) { + list.add(prompt); + } + return list; + } + + /** + * 保存聊天记录 + * + * @param source 聊天来源,用于区分不同的聊天场景或平台 + * @param userId 用户ID,用于关联用户信息 + * @param chatId 聊天ID,用于标识特定的聊天会话 + * @param item 聊天项内容,包括用户的问题和AI的回答 + */ + @Override + public void saveRecord(AISourceEnum source, Long userId, String chatId, ChatItemVo item) { + // 写入 MySQL + userAiService.pushChatItem(source, userId, item); + + // 更新redis缓存数据 + String key = getChatIdKey(source, userId, chatId); + RedisClient.lPush(key, item); + + // 维护对话记录 + String sessionKey = ChatConstants.getAiChatListKey(source, userId); + ChatSessionItemVo session = RedisClient.hGet(sessionKey, chatId, ChatSessionItemVo.class); + if (session == null) { + // 如果当前会话不存在,则创建新会话记录 + session = new ChatSessionItemVo(); + session.setChatId(chatId); + session.setTitle(!item.getQuestion().startsWith(ChatConstants.PROMPT_TAG) ? item.getQuestion() : item.getQuestion().substring(ChatConstants.PROMPT_TAG.length())); + session.setCreatTime(System.currentTimeMillis()); + session.setUpdateTime(session.getCreatTime()); + session.setQasCnt(1); + } else { + // 如果会话已存在,则更新会话记录 + session.setUpdateTime(System.currentTimeMillis()); + session.setQasCnt(session.getQasCnt() + 1); + } + RedisClient.hSet(sessionKey, chatId, session); + + // 限制对话记录数量,最多保存五百条历史聊天记录 + if (session.getQasCnt() > ChatConstants.MAX_HISTORY_RECORD_ITEMS) { + RedisClient.lTrim(key, 0, ChatConstants.MAX_HISTORY_RECORD_ITEMS); + } + + } + + /** + * 更新聊天会话的名称 + * + * @param source 聊天会话的来源,用于区分不同的AI来源 + * @param chatId 聊天会话的唯一标识符 + * @param title 新的聊天会话名称 + * @param userId 用户的唯一标识符 + * @return 如果会话名称被更新,则返回true;否则返回false + */ + @Override + public Boolean updateChatSessionName(AISourceEnum source, String chatId, String title, Long userId) { + // 构造Redis中存储聊天列表的键 + String key = ChatConstants.getAiChatListKey(source, userId); + + // 从Redis中获取指定聊天会话的详细信息 + ChatSessionItemVo item = RedisClient.hGet(key, chatId, ChatSessionItemVo.class); + + // 检查获取到的聊天会话信息是否不为空,并且新标题与旧标题不同 + if (item != null && !Objects.equals(item.getTitle(), title)) { + // 更新聊天会话的标题 + item.setTitle(title); + + // 将更新后的聊天会话信息保存回Redis + return RedisClient.hSet(key, chatId, item); + } + // 如果聊天会话信息未更改,则直接返回true + return true; + } + + /** + * 重写移除聊天会话的方法 + * + * @param source 数据源枚举,用于区分不同的AI来源 + * @param chatId 聊天会话的唯一标识符 + * @param userId 用户的唯一标识符 + * @return 返回操作的布尔结果,表示是否成功移除会话 + */ + @Override + public Boolean removeChatSession(AISourceEnum source, String chatId, Long userId) { + // 构造Redis中AI聊天列表的键 + String key = ChatConstants.getAiChatListKey(source, userId); + // 使用Redis的hDel命令移除指定的聊天会话,并返回操作结果 + RedisClient.hDel(key, chatId); + return true; + } + + private String getChatIdKey(AISourceEnum source, Long userId, String chatId) { + return StringUtils.isBlank(chatId) ? ChatConstants.getAiHistoryRecordsKey(source, userId) : ChatConstants.getAiHistoryRecordsKey(source, userId + ":" + chatId); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/ali/AliAiServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/ali/AliAiServiceImpl.java new file mode 100644 index 000000000..b78582b42 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/ali/AliAiServiceImpl.java @@ -0,0 +1,47 @@ +package com.github.paicoding.forum.service.chatai.service.impl.ali; + +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.enums.ai.AiChatStatEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.chat.ChatRecordsVo; +import com.github.paicoding.forum.service.chatai.service.AbsChatService; +import com.github.paicoding.forum.service.chatai.service.impl.zhipu.ZhipuIntegration; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.function.BiConsumer; + +@Slf4j +@Service +public class AliAiServiceImpl extends AbsChatService { + + @Autowired + private AliIntegration aliIntegration; + + @Override + public AiChatStatEnum doAnswer(Long user, ChatItemVo chat) { + if (aliIntegration.directReturn(user, chat)) { + return AiChatStatEnum.END; + } + return AiChatStatEnum.ERROR; + } + + @Override + public AiChatStatEnum doAsyncAnswer(Long user, ChatRecordsVo chatRes, BiConsumer consumer) { + aliIntegration.streamReturn(user, chatRes, consumer); + return AiChatStatEnum.IGNORE; + } + + @Override + public AISourceEnum source() { + return AISourceEnum.ALI_AI; + } + + @Override + public boolean asyncFirst() { + // true 表示优先使用异步返回; false 表示同步等待结果 + return true; + } + +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/ali/AliIntegration.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/ali/AliIntegration.java new file mode 100644 index 000000000..9fe66119a --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/ali/AliIntegration.java @@ -0,0 +1,148 @@ +package com.github.paicoding.forum.service.chatai.service.impl.ali; + +import cn.idev.excel.util.StringUtils; +import com.alibaba.dashscope.aigc.generation.Generation; +import com.alibaba.dashscope.aigc.generation.GenerationParam; +import com.alibaba.dashscope.aigc.generation.GenerationResult; +import com.alibaba.dashscope.common.Message; +import com.alibaba.dashscope.common.ResultCallback; +import com.alibaba.dashscope.common.Role; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.InputRequiredException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.alibaba.dashscope.utils.JsonUtils; +import com.github.paicoding.forum.api.model.enums.ChatAnswerTypeEnum; +import com.github.paicoding.forum.api.model.enums.ai.AiChatStatEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.chat.ChatRecordsVo; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.service.chatai.constants.ChatConstants; +import com.zhipu.oapi.service.v4.model.ChatMessageAccumulator; +import com.zhipu.oapi.service.v4.model.ModelData; +import io.reactivex.Flowable; +import lombok.Data; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Semaphore; +import java.util.function.BiConsumer; + +@Slf4j +@Setter +@Component +public class AliIntegration { + @Autowired + private AliConfig config; + + public void streamReturn(Long user, ChatRecordsVo chatRecord, BiConsumer callback) { + try { + ChatItemVo item = chatRecord.getRecords().get(0); + + Generation gen = new Generation(); + // 支持上下文的多轮聊天 + List userMsgList = ChatConstants.toMsgList(chatRecord.getRecords(), this::toMsg); + GenerationParam param = GenerationParam.builder() + .model(config.getModel()) + .messages(userMsgList) + .resultFormat(GenerationParam.ResultFormat.MESSAGE) + .incrementalOutput(true) + .build(); + Semaphore semaphore = new Semaphore(0); + StringBuilder fullContent = new StringBuilder(); + + gen.streamCall(param, new ResultCallback() { + @Override + public void onEvent(GenerationResult message) { + String content = message.getOutput().getChoices().get(0).getMessage().getContent(); + fullContent.append(content); + log.info("Received message: {}", JsonUtils.toJson(message)); + item.appendAnswer(content); + callback.accept(AiChatStatEnum.MID, chatRecord); + } + + @Override + public void onError(Exception err) { + callback.accept(AiChatStatEnum.ERROR, chatRecord); + log.error("Exception occurred: {}", err.getMessage()); + semaphore.release(); + } + + @Override + public void onComplete() { + item.setAnswerType(ChatAnswerTypeEnum.STREAM_END); + callback.accept(AiChatStatEnum.END, chatRecord); + log.info("Completed"); + semaphore.release(); + } + }); + + semaphore.acquire(); + log.info("Full content: \n{}", fullContent.toString()); + } catch (ApiException | NoApiKeyException | InputRequiredException | InterruptedException e) { + log.error("An exception occurred: {}", e.getMessage()); + } + } + + @Component + @ConfigurationProperties(prefix = "ali") + @Data + public static class AliConfig { + public String model; + } + + public boolean directReturn(Long user, ChatItemVo chat) { + Generation gen = new Generation(); + Message systemMsg = Message.builder() + .role(Role.SYSTEM.getValue()) + .content("You are a helpful assistant.") + .build(); + Message userMsg = Message.builder() + .role(Role.USER.getValue()) + .content(chat.getQuestion()) + .build(); + GenerationParam param = GenerationParam.builder() + .model(config.getModel()) + .messages(Arrays.asList(systemMsg, userMsg)) + .resultFormat(GenerationParam.ResultFormat.MESSAGE) + .build(); + + try { + GenerationResult invokeModelApiResp = gen.call(param); + + chat.initAnswer(JsonUtil.toStr(invokeModelApiResp), ChatAnswerTypeEnum.JSON); + log.info("阿里 AI 试用! 传参:{}, 返回:{}", chat, invokeModelApiResp); + } catch (NoApiKeyException | InputRequiredException e) { + throw new RuntimeException(e); + } + + return true; + } + + public static Flowable mapStreamToAccumulator(Flowable flowable) { + return flowable.map(chunk -> { + return new ChatMessageAccumulator(chunk.getChoices().get(0).getDelta(), null, chunk.getChoices().get(0), chunk.getUsage(), chunk.getCreated(), chunk.getId()); + }); + } + + private List toMsg(ChatItemVo item) { + List list = new ArrayList<>(2); + if (item.getQuestion().startsWith(ChatConstants.PROMPT_TAG)) { + // 提示词消息 + list.add(Message.builder().role(Role.SYSTEM.getValue()).content(item.getQuestion().substring(ChatConstants.PROMPT_TAG.length())).build()); + return list; + } + + // 用户问答 + list.add(Message.builder().role(Role.USER.getValue()).content(item.getQuestion()).build()); + if (StringUtils.isNotBlank(item.getAnswer())) { + list.add(Message.builder().role(Role.ASSISTANT.getValue()).content(item.getAnswer()).build()); + } + return list; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/chatgpt/ChatGptAiServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/chatgpt/ChatGptAiServiceImpl.java new file mode 100644 index 000000000..9f7a8cc62 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/chatgpt/ChatGptAiServiceImpl.java @@ -0,0 +1,97 @@ +package com.github.paicoding.forum.service.chatai.service.impl.chatgpt; + +import com.github.paicoding.forum.api.model.enums.ChatAnswerTypeEnum; +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.enums.ai.AiChatStatEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.chat.ChatRecordsVo; +import com.github.paicoding.forum.service.chatai.service.AbsChatService; +import com.plexpt.chatgpt.listener.AbstractStreamListener; +import lombok.extern.slf4j.Slf4j; +import okhttp3.sse.EventSource; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.function.BiConsumer; + +/** + * @author YiHui + * @date 2023/6/12 + */ +@Slf4j +@Service +public class ChatGptAiServiceImpl extends AbsChatService { + @Autowired + private ChatGptIntegration chatGptIntegration; + + @Override + public AiChatStatEnum doAnswer(Long user, ChatItemVo chat) { + if (chatGptIntegration.directReturn(user, chat)) { + return AiChatStatEnum.END; + } + return AiChatStatEnum.ERROR; + } + + @Override + public AiChatStatEnum doAsyncAnswer(Long user, ChatRecordsVo chatRes, BiConsumer consumer) { + ChatItemVo item = chatRes.getRecords().get(0); + AbstractStreamListener listener = new AbstractStreamListener() { + @Override + public void onMsg(String message) { + // 成功返回结果的场景 + if (StringUtils.isNotBlank(message)) { + item.appendAnswer(message); + consumer.accept(AiChatStatEnum.MID, chatRes); + if (log.isDebugEnabled()) { + log.debug("ChatGpt返回内容: {}", lastMessage); + } + } + } + + @Override + public void onClosed(EventSource eventSource) { + super.onClosed(eventSource); + // 检查是否正常结束对话 + if (item.getAnswerType() != ChatAnswerTypeEnum.STREAM_END) { + // 主动结束这一次的对话 + if (StringUtils.isBlank(lastMessage)) { + item.appendAnswer("大模型超时未返回结果,主动关闭会话;请重新提问吧\n").setAnswerType(ChatAnswerTypeEnum.STREAM_END); + consumer.accept(AiChatStatEnum.ERROR, chatRes); + } else { + item.appendAnswer("\n").setAnswerType(ChatAnswerTypeEnum.STREAM_END); + consumer.accept(AiChatStatEnum.END, chatRes); + } + } + } + + @Override + public void onError(Throwable throwable, String response) { + // 返回异常的场景 + item.appendAnswer("Error:" + (StringUtils.isBlank(response) ? throwable.getMessage() : response)) + .setAnswerType(ChatAnswerTypeEnum.STREAM_END); + consumer.accept(AiChatStatEnum.ERROR, chatRes); + } + }; + + // 注册回答结束的回调钩子 + listener.setOnComplate((s) -> { + item.appendAnswer("\n") + .setAnswerType(ChatAnswerTypeEnum.STREAM_END); + consumer.accept(AiChatStatEnum.END, chatRes); + }); + chatGptIntegration.streamReturn(user, chatRes.getRecords(), listener); + return AiChatStatEnum.IGNORE; + } + + @Override + public AISourceEnum source() { + return AISourceEnum.CHAT_GPT_3_5; + } + + @Override + public boolean asyncFirst() { + // true 表示优先使用异步返回; false 表示同步等待结果 + return true; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/chatgpt/ChatGptIntegration.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/chatgpt/ChatGptIntegration.java new file mode 100644 index 000000000..bcd8bdbd7 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/chatgpt/ChatGptIntegration.java @@ -0,0 +1,277 @@ +package com.github.paicoding.forum.service.chatai.service.impl.chatgpt; + +import cn.hutool.core.util.RandomUtil; +import com.github.paicoding.forum.api.model.enums.ChatAnswerTypeEnum; +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.core.net.ProxyCenter; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.service.chatai.constants.ChatConstants; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.plexpt.chatgpt.ChatGPT; +import com.plexpt.chatgpt.ChatGPTStream; +import com.plexpt.chatgpt.entity.billing.CreditGrantsResponse; +import com.plexpt.chatgpt.entity.chat.ChatChoice; +import com.plexpt.chatgpt.entity.chat.ChatCompletion; +import com.plexpt.chatgpt.entity.chat.ChatCompletionResponse; +import com.plexpt.chatgpt.entity.chat.Message; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import okhttp3.sse.EventSourceListener; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + + +/** + * chatgpt的交互封装集成 + * + * @author YiHui + * @date 2023/4/20 + */ +@Slf4j +@Service +public class ChatGptIntegration { + @Autowired + private ChatGptConfig config; + + @Data + @Configuration + @ConfigurationProperties(prefix = "chatgpt") + public static class ChatGptConfig { + /** + * 默认的模型 + */ + private AISourceEnum main; + private Map conf; + } + + @Data + public static class GptConf { + private List keys; + private boolean proxy; + private String apiHost; + private int timeOut; + private int maxToken; + + public String fetchKey() { + int index = RandomUtil.randomInt(keys.size()); + return keys.get(index); + } + } + + public static ChatCompletion.Model parse2GptMode(AISourceEnum model) { + if (model == AISourceEnum.CHAT_GPT_4) { + return ChatCompletion.Model.GPT_4; + } + return ChatCompletion.Model.GPT_3_5_TURBO; + } + + @PostConstruct + public void init() { + log.info("ChatGpt配置初始化完成: {}", config); + } + + /** + * 每个用户的会话缓存 + */ + public LoadingCache, ImmutablePair> cacheStream; + + @PostConstruct + public void initKey() { + cacheStream = CacheBuilder.newBuilder().expireAfterWrite(300, TimeUnit.SECONDS) + .build(new CacheLoader, ImmutablePair>() { + @Override + public ImmutablePair load(ImmutablePair s) throws Exception { + return ImmutablePair.of(null, null); + } + }); + } + + /** + * 基于routingkey进行路由,创建一个简单的GPTClient + * + * @param routingKey + * @return + */ + private ChatGPT simpleGPT(Long routingKey, AISourceEnum model) { + GptConf conf = config.getConf().getOrDefault(model, config.getConf().get(config.getMain())); + Proxy proxy = conf.isProxy() ? ProxyCenter.loadProxy(String.valueOf(routingKey)) : Proxy.NO_PROXY; + + return ChatGPT.builder().apiKeyList(conf.getKeys()).proxy(proxy).apiHost(conf.getApiHost()) //反向代理地址 + .timeout(conf.getTimeOut()).build().init(); + } + + /** + * 基于routingkey进行路由,创建一个简单的流式GPTClientStream + * + * @param routingKey + * @return + */ + private ChatGPTStream simpleStreamGPT(Long routingKey, AISourceEnum model) { + GptConf conf = config.getConf().getOrDefault(model, config.getConf().get(config.getMain())); + Proxy proxy = conf.isProxy() ? ProxyCenter.loadProxy(String.valueOf(routingKey)) : Proxy.NO_PROXY; + + return ChatGPTStream.builder().timeout(conf.getTimeOut()).apiKey(conf.fetchKey()).proxy(proxy) + .apiHost(conf.getApiHost()).build().init(); + } + + public ChatGPT getGpt(Long routingKey, AISourceEnum model) { + ImmutablePair key = ImmutablePair.of(routingKey, model); + ImmutablePair pair = cacheStream.getUnchecked(key); + ChatGPT gpt = pair.left; + if (gpt == null) { + gpt = simpleGPT(routingKey, model); + cacheStream.put(key, ImmutablePair.of(gpt, pair.right)); + } + return gpt; + } + + public ChatGPTStream getGptStream(Long routingKey, AISourceEnum model) { + ImmutablePair key = ImmutablePair.of(routingKey, model); + ImmutablePair pair = cacheStream.getUnchecked(key); + ChatGPTStream gpt = pair.right; + if (gpt == null) { + gpt = simpleStreamGPT(routingKey, model); + cacheStream.put(key, ImmutablePair.of(pair.left, gpt)); + } + return gpt; + } + + /** + * 账户信息 + * + * @return + */ + public CreditGrantsResponse creditInfo(AISourceEnum model) { + CreditGrantsResponse response = getGpt(0L, model).creditGrants(); + return response; + } + + public boolean directReturn(Long routingKey, ChatItemVo chat) { + + AISourceEnum selectModel = config.getMain(); + GptConf conf = config.getConf().getOrDefault(selectModel, config.getConf().get(config.getMain())); + ChatGPT gpt = getGpt(routingKey, config.getMain()); + try { + ChatCompletion chatCompletion = ChatCompletion.builder().model(parse2GptMode(selectModel).getName()) + .messages(Arrays.asList(Message.of(chat.getQuestion()))).maxTokens(conf.getMaxToken()).build(); + ChatCompletionResponse response = gpt.chatCompletion(chatCompletion); + List list = response.getChoices(); + chat.initAnswer(JsonUtil.toStr(list), ChatAnswerTypeEnum.JSON); + log.info("chatgpt试用! 传参:{}, 返回:{}", chat, list); + return true; + } catch (Exception e) { + // 对于系统异常,不用继续等待了 + chat.initAnswer(e.getMessage()); + log.info("chatgpt执行异常! key:{}", chat, e); + return false; + } + } + + /** + * 异步流式返回 + * + * @param routingKey + * @param chat + * @param listener + * @return + */ + public boolean streamReturn(Long routingKey, ChatItemVo chat, EventSourceListener listener) { + AISourceEnum selectModel = config.getMain(); + GptConf conf = config.getConf().getOrDefault(selectModel, config.getConf().get(config.getMain())); + ChatGPTStream chatGPTStream = simpleStreamGPT(routingKey, selectModel); + + ChatCompletion chatCompletion = ChatCompletion.builder().model(parse2GptMode(selectModel).getName()) + .messages(toMsg(chat)).maxTokens(conf.getMaxToken()).build(); + chatGPTStream.streamChatCompletion(chatCompletion, listener); + return true; + } + + /** + * 多轮对话,传递历史聊天上下文 + *

+ * 该方法负责将给定的聊天记录列表转换为消息列表,并使用选定的模型进行流式聊天完成处理 + * + * @param routingKey 路由键,用于选择不同的代理 + * @param chatList 聊天记录列表,包含多个ChatItemVo对象,最新的问答在前面 + * @param listener 事件源监听器,用于处理流式处理过程中的事件 + * @return 总是返回true,表示方法执行过程中的一个固定行为 + */ + public boolean streamReturn(Long routingKey, List chatList, EventSourceListener listener) { + // 选择要使用的模型 + AISourceEnum selectModel = config.getMain(); + // 获取配置,如果未找到对应模型的配置,则使用主配置 + GptConf conf = config.getConf().getOrDefault(selectModel, config.getConf().get(config.getMain())); + // 创建一个流式聊天GPT实例 + ChatGPTStream chatGPTStream = simpleStreamGPT(routingKey, selectModel); + + // 构建多轮聊天的上下文 + List msgList = ChatConstants.toMsgList(chatList, this::toMsg); + // 构建聊天完成对象,包括选定的模型、消息列表和最大令牌数配置 + ChatCompletion chatCompletion = ChatCompletion.builder().model(parse2GptMode(selectModel).getName()) + .messages(msgList).maxTokens(conf.getMaxToken()).build(); + + // 使用流式处理聊天完成,并通过监听器处理事件 + chatGPTStream.streamChatCompletion(chatCompletion, listener); + + // 固定返回值,表示方法执行完毕 + return true; + } + + /** + * 一个基础的chatgpt问答, 给微信公众号自动问答使用 + * + * @param routingKey + * @param record + * @return + */ + public boolean directReturn(Long routingKey, ChatRecordWxVo record) { + AISourceEnum selectModel = config.getMain(); + GptConf conf = config.getConf().getOrDefault(selectModel, config.getConf().get(config.getMain())); + ChatGPT gpt = getGpt(routingKey, config.getMain()); + try { + ChatCompletion chatCompletion = ChatCompletion.builder().model(parse2GptMode(selectModel).getName()) + .messages(Arrays.asList(Message.of(record.getQas()))).maxTokens(conf.getMaxToken()).build(); + ChatCompletionResponse response = gpt.chatCompletion(chatCompletion); + List list = response.getChoices(); + log.info("chatgpt试用! 传参:{}, 返回:{}", record.getQas(), list); + record.setRes(list); + return true; + } catch (Exception e) { + // 对于系统异常,不用继续等待了 + record.setSysErr(e.getMessage()); + log.info("chatgpt执行异常! key:{}", record.getQas(), e); + return false; + } + } + + private List toMsg(ChatItemVo item) { + List list = new ArrayList<>(2); + if (item.getQuestion().startsWith(ChatConstants.PROMPT_TAG)) { + // 提示词,构建完之后直接返回 + list.add(Message.ofSystem(item.getQuestion().substring(ChatConstants.PROMPT_TAG.length()))); + return list; + } + + // 用户问答消息 + list.add(Message.of(item.getQuestion())); + if (StringUtils.isNotBlank(item.getAnswer())) { + list.add(Message.ofAssistant(item.getAnswer())); + } + return list; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/chatgpt/ChatGptWxServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/chatgpt/ChatGptWxServiceImpl.java new file mode 100644 index 000000000..698195d9f --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/chatgpt/ChatGptWxServiceImpl.java @@ -0,0 +1,174 @@ +package com.github.paicoding.forum.service.chatai.service.impl.chatgpt; + +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.core.async.AsyncUtil; +import com.github.paicoding.forum.core.cache.RedisClient; +import com.github.paicoding.forum.service.chatai.constants.ChatConstants; +import com.github.paicoding.forum.service.chatai.service.ChatgptService; +import com.github.paicoding.forum.service.user.repository.entity.UserDO; +import com.github.paicoding.forum.service.user.service.UserService; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * chatgpt 服务: + *

+ * 1. 同一个用户,只有50次的提问机会,采用redis计数 + * 2. 因为微信有5s的自动回复超时,因此需要做一个容错兼容,当执行超过3.5s就提前返回,将结果保存到内存中,等待下次交互再进行返回 + * + * @author YiHui + * @date 2023/6/2 + */ +@Slf4j +@Service +public class ChatGptWxServiceImpl implements ChatgptService { + @Autowired + private ChatGptIntegration chatGptHelper; + private LoadingCache chatCache = CacheBuilder.newBuilder() + .expireAfterWrite(10, TimeUnit.MINUTES) + .maximumSize(50).build(new CacheLoader() { + @Override + public ChatRecordWxVo load(Long userId) throws Exception { + return new ChatRecordWxVo(); + } + }); + + private boolean rateLimit(Long userId) { + Integer cnt = RedisClient.hGet(ChatConstants.getAiRateKey(AISourceEnum.CHAT_GPT_3_5), String.valueOf(userId), Integer.class); + if (cnt == null) { + cnt = 0; + } + return cnt < ChatConstants.MAX_CHATGPT_QAS_CNT; + } + + private Long incrCnt(Long userId) { + return RedisClient.hIncr(ChatConstants.getAiRateKey(AISourceEnum.CHAT_GPT_3_5), String.valueOf(userId), 1); + } + + @Autowired + private UserService userService; + + + @Override + public boolean inChat(String wxUuid, String content) { + if (content != null && content.toLowerCase().trim().startsWith("chat")) { + return true; + } + + UserDO user = userService.getWxUser(wxUuid); + if (user == null) { + return false; + } + + // 存在会话记录,表示在会话中 + ReqInfoContext.getReqInfo().setUserId(user.getId()); + chatCache.cleanUp(); + return chatCache.getIfPresent(user.getId()) != null; + } + + @Override + public String chat(String wxUuid, String content) { + if (content.toLowerCase().trim().startsWith("chat")) { + // 开始会话 + UserDO user = userService.getWxUser(wxUuid); + if (user == null) { + return ChatConstants.CHAT_REPLY_RECOMMEND; + } + + chatCache.put(user.getId(), new ChatRecordWxVo()); + return ChatConstants.CHAT_REPLY_BEGIN; + } + + // 正常对话 + Long userId = ReqInfoContext.getReqInfo().getUserId(); + if (content.toLowerCase().trim().equalsIgnoreCase("end") || content.trim().startsWith("结束")) { + // 结束会话 + chatCache.invalidate(userId); + chatCache.cleanUp(); + return ChatConstants.CHAT_REPLY_OVER; + } + + if (!rateLimit(userId)) { + // 次数已经用完了,直接返回 + chatCache.cleanUp(); + return ChatConstants.CHAT_REPLY_CNT_OVER; + } + + // 判断用户的上一次访问结果有没有正确返回,如果没有,那么这一次的交互不响应,直接返回上一次的返回结果; + ChatRecordWxVo chatRecord = chatCache.getUnchecked(userId); + if (System.currentTimeMillis() - chatRecord.getQasTime() < ChatConstants.QAS_TIME_INTERVAL) { + // 限制交互频率 + if (chatRecord.canReply()) { + // 上次没有回复时;如果现在有结论了,那就回复一下 + return chatRecord.reply(); + } else { + return ChatConstants.CHAT_REPLY_QAS_TOO_FAST; + } + } + + + // 执行正常的提问、应答; 针对上次结果还没有拿到的场景做一个兼容,只有拿到结果之后,才继续响应后续的问答 + if (StringUtils.isBlank(chatRecord.getQas()) || chatRecord.isLastReturn()) { + // 首次提问 或者上次的提问正确返回了结果 + ChatRecordWxVo newRecord = doQuery(userId, content, chatRecord); + if (newRecord.canReply()) { + return chatRecord.reply(); + } else { + // 只有超时没拿到结果的场景,会走这里 + return ChatConstants.CHAT_REPLY_TIME_WAITING; + } + } else if (chatRecord.canReply()) { + // 判断上次的结果是否已经获取到了 + return chatRecord.reply(); + } else { + // 结果还没有拿到,继续等待 + return ChatConstants.CHAT_REPLY_TIME_WAITING; + } + } + + /** + * 执行具体的chatgpt请求,并做一个超时的限制 + * + * @param userId + * @param content + * @param currentChat + * @return + */ + private ChatRecordWxVo doQuery(Long userId, String content, ChatRecordWxVo currentChat) { + // 访问计数+1 + Long cnt = incrCnt(userId); + + // 重新构建当前的聊天记录 + ChatRecordWxVo newRecord = new ChatRecordWxVo().setPre(currentChat) + .setQasIndex(Optional.ofNullable(cnt).orElse(1L).intValue()); + newRecord.setQas(content).setLastReturn(false).setQasTime(System.currentTimeMillis()); + currentChat.setNext(newRecord); + chatCache.put(userId, newRecord); + + + try { + AsyncUtil.callWithTimeLimit(3500, TimeUnit.MILLISECONDS, () -> chatGptHelper.directReturn(userId, newRecord)); + } catch (TimeoutException | InterruptedException e) { + // 超时中断的场景 + newRecord.setLastReturn(false); + } catch (Exception e) { + log.warn("chatgpt出现了非预期异常! content:{}", content, e); + newRecord.setLastReturn(true); + if (newRecord.getSysErr() == null) { + newRecord.setSysErr(e.getMessage()); + } + } + return newRecord; + } + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/chatgpt/ChatRecordWxVo.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/chatgpt/ChatRecordWxVo.java new file mode 100644 index 000000000..f2c15bc4b --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/chatgpt/ChatRecordWxVo.java @@ -0,0 +1,66 @@ +package com.github.paicoding.forum.service.chatai.service.impl.chatgpt; + +import com.plexpt.chatgpt.entity.chat.ChatChoice; +import lombok.Data; +import lombok.experimental.Accessors; +import org.springframework.util.CollectionUtils; + +import java.util.List; + +@Data +@Accessors(chain = true) +public class ChatRecordWxVo { + /** + * 提问的次数 + */ + private int qasIndex; + /** + * 提问内容 + */ + private String qas; + /** + * 提问时间 + */ + private Long qasTime; + private List res; + private ChatRecordWxVo next; + private ChatRecordWxVo pre; + private volatile boolean lastReturn; + private String sysErr; + + public ChatRecordWxVo() { + qasTime = 0L; + sysErr = null; + lastReturn = false; + } + + /** + * 之前没有回复过,且chatgpt出错,或者有结果了,才能继续回复 + * + * @return + */ + public boolean canReply() { + return !lastReturn && (sysErr != null || res != null); + } + + public String reply() { + lastReturn = true; + if (!CollectionUtils.isEmpty(res)) { + return buildResPrefix() + buildRes(); + } + + return buildResPrefix() + sysErr; + } + + private String buildResPrefix() { + return qasIndex + "/50: " + qas + "\n================\n"; + } + + private String buildRes() { + StringBuilder builder = new StringBuilder(); + for (ChatChoice choice : res) { + builder.append(choice.getMessage().getContent()).append("\n--------------\n\n"); + } + return builder.toString(); + } +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/deepseek/DeepSeekChatServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/deepseek/DeepSeekChatServiceImpl.java new file mode 100644 index 000000000..009716ba8 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/deepseek/DeepSeekChatServiceImpl.java @@ -0,0 +1,135 @@ +package com.github.paicoding.forum.service.chatai.service.impl.deepseek; + +import com.github.paicoding.forum.api.model.enums.ChatAnswerTypeEnum; +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.enums.ai.AiChatStatEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.chat.ChatRecordsVo; +import com.github.paicoding.forum.service.chatai.service.AbsChatService; +import com.plexpt.chatgpt.listener.AbstractStreamListener; +import lombok.extern.slf4j.Slf4j; +import okhttp3.Response; +import okhttp3.sse.EventSource; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.function.BiConsumer; + +/** + * deepSeek 聊天接入 + * + * @author YiHui + * @date 2025/2/6 + */ +@Slf4j +@Service +public class DeepSeekChatServiceImpl extends AbsChatService { + @Autowired + private DeepSeekIntegration deepSeekIntegration; + + /** + * 同步的响应返回结果 + * + * @param user + * @param chat + * @return + */ + @Override + public AiChatStatEnum doAnswer(Long user, ChatItemVo chat) { + if (deepSeekIntegration.directReturn(chat)) { + return AiChatStatEnum.END; + } + return AiChatStatEnum.ERROR; + } + + /** + * 异步流式的返回结果 + * + * @param user 用户ID,用于标识提问的用户 + * @param response 保存提问 & 返回的结果,最终会返回给前端用户 + * @param consumer 具体将 response 写回前端的实现策略 + * @return 返回聊天的状态枚举 + */ + @Override + public AiChatStatEnum doAsyncAnswer(Long user, ChatRecordsVo response, BiConsumer consumer) { + // 获取问答中的最新的记录,用于问答 + ChatItemVo item = response.getRecords().get(0); + // 创建一个抽象流监听器来处理流式返回的结果 + AbstractStreamListener listener = new AbstractStreamListener() { + // 当连接打开时的处理 + @Override + public void onOpen(EventSource eventSource, Response response) { + super.onOpen(eventSource, response); + if (log.isDebugEnabled()) { + log.debug("正确建立了连接: {}, res: {}", eventSource, response); + } + } + + // 当连接关闭时的处理 + @Override + public void onClosed(EventSource eventSource) { + super.onClosed(eventSource); + if (log.isDebugEnabled()) { + log.debug("已经关闭了连接: {}", eventSource); + } + // 检查是否正常结束对话 + if (item.getAnswerType() != ChatAnswerTypeEnum.STREAM_END) { + // 主动结束这一次的对话 + if (StringUtils.isBlank(lastMessage)) { + item.appendAnswer("大模型超时未返回结果,主动关闭会话;请重新提问吧\n") + .setAnswerType(ChatAnswerTypeEnum.STREAM_END); + consumer.accept(AiChatStatEnum.ERROR, response); + } else { + item.appendAnswer("\n") + .setAnswerType(ChatAnswerTypeEnum.STREAM_END); + consumer.accept(AiChatStatEnum.END, response); + } + } + } + + // 当接收到消息时的处理 + @Override + public void onMsg(String message) { + // 成功返回结果的场景, 过滤掉开头的空行 + if (StringUtils.isNotBlank(lastMessage)) { + item.appendAnswer(message); + consumer.accept(AiChatStatEnum.MID, response); + if (log.isDebugEnabled()) { + log.debug("DeepSeek返回内容: {}", lastMessage); + } + } + } + + // 当遇到错误时的处理 + @Override + public void onError(Throwable throwable, String res) { + // 返回异常的场景 + item.appendAnswer("Error:" + (StringUtils.isBlank(res) ? throwable.getMessage() : res)) + .setAnswerType(ChatAnswerTypeEnum.STREAM_END); + consumer.accept(AiChatStatEnum.ERROR, response); + if (log.isDebugEnabled()) { + log.debug("DeepSeek返回异常: {}", lastMessage); + } + } + }; + + // 注册回答结束的回调钩子 + listener.setOnComplate((s) -> { + if (log.isDebugEnabled()) { + log.debug("这一轮对话聊天已结束,完整的返回结果是:{}", s); + } + item.appendAnswer("\n") + .setAnswerType(ChatAnswerTypeEnum.STREAM_END); + consumer.accept(AiChatStatEnum.END, response); + }); + // 调用深度寻求流式返回的方法 + deepSeekIntegration.streamReturn(response.getRecords(), listener); + return AiChatStatEnum.IGNORE; + } + + @Override + public AISourceEnum source() { + return AISourceEnum.DEEP_SEEK; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/deepseek/DeepSeekIntegration.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/deepseek/DeepSeekIntegration.java new file mode 100644 index 000000000..ff2a5b0af --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/deepseek/DeepSeekIntegration.java @@ -0,0 +1,194 @@ +package com.github.paicoding.forum.service.chatai.service.impl.deepseek; + +import cn.hutool.http.ContentType; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.service.chatai.constants.ChatConstants; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.sse.EventSource; +import okhttp3.sse.EventSourceListener; +import okhttp3.sse.EventSources; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * DeepSeek的集成类,主要负责与DeepSeek进行交互 + * + * @author YiHui + * @date 2025/2/6 + */ +@Slf4j +@Component +public class DeepSeekIntegration { + @Autowired + private DeepSeekConf deepSeekConf; + + private OkHttpClient okHttpClient; + + @PostConstruct + public void init() { + this.okHttpClient = new OkHttpClient.Builder() + .connectTimeout(deepSeekConf.getTimeout(), TimeUnit.SECONDS) // 建立连接的超时时间 + .readTimeout(deepSeekConf.getTimeout(), TimeUnit.SECONDS) // 建立连接后读取数据的超时时间 + .writeTimeout(deepSeekConf.getTimeout(), TimeUnit.SECONDS) + .build(); + } + + /** + * 一次性返回的交互方式 + * todo 待实现; 目前技术派主推流式交互,暂无下面的应用场景,留待有缘人补全 + */ + public boolean directReturn(ChatItemVo item) { + return false; + } + + /** + * todo: 如果希望进行多轮对话,或者对上一次的对话进行补全,则可以在这里进行扩展,将历史的聊天记录传递给机器人,以获取更好的结果 + *

+ * 流式的操作交互方式 + * + * @param item 聊天项目,包含了用户的问题或消息 + * @param listener 事件源监听器,用于处理流式返回的数据 + */ + public void streamReturn(ChatItemVo item, EventSourceListener listener) { + // 创建一个新的聊天消息对象,设置角色为用户,并填充用户的问题 + List msg = toMsg(item); + // 执行流式聊天,传入包含用户消息的消息列表和监听器 + this.executeStreamChat(msg, listener); + } + + /** + * 多轮对话的场景,将历史聊天记录,传递给聊天机器人,以获取更好的结果 + * + * @param list 包含历史聊天记录的列表,用于构建对话上下文 + * @param listener 事件源监听器,用于处理聊天机器人的响应事件 + */ + public void streamReturn(List list, EventSourceListener listener) { + // 构建多轮聊天的会话上下文 + List msgList = ChatConstants.toMsgList(list, this::toMsg); + // 执行流式聊天,将构建好的对话上下文传递给聊天机器人,并监听响应事件 + this.executeStreamChat(msgList, listener); + } + + + /** + * 使用流式聊天接口发送聊天请求 + * 该方法将聊天请求转换为流式请求,并使用EventSource监听器处理响应 + * + * @param req 聊天请求对象,包含聊天所需的参数 + * @param listener EventSource监听器,用于处理服务器发送的事件 + */ + private void executeStreamChat(ChatReq req, EventSourceListener listener) { + // 设置请求为流式请求 + req.setStream(true); + + try { + // 创建EventSource工厂,用于生成EventSource对象 + EventSource.Factory factory = EventSources.createFactory(okHttpClient); + + // 将聊天请求对象转换为JSON字符串 + String body = JsonUtil.toStr(req); + // 构建请求对象,指定URL、认证头、内容类型头以及请求体 + Request request = new Request.Builder() + .url(deepSeekConf.getApiHost() + "/chat/completions") + .addHeader("Authorization", "Bearer " + deepSeekConf.getApiKey()) + .addHeader("Content-Type", "application/json") + .post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), body)) + .build(); + // 使用工厂创建新的EventSource,并传入请求和监听器 + factory.newEventSource(request, listener); + } catch (Exception e) { + // 记录请求失败的日志 + log.error("deepseek联调请求失败: {}", req, e); + } + } + + private void executeStreamChat(List list, EventSourceListener listener) { + ChatReq req = new ChatReq(); + req.setModel("deepseek-chat"); + req.setMessages(list); + this.executeStreamChat(req, listener); + } + + @Data + @Component + @ConfigurationProperties(prefix = "deepseek") + private class DeepSeekConf { + private String apiKey; + private String apiHost; + private Long timeout; + } + + + /** + * 提问的请求实体 + * todo: 这里只封装了最基础的请求传参,更多的参数可以根据官方文档进行补全 + * + */ + @Data + public static class ChatReq { + /** + * 模型,官方当前支持两个: + * 1. deepseek-chat + * 2. deepseek-reasoner --> 推理模型 + */ + private String model; + + /** + * true 来使用流式输出 + */ + private boolean stream; + + /** + * 对话内容 + */ + private List messages; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ChatMsg { + /** + * 角色:用于AI了解它应该如何行为以及谁在发起调用 + * - system: 了解它应该如何行为以及谁在发起调用, 如 content = 你现在是一个资深后端java工程师 + * - user: 消息/提示来自最终用户或人类 + * - assistant: 消息是助手(聊天模型)的响应 --> 即ai的返回结果,在多轮对话中,我们需要将之前的聊天记录传递给机器人,以获取更好的结果 + */ + private String role; + + /** + * 具体的内容 + */ + private String content; + } + + private List toMsg(ChatItemVo item) { + List list = new ArrayList<>(2); + if (item.getQuestion().startsWith(ChatConstants.PROMPT_TAG)) { + // 提示词 + list.add(new ChatMsg("system", item.getQuestion().substring(ChatConstants.PROMPT_TAG.length()))); + } else { + // 用户问答 + list.add(new ChatMsg("user", item.getQuestion())); + if (StringUtils.isNotBlank(item.getAnswer())) { + list.add(new ChatMsg("assistant", item.getAnswer())); + } + } + return list; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/doubao/DoubaoAiServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/doubao/DoubaoAiServiceImpl.java new file mode 100644 index 000000000..628b4b8c9 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/doubao/DoubaoAiServiceImpl.java @@ -0,0 +1,54 @@ +package com.github.paicoding.forum.service.chatai.service.impl.doubao; + +import com.github.paicoding.forum.api.model.enums.ChatAnswerTypeEnum; +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.enums.ai.AiChatStatEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.chat.ChatRecordsVo; +import com.github.paicoding.forum.service.chatai.constants.ChatConstants; +import com.github.paicoding.forum.service.chatai.service.AbsChatService; +import com.volcengine.ApiException; +import com.volcengine.ark.runtime.model.completion.chat.ChatCompletionRequest; +import com.volcengine.ark.runtime.model.completion.chat.ChatMessage; +import com.volcengine.ark.runtime.model.completion.chat.ChatMessageRole; +import com.volcengine.ark.runtime.service.ArkService; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.DependsOn; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; + +@Slf4j +@Service +public class DoubaoAiServiceImpl extends AbsChatService { + @Autowired + private DoubaoIntegration doubaoIntegration; + + + @Override + public AiChatStatEnum doAnswer(Long user, ChatItemVo chat) { + return doubaoIntegration.directAnswer(user, chat); + } + + @Override + public AiChatStatEnum doAsyncAnswer(Long user, ChatRecordsVo chatRes, BiConsumer consumer) { + return doubaoIntegration.streamAsyncAnswer(user, chatRes, consumer); + } + + @Override + public AISourceEnum source() { + return AISourceEnum.DOU_BAO_AI; + } + + @Override + public boolean asyncFirst() { + return true; + } + + +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/doubao/DoubaoConfig.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/doubao/DoubaoConfig.java new file mode 100644 index 000000000..8b16c10d7 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/doubao/DoubaoConfig.java @@ -0,0 +1,21 @@ +package com.github.paicoding.forum.service.chatai.service.impl.doubao; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + +@Data +@Configuration +@ConfigurationProperties(prefix = "doubao") +public class DoubaoConfig{ + + @Value("${doubao.api-key}") + private String apiKey; + @Value("${doubao.api-host}") + private String apiHost; + @Value("${doubao.end-point}") + private String endPoint; +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/doubao/DoubaoIntegration.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/doubao/DoubaoIntegration.java new file mode 100644 index 000000000..b0ae6801b --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/doubao/DoubaoIntegration.java @@ -0,0 +1,150 @@ +package com.github.paicoding.forum.service.chatai.service.impl.doubao; + +import com.github.paicoding.forum.api.model.enums.ChatAnswerTypeEnum; +import com.github.paicoding.forum.api.model.enums.ai.AiChatStatEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.chat.ChatRecordsVo; +import com.github.paicoding.forum.service.chatai.constants.ChatConstants; +import com.volcengine.ApiException; +import com.volcengine.ark.runtime.model.completion.chat.ChatCompletionRequest; +import com.volcengine.ark.runtime.model.completion.chat.ChatMessage; +import com.volcengine.ark.runtime.model.completion.chat.ChatMessageRole; +import com.volcengine.ark.runtime.service.ArkService; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; + +@Slf4j +@Service +public class DoubaoIntegration { + @Autowired + private final DoubaoConfig doubaoConfig; + private final ArkService service; + + + public DoubaoIntegration(DoubaoConfig doubaoConfig) { + this.doubaoConfig = doubaoConfig; + String baseUrl = "https://ark.cn-beijing.volces.com/api/v3"; + if (!StringUtils.hasText(doubaoConfig.getApiKey())) { + log.info("豆包API KEY 未配置,停止初始化DoubaoIntegration"); + this.service = null; + return; + } + if(StringUtils.hasText(doubaoConfig.getApiHost()) ){ + baseUrl = this.doubaoConfig.getApiHost(); + }else { + log.warn("豆包API HOST 未配置,使用默认值"); + } + this.service = ArkService.builder() + .baseUrl(baseUrl) + .apiKey(this.doubaoConfig.getApiKey()) + .build(); + } + + + + + public AiChatStatEnum directAnswer(Long user, ChatItemVo chat) { + if (service == null) { + log.warn("豆包ai服务未初始化成功 目前apikey:{},目前apiHost:{}",doubaoConfig.getApiKey(),doubaoConfig.getApiHost()); + chat.initAnswer("Service not initialized"); + return AiChatStatEnum.ERROR; + } + List messages = new ArrayList<>(); + messages.add(ChatMessage.builder().role(ChatMessageRole.SYSTEM).content("你是豆包,是由字节跳动开发的 AI 人工智能助手").build()); + messages.add(ChatMessage.builder().role(ChatMessageRole.USER).content(chat.getQuestion()).build()); + + ChatCompletionRequest request = ChatCompletionRequest.builder() + .model(doubaoConfig.getEndPoint()) + .messages(messages) + .build(); + + try { + String response = (String) service.createChatCompletion(request).getChoices().get(0).getMessage().getContent(); + chat.initAnswer(response, ChatAnswerTypeEnum.TEXT); + return AiChatStatEnum.END; + } catch (Exception e) { + chat.initAnswer("Error: " + e.getMessage()); + return AiChatStatEnum.ERROR; + } + } + + public AiChatStatEnum streamAsyncAnswer(Long user, ChatRecordsVo chatRes, BiConsumer consumer) { + if (service == null) { + log.warn("豆包ai服务未初始化成功 目前apikey:{},目前apiHost:{}",doubaoConfig.getApiKey(),doubaoConfig.getApiHost()); + ChatItemVo item = chatRes.getRecords().get(0); + item.appendAnswer("Service not initialized").setAnswerType(ChatAnswerTypeEnum.STREAM_END); + consumer.accept(AiChatStatEnum.ERROR, chatRes); + return AiChatStatEnum.ERROR; + } + ChatItemVo item = chatRes.getRecords().get(0); + List messages = ChatConstants.toMsgList(chatRes.getRecords(), this::toMsg); + + + ChatCompletionRequest request = ChatCompletionRequest.builder() + .model(doubaoConfig.getEndPoint()) + .messages(messages) + .build(); + // 异步返回 + Disposable disposable = service.streamChatCompletion(request) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.computation()) // 如果不耗时 可以更换成Schedulers.single() 减少切换上下文的开销 + .doFinally(() -> { + // 流结束的逻辑 + if(item.getAnswerType() != ChatAnswerTypeEnum.STREAM_END) { + // 检查下是不是已经结束了。 + item.appendAnswer("\n").setAnswerType(ChatAnswerTypeEnum.STREAM_END); + consumer.accept(AiChatStatEnum.END, chatRes); + } + }) + .subscribe(choice -> { + if (!choice.getChoices().isEmpty()) { + item.appendAnswer((String) choice.getChoices().get(0).getMessage().getContent()); + consumer.accept(AiChatStatEnum.MID, chatRes); + } + }, throwable -> { + String errorMessage = "Error: " + throwable.getMessage(); + if (throwable instanceof ApiException) { + ApiException apiException = (ApiException) throwable; + errorMessage = String.format("Error: %s, Code: %s, Param: %s", + apiException.getMessage(), apiException.getCode(), apiException.getCause()); + } + item.appendAnswer(errorMessage).setAnswerType(ChatAnswerTypeEnum.STREAM_END); + consumer.accept(AiChatStatEnum.ERROR, chatRes); + } + ); + + + return AiChatStatEnum.IGNORE; + } + + + private List toMsg(ChatItemVo item) { + List list = new ArrayList<>(2); + if (item.getQuestion().startsWith(ChatConstants.PROMPT_TAG)) { + // Prompt + list.add(ChatMessage.builder().role(ChatMessageRole.SYSTEM).content(item.getQuestion().substring(ChatConstants.PROMPT_TAG.length())).build()); + } else { + // 用户提问和回答 + list.add(ChatMessage.builder().role(ChatMessageRole.USER).content(item.getQuestion()).build()); + if (StringUtils.hasText(item.getAnswer())) { + list.add(ChatMessage.builder().role(ChatMessageRole.ASSISTANT).content(item.getAnswer()).build()); + } + } + return list; + } + + +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/pai/PaiAiDemoServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/pai/PaiAiDemoServiceImpl.java new file mode 100644 index 000000000..f80e54a6a --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/pai/PaiAiDemoServiceImpl.java @@ -0,0 +1,63 @@ +package com.github.paicoding.forum.service.chatai.service.impl.pai; + +import com.github.paicoding.forum.api.model.enums.ChatAnswerTypeEnum; +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.enums.ai.AiChatStatEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.chat.ChatRecordsVo; +import com.github.paicoding.forum.core.async.AsyncUtil; +import com.github.paicoding.forum.service.chatai.constants.ChatConstants; +import com.github.paicoding.forum.service.chatai.service.AbsChatService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.function.BiConsumer; + +/** + * 技术派价值一个亿的AI + * + * @author YiHui + * @date 2023/6/9 + */ +@Service +public class PaiAiDemoServiceImpl extends AbsChatService { + + @Override + public AISourceEnum source() { + return AISourceEnum.PAI_AI; + } + + @Override + public AiChatStatEnum doAnswer(Long user, ChatItemVo chat) { + chat.initAnswer(qa(chat.getQuestion())); + return AiChatStatEnum.END; + } + + @Override + public AiChatStatEnum doAsyncAnswer(Long user, ChatRecordsVo response, BiConsumer consumer) { + AsyncUtil.execute(() -> { + AsyncUtil.sleep(1500); + ChatItemVo item = response.getRecords().get(0); + item.appendAnswer(qa(item.getQuestion())); + consumer.accept(AiChatStatEnum.FIRST, response); + + AsyncUtil.sleep(1200); + item.appendAnswer("\n" + ChatConstants.SWITCH_TO_OTHER_MODEL); + item.setAnswerType(ChatAnswerTypeEnum.STREAM_END); + consumer.accept(AiChatStatEnum.END, response); + }); + return AiChatStatEnum.END; + } + + private String qa(String q) { + String ans = q.replace("吗", ""); + ans = StringUtils.replace(ans, "?", "!"); + ans = StringUtils.replace(ans, "?", "!"); + return ans; + } + + @Override + protected int getMaxQaCnt(Long user) { + return 65535; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/xunfei/XunFeiAiServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/xunfei/XunFeiAiServiceImpl.java new file mode 100644 index 000000000..ba6cfe326 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/xunfei/XunFeiAiServiceImpl.java @@ -0,0 +1,189 @@ +package com.github.paicoding.forum.service.chatai.service.impl.xunfei; + +import com.github.paicoding.forum.api.model.enums.ChatAnswerTypeEnum; +import com.github.paicoding.forum.api.model.enums.WsConnectStateEnum; +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.enums.ai.AiChatStatEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.chat.ChatRecordsVo; +import com.github.paicoding.forum.service.chatai.service.AbsChatService; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.function.BiConsumer; + +/** + * 讯飞星火大模型 + * + * + * @author YiHui + * @date 2023/6/12 + */ +@Slf4j +@Service +public class XunFeiAiServiceImpl extends AbsChatService { + + @Autowired + private XunFeiIntegration xunFeiIntegration; + + /** + * 不支持同步提问 + * + * @param user + * @param chat + * @return + */ + @Override + public AiChatStatEnum doAnswer(Long user, ChatItemVo chat) { + return AiChatStatEnum.IGNORE; + } + + /** + * 异步回答提问 + * + * @param user + * @param chatRes 保存提问 & 返回的结果,最终会返回给前端用户 + * @param consumer 具体将 response 写回前端的实现策略 + */ + @Override + public AiChatStatEnum doAsyncAnswer(Long user, ChatRecordsVo chatRes, BiConsumer consumer) { + XunFeiChatWrapper chat = new XunFeiChatWrapper(String.valueOf(user), chatRes, consumer); + chat.initAndQuestion(); + return AiChatStatEnum.IGNORE; + } + + @Override + public AISourceEnum source() { + return AISourceEnum.XUN_FEI_AI; + } + + /** + * 一个简单的ws装饰器,用于包装一下讯飞长连接的交互情况 + * 比较蛋疼的是讯飞建立连接60s没有返回主动断开,问了一次返回结果之后也主动断开,下次需要重连 + */ + @Data + public class XunFeiChatWrapper { + private OkHttpClient client; + private WebSocket webSocket; + private Request request; + + private BiConsumer onMsg; + + private XunFeiMsgListener listener; + + private ChatItemVo item; + + public XunFeiChatWrapper(String uid, ChatRecordsVo chatRes, BiConsumer consumer) { + client = xunFeiIntegration.getOkHttpClient(); + String url = xunFeiIntegration.buildXunFeiUrl(); + request = new Request.Builder().url(url).build(); + listener = new XunFeiMsgListener(uid, chatRes, consumer); + } + + /** + * 首次使用时,开启提问 + */ + public void initAndQuestion() { + webSocket = client.newWebSocket(request, listener); + } + + /** + * 追加的提问, 主要是为了复用websocket的构造参数 + */ + public void appendQuestion(String uid, ChatRecordsVo chatRes, BiConsumer consumer) { + listener = new XunFeiMsgListener(uid, chatRes, consumer); + webSocket = client.newWebSocket(request, listener); + } + + } + + @Getter + @Setter + public class XunFeiMsgListener extends WebSocketListener { + private volatile WsConnectStateEnum connectState; + + private String user; + + private ChatRecordsVo chatRecord; + + private BiConsumer callback; + + public XunFeiMsgListener(String user, ChatRecordsVo chatRecord, BiConsumer callback) { + this.connectState = WsConnectStateEnum.INIT; + this.user = user; + this.chatRecord = chatRecord; + this.callback = callback; + } + + //重写onopen + @Override + public void onOpen(WebSocket webSocket, Response response) { + super.onOpen(webSocket, response); + connectState = WsConnectStateEnum.CONNECTED; + // 连接成功之后,发送消息buildSendMsg + webSocket.send(xunFeiIntegration.buildSendMsg(user, chatRecord.getRecords())); + } + + @Override + public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { + super.onMessage(webSocket, text); + ChatItemVo item = chatRecord.getRecords().get(0); + XunFeiIntegration.ResponseData responseData = xunFeiIntegration.parse2response(text); + if (responseData.successReturn()) { + // 成功获取到结果 + StringBuilder msg = new StringBuilder(); + XunFeiIntegration.Payload pl = responseData.getPayload(); + pl.getChoices().getText().forEach(s -> { + msg.append(s.getContent()); + }); + item.appendAnswer(msg.toString()); + + if (responseData.firstResonse()) { + callback.accept(AiChatStatEnum.FIRST, chatRecord); + } else if (responseData.endResponse()) { + // 标记流式回答已完成 + item.setAnswerType(ChatAnswerTypeEnum.STREAM_END); + // 最后一次返回结果时,打印一下剩余的tokens + XunFeiIntegration.UsageText tokens = pl.getUsage().getText(); + log.info("使用tokens:\n" + tokens); + webSocket.close(1001, "会话结束"); + callback.accept(AiChatStatEnum.END, chatRecord); + } else { + callback.accept(AiChatStatEnum.MID, chatRecord); + } + } else { + item.initAnswer("AI返回异常:" + responseData.getHeader()); + callback.accept(AiChatStatEnum.ERROR, chatRecord); + webSocket.close(responseData.getHeader().getCode(), responseData.getHeader().getMessage()); + } + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + super.onFailure(webSocket, t, response); + log.warn("websocket 连接失败! {}", response, t); + connectState = WsConnectStateEnum.FAILED; + chatRecord.getRecords().get(0).initAnswer("讯飞AI连接失败了!" + t.getMessage()); + callback.accept(AiChatStatEnum.ERROR, chatRecord); + } + + @Override + public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) { + super.onClosed(webSocket, code, reason); + if (log.isDebugEnabled()) { + log.debug("连接中断! code={}, reason={}", code, reason); + } + connectState = WsConnectStateEnum.CLOSED; + } + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/xunfei/XunFeiIntegration.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/xunfei/XunFeiIntegration.java new file mode 100644 index 000000000..99e514da3 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/xunfei/XunFeiIntegration.java @@ -0,0 +1,339 @@ +package com.github.paicoding.forum.service.chatai.service.impl.xunfei; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.service.chatai.constants.ChatConstants; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Base64; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +/** + * 主体来自讯飞官方java sdk + * + * + * + * @author YiHui + * @date 2023/6/12 + */ +@Slf4j +@Setter +@Component +public class XunFeiIntegration { + + @Autowired + private XunFeiConfig xunFeiConfig; + + @Getter + private OkHttpClient okHttpClient; + + @PostConstruct + public void init() { + okHttpClient = new OkHttpClient.Builder().build(); + } + + public String buildXunFeiUrl() { + try { + String authUrl = getAuthorizationUrl(xunFeiConfig.hostUrl, xunFeiConfig.apiKey, xunFeiConfig.apiSecret); + String url = authUrl.replace("https://", "wss://").replace("http://", "ws://"); + return url; + } catch (Exception e) { + log.warn("讯飞url创建失败", e); + return null; + } + } + + /** + * 构建授权url + * + * @param hostUrl + * @param apikey + * @param apisecret + * @return + * @throws Exception + */ + public String getAuthorizationUrl(String hostUrl, String apikey, String apisecret) throws Exception { + //获取host + URL url = new URL(hostUrl); + //获取鉴权时间 date + SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("GMT")); + String date = format.format(new Date()); + //获取signature_origin字段 + String builder = "host: " + url.getHost() + "\n" + + "date: " + date + "\n" + + "GET " + url.getPath() + " HTTP/1.1"; + //获得signatue + Charset charset = StandardCharsets.UTF_8; + Mac mac = Mac.getInstance("hmacsha256"); + SecretKeySpec sp = new SecretKeySpec(apisecret.getBytes(charset), "hmacsha256"); + mac.init(sp); + String signature = Base64.getEncoder().encodeToString(mac.doFinal(builder.getBytes(charset))); + //获得 authorization_origin + String authorizationOrigin = String.format("api_key=\"%s\",algorithm=\"%s\",headers=\"%s\",signature=\"%s\"", apikey, "hmac-sha256", "host date request-line", signature); + //获得authorization + String authorization = Base64.getEncoder().encodeToString(authorizationOrigin.getBytes(charset)); + //获取httpurl + HttpUrl httpUrl = HttpUrl.parse("https://" + url.getHost() + url.getPath()).newBuilder(). + addQueryParameter("authorization", authorization). + addQueryParameter("date", date). + addQueryParameter("host", url.getHost()). + build(); + return httpUrl.toString(); + } + + public String buildSendMsg(String uid, String question) { + JsonObject frame = new JsonObject(); + JsonObject header = new JsonObject(); + JsonObject chat = new JsonObject(); + JsonObject parameter = new JsonObject(); + JsonObject payload = new JsonObject(); + JsonObject message = new JsonObject(); + JsonObject text = new JsonObject(); + JsonArray ja = new JsonArray(); + + //填充header + header.addProperty("app_id", xunFeiConfig.appId); + header.addProperty("uid", uid); + //填充parameter + chat.addProperty("domain", xunFeiConfig.domain); + chat.addProperty("random_threshold", 0); + chat.addProperty("max_tokens", 1024); + chat.addProperty("auditing", "default"); + parameter.add("chat", chat); + //填充payload + text.addProperty("role", "user"); + text.addProperty("content", question); + ja.add(text); + message.add("text", ja); + payload.add("message", message); + frame.add("header", header); + frame.add("parameter", parameter); + frame.add("payload", payload); + return frame.toString(); + } + + /** + * 结合上下文的回答 + * + * @param uid + * @param items + * @return + */ + public String buildSendMsg(String uid, List items) { + JsonObject frame = new JsonObject(); + JsonObject header = new JsonObject(); + JsonObject chat = new JsonObject(); + JsonObject parameter = new JsonObject(); + JsonObject payload = new JsonObject(); + JsonObject message = new JsonObject(); + JsonArray ja = new JsonArray(); + + //填充header + header.addProperty("app_id", xunFeiConfig.appId); + header.addProperty("uid", uid); + //填充parameter + chat.addProperty("domain", xunFeiConfig.domain); + chat.addProperty("random_threshold", 0); + chat.addProperty("max_tokens", 1024); + chat.addProperty("auditing", "default"); + parameter.add("chat", chat); + + //填充payload + for (int i = items.size() - 1; i >= 0; i--) { + ChatItemVo item = items.get(i); + ja.addAll(toText(item)); + } + + message.add("text", ja); + payload.add("message", message); + frame.add("header", header); + frame.add("parameter", parameter); + frame.add("payload", payload); + return frame.toString(); + } + + /** + * 构建提问消息 + * + * @param item + * @return + */ + private static JsonArray toText(ChatItemVo item) { + JsonArray ary = new JsonArray(); + + if (item.getQuestion().startsWith(ChatConstants.PROMPT_TAG)) { + // 提示词 + JsonObject obj = new JsonObject(); + obj.addProperty("role", "user"); + obj.addProperty("content", item.getQuestion().substring(ChatConstants.PROMPT_TAG.length())); + ary.add(obj); + return ary; + } + + // 用户问答消息 + JsonObject obj = new JsonObject(); + obj.addProperty("role", "user"); + obj.addProperty("content", item.getQuestion()); + ary.add(obj); + if (StringUtils.isNotBlank(item.getAnswer())) { + JsonObject obj2 = new JsonObject(); + obj2.addProperty("role", "assistant"); + obj2.addProperty("content", item.getAnswer()); + ary.add(obj); + } + return ary; + } + + public ResponseData parse2response(String text) { + return JsonUtil.toObj(text, ResponseData.class); + } + + + @Component + @ConfigurationProperties(prefix = "xunfei") + @Data + public static class XunFeiConfig { + public String hostUrl = "http://spark-api.xf-yun.com/v1.1/chat"; + public String appId = ""; + public String apiKey = ""; + public String apiSecret = ""; + // 指定访问的领域,general指向V1.5版本 generalv2指向V2版本。注意:不同的取值对应的url也不一样! + public String domain = "general"; + } + + @Data + public static class ResponseData { + private Header header; + private Payload payload; + + public boolean successReturn() { + return header != null && header.code == 0; + } + + /** + * 首次返回结果 + * + * @return + */ + public boolean firstResonse() { + return header != null && "0".equalsIgnoreCase(header.status); + } + + /** + * 判断是否是最后一次返回的结果 + * + * @return + */ + public boolean endResponse() { + return header != null && "2".equalsIgnoreCase(header.status); + } + } + + @Data + public static class Header { + /** + * 错误码,0表示正常,非0表示出错;详细释义可在接口说明文档最后的错误码说明了解 + */ + private int code; + /** + * 会话是否成功的描述信息 + */ + private String message; + /** + * 会话的唯一id,用于讯飞技术人员查询服务端会话日志使用,出现调用错误时建议留存该字段 + */ + private String sid; + /** + * 会话状态,取值为[0,1,2];0代表首次结果;1代表中间结果;2代表最后一个结果 + */ + private String status; + } + + @Data + public static class Payload { + private Choices choices; + private Usage usage; + } + + @Data + public static class Choices { + /** + * 文本响应状态,取值为[0,1,2]; 0代表首个文本结果;1代表中间文本结果;2代表最后一个文本结果 + */ + private int status; + /** + * 返回的数据序号,取值为[0,9999999] + */ + private int seq; + + private List text; + } + + @Data + public static class ChoicesText { + /** + * 结果序号,取值为[0,10]; 当前为保留字段,开发者可忽略 + */ + private int index; + /** + * 角色标识,固定为assistant,标识角色为AI + */ + private String role; + /** + * AI的回答内容 + */ + private String content; + } + + @Data + public static class Usage { + private UsageText text; + } + + @Data + public static class UsageText { + /** + * 保留字段,可忽略 + */ + @JsonAlias("question_tokens") + private int questionTokens; + /** + * 包含历史问题的总tokens大小 + */ + @JsonAlias("prompt_tokens") + private int promptTokens; + /** + * 回答的tokens大小 + */ + @JsonAlias("completion_tokens") + private int completionTokens; + /** + * prompt_tokens和completion_tokens的和,也是本次交互计费的tokens大小 + */ + @JsonAlias("total_tokens") + private int totalTokens; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/zhipu/ZhipuAiServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/zhipu/ZhipuAiServiceImpl.java new file mode 100644 index 000000000..3afa6d435 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/zhipu/ZhipuAiServiceImpl.java @@ -0,0 +1,55 @@ +package com.github.paicoding.forum.service.chatai.service.impl.zhipu; + +import com.github.paicoding.forum.api.model.enums.ChatAnswerTypeEnum; +import com.github.paicoding.forum.api.model.enums.WsConnectStateEnum; +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.enums.ai.AiChatStatEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.chat.ChatRecordsVo; +import com.github.paicoding.forum.service.chatai.service.AbsChatService; +import com.github.paicoding.forum.service.chatai.service.impl.xunfei.XunFeiAiServiceImpl; +import com.github.paicoding.forum.service.chatai.service.impl.xunfei.XunFeiIntegration; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.function.BiConsumer; + +@Slf4j +@Service +public class ZhipuAiServiceImpl extends AbsChatService { + + @Autowired + private ZhipuIntegration zhipuIntegration; + + @Override + public AiChatStatEnum doAnswer(Long user, ChatItemVo chat) { + if (zhipuIntegration.directReturn(user, chat)) { + return AiChatStatEnum.END; + } + return AiChatStatEnum.ERROR; + } + + @Override + public AiChatStatEnum doAsyncAnswer(Long user, ChatRecordsVo chatRes, BiConsumer consumer) { + zhipuIntegration.streamReturn(user, chatRes, consumer); + return AiChatStatEnum.IGNORE; + } + + @Override + public AISourceEnum source() { + return AISourceEnum.ZHI_PU_AI; + } + + @Override + public boolean asyncFirst() { + // true 表示优先使用异步返回; false 表示同步等待结果 + return true; + } + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/zhipu/ZhipuIntegration.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/zhipu/ZhipuIntegration.java new file mode 100644 index 000000000..8dd1d8129 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/chatai/service/impl/zhipu/ZhipuIntegration.java @@ -0,0 +1,229 @@ +package com.github.paicoding.forum.service.chatai.service.impl.zhipu; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.paicoding.forum.api.model.enums.ChatAnswerTypeEnum; +import com.github.paicoding.forum.api.model.enums.ai.AiChatStatEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.chat.ChatRecordsVo; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.service.chatai.constants.ChatConstants; +import com.zhipu.oapi.ClientV4; +import com.zhipu.oapi.Constants; +import com.zhipu.oapi.service.v4.deserialize.MessageDeserializeFactory; +import com.zhipu.oapi.service.v4.model.ChatCompletionRequest; +import com.zhipu.oapi.service.v4.model.ChatMessage; +import com.zhipu.oapi.service.v4.model.ChatMessageAccumulator; +import com.zhipu.oapi.service.v4.model.ChatMessageRole; +import com.zhipu.oapi.service.v4.model.ChatTool; +import com.zhipu.oapi.service.v4.model.Choice; +import com.zhipu.oapi.service.v4.model.ModelApiResponse; +import com.zhipu.oapi.service.v4.model.ModelData; +import com.zhipu.oapi.service.v4.model.WebSearch; +import io.reactivex.Flowable; +import lombok.Data; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; + +@Slf4j +@Setter +@Component +public class ZhipuIntegration { + @Autowired + private ZhipuConfig config; + + public void streamReturn(Long user, ChatRecordsVo chatRecord, BiConsumer callback) { + List messages = ChatConstants.toMsgList(chatRecord.getRecords(), this::toMsg); + + ChatItemVo item = chatRecord.getRecords().get(0); + String requestId = String.format(config.requestIdTemplate, System.currentTimeMillis()); + // 函数调用参数构建部分 + List chatToolList = new ArrayList<>(); + ChatTool chatTool = new ChatTool(); + + chatTool.setType("web_search"); +// Retrieval retrieval = new Retrieval(); +// retrieval.setKnowledge_id("1826571496106102784"); + WebSearch webSearch = new WebSearch(); + webSearch.setEnable(Boolean.TRUE); + chatTool.setWeb_search(webSearch); +// chatTool.setType("code_interpreter"); + chatToolList.add(chatTool); + + // 请求参数封装 + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() + .model(config.getModel()) + .stream(Boolean.TRUE) + .invokeMethod(Constants.invokeMethod) + .messages(messages) + .tools(chatToolList) + .userId("paicoding-" + String.valueOf(user)) + .toolChoice("auto") + .requestId(requestId) + .build(); + ClientV4 client = new ClientV4.Builder(config.apiSecretKey) + .networkConfig(300, 100, 100, 100, TimeUnit.SECONDS) + .connectionPool(new okhttp3.ConnectionPool(8, 1, TimeUnit.SECONDS)) + .build(); + + // 调用模型接口 + ModelApiResponse sseModelApiResp = client.invokeModelApi(chatCompletionRequest); + + // 序列化输出 + ObjectMapper mapper = MessageDeserializeFactory.defaultObjectMapper(); + + // 处理返回结果 + if (sseModelApiResp.isSuccess()) { + AtomicBoolean isFirst = new AtomicBoolean(true); + List choices = new ArrayList<>(); + AtomicReference lastAccumulator = new AtomicReference<>(); + + mapStreamToAccumulator(sseModelApiResp.getFlowable()).doOnNext(accumulator -> { + { + if (isFirst.getAndSet(false)) { + log.info("Response: "); + } + if (accumulator.getDelta() != null && accumulator.getDelta().getTool_calls() != null) { + accumulator.getDelta().getTool_calls().forEach(toolCall -> { + log.info("tool_call: {}", toolCall); + JsonNode codeInterpreter = toolCall.get("code_interpreter"); + if (codeInterpreter != null) { + // 检查并处理 outputs 字段 + JsonNode outputs = codeInterpreter.get("outputs"); + if (outputs != null) { + outputs.forEach(output -> { + log.info("output: {}", output); + if (output.has("type") && output.get("type").asText().equals("file")) { + log.info("output file: {}", output.get("file")); + // 组装成 Markdown 返回 + String content = "![file](" + output.get("file").asText() + ")"; + item.appendAnswer(content); + callback.accept(AiChatStatEnum.MID, chatRecord); + } + }); + } + } + }); + String jsonString = mapper.writeValueAsString(accumulator.getDelta().getTool_calls()); + log.info("tool_calls: {}", jsonString); + } + if (accumulator.getDelta() != null && accumulator.getDelta().getContent() != null) { + String content = accumulator.getDelta().getContent(); + item.appendAnswer(content); + callback.accept(AiChatStatEnum.MID, chatRecord); + log.info("回复内容: {}", content); + } + choices.add(accumulator.getChoice()); + lastAccumulator.set(accumulator); + } + }) + .doOnComplete(() -> { + log.info("Stream completed."); + item.setAnswerType(ChatAnswerTypeEnum.STREAM_END); + callback.accept(AiChatStatEnum.END, chatRecord); + }) + .doOnError(throwable -> { + log.error("Error: {}", throwable); + callback.accept(AiChatStatEnum.ERROR, chatRecord); + }) // Handle errors + .blockingSubscribe();// Use blockingSubscribe instead of blockingGet() + + ChatMessageAccumulator chatMessageAccumulator = lastAccumulator.get(); + ModelData data = new ModelData(); + data.setChoices(choices); + if (chatMessageAccumulator != null) { + data.setUsage(chatMessageAccumulator.getUsage()); + data.setId(chatMessageAccumulator.getId()); + data.setCreated(chatMessageAccumulator.getCreated()); + } + data.setRequestId(chatCompletionRequest.getRequestId()); + sseModelApiResp.setFlowable(null);// 打印前置空 + sseModelApiResp.setData(data); + } + try { + log.info("model output: {}", mapper.writeValueAsString(sseModelApiResp)); + } catch (JsonProcessingException e) { + log.error("An exception occurred: {}", e.getMessage()); + throw new RuntimeException(e); + } + client.getConfig().getHttpClient().dispatcher().executorService().shutdown(); + + client.getConfig().getHttpClient().connectionPool().evictAll(); + // List all active threads + for (Thread t : Thread.getAllStackTraces().keySet()) { + log.info("Thread: " + t.getName() + " State: " + t.getState()); + } + } + + @Component + @ConfigurationProperties(prefix = "zhipu") + @Data + public static class ZhipuConfig { + public String requestIdTemplate; + public String apiSecretKey; + public String model; + } + + public boolean directReturn(Long user, ChatItemVo chat) { + List messages = new ArrayList<>(); + ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), chat.getQuestion()); + messages.add(chatMessage); + String requestId = String.format(config.requestIdTemplate, user + System.currentTimeMillis()); + + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() + .model(Constants.ModelChatGLM4) + .stream(Boolean.FALSE) + .invokeMethod(Constants.invokeMethod) + .messages(messages) + .requestId(requestId) + .build(); + ClientV4 client = new ClientV4.Builder(config.apiSecretKey) + .networkConfig(300, 100, 100, 100, TimeUnit.SECONDS) + .connectionPool(new okhttp3.ConnectionPool(8, 1, TimeUnit.SECONDS)) + .build(); + ModelApiResponse invokeModelApiResp = client.invokeModelApi(chatCompletionRequest); + if (invokeModelApiResp.isSuccess()) { + invokeModelApiResp.getData().getChoices().forEach(choice -> { + chat.initAnswer(JsonUtil.toStr(choice.getMessage().getContent()), ChatAnswerTypeEnum.JSON); + log.info("智谱 AI 试用! 传参:{}, 返回:{}", chat, invokeModelApiResp); + }); + } + + + return true; + } + + public static Flowable mapStreamToAccumulator(Flowable flowable) { + return flowable.map(chunk -> { + return new ChatMessageAccumulator(chunk.getChoices().get(0).getDelta(), null, chunk.getChoices().get(0), chunk.getUsage(), chunk.getCreated(), chunk.getId()); + }); + } + + private List toMsg(ChatItemVo item) { + List list = new ArrayList<>(2); + if (item.getQuestion().startsWith(ChatConstants.PROMPT_TAG)) { + // 提示词消息 + list.add(new ChatMessage(ChatMessageRole.SYSTEM.value(), item.getQuestion().substring(ChatConstants.PROMPT_TAG.length()))); + return list; + } + + // 用户问答 + list.add(new ChatMessage(ChatMessageRole.USER.value(), item.getQuestion())); + if (StringUtils.isNotBlank(item.getAnswer())) { + list.add(new ChatMessage(ChatMessageRole.ASSISTANT.value(), item.getAnswer())); + } + return list; + } +} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/converter/CommentConverter.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/converter/CommentConverter.java similarity index 78% rename from forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/converter/CommentConverter.java rename to paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/converter/CommentConverter.java index 074afc921..06ef22c1e 100644 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/converter/CommentConverter.java +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/converter/CommentConverter.java @@ -1,10 +1,10 @@ -package com.github.liuyueyi.forum.service.comment.converter; +package com.github.paicoding.forum.service.comment.converter; -import com.github.liueyueyi.forum.api.model.vo.comment.CommentSaveReq; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.BaseCommentDTO; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.SubCommentDTO; -import com.github.liueyueyi.forum.api.model.vo.comment.dto.TopCommentDTO; -import com.github.liuyueyi.forum.service.comment.repository.entity.CommentDO; +import com.github.paicoding.forum.api.model.vo.comment.CommentSaveReq; +import com.github.paicoding.forum.api.model.vo.comment.dto.BaseCommentDTO; +import com.github.paicoding.forum.api.model.vo.comment.dto.SubCommentDTO; +import com.github.paicoding.forum.api.model.vo.comment.dto.TopCommentDTO; +import com.github.paicoding.forum.service.comment.repository.entity.CommentDO; import java.util.ArrayList; diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/package-info.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/package-info.java new file mode 100644 index 000000000..79486fe98 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/package-info.java @@ -0,0 +1,7 @@ +/** + * 评论相关服务包 + * + * @author YiHui + * @date 2022/7/6 + */ +package com.github.paicoding.forum.service.comment; \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/repository/dao/CommentDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/repository/dao/CommentDao.java new file mode 100644 index 000000000..6c51a7888 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/repository/dao/CommentDao.java @@ -0,0 +1,75 @@ +package com.github.paicoding.forum.service.comment.repository.dao; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.paicoding.forum.api.model.enums.YesOrNoEnum; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.service.comment.repository.entity.CommentDO; +import com.github.paicoding.forum.service.comment.repository.mapper.CommentMapper; +import org.springframework.stereotype.Repository; +import org.springframework.util.CollectionUtils; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * @author YiHui + * @date 2022/9/2 + */ +@Repository +public class CommentDao extends ServiceImpl { + + /** + * 获取评论列表 + * + * @param pageParam + * @return + */ + public List listTopCommentList(Long articleId, PageParam pageParam) { + return lambdaQuery() + .eq(CommentDO::getTopCommentId, 0) + .eq(CommentDO::getArticleId, articleId) + .eq(CommentDO::getDeleted, YesOrNoEnum.NO.getCode()) + .last(PageParam.getLimitSql(pageParam)) + .orderByDesc(CommentDO::getId).list(); + } + + /** + * 查询所有的子评论 + * + * @param articleId + * @return + */ + public List listSubCommentIdMappers(Long articleId, Collection topCommentIds) { + return lambdaQuery() + .in(CommentDO::getTopCommentId, topCommentIds) + .eq(CommentDO::getArticleId, articleId) + .eq(CommentDO::getDeleted, YesOrNoEnum.NO.getCode()).list(); + } + + + /** + * 查询有效评论数 + * + * @param articleId + * @return + */ + public int commentCount(Long articleId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(CommentDO::getArticleId, articleId) + .eq(CommentDO::getDeleted, YesOrNoEnum.NO.getCode()); + return baseMapper.selectCount(queryWrapper).intValue(); + } + + public CommentDO getHotComment(Long articleId) { + Map map = baseMapper.getHotTopCommentId(articleId); + if (CollectionUtils.isEmpty(map)) { + return null; + } + + return baseMapper.selectById(Long.parseLong(String.valueOf(map.get("top_comment_id")))); + } + +} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/repository/entity/CommentDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/repository/entity/CommentDO.java similarity index 75% rename from forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/repository/entity/CommentDO.java rename to paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/repository/entity/CommentDO.java index 544e787b6..fbe0b47bc 100644 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/comment/repository/entity/CommentDO.java +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/repository/entity/CommentDO.java @@ -1,7 +1,8 @@ -package com.github.liuyueyi.forum.service.comment.repository.entity; +package com.github.paicoding.forum.service.comment.repository.entity; import com.baomidou.mybatisplus.annotation.TableName; -import com.github.liueyueyi.forum.api.model.entity.BaseDO; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import com.github.paicoding.forum.core.senstive.ano.SensitiveField; import lombok.Data; import lombok.EqualsAndHashCode; @@ -31,6 +32,7 @@ public class CommentDO extends BaseDO { /** * 评论内容 */ + @SensitiveField(bind = "content") private String content; /** diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/repository/mapper/CommentMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/repository/mapper/CommentMapper.java new file mode 100644 index 000000000..ac5a12626 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/repository/mapper/CommentMapper.java @@ -0,0 +1,18 @@ +package com.github.paicoding.forum.service.comment.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.service.comment.repository.entity.CommentDO; +import org.apache.ibatis.annotations.Param; + +import java.util.Map; + +/** + * 评论mapper接口 + * + * @author louzai + * @date 2022-07-18 + */ +public interface CommentMapper extends BaseMapper { + Map getHotTopCommentId(@Param("articleId") Long articleId); + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/service/CommentReadService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/service/CommentReadService.java new file mode 100644 index 000000000..89124bd1c --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/service/CommentReadService.java @@ -0,0 +1,49 @@ +package com.github.paicoding.forum.service.comment.service; + +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.comment.dto.TopCommentDTO; +import com.github.paicoding.forum.service.comment.repository.entity.CommentDO; + +import java.util.List; + +/** + * 评论Service接口 + * + * @author louzai + * @date 2022-07-24 + */ +public interface CommentReadService { + + /** + * 根据评论id查询评论信息 + * + * @param commentId + * @return + */ + CommentDO queryComment(Long commentId); + + /** + * 查询文章评论列表 + * + * @param articleId + * @param page + * @return + */ + List getArticleComments(Long articleId, PageParam page); + + /** + * 查询热门评论 + * + * @param articleId + * @return + */ + TopCommentDTO queryHotComment(Long articleId); + + /** + * 文章的有效评论数 + * + * @param articleId + * @return + */ + int queryCommentCount(Long articleId); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/service/CommentWriteService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/service/CommentWriteService.java new file mode 100644 index 000000000..f19fd9ed6 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/service/CommentWriteService.java @@ -0,0 +1,29 @@ +package com.github.paicoding.forum.service.comment.service; + +import com.github.paicoding.forum.api.model.vo.comment.CommentSaveReq; + +/** + * 评论Service接口 + * + * @author louzai + * @date 2022-07-24 + */ +public interface CommentWriteService { + + /** + * 更新/保存评论 + * + * @param commentSaveReq + * @return + */ + Long saveComment(CommentSaveReq commentSaveReq); + + /** + * 删除评论 + * + * @param commentId + * @throws Exception + */ + void deleteComment(Long commentId, Long userId); + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/service/impl/CommentReadServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/service/impl/CommentReadServiceImpl.java new file mode 100644 index 000000000..5e338beb9 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/service/impl/CommentReadServiceImpl.java @@ -0,0 +1,178 @@ +package com.github.paicoding.forum.service.comment.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.api.model.enums.DocumentTypeEnum; +import com.github.paicoding.forum.api.model.enums.PraiseStatEnum; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.comment.dto.BaseCommentDTO; +import com.github.paicoding.forum.api.model.vo.comment.dto.SubCommentDTO; +import com.github.paicoding.forum.api.model.vo.comment.dto.TopCommentDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; +import com.github.paicoding.forum.service.comment.converter.CommentConverter; +import com.github.paicoding.forum.service.comment.repository.dao.CommentDao; +import com.github.paicoding.forum.service.comment.repository.entity.CommentDO; +import com.github.paicoding.forum.service.comment.service.CommentReadService; +import com.github.paicoding.forum.service.user.repository.entity.UserFootDO; +import com.github.paicoding.forum.service.statistics.service.CountService; +import com.github.paicoding.forum.service.user.service.UserFootService; +import com.github.paicoding.forum.service.user.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 评论Service + * + * @author louzai + * @date 2022-07-24 + */ +@Service +public class CommentReadServiceImpl implements CommentReadService { + + @Autowired + private CommentDao commentDao; + + @Autowired + private UserService userService; + + @Autowired + private CountService countService; + + @Autowired + private UserFootService userFootService; + + @Override + public CommentDO queryComment(Long commentId) { + return commentDao.getById(commentId); + } + + @Override + public List getArticleComments(Long articleId, PageParam page) { + // 1.查询一级评论 + List comments = commentDao.listTopCommentList(articleId, page); + if (CollectionUtils.isEmpty(comments)) { + return Collections.emptyList(); + } + // map 存 commentId -> 评论 + Map topComments = comments.stream().collect(Collectors.toMap(CommentDO::getId, CommentConverter::toTopDto)); + + // 2.查询非一级评论 + List subComments = commentDao.listSubCommentIdMappers(articleId, topComments.keySet()); + + // 3.构建一级评论的子评论 + buildCommentRelation(subComments, topComments); + + // 4.挑出需要返回的数据,排序,并补齐对应的用户信息,最后排序返回 + List result = new ArrayList<>(); + comments.forEach(comment -> { + TopCommentDTO dto = topComments.get(comment.getId()); + fillTopCommentInfo(dto); + result.add(dto); + }); + + // 返回结果根据时间进行排序 + Collections.sort(result); + return result; + } + + /** + * 构建父子评论关系 + */ + private void buildCommentRelation(List subComments, Map topComments) { + Map subCommentMap = subComments.stream().collect(Collectors.toMap(CommentDO::getId, CommentConverter::toSubDto)); + subComments.forEach(comment -> { + TopCommentDTO top = topComments.get(comment.getTopCommentId()); + if (top == null) { + return; + } + SubCommentDTO sub = subCommentMap.get(comment.getId()); + top.getChildComments().add(sub); + if (Objects.equals(comment.getTopCommentId(), comment.getParentCommentId())) { + return; + } + + SubCommentDTO parent = subCommentMap.get(comment.getParentCommentId()); + sub.setParentContent(parent == null ? "~~已删除~~" : parent.getCommentContent()); + }); + } + + /** + * 填充评论对应的信息 + * + * @param comment + */ + private void fillTopCommentInfo(TopCommentDTO comment) { + fillCommentInfo(comment); + comment.getChildComments().forEach(this::fillCommentInfo); + Collections.sort(comment.getChildComments()); + } + + /** + * 填充评论对应的信息,如用户信息,点赞数等 + * + * @param comment + */ + private void fillCommentInfo(BaseCommentDTO comment) { + BaseUserInfoDTO userInfoDO = userService.queryBasicUserInfo(comment.getUserId()); + if (userInfoDO == null) { + // 如果用户注销,给一个默认的用户 + comment.setUserName("默认用户"); + comment.setUserPhoto(""); + if (comment instanceof TopCommentDTO) { + ((TopCommentDTO) comment).setCommentCount(0); + } + } else { + comment.setUserName(userInfoDO.getUserName()); + comment.setUserPhoto(userInfoDO.getPhoto()); + if (comment instanceof TopCommentDTO) { + ((TopCommentDTO) comment).setCommentCount(((TopCommentDTO) comment).getChildComments().size()); + } + } + + // 查询点赞数 + Long praiseCount = countService.queryCommentPraiseCount(comment.getCommentId()); + comment.setPraiseCount(praiseCount.intValue()); + + // 查询当前登录用于是否点赞过 + Long loginUserId = ReqInfoContext.getReqInfo().getUserId(); + if (loginUserId != null) { + // 判断当前用户是否点过赞 + UserFootDO foot = userFootService.queryUserFoot(comment.getCommentId(), DocumentTypeEnum.COMMENT.getCode(), loginUserId); + comment.setPraised(foot != null && Objects.equals(foot.getPraiseStat(), PraiseStatEnum.PRAISE.getCode())); + } else { + comment.setPraised(false); + } + } + + /** + * 查询回帖最多的评论 + * + * @param articleId + * @return + */ + @Override + public TopCommentDTO queryHotComment(Long articleId) { + CommentDO comment = commentDao.getHotComment(articleId); + if (comment == null) { + return null; + } + + TopCommentDTO result = CommentConverter.toTopDto(comment); + // 查询子评论 + List subComments = commentDao.listSubCommentIdMappers(articleId, Collections.singletonList(comment.getId())); + List subs = subComments.stream().map(CommentConverter::toSubDto).collect(Collectors.toList()); + result.setChildComments(subs); + + // 填充评论信息 + fillTopCommentInfo(result); + return result; + } + + @Override + public int queryCommentCount(Long articleId) { + return commentDao.commentCount(articleId); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/service/impl/CommentWriteServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/service/impl/CommentWriteServiceImpl.java new file mode 100644 index 000000000..e286afabb --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/comment/service/impl/CommentWriteServiceImpl.java @@ -0,0 +1,196 @@ +package com.github.paicoding.forum.service.comment.service.impl; + +import com.github.paicoding.forum.api.model.enums.NotifyTypeEnum; +import com.github.paicoding.forum.api.model.enums.YesOrNoEnum; +import com.github.paicoding.forum.api.model.enums.ai.AiBotEnum; +import com.github.paicoding.forum.api.model.exception.ExceptionUtil; +import com.github.paicoding.forum.api.model.vo.comment.CommentSaveReq; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import com.github.paicoding.forum.api.model.vo.notify.NotifyMsgEvent; +import com.github.paicoding.forum.core.util.NumUtil; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDO; +import com.github.paicoding.forum.service.article.service.ArticleReadService; +import com.github.paicoding.forum.service.chatai.bot.HaterBot; +import com.github.paicoding.forum.service.comment.converter.CommentConverter; +import com.github.paicoding.forum.service.comment.repository.dao.CommentDao; +import com.github.paicoding.forum.service.comment.repository.entity.CommentDO; +import com.github.paicoding.forum.service.comment.service.CommentWriteService; +import com.github.paicoding.forum.service.user.service.UserFootService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.Objects; + +/** + * 评论Service + * + * @author louzai + * @date 2022-07-24 + */ +@Slf4j +@Service +public class CommentWriteServiceImpl implements CommentWriteService { + + @Autowired + private CommentDao commentDao; + + @Autowired + private ArticleReadService articleReadService; + + @Autowired + private UserFootService userFootWriteService; + @Autowired + private HaterBot haterBot; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long saveComment(CommentSaveReq commentSaveReq) { + // 保存评论 + CommentDO comment; + if (NumUtil.nullOrZero(commentSaveReq.getCommentId())) { + comment = addComment(commentSaveReq); + } else { + comment = updateComment(commentSaveReq); + } + return comment.getId(); + } + + private CommentDO addComment(CommentSaveReq commentSaveReq) { + // 0.获取父评论信息,校验是否存在 + CommentDO parentComment = getParentCommentUser(commentSaveReq.getParentCommentId()); + Long parentUser = parentComment == null ? null : parentComment.getUserId(); + + // 1. 保存评论内容 + CommentDO commentDO = CommentConverter.toDo(commentSaveReq); + Date now = new Date(); + commentDO.setCreateTime(now); + commentDO.setUpdateTime(now); + commentDao.save(commentDO); + + // 2. 保存足迹信息 : 文章的已评信息 + 评论的已评信息 + ArticleDO article = articleReadService.queryBasicArticle(commentSaveReq.getArticleId()); + if (article == null) { + throw ExceptionUtil.of(StatusEnum.ARTICLE_NOT_EXISTS, commentSaveReq.getArticleId()); + } + userFootWriteService.saveCommentFoot(commentDO, article.getUserId(), parentUser); + + // 3. 触发杠精机器人 + this.haterBotTrigger(commentDO, parentComment); + + // 4. 发布添加/回复评论事件 + SpringUtil.publishEvent(new NotifyMsgEvent<>(this, NotifyTypeEnum.COMMENT, commentDO)); + if (NumUtil.upZero(parentUser)) { + // 评论回复事件 + SpringUtil.publishEvent(new NotifyMsgEvent<>(this, NotifyTypeEnum.REPLY, commentDO)); + } + return commentDO; + } + + private CommentDO updateComment(CommentSaveReq commentSaveReq) { + // 更新评论 + CommentDO commentDO = commentDao.getById(commentSaveReq.getCommentId()); + if (commentDO == null) { + throw ExceptionUtil.of(StatusEnum.COMMENT_NOT_EXISTS, commentSaveReq.getCommentId()); + } + commentDO.setContent(commentSaveReq.getCommentContent()); + commentDao.updateById(commentDO); + return commentDO; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteComment(Long commentId, Long userId) { + CommentDO commentDO = commentDao.getById(commentId); + // 1.校验评论,是否越权,文章是否存在 + if (commentDO == null) { + throw ExceptionUtil.of(StatusEnum.COMMENT_NOT_EXISTS, "评论ID=" + commentId); + } + if (Objects.equals(commentDO.getUserId(), userId)) { + throw ExceptionUtil.of(StatusEnum.FORBID_ERROR_MIXED, "无权删除评论"); + } + // 获取文章信息 + ArticleDO article = articleReadService.queryBasicArticle(commentDO.getArticleId()); + if (article == null) { + throw ExceptionUtil.of(StatusEnum.ARTICLE_NOT_EXISTS, commentDO.getArticleId()); + } + + // 2.删除评论、足迹 + commentDO.setDeleted(YesOrNoEnum.YES.getCode()); + commentDao.updateById(commentDO); + CommentDO parentComment = getParentCommentUser(commentDO.getParentCommentId()); + userFootWriteService.removeCommentFoot(commentDO, article.getUserId(), parentComment == null ? null : parentComment.getUserId()); + + // 3. 发布删除评论事件 + SpringUtil.publishEvent(new NotifyMsgEvent<>(this, NotifyTypeEnum.DELETE_COMMENT, commentDO)); + if (NumUtil.upZero(commentDO.getParentCommentId())) { + // 评论 + SpringUtil.publishEvent(new NotifyMsgEvent<>(this, NotifyTypeEnum.DELETE_REPLY, commentDO)); + } + } + + + private CommentDO getParentCommentUser(Long parentCommentId) { + if (NumUtil.nullOrZero(parentCommentId)) { + return null; + + } + CommentDO parent = commentDao.getById(parentCommentId); + if (parent == null) { + throw ExceptionUtil.of(StatusEnum.COMMENT_NOT_EXISTS, "父评论=" + parentCommentId); + } + return parent; + } + + + /** + * 机器人回复 + * + * @param comment 当前评论内容 + * @param parent 当前评论的父评论 + */ + private void haterBotTrigger(CommentDO comment, CommentDO parent) { + boolean trigger = false; + Long haterBotUserId = haterBot.getBotUser().getUserId(); + Long topCommentId = 0L; + if (parent == null) { + // 当前的评论就是顶级评论,根据回复内容是否有触发词来决定是否需要进行触发 + String tag = "@" + AiBotEnum.HATER_BOT.getNickName(); + if (comment.getContent().contains(tag)) { + comment.setContent(StringUtils.replace(comment.getContent(), tag, "")); + trigger = true; + } + topCommentId = comment.getId(); + } else { + // 回复内容,根据回复的用户是否为机器人,来判定是否需要进行触发 + if (Objects.equals(haterBotUserId, parent.getUserId())) { + trigger = true; + } + topCommentId = comment.getTopCommentId(); + } + + // 评论中,@了机器人,那么开启评论对线模式 + if (trigger) { + log.info("评论「{}」 开启了在线互怼模式", comment); + // sourceBizId: 主要用于构建聊天对话,以顶级评论 + 用户id作为唯一标识 + // 避免出现一个顶级评论开启对线,后续的回复中有其他用户参与进来时,因为用户id不同,这样传递给大模型的上下文就不会出现交叉 + haterBot.trigger(comment.getContent(), "comment:" + topCommentId + "_" + comment.getUserId(), reply -> { + aiReply(haterBotUserId, reply, comment); + }); + } + } + + private void aiReply(Long aiUserId, String replyContent, CommentDO parentComment) { + CommentSaveReq save = new CommentSaveReq(); + save.setArticleId(parentComment.getArticleId()); + save.setCommentContent(replyContent); + save.setUserId(aiUserId); + save.setParentCommentId(parentComment.getId()); + save.setTopCommentId(NumUtil.upZero(parentComment.getTopCommentId()) ? parentComment.getTopCommentId() : parentComment.getId()); + SpringUtil.getBean(CommentWriteService.class).saveComment(save); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/converter/ConfigConverter.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/converter/ConfigConverter.java new file mode 100644 index 000000000..6e84e8099 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/converter/ConfigConverter.java @@ -0,0 +1,61 @@ +package com.github.paicoding.forum.service.config.converter; + +import com.github.paicoding.forum.api.model.vo.banner.ConfigReq; +import com.github.paicoding.forum.api.model.vo.banner.dto.ConfigDTO; +import com.github.paicoding.forum.service.config.repository.entity.ConfigDO; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Banner转换 + * + * @author louzai + * @date 2022-09-20 + */ +public class ConfigConverter { + + public static List toDTOS(List records) { + if (CollectionUtils.isEmpty(records)) { + return Collections.emptyList(); + } + return records.stream().map(ConfigConverter::toDTO).collect(Collectors.toList()); + } + + public static ConfigDTO toDTO(ConfigDO configDO) { + if (configDO == null) { + return null; + } + ConfigDTO configDTO = new ConfigDTO(); + configDTO.setType(configDO.getType()); + configDTO.setName(configDO.getName()); + configDTO.setBannerUrl(configDO.getBannerUrl()); + configDTO.setJumpUrl(configDO.getJumpUrl()); + configDTO.setContent(configDO.getContent()); + configDTO.setRank(configDO.getRank()); + configDTO.setStatus(configDO.getStatus()); + configDTO.setId(configDO.getId()); + configDTO.setTags(configDO.getTags()); + configDTO.setExtra(configDO.getExtra()); + configDTO.setCreateTime(configDO.getCreateTime()); + configDTO.setUpdateTime(configDO.getUpdateTime()); + return configDTO; + } + + public static ConfigDO toDO(ConfigReq configReq) { + if (configReq == null) { + return null; + } + ConfigDO configDO = new ConfigDO(); + configDO.setType(configReq.getType()); + configDO.setName(configReq.getName()); + configDO.setBannerUrl(configReq.getBannerUrl()); + configDO.setJumpUrl(configReq.getJumpUrl()); + configDO.setContent(configReq.getContent()); + configDO.setRank(configReq.getRank()); + configDO.setTags(configReq.getTags()); + return configDO; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/converter/ConfigStructMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/converter/ConfigStructMapper.java new file mode 100644 index 000000000..2f1298fb9 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/converter/ConfigStructMapper.java @@ -0,0 +1,50 @@ +package com.github.paicoding.forum.service.config.converter; + +import com.github.paicoding.forum.api.model.vo.banner.ConfigReq; +import com.github.paicoding.forum.api.model.vo.banner.SearchConfigReq; +import com.github.paicoding.forum.api.model.vo.banner.dto.ConfigDTO; +import com.github.paicoding.forum.api.model.vo.config.GlobalConfigReq; +import com.github.paicoding.forum.api.model.vo.config.SearchGlobalConfigReq; +import com.github.paicoding.forum.api.model.vo.config.dto.GlobalConfigDTO; +import com.github.paicoding.forum.service.config.repository.entity.ConfigDO; +import com.github.paicoding.forum.service.config.repository.entity.GlobalConfigDO; +import com.github.paicoding.forum.service.config.repository.params.SearchConfigParams; +import com.github.paicoding.forum.service.config.repository.params.SearchGlobalConfigParams; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface ConfigStructMapper { + // instance + ConfigStructMapper INSTANCE = Mappers.getMapper( ConfigStructMapper.class ); + + // req to params + @Mapping(source = "pageNumber", target = "pageNum") + SearchConfigParams toSearchParams(SearchConfigReq req); + + // req to params + @Mapping(source = "pageNumber", target = "pageNum") + // key to keywords + @Mapping(source = "keywords", target = "key") + SearchGlobalConfigParams toSearchGlobalParams(SearchGlobalConfigReq req); + + // do to dto + ConfigDTO toDTO(ConfigDO configDO); + + List toDTOS(List configDOS); + + ConfigDO toDO(ConfigReq configReq); + + // do to dto + // key to keywords + @Mapping(source = "key", target = "keywords") + GlobalConfigDTO toGlobalDTO(GlobalConfigDO configDO); + + List toGlobalDTOS(List configDOS); + + @Mapping(source = "keywords", target = "key") + GlobalConfigDO toGlobalDO(GlobalConfigReq req); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/converter/DictCommonConverter.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/converter/DictCommonConverter.java new file mode 100644 index 000000000..36a84bf00 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/converter/DictCommonConverter.java @@ -0,0 +1,37 @@ +package com.github.paicoding.forum.service.config.converter; + +import com.github.paicoding.forum.api.model.vo.article.dto.DictCommonDTO; +import com.github.paicoding.forum.service.config.repository.entity.DictCommonDO; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Banner转换 + * + * @author louzai + * @date 2022-09-20 + */ +public class DictCommonConverter { + + public static List toDTOS(List records) { + if (CollectionUtils.isEmpty(records)) { + return Collections.emptyList(); + } + return records.stream().map(DictCommonConverter::toDTO).collect(Collectors.toList()); + } + + public static DictCommonDTO toDTO(DictCommonDO dictCommonDO) { + if (dictCommonDO == null) { + return null; + } + DictCommonDTO dictCommonDTO = new DictCommonDTO(); + dictCommonDTO.setTypeCode(dictCommonDO.getTypeCode()); + dictCommonDTO.setDictCode(dictCommonDO.getDictCode()); + dictCommonDTO.setDictDesc(dictCommonDO.getDictDesc()); + dictCommonDTO.setSortNo(dictCommonDO.getSortNo()); + return dictCommonDTO; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/es/ElasticsearchConfig.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/es/ElasticsearchConfig.java new file mode 100644 index 000000000..99fc7fe02 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/es/ElasticsearchConfig.java @@ -0,0 +1,102 @@ +package com.github.paicoding.forum.service.config.es; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; +import org.elasticsearch.client.RestHighLevelClient; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * es配置类 + * + * @author ygl + * @since 2023-05-25 + **/ +@Slf4j +@Data +@Configuration +// 下面这个表示只有 elasticsearch.open = true 时,采进行es的配置初始化;当不使用es时,则不会实例 RestHighLevelClient +@ConditionalOnProperty(prefix = "elasticsearch", name = "open") +@ConfigurationProperties(prefix = "elasticsearch") +public class ElasticsearchConfig { + + // 是否开启ES + private Boolean open; + + // es host ip 地址(集群) + private String hosts; + + // es用户名 + private String userName; + + // es密码 + private String password; + + // es 请求方式 + private String scheme; + + // es集群名称 + private String clusterName; + + // es 连接超时时间 + private int connectTimeOut; + + // es socket 连接超时时间 + private int socketTimeOut; + + // es 请求超时时间 + private int connectionRequestTimeOut; + + // es 最大连接数 + private int maxConnectNum; + + // es 每个路由的最大连接数 + private int maxConnectNumPerRoute; + + + /** + * 如果@Bean没有指定bean的名称,那么这个bean的名称就是方法名 + */ + @Bean(name = "restHighLevelClient") + public RestHighLevelClient restHighLevelClient() { + + // 此处为单节点es + String host = hosts.split(":")[0]; + String port = hosts.split(":")[1]; + HttpHost httpHost = new HttpHost(host, Integer.parseInt(port)); + + // 构建连接对象 + RestClientBuilder builder = RestClient.builder(httpHost); + + // 设置用户名、密码 + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(userName, password)); + + // 连接延时配置 + builder.setRequestConfigCallback(requestConfigBuilder -> { + requestConfigBuilder.setConnectTimeout(connectTimeOut); + requestConfigBuilder.setSocketTimeout(socketTimeOut); + requestConfigBuilder.setConnectionRequestTimeout(connectionRequestTimeOut); + return requestConfigBuilder; + }); + // 连接数配置 + builder.setHttpClientConfigCallback(httpClientBuilder -> { + httpClientBuilder.setMaxConnTotal(maxConnectNum); + httpClientBuilder.setMaxConnPerRoute(maxConnectNumPerRoute); + httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); + return httpClientBuilder; + }); + + return new RestHighLevelClient(builder); + } +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/dao/ConfigDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/dao/ConfigDao.java new file mode 100644 index 000000000..39d00117d --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/dao/ConfigDao.java @@ -0,0 +1,191 @@ +package com.github.paicoding.forum.service.config.repository.dao; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.paicoding.forum.api.model.enums.ConfigTypeEnum; +import com.github.paicoding.forum.api.model.enums.PushStatusEnum; +import com.github.paicoding.forum.api.model.enums.YesOrNoEnum; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.banner.dto.ConfigDTO; +import com.github.paicoding.forum.service.config.converter.ConfigConverter; +import com.github.paicoding.forum.service.config.converter.ConfigStructMapper; +import com.github.paicoding.forum.service.config.repository.entity.ConfigDO; +import com.github.paicoding.forum.service.config.repository.entity.GlobalConfigDO; +import com.github.paicoding.forum.service.config.repository.mapper.ConfigMapper; +import com.github.paicoding.forum.service.config.repository.mapper.GlobalConfigMapper; +import com.github.paicoding.forum.service.config.repository.params.SearchConfigParams; +import com.github.paicoding.forum.service.config.repository.params.SearchGlobalConfigParams; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; + +/** + * @author YiHui + * @date 2022/9/2 + */ +@Repository +public class ConfigDao extends ServiceImpl { + @Resource + private GlobalConfigMapper globalConfigMapper; + + /** + * 根据类型获取配置列表(无需分页) + * + * @param type + * @return + */ + public List listConfigByType(Integer type) { + List configDOS = lambdaQuery() + .eq(ConfigDO::getType, type) + .eq(ConfigDO::getStatus, PushStatusEnum.ONLINE.getCode()) + .eq(ConfigDO::getDeleted, YesOrNoEnum.NO.getCode()) + .orderByAsc(ConfigDO::getRank) + .list(); + return ConfigConverter.toDTOS(configDOS); + } + + private LambdaQueryChainWrapper createConfigQuery(SearchConfigParams params) { + return lambdaQuery() + .eq(ConfigDO::getDeleted, YesOrNoEnum.NO.getCode()) + .like(StringUtils.isNotBlank(params.getName()), ConfigDO::getName, params.getName()) + .eq(params.getType() != null && params.getType() != -1, ConfigDO::getType, params.getType()); + } + + /** + * 获取所有 Banner 列表(分页) + * + * @return + */ + public List listBanner(SearchConfigParams params) { + List configDOS = createConfigQuery(params) + .orderByDesc(ConfigDO::getUpdateTime) + .orderByAsc(ConfigDO::getRank) + .last(PageParam.getLimitSql( + PageParam.newPageInstance(params.getPageNum(), params.getPageSize()))) + .list(); + return ConfigStructMapper.INSTANCE.toDTOS(configDOS); + } + + /** + * 获取所有 Banner 总数(分页) + * + * @return + */ + public Long countConfig(SearchConfigParams params) { + return createConfigQuery(params) + .count(); + } + + /** + * 获取所有公告列表(分页) + * + * @return + */ + public List listNotice(PageParam pageParam) { + List configDOS = lambdaQuery() + .eq(ConfigDO::getType, ConfigTypeEnum.NOTICE.getCode()) + .eq(ConfigDO::getDeleted, YesOrNoEnum.NO.getCode()) + .orderByDesc(ConfigDO::getCreateTime) + .last(PageParam.getLimitSql(pageParam)) + .list(); + return ConfigConverter.toDTOS(configDOS); + } + + /** + * 获取所有公告总数(分页) + * + * @return + */ + public Integer countNotice() { + return lambdaQuery() + .eq(ConfigDO::getType, ConfigTypeEnum.NOTICE.getCode()) + .eq(ConfigDO::getDeleted, YesOrNoEnum.NO.getCode()) + .count() + .intValue(); + } + + /** + * 更新阅读相关计数 + */ + public void updatePdfConfigVisitNum(long configId, String extra) { + lambdaUpdate().set(ConfigDO::getExtra, extra) + .eq(ConfigDO::getId, configId) + .update(); + } + + public List listGlobalConfig(SearchGlobalConfigParams params) { + LambdaQueryWrapper query = buildQuery(params); + query.select(GlobalConfigDO::getId, + GlobalConfigDO::getKey, + GlobalConfigDO::getValue, + GlobalConfigDO::getComment); + return globalConfigMapper.selectList(query); + } + + public Long countGlobalConfig(SearchGlobalConfigParams params) { + return globalConfigMapper.selectCount(buildQuery(params)); + } + + private LambdaQueryWrapper buildQuery(SearchGlobalConfigParams params) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + + query.and(!StringUtils.isEmpty(params.getKey()), + k -> k.like(GlobalConfigDO::getKey, params.getKey())) + .and(!StringUtils.isEmpty(params.getValue()), + v -> v.like(GlobalConfigDO::getValue, params.getValue())) + .and(!StringUtils.isEmpty(params.getComment()), + c -> c.like(GlobalConfigDO::getComment, params.getComment())) + .eq(GlobalConfigDO::getDeleted, YesOrNoEnum.NO.getCode()) + .orderByDesc(GlobalConfigDO::getUpdateTime); + return query; + } + + public void save(GlobalConfigDO globalConfigDO) { + globalConfigMapper.insert(globalConfigDO); + } + + public void updateById(GlobalConfigDO globalConfigDO) { + globalConfigDO.setUpdateTime(new Date()); + globalConfigMapper.updateById(globalConfigDO); + } + + /** + * 根据id查询全局配置 + * + * @param id + * @return + */ + public GlobalConfigDO getGlobalConfigById(Long id) { + // 查询的时候 deleted 为 0 + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.select(GlobalConfigDO::getId, GlobalConfigDO::getKey, GlobalConfigDO::getValue, GlobalConfigDO::getComment) + .eq(GlobalConfigDO::getId, id) + .eq(GlobalConfigDO::getDeleted, YesOrNoEnum.NO.getCode()); + return globalConfigMapper.selectOne(query); + } + + /** + * 根据key查询全局配置 + * + * @param key + * @return + */ + public GlobalConfigDO getGlobalConfigByKey(String key) { + // 查询的时候 deleted 为 0 + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.select(GlobalConfigDO::getId, GlobalConfigDO::getKey, GlobalConfigDO::getValue, GlobalConfigDO::getComment) + .eq(GlobalConfigDO::getKey, key) + .eq(GlobalConfigDO::getDeleted, YesOrNoEnum.NO.getCode()); + return globalConfigMapper.selectOne(query); + } + + public void delete(GlobalConfigDO globalConfigDO) { + globalConfigDO.setDeleted(YesOrNoEnum.YES.getCode()); + globalConfigMapper.updateById(globalConfigDO); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/dao/DictCommonDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/dao/DictCommonDao.java new file mode 100644 index 000000000..7e85efd76 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/dao/DictCommonDao.java @@ -0,0 +1,27 @@ +package com.github.paicoding.forum.service.config.repository.dao; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.paicoding.forum.api.model.vo.article.dto.DictCommonDTO; +import com.github.paicoding.forum.service.config.converter.DictCommonConverter; +import com.github.paicoding.forum.service.config.repository.entity.DictCommonDO; +import com.github.paicoding.forum.service.config.repository.mapper.DictCommonMapper; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author Louzai + * @date 2022/9/2 + */ +@Repository +public class DictCommonDao extends ServiceImpl { + + /** + * 获取所有字典列表 + * @return + */ + public List getDictList() { + List list = lambdaQuery().list(); + return DictCommonConverter.toDTOS(list); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/entity/ConfigDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/entity/ConfigDO.java new file mode 100644 index 000000000..398b26214 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/entity/ConfigDO.java @@ -0,0 +1,76 @@ +package com.github.paicoding.forum.service.config.repository.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import com.github.paicoding.forum.api.model.enums.ConfigTagEnum; +import com.github.paicoding.forum.api.model.enums.ConfigTypeEnum; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 评论表 + * + * @author louzai + * @date 2022-07-18 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("config") +public class ConfigDO extends BaseDO { + private static final long serialVersionUID = -6122208316544171303L; + /** + * 类型 + * @see ConfigTypeEnum#getCode() + */ + private Integer type; + + /** + * 名称 + */ + @TableField("`name`") + private String name; + + /** + * 图片链接 + */ + private String bannerUrl; + + /** + * 跳转链接 + */ + private String jumpUrl; + + /** + * 内容 + */ + private String content; + + /** + * 排序 + */ + @TableField("`rank`") + private Integer rank; + + /** + * 状态:0-未发布,1-已发布 + */ + private Integer status; + + /** + * 0未删除 1 已删除 + */ + private Integer deleted; + + /** + * 配置对应的标签,英文逗号分隔 + * + * @see ConfigTagEnum#getCode() + */ + private String tags; + + /** + * 扩展信息,如记录 评分,阅读人数,下载次数等 + */ + private String extra; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/entity/DictCommonDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/entity/DictCommonDO.java new file mode 100644 index 000000000..e0b735e07 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/entity/DictCommonDO.java @@ -0,0 +1,52 @@ +package com.github.paicoding.forum.service.config.repository.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + *

+ * 通用数据字典 + *

+ * + * @author liudongshan + * @since 2021-05-31 + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +@TableName("dict_common") +public class DictCommonDO extends BaseDO { + /** + * 字典类型 + */ + @TableField("type_code") + private String typeCode; + + /** + * 字典类型的值编码 + */ + @TableField("dict_code") + private String dictCode; + + /** + * 字典类型的值描述 + */ + @TableField("dict_desc") + private String dictDesc; + + /** + * 排序编号 + */ + @TableField("sort_no") + private Integer sortNo; + + /** + * 备注 + */ + @TableField("remark") + private String remark; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/entity/GlobalConfigDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/entity/GlobalConfigDO.java new file mode 100644 index 000000000..4d4b050c9 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/entity/GlobalConfigDO.java @@ -0,0 +1,30 @@ +package com.github.paicoding.forum.service.config.repository.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 评论表 + * + * @author louzai + * @date 2022-07-18 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("global_conf") +public class GlobalConfigDO extends BaseDO { + private static final long serialVersionUID = -6122208316544171301L; + + // 配置项名称 + @TableField("`key`") + private String key; + // 配置项值 + private String value; + // 备注 + private String comment; + // 删除 + private Integer deleted; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/mapper/ConfigMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/mapper/ConfigMapper.java new file mode 100644 index 000000000..ee1925689 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/mapper/ConfigMapper.java @@ -0,0 +1,13 @@ +package com.github.paicoding.forum.service.config.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.service.config.repository.entity.ConfigDO; + +/** + * 配置mapper接口 + * + * @author louzai + * @date 2022-07-18 + */ +public interface ConfigMapper extends BaseMapper { +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/mapper/DictCommonMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/mapper/DictCommonMapper.java new file mode 100644 index 000000000..d7bb7b927 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/mapper/DictCommonMapper.java @@ -0,0 +1,13 @@ +package com.github.paicoding.forum.service.config.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.service.config.repository.entity.DictCommonDO; + +/** + * 字典mapper接口 + * + * @author louzai + * @date 2022-07-18 + */ +public interface DictCommonMapper extends BaseMapper { +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/mapper/GlobalConfigMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/mapper/GlobalConfigMapper.java new file mode 100644 index 000000000..bdebda343 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/mapper/GlobalConfigMapper.java @@ -0,0 +1,13 @@ +package com.github.paicoding.forum.service.config.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.service.config.repository.entity.GlobalConfigDO; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 6/30/23 + */ +public interface GlobalConfigMapper extends BaseMapper { +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/params/SearchConfigParams.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/params/SearchConfigParams.java new file mode 100644 index 000000000..fd6c71c08 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/params/SearchConfigParams.java @@ -0,0 +1,14 @@ +package com.github.paicoding.forum.service.config.repository.params; + +import com.github.paicoding.forum.api.model.vo.PageParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class SearchConfigParams extends PageParam { + // 类型 + private Integer type; + // 名称 + private String name; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/params/SearchGlobalConfigParams.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/params/SearchGlobalConfigParams.java new file mode 100644 index 000000000..957323385 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/repository/params/SearchGlobalConfigParams.java @@ -0,0 +1,22 @@ +package com.github.paicoding.forum.service.config.repository.params; + +import com.github.paicoding.forum.api.model.vo.PageParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 6/30/23 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class SearchGlobalConfigParams extends PageParam { + // 配置项名称 + private String key; + // 配置项值 + private String value; + // 备注 + private String comment; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/security/SecurityConfig.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/security/SecurityConfig.java new file mode 100644 index 000000000..29393c000 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/security/SecurityConfig.java @@ -0,0 +1,15 @@ +package com.github.paicoding.forum.service.config.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class SecurityConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/ConfigService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/ConfigService.java new file mode 100644 index 000000000..438a7d72c --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/ConfigService.java @@ -0,0 +1,31 @@ +package com.github.paicoding.forum.service.config.service; + +import com.github.paicoding.forum.api.model.enums.ConfigTypeEnum; +import com.github.paicoding.forum.api.model.vo.banner.dto.ConfigDTO; + +import java.util.List; + +/** + * Banner前台接口 + * + * @author louzai + * @date 2022-07-24 + */ +public interface ConfigService { + + /** + * 获取 Banner 列表 + * + * @param configTypeEnum + * @return + */ + List getConfigList(ConfigTypeEnum configTypeEnum); + + /** + * 阅读次数+1 + * + * @param configId + * @param extra + */ + void updateVisit(long configId, String extra); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/ConfigSettingService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/ConfigSettingService.java new file mode 100644 index 000000000..23fd09c50 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/ConfigSettingService.java @@ -0,0 +1,50 @@ +package com.github.paicoding.forum.service.config.service; + +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.banner.ConfigReq; +import com.github.paicoding.forum.api.model.vo.banner.SearchConfigReq; +import com.github.paicoding.forum.api.model.vo.banner.dto.ConfigDTO; + +/** + * Banner后台接口 + * + * @author louzai + * @date 2022-07-24 + */ +public interface ConfigSettingService { + + /** + * 保存 + * + * @param configReq + */ + void saveConfig(ConfigReq configReq); + + /** + * 删除 + * + * @param bannerId + */ + void deleteConfig(Integer bannerId); + + /** + * 操作(上线/下线) + * + * @param bannerId + */ + void operateConfig(Integer bannerId, Integer pushStatus); + + /** + * 获取 Banner 列表 + */ + PageVo getConfigList(SearchConfigReq params); + + /** + * 获取公告列表 + * + * @param pageParam + * @return + */ + PageVo getNoticeList(PageParam pageParam); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/DictCommonService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/DictCommonService.java new file mode 100644 index 000000000..1eec856be --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/DictCommonService.java @@ -0,0 +1,18 @@ +package com.github.paicoding.forum.service.config.service; + +import java.util.Map; + +/** + * 字典Service + * + * @author louzai + * @date 2022-07-20 + */ +public interface DictCommonService { + + /** + * 获取字典值 + * @return + */ + Map getDict(); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/GlobalConfigService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/GlobalConfigService.java new file mode 100644 index 000000000..eb568d4ed --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/GlobalConfigService.java @@ -0,0 +1,27 @@ +package com.github.paicoding.forum.service.config.service; + +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.config.GlobalConfigReq; +import com.github.paicoding.forum.api.model.vo.config.SearchGlobalConfigReq; +import com.github.paicoding.forum.api.model.vo.config.dto.GlobalConfigDTO; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 6/30/23 + */ +public interface GlobalConfigService { + PageVo getList(SearchGlobalConfigReq req); + + void save(GlobalConfigReq req); + + void delete(Long id); + + /** + * 添加敏感词白名单 + * + * @param word + */ + void addSensitiveWhiteWord(String word); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/impl/ConfigServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/impl/ConfigServiceImpl.java new file mode 100644 index 000000000..5214e4908 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/impl/ConfigServiceImpl.java @@ -0,0 +1,39 @@ +package com.github.paicoding.forum.service.config.service.impl; + +import com.github.paicoding.forum.api.model.enums.ConfigTypeEnum; +import com.github.paicoding.forum.api.model.vo.banner.dto.ConfigDTO; +import com.github.paicoding.forum.service.config.repository.dao.ConfigDao; +import com.github.paicoding.forum.service.config.service.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * Banner前台接口 + * + * @author louzai + * @date 2022-07-24 + */ +@Service +public class ConfigServiceImpl implements ConfigService { + + @Autowired + private ConfigDao configDao; + + @Override + public List getConfigList(ConfigTypeEnum configTypeEnum) { + return configDao.listConfigByType(configTypeEnum.getCode()); + } + + /** + * 配置发生变更之后,失效本地缓存,这里主要是配合 SidebarServiceImpl 中的缓存使用 + * + * @param configId + * @param extra + */ + @Override + public void updateVisit(long configId, String extra) { + configDao.updatePdfConfigVisitNum(configId, extra); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/impl/ConfigSettingServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/impl/ConfigSettingServiceImpl.java new file mode 100644 index 000000000..41a89ee8a --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/impl/ConfigSettingServiceImpl.java @@ -0,0 +1,77 @@ +package com.github.paicoding.forum.service.config.service.impl; + +import com.github.paicoding.forum.api.model.enums.YesOrNoEnum; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.banner.ConfigReq; +import com.github.paicoding.forum.api.model.vo.banner.SearchConfigReq; +import com.github.paicoding.forum.api.model.vo.banner.dto.ConfigDTO; +import com.github.paicoding.forum.core.util.NumUtil; +import com.github.paicoding.forum.service.config.converter.ConfigStructMapper; +import com.github.paicoding.forum.service.config.repository.dao.ConfigDao; +import com.github.paicoding.forum.service.config.repository.entity.ConfigDO; +import com.github.paicoding.forum.service.config.repository.params.SearchConfigParams; +import com.github.paicoding.forum.service.config.service.ConfigSettingService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * Banner后台接口 + * + * @author louzai + * @date 2022-07-24 + */ +@Service +public class ConfigSettingServiceImpl implements ConfigSettingService { + + @Autowired + private ConfigDao configDao; + + @Override + public void saveConfig(ConfigReq configReq) { + ConfigDO configDO = ConfigStructMapper.INSTANCE.toDO(configReq); + if (NumUtil.nullOrZero(configReq.getConfigId())) { + configDao.save(configDO); + } else { + configDO.setId(configReq.getConfigId()); + configDao.updateById(configDO); + } + } + + @Override + public void deleteConfig(Integer configId) { + ConfigDO configDO = configDao.getById(configId); + if (configDO != null){ + configDO.setDeleted(YesOrNoEnum.YES.getCode()); + configDao.updateById(configDO); + } + } + + @Override + public void operateConfig(Integer configId, Integer pushStatus) { + ConfigDO configDO = configDao.getById(configId); + if (configDO != null){ + configDO.setStatus(pushStatus); + configDao.updateById(configDO); + } + } + + @Override + public PageVo getConfigList(SearchConfigReq req) { + // 转换 + SearchConfigParams params = ConfigStructMapper.INSTANCE.toSearchParams(req); + // 查询 + List configDTOS = configDao.listBanner(params); + Long totalCount = configDao.countConfig(params); + return PageVo.build(configDTOS, params.getPageSize(), params.getPageNum(), totalCount); + } + + @Override + public PageVo getNoticeList(PageParam pageParam) { + List configDTOS = configDao.listNotice(pageParam); + Integer totalCount = configDao.countNotice(); + return PageVo.build(configDTOS, pageParam.getPageSize(), pageParam.getPageNum(), totalCount); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/impl/DictCommonServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/impl/DictCommonServiceImpl.java new file mode 100644 index 000000000..ceab1aee9 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/impl/DictCommonServiceImpl.java @@ -0,0 +1,58 @@ +package com.github.paicoding.forum.service.config.service.impl; + +import com.github.paicoding.forum.api.model.vo.article.dto.CategoryDTO; +import com.github.paicoding.forum.api.model.vo.article.dto.DictCommonDTO; +import com.github.paicoding.forum.service.article.service.CategoryService; +import com.github.paicoding.forum.service.config.repository.dao.DictCommonDao; +import com.github.paicoding.forum.service.config.service.DictCommonService; +import com.google.common.collect.Maps; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 字典Service + * + * @author louzai + * @date 2022-07-20 + */ +@Service +public class DictCommonServiceImpl implements DictCommonService { + + @Resource + private DictCommonDao dictCommonDao; + + @Autowired + private CategoryService categoryService; + + @Override + public Map getDict() { + Map result = Maps.newLinkedHashMap(); + + List dictCommonList = dictCommonDao.getDictList(); + + Map> dictCommonMap = Maps.newLinkedHashMap(); + for (DictCommonDTO dictCommon : dictCommonList) { + Map codeMap = dictCommonMap.get(dictCommon.getTypeCode()); + if (codeMap == null || codeMap.isEmpty()) { + codeMap = Maps.newLinkedHashMap(); + dictCommonMap.put(dictCommon.getTypeCode(), codeMap); + } + codeMap.put(dictCommon.getDictCode(), dictCommon.getDictDesc()); + } + + // 获取分类的字典信息 + List categoryDTOS = categoryService.loadAllCategories(); + Map codeMap = new HashMap<>(); + categoryDTOS.forEach(categoryDTO -> codeMap.put(categoryDTO.getCategoryId().toString(), categoryDTO.getCategory())); + dictCommonMap.put("CategoryType", codeMap); + + result.putAll(dictCommonMap); + return result; + } + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/impl/GlobalConfigServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/impl/GlobalConfigServiceImpl.java new file mode 100644 index 000000000..5fe3f1c42 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/config/service/impl/GlobalConfigServiceImpl.java @@ -0,0 +1,98 @@ +package com.github.paicoding.forum.service.config.service.impl; + +import com.github.paicoding.forum.api.model.event.ConfigRefreshEvent; +import com.github.paicoding.forum.api.model.exception.ExceptionUtil; +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.config.GlobalConfigReq; +import com.github.paicoding.forum.api.model.vo.config.SearchGlobalConfigReq; +import com.github.paicoding.forum.api.model.vo.config.dto.GlobalConfigDTO; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import com.github.paicoding.forum.core.senstive.SensitiveProperty; +import com.github.paicoding.forum.core.senstive.SensitiveService; +import com.github.paicoding.forum.core.util.NumUtil; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.service.config.converter.ConfigStructMapper; +import com.github.paicoding.forum.service.config.repository.dao.ConfigDao; +import com.github.paicoding.forum.service.config.repository.entity.GlobalConfigDO; +import com.github.paicoding.forum.service.config.repository.params.SearchGlobalConfigParams; +import com.github.paicoding.forum.service.config.service.GlobalConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 6/30/23 + */ +@Service +public class GlobalConfigServiceImpl implements GlobalConfigService { + @Autowired + private ConfigDao configDao; + + @Override + public PageVo getList(SearchGlobalConfigReq req) { + ConfigStructMapper mapper = ConfigStructMapper.INSTANCE; + // 转换 + SearchGlobalConfigParams params = mapper.toSearchGlobalParams(req); + // 查询 + List list = configDao.listGlobalConfig(params); + // 总数 + Long total = configDao.countGlobalConfig(params); + + return PageVo.build(mapper.toGlobalDTOS(list), params.getPageSize(), params.getPageNum(), total); + } + + @Override + public void save(GlobalConfigReq req) { + GlobalConfigDO globalConfigDO = ConfigStructMapper.INSTANCE.toGlobalDO(req); + // id 不为空 + if (NumUtil.nullOrZero(globalConfigDO.getId())) { + configDao.save(globalConfigDO); + } else { + configDao.updateById(globalConfigDO); + } + + // 配置更新之后,主动触发配置的动态加载 + SpringUtil.publishEvent(new ConfigRefreshEvent(this, req.getKeywords(), req.getValue())); + } + + @Override + public void delete(Long id) { + GlobalConfigDO globalConfigDO = configDao.getGlobalConfigById(id); + if (globalConfigDO != null) { + configDao.delete(globalConfigDO); + } else { + throw ExceptionUtil.of(StatusEnum.RECORDS_NOT_EXISTS, "记录不存在"); + } + } + + /** + * 添加敏感词白名单 + * + * @param word + */ + @Override + public void addSensitiveWhiteWord(String word) { + String key = SensitiveProperty.SENSITIVE_KEY_PREFIX + ".allow"; + GlobalConfigReq req = new GlobalConfigReq(); + req.setKeywords(key); + + GlobalConfigDO config = configDao.getGlobalConfigByKey(key); + if (config == null) { + req.setValue(word); + req.setComment("敏感词白名单"); + } else { + req.setValue(config.getValue() + "," + word); + req.setComment(config.getComment()); + req.setId(config.getId()); + } + // 更新敏感词白名单 + save(req); + + // 移除敏感词记录 + SpringUtil.getBean(SensitiveService.class).removeSensitiveWord(word); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/constant/EsFieldConstant.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/constant/EsFieldConstant.java new file mode 100644 index 000000000..60280c6c3 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/constant/EsFieldConstant.java @@ -0,0 +1,24 @@ +package com.github.paicoding.forum.service.constant; + +/** + * ES 过滤字段常量 + * + * @ClassName: EsFieldConstant + * @Author: ygl + * @Date: 2023/5/26 09:39 + * @Version: 1.0 + */ +public class EsFieldConstant { + + /** + * title字段 + */ + public static final String ES_FIELD_TITLE = "title"; + + /** + * short_title字段 + */ + public static final String ES_FIELD_SHORT_TITLE = "short_title"; + + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/constant/EsIndexConstant.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/constant/EsIndexConstant.java new file mode 100644 index 000000000..0e2877e70 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/constant/EsIndexConstant.java @@ -0,0 +1,18 @@ +package com.github.paicoding.forum.service.constant; + +/** + * ES index + * + * @ClassName: EsFieldConstant + * @Author: ygl + * @Date: 2023/5/26 09:39 + * @Version: 1.0 + */ +public class EsIndexConstant { + + /** + * article索引 + */ + public static final String ES_INDEX_ARTICLE = "article"; + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/oss/ImageUploader.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/oss/ImageUploader.java new file mode 100644 index 000000000..4023d2981 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/oss/ImageUploader.java @@ -0,0 +1,56 @@ +package com.github.paicoding.forum.service.image.oss; + +import com.github.hui.quick.plugin.base.constants.MediaType; +import com.github.hui.quick.plugin.base.file.FileReadUtil; +import org.apache.commons.lang3.StringUtils; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * @author YiHui + * @date 2023/1/12 + */ +public interface ImageUploader { + String DEFAULT_FILE_TYPE = "txt"; + Set STATIC_IMG_TYPE = new HashSet<>(Arrays.asList(MediaType.ImagePng, MediaType.ImageJpg, MediaType.ImageWebp, MediaType.ImageGif)); + + /** + * 文件上传 + * + * @param input + * @param fileType + * @return + */ + String upload(InputStream input, String fileType); + + /** + * 判断外网图片是否依然需要处理 + * + * @param fileUrl + * @return true 表示忽略,不需要转存 + */ + boolean uploadIgnore(String fileUrl); + + /** + * 获取文件类型 + * + * @param input + * @param fileType + * @return + */ + default String getFileType(ByteArrayInputStream input, String fileType) { + if (StringUtils.isNotBlank(fileType)) { + return fileType; + } + + MediaType type = MediaType.typeOfMagicNum(FileReadUtil.getMagicNum(input)); + if (STATIC_IMG_TYPE.contains(type)) { + return type.getExt(); + } + return DEFAULT_FILE_TYPE; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/oss/impl/AliOssWrapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/oss/impl/AliOssWrapper.java new file mode 100644 index 000000000..878086bbd --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/oss/impl/AliOssWrapper.java @@ -0,0 +1,165 @@ +package com.github.paicoding.forum.service.image.oss.impl; + +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClientBuilder; +import com.aliyun.oss.OSSException; +import com.aliyun.oss.model.PutObjectRequest; +import com.aliyun.oss.model.PutObjectResult; +import com.github.paicoding.forum.core.autoconf.DynamicConfigContainer; +import com.github.paicoding.forum.core.config.ImageProperties; +import com.github.paicoding.forum.core.util.Md5Util; +import com.github.paicoding.forum.core.util.StopWatchUtil; +import com.github.paicoding.forum.service.image.oss.ImageUploader; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; +import org.springframework.util.StreamUtils; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +/** + * 阿里云oss文件上传 + * + * @author YiHui + * @date 2023/1/12 + */ +@Slf4j +@ConditionalOnExpression(value = "#{'ali'.equals(environment.getProperty('image.oss.type'))}") +@Component +public class AliOssWrapper implements ImageUploader, InitializingBean, DisposableBean { + private static final int SUCCESS_CODE = 200; + @Autowired + @Setter + @Getter + private ImageProperties properties; + private OSS ossClient; + + @Autowired + private DynamicConfigContainer dynamicConfigContainer; + + public String upload(InputStream input, String fileType) { + try { + // 创建PutObjectRequest对象。 + byte[] bytes = StreamUtils.copyToByteArray(input); + return upload(bytes, fileType); + } catch (OSSException oe) { + log.error("Oss rejected with an error response! msg:{}, code:{}, reqId:{}, host:{}", oe.getErrorMessage(), oe.getErrorCode(), oe.getRequestId(), oe.getHostId()); + return ""; + } catch (Exception ce) { + log.error("Caught an ClientException, which means the client encountered " + + "a serious internal problem while trying to communicate with OSS, " + + "such as not being able to access the network. {}", ce.getMessage()); + return ""; + } + } + + public String upload(byte[] bytes, String fileType) { + StopWatchUtil stopWatchUtil = StopWatchUtil.init("图片上传"); + try { + // 计算md5作为文件名,避免重复上传 + String fileName = stopWatchUtil.record("md5计算", () -> Md5Util.encode(bytes)); + ByteArrayInputStream input = new ByteArrayInputStream(bytes); + fileName = properties.getOss().getPrefix() + fileName + "." + getFileType(input, fileType); + // 创建PutObjectRequest对象。 + PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getOss().getBucket(), fileName, input); + // 设置该属性可以返回response。如果不设置,则返回的response为空。 + putObjectRequest.setProcess("true"); + + // 上传文件 + PutObjectResult result = stopWatchUtil.record("文件上传", () -> ossClient.putObject(putObjectRequest)); + if (SUCCESS_CODE == result.getResponse().getStatusCode()) { + return properties.getOss().getHost() + fileName; + } else { + log.error("upload to oss error! response:{}", result.getResponse().getStatusCode()); + // Guava 不允许回传 null + return ""; + } + } catch (OSSException oe) { + log.error("Oss rejected with an error response! msg:{}, code:{}, reqId:{}, host:{}", oe.getErrorMessage(), oe.getErrorCode(), oe.getRequestId(), oe.getHostId()); + return ""; + } catch (Exception ce) { + log.error("Caught an ClientException, which means the client encountered " + + "a serious internal problem while trying to communicate with OSS, " + + "such as not being able to access the network. {}", ce.getMessage()); + return ""; + } finally { + if (log.isDebugEnabled()) { + log.debug("upload image size:{} cost: {}", bytes.length, stopWatchUtil.prettyPrint()); + } + } + } + + public String uploadWithFileName(byte[] bytes, String fileName) { + StopWatchUtil stopWatchUtil = StopWatchUtil.init("图片上传"); + try { + // 计算md5作为文件名,避免重复上传 + ByteArrayInputStream input = new ByteArrayInputStream(bytes); + fileName = properties.getOss().getPrefix() + fileName; + // 创建PutObjectRequest对象。 + PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getOss().getBucket(), fileName, input); + // 设置该属性可以返回response。如果不设置,则返回的response为空。 + putObjectRequest.setProcess("true"); + + // 上传文件 + PutObjectResult result = stopWatchUtil.record("文件上传", () -> ossClient.putObject(putObjectRequest)); + if (SUCCESS_CODE == result.getResponse().getStatusCode()) { + return properties.getOss().getHost() + fileName; + } else { + log.error("upload to oss error! response:{}", result.getResponse().getStatusCode()); + // Guava 不允许回传 null + return ""; + } + } catch (OSSException oe) { + log.error("Oss rejected with an error response! msg:{}, code:{}, reqId:{}, host:{}", oe.getErrorMessage(), oe.getErrorCode(), oe.getRequestId(), oe.getHostId()); + return ""; + } catch (Exception ce) { + log.error("Caught an ClientException, which means the client encountered " + + "a serious internal problem while trying to communicate with OSS, " + + "such as not being able to access the network. {}", ce.getMessage()); + return ""; + } finally { + if (log.isDebugEnabled()) { + log.debug("upload image size:{} cost: {}", bytes.length, stopWatchUtil.prettyPrint()); + } + } + } + + @Override + public boolean uploadIgnore(String fileUrl) { + if (StringUtils.isNotBlank(properties.getOss().getHost()) && fileUrl.startsWith(properties.getOss().getHost())) { + return true; + } + + return !fileUrl.startsWith("http"); + } + + @Override + public void destroy() { + if (ossClient != null) { + ossClient.shutdown(); + } + } + + private void init() { + // 创建OSSClient实例。 + log.info("init ossClient"); + ossClient = new OSSClientBuilder().build(properties.getOss().getEndpoint(), properties.getOss().getAk(), properties.getOss().getSk()); + } + + @Override + public void afterPropertiesSet() { + init(); +// // 监听配置变更,然后重新初始化OSSClient实例 +// dynamicConfigContainer.registerRefreshCallback(properties, () -> { +// init(); +// log.info("ossClient refreshed!"); +// }); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/oss/impl/LocalStorageWrapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/oss/impl/LocalStorageWrapper.java new file mode 100644 index 000000000..6b3f04e8b --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/oss/impl/LocalStorageWrapper.java @@ -0,0 +1,96 @@ +package com.github.paicoding.forum.service.image.oss.impl; + +import com.github.hui.quick.plugin.base.file.FileWriteUtil; +import com.github.paicoding.forum.api.model.exception.ExceptionUtil; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import com.github.paicoding.forum.core.config.ImageProperties; +import com.github.paicoding.forum.core.util.StopWatchUtil; +import com.github.paicoding.forum.service.image.oss.ImageUploader; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; +import org.springframework.util.StreamUtils; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Random; + +/** + * 本地保存上传文件 + * + * @author YiHui + * @date 2023/1/12 + */ +@Slf4j +@ConditionalOnExpression(value = "#{'local'.equals(environment.getProperty('image.oss.type'))}") +@Component +public class LocalStorageWrapper implements ImageUploader { + @Autowired + private ImageProperties imageProperties; + private Random random; + + public LocalStorageWrapper() { + random = new Random(); + } + + @Override + public String upload(InputStream input, String fileType) { + // 记录耗时分布 + StopWatchUtil stopWatchUtil = StopWatchUtil.init("图片上传"); + try { + if (fileType == null) { + // 根据魔数判断文件类型 + InputStream finalInput = input; + byte[] bytes = stopWatchUtil.record("流转字节", () -> StreamUtils.copyToByteArray(finalInput)); + input = new ByteArrayInputStream(bytes); + fileType = getFileType((ByteArrayInputStream) input, fileType); + } + + String path = imageProperties.getAbsTmpPath() + imageProperties.getWebImgPath(); + String fileName = genTmpFileName(); + + InputStream finalInput = input; + String finalFileType = fileType; + FileWriteUtil.FileInfo file = stopWatchUtil.record("存储", () -> FileWriteUtil.saveFileByStream(finalInput, path, fileName, finalFileType)); + return imageProperties.buildImgUrl(imageProperties.getWebImgPath() + file.getFilename() + "." + file.getFileType()); + } catch (Exception e) { + log.error("Parse img from httpRequest to BufferedImage error! e:", e); + throw ExceptionUtil.of(StatusEnum.UPLOAD_PIC_FAILED); + } finally { + log.info("图片上传耗时: {}", stopWatchUtil.prettyPrint()); + } + } + + /** + * 获取文件临时名称 + * + * @return + */ + private String genTmpFileName() { + return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddhhmmssSSS")) + "_" + random.nextInt(100); + } + + /** + * 外网图片转存判定,对于没有转存过的,且是http开头的网络图片时,才需要进行转存 + * + * @param img + * @return true 表示不需要转存 + */ + @Override + public boolean uploadIgnore(String img) { + if (StringUtils.isNotBlank(imageProperties.getCdnHost()) && img.startsWith(imageProperties.getCdnHost())) { + return true; + } + + // 如果是oss的图片,也不需要转存 + if (StringUtils.isNotBlank(imageProperties.getOss().getHost()) && img.startsWith(imageProperties.getOss().getHost())) { + return true; + } + + return !img.startsWith("http"); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/oss/impl/RestOssWrapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/oss/impl/RestOssWrapper.java new file mode 100644 index 000000000..b76188eff --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/oss/impl/RestOssWrapper.java @@ -0,0 +1,57 @@ +package com.github.paicoding.forum.service.image.oss.impl; + +import com.github.paicoding.forum.core.config.ImageProperties; +import com.github.paicoding.forum.core.net.HttpRequestHelper; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.core.util.StopWatchUtil; +import com.github.paicoding.forum.service.image.oss.ImageUploader; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; +import org.springframework.util.StreamUtils; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * 基于http的文件上传 + * + * @author YiHui + * @date 2023/11/10 + */ +@Slf4j +@Component +@ConditionalOnExpression(value = "#{'rest'.equals(environment.getProperty('image.oss.type'))}") +public class RestOssWrapper implements ImageUploader { + @Autowired + private ImageProperties properties; + + @Override + public String upload(InputStream input, String fileType) { + StopWatchUtil stopWatchUtil = StopWatchUtil.init("图片上传"); + try { + byte[] bytes = stopWatchUtil.record("转字节", () -> StreamUtils.copyToByteArray(input)); + String res = stopWatchUtil.record("上传", () -> HttpRequestHelper.upload(properties.getOss().getEndpoint(), "image", "img." + fileType, bytes)); + HashMap map = JsonUtil.toObj(res, HashMap.class); + return (String) ((Map) map.get("result")).get("imagePath"); + } catch (Exception e) { + log.error("upload image error response! uri:{}", properties.getOss().getEndpoint(), e); + return null; + } finally { + if (log.isDebugEnabled()) { + log.debug("upload Image cost: {}", stopWatchUtil.prettyPrint()); + } + } + } + + @Override + public boolean uploadIgnore(String fileUrl) { + if (StringUtils.isNotBlank(properties.getOss().getHost()) && fileUrl.startsWith(properties.getOss().getHost())) { + return true; + } + return !fileUrl.startsWith("http"); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/service/ImageService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/service/ImageService.java new file mode 100644 index 000000000..41b8a9673 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/service/ImageService.java @@ -0,0 +1,33 @@ +package com.github.paicoding.forum.service.image.service; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author LouZai + * @date 2022/9/7 + */ +public interface ImageService { + /** + * 图片转存 + * @param content + * @return + */ + String mdImgReplace(String content); + + + /** + * 外网图片转存 + * + * @param img + * @return + */ + String saveImg(String img); + + /** + * 保存图片 + * + * @param request + * @return + */ + String saveImg(HttpServletRequest request); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/service/ImageServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/service/ImageServiceImpl.java new file mode 100644 index 000000000..0bc238033 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/image/service/ImageServiceImpl.java @@ -0,0 +1,245 @@ +package com.github.paicoding.forum.service.image.service; + +import com.github.hui.quick.plugin.base.constants.MediaType; +import com.github.hui.quick.plugin.base.file.FileReadUtil; +import com.github.paicoding.forum.api.model.exception.ExceptionUtil; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import com.github.paicoding.forum.core.async.AsyncExecute; +import com.github.paicoding.forum.core.async.AsyncUtil; +import com.github.paicoding.forum.core.mdc.MdcDot; +import com.github.paicoding.forum.core.util.MdImgLoader; +import com.github.paicoding.forum.service.image.oss.ImageUploader; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; + +import javax.servlet.http.HttpServletRequest; +import java.io.*; +import java.net.URI; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * @author LouZai + * @date 2022/9/7 + */ +@Slf4j +@Service +public class ImageServiceImpl implements ImageService { + + @Autowired + private ImageUploader imageUploader; + + /** + * 外网图片转存缓存 + */ + private Cache imgReplaceCache = CacheBuilder + .newBuilder() + .maximumSize(300) + .expireAfterWrite(5, TimeUnit.MINUTES) + .build(); + + @Override + public String saveImg(HttpServletRequest request) { + MultipartFile file = null; + if (request instanceof MultipartHttpServletRequest) { + file = ((MultipartHttpServletRequest) request).getFile("image"); + } + if (file == null) { + throw ExceptionUtil.of(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "缺少需要上传的图片"); + } + + // 目前只支持 jpg, png, webp 等静态图片格式 + String fileType = validateStaticImg(file.getContentType()); + if (fileType == null) { + throw ExceptionUtil.of(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "图片只支持png,jpg,gif"); + } + + try { + // 先获取图像摘要,根据摘要确定缓存中是否已经包含图像。 + String digest = calculateSHA256(file.getInputStream()); + String ans = imgReplaceCache.getIfPresent(digest); + if (StringUtils.isBlank(ans)) { + ans = imageUploader.upload(file.getInputStream(), fileType); + imgReplaceCache.put(digest, ans); + } + return ans; + } catch (IOException | NoSuchAlgorithmException e) { + log.error("Parse img from httpRequest to BufferedImage error! e:", e); + throw ExceptionUtil.of(StatusEnum.UPLOAD_PIC_FAILED); + } + } + + /** + * 外网图片转存 + * + * @param img + * @return + */ + @Override + public String saveImg(String img) { + if (imageUploader.uploadIgnore(img)) { + // 已经转存过,不需要再次转存;非http图片,不处理 + return img; + } + + try { + InputStream stream = FileReadUtil.getStreamByFileName(img); + URI uri = URI.create(img); + String path = uri.getPath(); + int index = path.lastIndexOf("."); + String fileType = null; + if (index > 0) { + // 从url中获取文件类型 + fileType = path.substring(index + 1); + } + String digest = calculateSHA256(stream); + String ans = imgReplaceCache.getIfPresent(digest); + if (StringUtils.isBlank(ans)) { + ans = imageUploader.upload(stream, fileType); + imgReplaceCache.put(digest, ans); + } + if (StringUtils.isBlank(ans)) { + return buildUploadFailImgUrl(img); + } + return ans; + } catch (Exception e) { + log.error("外网图片转存异常! img:{}", img, e); + return buildUploadFailImgUrl(img); + } + } + + /** + * 外网图片自动转存,添加了执行日志,超时限制;避免出现因为超时导致发布文章异常 + * + * @param content + * @return + */ + @Override + @MdcDot + @AsyncExecute(timeOutRsp = "#content") + public String mdImgReplace(String content) { + List imgList = MdImgLoader.loadImgs(content); + if (CollectionUtils.isEmpty(imgList)) { + return content; + } + + if (imgList.size() == 1) { + // 只有一张图片时,没有必要走异步,直接转存并返回 + MdImgLoader.MdImg img = imgList.get(0); + String newImg = saveImg(img.getUrl()); + return StringUtils.replace(content, img.getOrigin(), "![" + img.getDesc() + "](" + newImg + ")"); + } + + // 超过1张图片时,做并发的图片转存,提升性能 + Map imgReplaceMap = new ConcurrentHashMap<>(); + try(AsyncUtil.CompletableFutureBridge bridge = AsyncUtil.concurrentExecutor("MdImgReplace")) { + for (MdImgLoader.MdImg img : imgList) { + bridge.async(() -> { + imgReplaceMap.put(img, saveImg(img.getUrl())); + }, img.getUrl()); + } + } + + // 图片替换 + for (Map.Entry entry : imgReplaceMap.entrySet()) { + MdImgLoader.MdImg img = entry.getKey(); + String newImg = entry.getValue(); + content = StringUtils.replace(content, img.getOrigin(), "![" + img.getDesc() + "](" + newImg + ")"); + } + return content; + } + + private String buildUploadFailImgUrl(String img) { + return img.contains("saveError") ? img : img + "?&cause=saveError!"; + } + + /** + * 图片格式校验 + * + * @param mime + * @return + */ + private String validateStaticImg(String mime) { + if ("svg".equalsIgnoreCase(mime)) { + // fixme 上传文件保存到服务器本地时,做好安全保护, 避免上传了要给攻击性的脚本 + return "svg"; + } + + if (mime.contains(MediaType.ImageJpg.getExt())) { + mime = mime.replace("jpg", "jpeg"); + } + for (MediaType type : ImageUploader.STATIC_IMG_TYPE) { + if (type.getMime().equals(mime)) { + return type.getExt(); + } + } + return null; + } + + /** + * 图片摘要生成 + * + * @param inputStream + * @return + */ + private String calculateSHA256(InputStream inputStream) throws NoSuchAlgorithmException, IOException { + + inputStream = toByteArrayInputStream(inputStream); + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] buffer = new byte[1024]; + int bytesRead; + + // 读取 InputStream 并更新到 MessageDigest + while ((bytesRead = inputStream.read(buffer)) != -1) { + md.update(buffer, 0, bytesRead); + } + + // 获取摘要并将其转换为十六进制字符串 + byte[] digest = md.digest(); + StringBuilder hexString = new StringBuilder(); + for (byte b : digest) { + hexString.append(String.format("%02x", b)); + } + inputStream.reset(); + return hexString.toString(); + } + + /** + * 转换为字节数组输入流,可以重复消费流中数据 + * + * @param inputStream + * @return + * @throws IOException + */ + public ByteArrayInputStream toByteArrayInputStream(InputStream inputStream) throws IOException { + if (inputStream instanceof ByteArrayInputStream) { + return (ByteArrayInputStream) inputStream; + } + + try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + BufferedInputStream br = new BufferedInputStream(inputStream); + byte[] b = new byte[1024]; + for (int c; (c = br.read(b)) != -1; ) { + bos.write(b, 0, c); + } + // 主动告知回收 + b = null; + br.close(); + inputStream.close(); + return new ByteArrayInputStream(bos.toByteArray()); + } + } + + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/config/RabbitMqAutoConfig.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/config/RabbitMqAutoConfig.java new file mode 100644 index 000000000..c2e3586f5 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/config/RabbitMqAutoConfig.java @@ -0,0 +1,42 @@ +package com.github.paicoding.forum.service.notify.config; + +import com.github.paicoding.forum.core.async.AsyncUtil; +import com.github.paicoding.forum.core.config.RabbitmqProperties; +import com.github.paicoding.forum.core.rabbitmq.RabbitmqConnectionPool; +import com.github.paicoding.forum.service.notify.service.RabbitmqService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.Resource; + +/** + * @author YiHui + * @date 2023/6/9 + */ +@Configuration +@ConditionalOnProperty(value = "rabbitmq.switchFlag") +@EnableConfigurationProperties(RabbitmqProperties.class) +public class RabbitMqAutoConfig implements ApplicationRunner { + @Resource + private RabbitmqService rabbitmqService; + + @Autowired + private RabbitmqProperties rabbitmqProperties; + + + @Override + public void run(ApplicationArguments args) throws Exception { + String host = rabbitmqProperties.getHost(); + Integer port = rabbitmqProperties.getPort(); + String userName = rabbitmqProperties.getUsername(); + String password = rabbitmqProperties.getPassport(); + String virtualhost = rabbitmqProperties.getVirtualhost(); + Integer poolSize = rabbitmqProperties.getPoolSize(); + RabbitmqConnectionPool.initRabbitmqConnectionPool(host, port, userName, password, virtualhost, poolSize); + AsyncUtil.execute(() -> rabbitmqService.processConsumerMsg()); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/help/MsgNotifyHelper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/help/MsgNotifyHelper.java new file mode 100644 index 000000000..2ca816f33 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/help/MsgNotifyHelper.java @@ -0,0 +1,37 @@ +package com.github.paicoding.forum.service.notify.help; + +import com.github.paicoding.forum.api.model.enums.NotifyTypeEnum; +import com.github.paicoding.forum.api.model.vo.notify.NotifyMsgEvent; +import com.github.paicoding.forum.core.util.SpringUtil; +import org.springframework.stereotype.Service; + +/** + * @author YiHui + * @date 2024/11/27 + */ +@Service +public class MsgNotifyHelper { + + /** + * 消息广播通知 + * + * @param type 消息类型 + * @param content 消息内容 + * @param 消息类型 + */ + public void publishMsg(NotifyTypeEnum type, T content) { + SpringUtil.publishEvent(new NotifyMsgEvent<>(this, type, content)); + } + + + /** + * 静态方法使用方式,简化调用方使用 + * + * @param type 消息类型 + * * @param content 消息内容 + * * @param 消息类型 + */ + public static void publish(NotifyTypeEnum type, T content) { + SpringUtil.getBean(MsgNotifyHelper.class).publishMsg(type, content); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/repository/dao/NotifyMsgDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/repository/dao/NotifyMsgDao.java new file mode 100644 index 000000000..f1a7a7803 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/repository/dao/NotifyMsgDao.java @@ -0,0 +1,113 @@ +package com.github.paicoding.forum.service.notify.repository.dao; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.paicoding.forum.api.model.enums.NotifyStatEnum; +import com.github.paicoding.forum.api.model.enums.NotifyTypeEnum; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.notify.dto.NotifyMsgDTO; +import com.github.paicoding.forum.service.notify.repository.entity.NotifyMsgDO; +import com.github.paicoding.forum.service.notify.repository.mapper.NotifyMsgMapper; +import org.springframework.stereotype.Repository; +import org.springframework.util.CollectionUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author YiHui + * @date 2022/9/3 + */ +@Repository +public class NotifyMsgDao extends ServiceImpl { + + /** + * 查询消息记录,用于幂等过滤 + * + * @param msg + * @return + */ + public NotifyMsgDO getByUserIdRelatedIdAndType(NotifyMsgDO msg) { + List list = lambdaQuery().eq(NotifyMsgDO::getNotifyUserId, msg.getNotifyUserId()) + .eq(NotifyMsgDO::getOperateUserId, msg.getOperateUserId()) + .eq(NotifyMsgDO::getType, msg.getType()) + .eq(NotifyMsgDO::getRelatedId, msg.getRelatedId()) + .orderByDesc(NotifyMsgDO::getId) + .page(new Page<>(0, 1)) + .getRecords(); + if (CollectionUtils.isEmpty(list)) { + return null; + } + return list.get(0); + } + + + /** + * 查询用户的消息通知数量 + * + * @param userId + * @return + */ + public int countByUserIdAndStat(long userId, Integer stat) { + return lambdaQuery() + .eq(NotifyMsgDO::getNotifyUserId, userId) + .eq(stat != null, NotifyMsgDO::getState, stat) + .count().intValue(); + } + + /** + * 查询用户各类型的未读消息数量 + * + * @param userId + * @return + */ + public Map groupCountByUserIdAndStat(long userId, Integer stat) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.select("type, count(*) as cnt"); + wrapper.eq("notify_user_id", userId); + if (stat != null) { + wrapper.eq("state", stat); + } + wrapper.groupBy("type"); + List> map = listMaps(wrapper); + Map result = new HashMap<>(); + map.forEach(s -> { + result.put(Integer.valueOf(s.get("type").toString()), Integer.valueOf(s.get("cnt").toString())); + }); + return result; + } + + /** + * 查询用户消息列表 + * + * @param userId + * @param type + * @return + */ + public List listNotifyMsgByUserIdAndType(long userId, NotifyTypeEnum type, PageParam page) { + switch (type) { + case REPLY: + case COMMENT: + case COLLECT: + case PRAISE: + return baseMapper.listArticleRelatedNotices(userId, type.getType(), page); + default: + return baseMapper.listNormalNotices(userId, type.getType(), page); + } + } + + /** + * 设置消息为已读 + * + * @param list + */ + public void updateNotifyMsgToRead(List list) { + List ids = list.stream().filter(s -> s.getState() == NotifyStatEnum.UNREAD.getStat()).map(NotifyMsgDTO::getMsgId).collect(Collectors.toList()); + if (!ids.isEmpty()) { + baseMapper.updateNoticeRead(ids); + } + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/repository/entity/NotifyMsgDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/repository/entity/NotifyMsgDO.java new file mode 100644 index 000000000..8f02489ad --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/repository/entity/NotifyMsgDO.java @@ -0,0 +1,53 @@ +package com.github.paicoding.forum.service.notify.repository.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import com.github.paicoding.forum.api.model.enums.NotifyTypeEnum; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @author YiHui + * @date 2022/9/3 + */ +@Data +@Accessors(chain = true) +@TableName("notify_msg") +public class NotifyMsgDO extends BaseDO { + private static final long serialVersionUID = -4043774744889659100L; + + /** + * 消息关联的主体 + * - 如文章收藏、评论、回复评论、点赞消息,这里存文章ID; + * - 如系统通知消息时,这里存的是系统通知消息正文主键,也可以是0 + * - 如关注,这里就是0 + */ + private Long relatedId; + + /** + * 消息内容 + */ + private String msg; + + /** + * 消息通知的用户id + */ + private Long notifyUserId; + + /** + * 触发这个消息的用户id + */ + private Long operateUserId; + + /** + * 消息类型 + * + * @see NotifyTypeEnum#getType() + */ + private Integer type; + + /** + * 0 未查看 1 已查看 + */ + private Integer state; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/repository/mapper/NotifyMsgMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/repository/mapper/NotifyMsgMapper.java new file mode 100644 index 000000000..f29ed2193 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/repository/mapper/NotifyMsgMapper.java @@ -0,0 +1,43 @@ +package com.github.paicoding.forum.service.notify.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.notify.dto.NotifyMsgDTO; +import com.github.paicoding.forum.service.notify.repository.entity.NotifyMsgDO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @author YiHui + * @date 2022/9/3 + */ +public interface NotifyMsgMapper extends BaseMapper { + + /** + * 查询文章相关的通知列表 + * + * @param userId + * @param type + * @param page 分页 + * @return + */ + List listArticleRelatedNotices(@Param("userId") long userId, @Param("type") int type, @Param("pageParam") PageParam page); + + /** + * 查询关注、系统等没有关联id的通知列表 + * + * @param userId + * @param type + * @param page 分页 + * @return + */ + List listNormalNotices(@Param("userId") long userId, @Param("type") int type, @Param("pageParam") PageParam page); + + /** + * 标记消息为已阅读 + * + * @param ids + */ + void updateNoticeRead(@Param("ids") List ids); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/service/NotifyService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/service/NotifyService.java new file mode 100644 index 000000000..1be57022c --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/service/NotifyService.java @@ -0,0 +1,74 @@ +package com.github.paicoding.forum.service.notify.service; + +import com.github.paicoding.forum.api.model.enums.NotifyTypeEnum; +import com.github.paicoding.forum.api.model.vo.PageListVo; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.notify.dto.NotifyMsgDTO; +import com.github.paicoding.forum.service.user.repository.entity.UserFootDO; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; + +import java.util.Map; + +/** + * 消息通知服务类 + * + * @author YiHui + * @date 2022/9/3 + */ +public interface NotifyService { + public static String NOTIFY_TOPIC = "/msg"; + + + /** + * 查询用户未读消息数量 + * + * @param userId + * @return + */ + int queryUserNotifyMsgCount(Long userId); + + /** + * 查询通知列表 + * + * @param userId + * @param type + * @param page + * @return + */ + PageListVo queryUserNotices(Long userId, NotifyTypeEnum type, PageParam page); + + /** + * 查询未读消息数 + * + * @param userId + * @return + */ + Map queryUnreadCounts(long userId); + + /** + * 保存通知 + * + * @param foot + * @param notifyTypeEnum + */ + void saveArticleNotify(UserFootDO foot, NotifyTypeEnum notifyTypeEnum); + + + // -------------------------------------------- 下面是与用户的websocket长连接维护相关实现 ------------------------- + + /** + * ws: 给用户发送消息通知 + * + * @param userId 用户id + * @param msg 通知内容 + */ + void notifyToUser(Long userId, String msg); + + + /** + * ws: 维护与用户的长连接通道 + * + * @param accessor + */ + void notifyChannelMaintain(StompHeaderAccessor accessor); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/service/RabbitmqService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/service/RabbitmqService.java new file mode 100644 index 000000000..d9f7efef8 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/service/RabbitmqService.java @@ -0,0 +1,47 @@ +package com.github.paicoding.forum.service.notify.service; + +import com.rabbitmq.client.BuiltinExchangeType; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + * @author YiHui + * @date 2022/9/3 + */ +public interface RabbitmqService { + + boolean enabled(); + + /** + * 发布消息 + * + * @param exchange + * @param exchangeType + * @param toutingKey + * @param message + * @throws IOException + * @throws TimeoutException + */ + void publishMsg(String exchange, + BuiltinExchangeType exchangeType, + String toutingKey, + String message); + + + /** + * 消费消息 + * + * @param exchange + * @param queue + * @param routingKey + * @throws IOException + * @throws TimeoutException + */ + void consumerMsg(String exchange, + String queue, + String routingKey) throws IOException, TimeoutException; + + + void processConsumerMsg(); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/service/impl/NotifyMsgListener.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/service/impl/NotifyMsgListener.java new file mode 100644 index 000000000..531b30cd2 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/service/impl/NotifyMsgListener.java @@ -0,0 +1,321 @@ +package com.github.paicoding.forum.service.notify.service.impl; + +import com.github.paicoding.forum.api.model.enums.DocumentTypeEnum; +import com.github.paicoding.forum.api.model.enums.NotifyStatEnum; +import com.github.paicoding.forum.api.model.enums.NotifyTypeEnum; +import com.github.paicoding.forum.api.model.enums.pay.PayStatusEnum; +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import com.github.paicoding.forum.api.model.vo.notify.NotifyMsgEvent; +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDO; +import com.github.paicoding.forum.service.article.repository.entity.ArticlePayRecordDO; +import com.github.paicoding.forum.service.article.service.ArticleReadService; +import com.github.paicoding.forum.service.comment.repository.entity.CommentDO; +import com.github.paicoding.forum.service.comment.service.CommentReadService; +import com.github.paicoding.forum.service.notify.repository.dao.NotifyMsgDao; +import com.github.paicoding.forum.service.notify.repository.entity.NotifyMsgDO; +import com.github.paicoding.forum.service.notify.service.NotifyService; +import com.github.paicoding.forum.service.user.repository.entity.UserFootDO; +import com.github.paicoding.forum.service.user.repository.entity.UserRelationDO; +import com.github.paicoding.forum.service.user.service.UserService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.ApplicationListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.util.Objects; + +/** + * @author YiHui + * @date 2022/9/3 + */ +@Slf4j +@Async +@Service +public class NotifyMsgListener implements ApplicationListener> { + private static final Long ADMIN_ID = 1L; + private final ArticleReadService articleReadService; + + private final CommentReadService commentReadService; + + private final NotifyMsgDao notifyMsgDao; + + private final NotifyService notifyService; + + private final UserService userService; + + public NotifyMsgListener(ArticleReadService articleReadService, + CommentReadService commentReadService, + NotifyService notifyService, + NotifyMsgDao notifyMsgDao, + UserService userService) { + this.articleReadService = articleReadService; + this.commentReadService = commentReadService; + this.notifyService = notifyService; + this.notifyMsgDao = notifyMsgDao; + this.userService = userService; + } + + @SuppressWarnings("unchecked") + @Override + public void onApplicationEvent(NotifyMsgEvent msgEvent) { + switch (msgEvent.getNotifyType()) { + case COMMENT: + saveCommentNotify((NotifyMsgEvent) msgEvent); + break; + case REPLY: + saveReplyNotify((NotifyMsgEvent) msgEvent); + break; + case PRAISE: + case COLLECT: + saveArticleNotify((NotifyMsgEvent) msgEvent); + break; + case CANCEL_PRAISE: + case CANCEL_COLLECT: + removeArticleNotify((NotifyMsgEvent) msgEvent); + break; + case FOLLOW: + saveFollowNotify((NotifyMsgEvent) msgEvent); + break; + case CANCEL_FOLLOW: + removeFollowNotify((NotifyMsgEvent) msgEvent); + break; + case LOGIN: + // todo 用户登录,判断是否需要插入新的通知消息,暂时先不做 + break; + case REGISTER: + // 首次注册,插入一个欢迎的消息 + saveRegisterSystemNotify((Long) msgEvent.getContent()); + break; + case PAYING: + case PAY: + // 文章支付回调/支付中的消息通知 + savePayNotify((NotifyMsgEvent) msgEvent); + default: + // todo 系统消息 + } + } + + /** + * 评论 + 回复 + * + * @param event + */ + private void saveCommentNotify(NotifyMsgEvent event) { + NotifyMsgDO msg = new NotifyMsgDO(); + CommentDO comment = event.getContent(); + ArticleDO article = articleReadService.queryBasicArticle(comment.getArticleId()); + msg.setNotifyUserId(article.getUserId()) + .setOperateUserId(comment.getUserId()) + .setRelatedId(article.getId()) + .setType(event.getNotifyType().getType()) + .setState(NotifyStatEnum.UNREAD.getStat()).setMsg(comment.getContent()); + // 对于评论而言,支持多次评论;因此若之前有也不删除 + notifyMsgDao.save(msg); + + // 消息通知 + notifyService.notifyToUser(msg.getNotifyUserId(), String.format("您的文章《%s》收到一个新的评论,快去看看吧", article.getTitle())); + } + + /** + * 评论回复消息 + * + * @param event + */ + private void saveReplyNotify(NotifyMsgEvent event) { + NotifyMsgDO msg = new NotifyMsgDO(); + CommentDO comment = event.getContent(); + CommentDO parent = commentReadService.queryComment(comment.getParentCommentId()); + msg.setNotifyUserId(parent.getUserId()) + .setOperateUserId(comment.getUserId()) + .setRelatedId(comment.getArticleId()) + .setType(event.getNotifyType().getType()) + .setState(NotifyStatEnum.UNREAD.getStat()).setMsg(comment.getContent()); + // 回复同样支持多次回复,不做幂等校验 + notifyMsgDao.save(msg); + + // 消息通知 + notifyService.notifyToUser(msg.getNotifyUserId(), String.format("您的评价《%s》收到一个新的回复,快去看看吧", parent.getContent())); + } + + /** + * 点赞 + 收藏 + * + * @param event + */ + private void saveArticleNotify(NotifyMsgEvent event) { + UserFootDO foot = event.getContent(); + NotifyMsgDO msg = new NotifyMsgDO().setRelatedId(foot.getDocumentId()) + .setNotifyUserId(foot.getDocumentUserId()) + .setOperateUserId(foot.getUserId()) + .setType(event.getNotifyType().getType()) + .setState(NotifyStatEnum.UNREAD.getStat()) + .setMsg(""); + if (Objects.equals(foot.getDocumentType(), DocumentTypeEnum.COMMENT.getCode())) { + // 点赞评论时,详情内容中显示评论信息 + CommentDO comment = commentReadService.queryComment(foot.getDocumentId()); + ArticleDO article = articleReadService.queryBasicArticle(comment.getArticleId()); + msg.setMsg(String.format("赞了您在文章 %s 下的评论 %s", article.getId(), article.getTitle(), comment.getContent())); + } + + NotifyMsgDO record = notifyMsgDao.getByUserIdRelatedIdAndType(msg); + if (record == null) { + // 若之前已经有对应的通知,则不重复记录;因为一个用户对一篇文章,可以重复的点赞、取消点赞,但是最终我们只通知一次 + notifyMsgDao.save(msg); + // 消息通知 + notifyService.notifyToUser(msg.getNotifyUserId(), String.format("太棒了,您的%s %s数+1!!!", + Objects.equals(foot.getDocumentType(), DocumentTypeEnum.ARTICLE.getCode()) ? "文章" : "评论", + event.getNotifyType().getMsg())); + } + } + + public void saveArticleNotify(UserFootDO foot, NotifyTypeEnum notifyTypeEnum) { + NotifyMsgDO msg = new NotifyMsgDO().setRelatedId(foot.getDocumentId()) + .setNotifyUserId(foot.getDocumentUserId()) + .setOperateUserId(foot.getUserId()) + .setType(notifyTypeEnum.getType()) + .setState(NotifyStatEnum.UNREAD.getStat()) + .setMsg(""); + NotifyMsgDO record = notifyMsgDao.getByUserIdRelatedIdAndType(msg); + if (record == null) { + // 若之前已经有对应的通知,则不重复记录;因为一个用户对一篇文章,可以重复的点赞、取消点赞,但是最终我们只通知一次 + notifyMsgDao.save(msg); + } + } + + /** + * 取消点赞,取消收藏 + * + * @param event + */ + private void removeArticleNotify(NotifyMsgEvent event) { + UserFootDO foot = event.getContent(); + NotifyMsgDO msg = new NotifyMsgDO() + .setRelatedId(foot.getDocumentId()) + .setNotifyUserId(foot.getDocumentUserId()) + .setOperateUserId(foot.getUserId()) + .setType(event.getNotifyType().getType()) + .setMsg(""); + NotifyMsgDO record = notifyMsgDao.getByUserIdRelatedIdAndType(msg); + if (record != null) { + notifyMsgDao.removeById(record.getId()); + } + } + + /** + * 关注 + * + * @param event + */ + private void saveFollowNotify(NotifyMsgEvent event) { + UserRelationDO relation = event.getContent(); + NotifyMsgDO msg = new NotifyMsgDO().setRelatedId(0L) + .setNotifyUserId(relation.getUserId()) + .setOperateUserId(relation.getFollowUserId()) + .setType(event.getNotifyType().getType()) + .setState(NotifyStatEnum.UNREAD.getStat()) + .setMsg(""); + NotifyMsgDO record = notifyMsgDao.getByUserIdRelatedIdAndType(msg); + if (record == null) { + // 若之前已经有对应的通知,则不重复记录;因为用户的关注是一对一的,可以重复的关注、取消,但是最终我们只通知一次 + notifyMsgDao.save(msg); + + notifyService.notifyToUser(msg.getNotifyUserId(), "恭喜您获得一枚新粉丝~"); + } + } + + /** + * 取消关注 + * + * @param event + */ + private void removeFollowNotify(NotifyMsgEvent event) { + UserRelationDO relation = event.getContent(); + NotifyMsgDO msg = new NotifyMsgDO() + .setRelatedId(0L) + .setNotifyUserId(relation.getUserId()) + .setOperateUserId(relation.getFollowUserId()) + .setType(event.getNotifyType().getType()) + .setMsg(""); + NotifyMsgDO record = notifyMsgDao.getByUserIdRelatedIdAndType(msg); + if (record != null) { + notifyMsgDao.removeById(record.getId()); + } + } + + private void saveRegisterSystemNotify(Long userId) { + NotifyMsgDO msg = new NotifyMsgDO().setRelatedId(0L) + .setNotifyUserId(userId) + .setOperateUserId(ADMIN_ID) + .setType(NotifyTypeEnum.REGISTER.getType()) + .setState(NotifyStatEnum.UNREAD.getStat()) + .setMsg(SpringUtil.getConfig("view.site.welcomeInfo")); + NotifyMsgDO record = notifyMsgDao.getByUserIdRelatedIdAndType(msg); + if (record == null) { + // 若之前已经有对应的通知,则不重复记录;因为用户的关注是一对一的,可以重复的关注、取消,但是最终我们只通知一次 + notifyMsgDao.save(msg); + + notifyService.notifyToUser(msg.getNotifyUserId(), "您有一个新的系统通知消息,请注意查收"); + } + } + + private void savePayNotify(NotifyMsgEvent pay) { + ArticlePayRecordDO record = pay.getContent(); + ArticleDO article = articleReadService.queryBasicArticle(record.getArticleId()); + + NotifyMsgDO msg; + PayStatusEnum payStatus = PayStatusEnum.statusOf(record.getPayStatus()); + if (PayStatusEnum.PAYING == payStatus) { + // 支付中,给作者发起一个通知 + BaseUserInfoDTO payUser = userService.queryBasicUserInfo(record.getPayUserId()); + + msg = new NotifyMsgDO().setRelatedId(record.getArticleId()) + .setNotifyUserId(record.getReceiveUserId()) + .setOperateUserId(record.getPayUserId()) + .setType(NotifyTypeEnum.PAY.getType()) + .setState(NotifyStatEnum.UNREAD.getStat()) + .setMsg(String.format("您的文章 %s 收到一份来自 %s 的 [%s] 打赏,点击 去确认~", + record.getArticleId(), article.getTitle(), + payUser.getUserId(), payUser.getUserName(), + StringUtils.isBlank(record.getPayWay()) || Objects.equals(record.getPayWay(), ThirdPayWayEnum.EMAIL.getPay()) ? "个人收款码" : "微信支付", + record.getId())); + } else { + // 作者执行的支付结果回调通知付费用户 + msg = new NotifyMsgDO().setRelatedId(record.getArticleId()) + .setNotifyUserId(record.getPayUserId()) + .setOperateUserId(record.getReceiveUserId()) + .setType(NotifyTypeEnum.PAY.getType()) + .setState(NotifyStatEnum.UNREAD.getStat()) + .setMsg( + PayStatusEnum.SUCCEED == payStatus + ? String.format("您对 %s 的支付已完成~", record.getArticleId(), article.getTitle()) + : String.format("您对 %s 的支付未完成哦~", record.getArticleId(), article.getTitle()) + ); + } + + NotifyMsgDO dbMsg = notifyMsgDao.getByUserIdRelatedIdAndType(msg); + if (dbMsg == null) { + // 未通知过,则新增一条通知记录 + notifyMsgDao.save(msg); + } else if (!Objects.equals(dbMsg.getMsg(), msg.getMsg())) { + // 由于可能出现第一次支付失败,然后第二次支付成功的场景,因此我们需要再新增一个消息通知 + notifyMsgDao.save(msg); + } else if (payStatus == PayStatusEnum.PAYING && Objects.equals(dbMsg.getState(), NotifyStatEnum.UNREAD.getStat())) { + // 根据作者是否看过通知,来决定是否需要重新给作者发送一个消息通知 + notifyMsgDao.save(msg); + } + + if (payStatus == PayStatusEnum.PAYING) { + // 支付中 + notifyService.notifyToUser(msg.getNotifyUserId(), String.format("您的文章《%s》收到一份打赏,请及时确认~", article.getTitle())); + } else if (payStatus == PayStatusEnum.SUCCEED) { + // 支付成功 + notifyService.notifyToUser(msg.getNotifyUserId(), String.format("您对文章《%s》的支付已完成,刷新即可阅读全文哦~", article.getTitle())); + } else if (payStatus == PayStatusEnum.FAIL) { + // 支付失败 + notifyService.notifyToUser(msg.getNotifyUserId(), String.format("您对文章《%s》的支付未成功,请重试一下吧~", article.getTitle())); + } + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/service/impl/NotifyServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/service/impl/NotifyServiceImpl.java new file mode 100644 index 000000000..68214b166 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/service/impl/NotifyServiceImpl.java @@ -0,0 +1,206 @@ +package com.github.paicoding.forum.service.notify.service.impl; + +import com.beust.jcommander.internal.Sets; +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.api.model.enums.NotifyStatEnum; +import com.github.paicoding.forum.api.model.enums.NotifyTypeEnum; +import com.github.paicoding.forum.api.model.vo.PageListVo; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.notify.dto.NotifyMsgDTO; +import com.github.paicoding.forum.core.util.NumUtil; +import com.github.paicoding.forum.core.ws.WebSocketResponseUtil; +import com.github.paicoding.forum.service.notify.repository.dao.NotifyMsgDao; +import com.github.paicoding.forum.service.notify.repository.entity.NotifyMsgDO; +import com.github.paicoding.forum.service.notify.service.NotifyService; +import com.github.paicoding.forum.service.user.repository.entity.UserFootDO; +import com.github.paicoding.forum.service.user.service.UserRelationService; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @author YiHui + * @date 2022/9/4 + */ +@Slf4j +@Service +public class NotifyServiceImpl implements NotifyService { + @Resource + private NotifyMsgDao notifyMsgDao; + + @Resource + private UserRelationService userRelationService; + + /** + * 记录用户与对应的jwt token之间的缓存关系;用于websocket的广播通知 + */ + private LoadingCache> wsUserSessionCache; + + @PostConstruct + public void init() { + wsUserSessionCache = CacheBuilder.newBuilder() + .maximumSize(500) + .expireAfterAccess(1, TimeUnit.HOURS) + .build(new CacheLoader>() { + @Override + public Set load(Long aLong) throws Exception { + return new HashSet<>(); + } + }); + } + + @Override + public int queryUserNotifyMsgCount(Long userId) { + return notifyMsgDao.countByUserIdAndStat(userId, NotifyStatEnum.UNREAD.getStat()); + } + + /** + * 查询消息通知列表 + * + * @return + */ + @Override + public PageListVo queryUserNotices(Long userId, NotifyTypeEnum type, PageParam page) { + List list = notifyMsgDao.listNotifyMsgByUserIdAndType(userId, type, page); + if (CollectionUtils.isEmpty(list)) { + return PageListVo.emptyVo(); + } + + // 设置消息为已读状态 + notifyMsgDao.updateNotifyMsgToRead(list); + // 更新全局总的消息数 + ReqInfoContext.getReqInfo().setMsgNum(queryUserNotifyMsgCount(userId)); + // 更新当前登录用户对粉丝的关注状态 + updateFollowStatus(userId, list); + return PageListVo.newVo(list, page.getPageSize()); + } + + private void updateFollowStatus(Long userId, List list) { + List targetUserIds = list.stream().filter(s -> s.getType() == NotifyTypeEnum.FOLLOW.getType()).map(NotifyMsgDTO::getOperateUserId).collect(Collectors.toList()); + if (targetUserIds.isEmpty()) { + return; + } + + // 查询userId已经关注过的用户列表;并将对应的msg设置为true,表示已经关注过了;不需要再关注 + Set followedUserIds = userRelationService.getFollowedUserId(targetUserIds, userId); + list.forEach(notify -> { + if (followedUserIds.contains(notify.getOperateUserId())) { + notify.setMsg("true"); + } else { + notify.setMsg("false"); + } + }); + } + + @Override + public Map queryUnreadCounts(long userId) { + Map map = Collections.emptyMap(); + if (ReqInfoContext.getReqInfo() != null && NumUtil.upZero(ReqInfoContext.getReqInfo().getMsgNum())) { + map = notifyMsgDao.groupCountByUserIdAndStat(userId, NotifyStatEnum.UNREAD.getStat()); + } + // 指定先后顺序 + Map ans = new LinkedHashMap<>(); + initCnt(NotifyTypeEnum.COMMENT, map, ans); + initCnt(NotifyTypeEnum.REPLY, map, ans); + initCnt(NotifyTypeEnum.PRAISE, map, ans); + initCnt(NotifyTypeEnum.COLLECT, map, ans); + initCnt(NotifyTypeEnum.FOLLOW, map, ans); + initCnt(NotifyTypeEnum.SYSTEM, map, ans); + return ans; + } + + private void initCnt(NotifyTypeEnum type, Map map, Map result) { + result.put(type.name().toLowerCase(), map.getOrDefault(type.getType(), 0)); + } + + @Override + public void saveArticleNotify(UserFootDO foot, NotifyTypeEnum notifyTypeEnum) { + NotifyMsgDO msg = new NotifyMsgDO().setRelatedId(foot.getDocumentId()) + .setNotifyUserId(foot.getDocumentUserId()) + .setOperateUserId(foot.getUserId()) + .setType(notifyTypeEnum.getType() ) + .setState(NotifyStatEnum.UNREAD.getStat()) + .setMsg(""); + NotifyMsgDO record = notifyMsgDao.getByUserIdRelatedIdAndType(msg); + if (record == null) { + // 若之前已经有对应的通知,则不重复记录;因为一个用户对一篇文章,可以重复的点赞、取消点赞,但是最终我们只通知一次 + notifyMsgDao.save(msg); + } + } + + // -------------------------------------------- 下面是与用户的websocket长连接维护相关实现 ------------------------- + + /** + * x用户发送 + * @param userId 用户id + * @param msg 通知内容 + */ + @Override + public void notifyToUser(Long userId, String msg) { + wsUserSessionCache.getUnchecked(userId).forEach(s -> { + WebSocketResponseUtil.sendMsgToUser(s, NOTIFY_TOPIC, msg); + }); + } + + /** + * 用户建立连接时,添加用户信息 + * + * @param userId 用户id + * @param session jwt token + */ + private void addUserToken(Long userId, String session) { + wsUserSessionCache.getUnchecked(userId).add(session); + } + + /** + * 断开连接时,移除用户信息 + * + * @param userId 用户id + * @param session jwt token + */ + private void releaseUserToken(Long userId, String session) { + wsUserSessionCache.getUnchecked(userId).remove(session); + } + + /** + * WebSocket通道管理 + * + * @param accessor + */ + @Override + public void notifyChannelMaintain(StompHeaderAccessor accessor) { + String destination = accessor.getDestination(); + if (StringUtils.isBlank(destination) || accessor.getCommand() == null) { + return; + } + + + // 全局私信、通知长连接入口 + ReqInfoContext.ReqInfo user = (ReqInfoContext.ReqInfo) accessor.getUser(); + if (user == null) { + log.info("websocket用户未登录! {}", accessor); + return; + } + switch (accessor.getCommand()) { + case SUBSCRIBE: + // 建立用户通信通道 + addUserToken(user.getUserId(), user.getSession()); + break; + case DISCONNECT: + // 中断链接,去掉用户的长连接会话 + releaseUserToken(user.getUserId(), user.getSession()); + break; + } + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/service/impl/RabbitmqServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/service/impl/RabbitmqServiceImpl.java new file mode 100644 index 000000000..74b80b979 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/notify/service/impl/RabbitmqServiceImpl.java @@ -0,0 +1,129 @@ +package com.github.paicoding.forum.service.notify.service.impl; + +import com.github.paicoding.forum.api.model.enums.NotifyTypeEnum; +import com.github.paicoding.forum.core.common.CommonConstants; +import com.github.paicoding.forum.core.rabbitmq.RabbitmqConnection; +import com.github.paicoding.forum.core.rabbitmq.RabbitmqConnectionPool; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.service.notify.service.NotifyService; +import com.github.paicoding.forum.service.notify.service.RabbitmqService; +import com.github.paicoding.forum.service.user.repository.entity.UserFootDO; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.BuiltinExchangeType; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PreDestroy; +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +@Slf4j +@Service +public class RabbitmqServiceImpl implements RabbitmqService { + + // 设置一个消费者的固定连接,从池中获取一个连接即可 + private RabbitmqConnection rabbitmqConsumerConnection; + private Channel rabbitmqConsumerChannel; + + @Autowired + private NotifyService notifyService; + + @Override + public boolean enabled() { + return "true".equalsIgnoreCase(SpringUtil.getConfig("rabbitmq.switchFlag")); + } + + @Override + public void publishMsg(String exchange, + BuiltinExchangeType exchangeType, + String toutingKey, + String message) { + try { + //创建连接 + RabbitmqConnection rabbitmqConnection = RabbitmqConnectionPool.getConnection(); + Connection connection = rabbitmqConnection.getConnection(); + //创建消息通道 + Channel channel = connection.createChannel(); + // 声明exchange中的消息为可持久化,不自动删除 + channel.exchangeDeclare(exchange, exchangeType, true, false, null); + // 发布消息 + channel.basicPublish(exchange, toutingKey, null, message.getBytes()); + log.info("Publish msg: {}", message); + channel.close(); + RabbitmqConnectionPool.returnConnection(rabbitmqConnection); + } catch (InterruptedException | IOException | TimeoutException e) { + log.error("rabbitMq消息发送异常: exchange: {}, msg: {}", exchange, message, e); + } + + } + + /** + * 阻塞式消费 + * @param exchange + * @param queueName + * @param routingKey + */ + @Override + public void consumerMsg(String exchange, + String queueName, + String routingKey) { + try { + //创建连接 + rabbitmqConsumerConnection = RabbitmqConnectionPool.getConnection(); + Connection connection = rabbitmqConsumerConnection.getConnection(); + //创建消息信道 + rabbitmqConsumerChannel = connection.createChannel(); + //消息队列 + rabbitmqConsumerChannel.queueDeclare(queueName, true, false, false, null); + //绑定队列到交换机 + rabbitmqConsumerChannel.queueBind(queueName, exchange, routingKey); + + Consumer consumer = new DefaultConsumer(rabbitmqConsumerChannel) { + @Override + public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, + byte[] body) throws IOException { + String message = new String(body, "UTF-8"); + log.info("Consumer msg: {}", message); + // 获取Rabbitmq消息,并保存到DB + // 说明:这里仅作为示例,如果有多种类型的消息,可以根据消息判定,简单的用 if...else 处理,复杂的用工厂 + 策略模式 + notifyService.saveArticleNotify(JsonUtil.toObj(message, UserFootDO.class), NotifyTypeEnum.PRAISE); + rabbitmqConsumerChannel.basicAck(envelope.getDeliveryTag(), false); + } + }; + // 取消自动ack, 自动监听消息 + rabbitmqConsumerChannel.basicConsume(queueName, false, consumer); + } catch (InterruptedException | IOException e) { + e.printStackTrace(); + } + } + + @Override + public void processConsumerMsg() { + log.info("Begin to processConsumerMsg."); + consumerMsg(CommonConstants.EXCHANGE_NAME_DIRECT, CommonConstants.QUERE_NAME_PRAISE, CommonConstants.QUERE_KEY_PRAISE); + } + + /** + * 关闭连接和通道,销毁时关闭并归还 + */ + @PreDestroy + public void destroy() { + try { + if (rabbitmqConsumerChannel != null && rabbitmqConsumerChannel.isOpen()) { + rabbitmqConsumerChannel.close(); + } + if (rabbitmqConsumerConnection != null) { + RabbitmqConnectionPool.returnConnection(rabbitmqConsumerConnection); + } + } catch (IOException | TimeoutException e) { + log.error("关闭 RabbitMQ 连接和通道时出错", e); + } + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/PayService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/PayService.java new file mode 100644 index 000000000..206d290bd --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/PayService.java @@ -0,0 +1,52 @@ +package com.github.paicoding.forum.service.pay; + +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import com.github.paicoding.forum.api.model.vo.pay.dto.PayInfoDTO; +import com.github.paicoding.forum.service.article.repository.entity.ArticlePayRecordDO; +import com.github.paicoding.forum.service.pay.model.PayCallbackBo; +import com.wechat.pay.java.service.refund.model.RefundNotification; +import org.springframework.http.ResponseEntity; + +import javax.servlet.http.HttpServletRequest; +import java.util.function.Function; + +/** + * 技术派的支付服务接口 + * + * @author YiHui + * @date 2024/12/9 + */ +public interface PayService { + + boolean support(ThirdPayWayEnum payWay); + + PayInfoDTO toPay(ArticlePayRecordDO record, boolean needRefresh); + + /** + * 前端告知后端,将支付状态更新为支付中时,支付服务的处理逻辑 + * + * @param record + * @return true 执行成功,record记录有变更,需要执行保存操作 false 无需变更 + */ + boolean paying(ArticlePayRecordDO record); + + + /** + * 支付结果回调 + * + * @param request + * @param payCallback + * @return + */ + ResponseEntity payCallback(HttpServletRequest request, Function payCallback); + + + /** + * 退款结果回调 + * + * @param request + * @param payCallback + * @return + */ + ResponseEntity refundCallback(HttpServletRequest request, Function payCallback); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/PayServiceFactory.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/PayServiceFactory.java new file mode 100644 index 000000000..d3a33751f --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/PayServiceFactory.java @@ -0,0 +1,38 @@ +package com.github.paicoding.forum.service.pay; + +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 技术派的支付服务接口 + * + * @author YiHui + * @date 2024/12/9 + */ +@Service +public class PayServiceFactory { + + @Autowired + private List payServiceList; + + public PayService getPayService(ThirdPayWayEnum payWay) { + for (PayService payService : payServiceList) { + if (payService.support(payWay)) { + return payService; + } + } + + return null; + } + + + // fixme 对于支付状态为支付中的场景,根据notify_time来判断,是否需要重新给作者发送邮件通知 / 或者查询微信的订单状态,避免出现支付状态一直不更新的问题 + public void autoUpdatePayingStatus() { + // 1. 查出 支付状态 == 支付中, notifyTime = null 或者 notifyTime 距离当前时间超过30分钟的数据 + // 2. 重新调用一下 payService.paying 实现给作者发邮件 or 查三方支付状态 + // 3. 更新校验时间 + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/config/WxPayConfig.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/config/WxPayConfig.java new file mode 100644 index 000000000..1e8be3a59 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/config/WxPayConfig.java @@ -0,0 +1,49 @@ +package com.github.paicoding.forum.service.pay.config; + +import com.github.hui.quick.plugin.base.file.FileReadUtil; +import lombok.Data; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +/** + * 微信支付配置 + * + * @author YiHui + * @date 2024/12/3 + */ +@Data +@Component +@ConditionalOnProperty(value = "wx.pay.enable") +@ConfigurationProperties(prefix = "wx.pay") +public class WxPayConfig { + //APPID + private String appId; + //mchid + private String merchantId; + //商户API私钥 + private String privateKey; + //商户证书序列号 + private String merchantSerialNumber; + //商户APIv3密钥 + private String apiV3Key; + //支付通知地址 + private String payNotifyUrl; + //退款通知地址 + private String refundNotifyUrl; + + /** + * 获取私钥信息 + * + * @return 私钥内容 + */ + public String getPrivateKeyContent() { + try { + return FileReadUtil.readAll(privateKey); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/model/PayCallbackBo.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/model/PayCallbackBo.java new file mode 100644 index 000000000..0e943d994 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/model/PayCallbackBo.java @@ -0,0 +1,38 @@ +package com.github.paicoding.forum.service.pay.model; + +import com.github.paicoding.forum.api.model.enums.pay.PayStatusEnum; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 支付回调通知业务对象 + * + * @author YiHui + * @date 2024/12/6 + */ +@Data +@Accessors(chain = true) +public class PayCallbackBo { + /** + * 传递给支付系统的唯一外部单号 + */ + private String outTradeNo; + /** + * 对应系统内的业务支付id + */ + private Long payId; + /** + * 支付成功时间 + */ + private Long successTime; + + /** + * 三方流水编号 + */ + private String thirdTransactionId; + + /** + * 支付状态 + */ + private PayStatusEnum payStatus; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/model/PrePayInfoResBo.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/model/PrePayInfoResBo.java new file mode 100644 index 000000000..635cb05c4 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/model/PrePayInfoResBo.java @@ -0,0 +1,55 @@ +package com.github.paicoding.forum.service.pay.model; + +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @author YiHui + * @date 2024/12/3 + */ +@Data +@Accessors(chain = true) +public class PrePayInfoResBo { + /** + * 支付方式 wx-jsapi, wx-h5, wx-native + * + * @see ThirdPayWayEnum#getPay() + */ + private ThirdPayWayEnum payWay; + + /** + * 传递给三方的外部系统编号 + */ + private String outTradeNo; + + /** + * 应用: appId + */ + private String appId; + + /** + * 时间戳信息 + */ + private String nonceStr; + + private String prePackage; + + private String paySign; + + private String timeStamp; + + private String signType; + + /** + * jsapi:返回的是用于唤起支付的 prePayId + * h5: 返回的是微信收银台中间页 url,用于访问之后唤起微信客户端的支付页面 + * native: 返回的是形如 weixin:// 的文本,用于生成二维码给微信扫一扫支付 + */ + private String prePayId; + + /** + * prePayId的失效的时间戳 + */ + private Long expireTime; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/model/ThirdPayOrderReqBo.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/model/ThirdPayOrderReqBo.java new file mode 100644 index 000000000..b23874eff --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/model/ThirdPayOrderReqBo.java @@ -0,0 +1,53 @@ +package com.github.paicoding.forum.service.pay.model; + +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 向三方支付平台下单的请求业务参数 + * + * @author YiHui + * @date 2024/12/3 + */ +@Data +@Accessors(chain = true) +public class ThirdPayOrderReqBo { + /** + * 订单号(业务) + */ + String outTradeNo; + /** + * 用户openId, 对于h5支付场景下,没有这个参数 + */ + String openId; + /** + * 订单描述 + */ + String description; + /** + * 订单总金额,单位为分 + */ + int total; + + /** + * 支付方式 + */ + ThirdPayWayEnum payWay; + + public ThirdPayOrderReqBo() { + } + + public ThirdPayOrderReqBo(String outTradeNo, String description, int total) { + this.outTradeNo = outTradeNo; + this.description = description; + this.total = total; + } + + public ThirdPayOrderReqBo(String outTradeNo, String openId, String description, int total) { + this.outTradeNo = outTradeNo; + this.openId = openId; + this.description = description; + this.total = total; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/EmailPayServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/EmailPayServiceImpl.java new file mode 100644 index 000000000..53783fe9f --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/EmailPayServiceImpl.java @@ -0,0 +1,127 @@ +package com.github.paicoding.forum.service.pay.service; + +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import com.github.paicoding.forum.api.model.vo.ResVo; +import com.github.paicoding.forum.api.model.vo.article.dto.PayConfirmDTO; +import com.github.paicoding.forum.api.model.vo.pay.dto.PayInfoDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; +import com.github.paicoding.forum.core.util.EmailUtil; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.core.util.id.IdUtil; +import com.github.paicoding.forum.service.article.conveter.PayConverter; +import com.github.paicoding.forum.service.article.repository.entity.ArticlePayRecordDO; +import com.github.paicoding.forum.service.article.service.ArticlePayService; +import com.github.paicoding.forum.service.pay.PayService; +import com.github.paicoding.forum.service.pay.model.PayCallbackBo; +import com.github.paicoding.forum.service.pay.model.PrePayInfoResBo; +import com.github.paicoding.forum.service.pay.model.ThirdPayOrderReqBo; +import com.github.paicoding.forum.service.user.service.UserService; +import com.wechat.pay.java.service.refund.model.RefundNotification; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring5.SpringTemplateEngine; + +import javax.servlet.http.HttpServletRequest; +import java.util.Date; +import java.util.function.Function; + +/** + * 个人收款码-基于邮件的支付流程 + * + * @author YiHui + * @date 2024/12/9 + */ +@Slf4j +@Service +public class EmailPayServiceImpl implements PayService { + @Autowired + private UserService userService; + + @Autowired + private SpringTemplateEngine springTemplateEngine; + + @Autowired + private ThirdPayHandler thirdPayFacade; + + @Override + public boolean support(ThirdPayWayEnum payWay) { + return payWay == ThirdPayWayEnum.EMAIL; + } + + /** + * 唤起支付,主要返回作者的收款码 + * + * @param record + * @param needRefresh true 表示需要刷新用户的收款码信息 + * @return + */ + @Override + public PayInfoDTO toPay(ArticlePayRecordDO record, boolean needRefresh) { + ThirdPayOrderReqBo req = new ThirdPayOrderReqBo() + .setOutTradeNo(record.getVerifyCode()) + .setOpenId(record.getReceiveUserId() + "") + .setPayWay(ThirdPayWayEnum.EMAIL); + + PrePayInfoResBo bo = thirdPayFacade.createPayOrder(req); + PayInfoDTO payInfo = new PayInfoDTO(); + payInfo.setPrePayId(bo.getPrePayId()); + payInfo.setPayQrCodeMap(PayConverter.formatPayCode(bo.getPrePayId())); + if (needRefresh) { + // 需要刷新的场景时,才重置失效时间 + payInfo.setPrePayExpireTime(System.currentTimeMillis() + ThirdPayWayEnum.EMAIL.getExpireTimePeriod()); + } + return payInfo; + } + + /** + * 给作者发送支付确认邮件 + * + * @param record + */ + @Override + public boolean paying(ArticlePayRecordDO record) { + if (record.getNotifyTime() != null && System.currentTimeMillis() - record.getNotifyTime().getTime() < 180_000) { + // 两次通知时间,小于10分钟,则直接幂等 + log.info("上次邮件确认时间是: {} 忽略本次通知! {}", record.getNotifyTime(), JsonUtil.toStr(record)); + return false; + } + + try { + record.setVerifyCode(IdUtil.genPayCode(ThirdPayWayEnum.ofPay(record.getPayWay()), record.getId())); + + PayConfirmDTO confirm = SpringUtil.getBean(ArticlePayService.class).buildPayConfirmInfo(record.getId(), record); + Context context = new Context(); + context.setVariable("vo", confirm); + String confirmHtmlContent = springTemplateEngine.process("PayConfirm", context); + log.info("输出邮件内容: \n {} \n", confirmHtmlContent); + + // 给作者发送邮件通知 + BaseUserInfoDTO author = userService.queryBasicUserInfo(record.getReceiveUserId()); + EmailUtil.sendMail(String.format("【%s】收到【%s】的打赏,请确认", confirm.getTitle(), confirm.getPayUser()), author.getEmail(), confirmHtmlContent); + + // 邮件发送成功,更新通知时间 + 次数 + 验证码 + record.setNotifyTime(new Date()); + record.setNotifyCnt(record.getNotifyCnt() + 1); + record.setUpdateTime(new Date()); + } catch (Exception e) { + log.error("发送邮件确认通知失败: {}", record, e); + } + return true; + } + + @Override + public ResponseEntity payCallback(HttpServletRequest request, Function payCallback) { + PayCallbackBo bo = thirdPayFacade.payCallback(request, ThirdPayWayEnum.EMAIL); + boolean ans = payCallback.apply(bo); + return ResponseEntity.ok(ResVo.ok(ans)); + } + + @Override + public ResponseEntity refundCallback(HttpServletRequest request, Function payCallback) { + return thirdPayFacade.refundCallback(request, ThirdPayWayEnum.EMAIL, payCallback); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/OnlinePayServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/OnlinePayServiceImpl.java new file mode 100644 index 000000000..37c56e81f --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/OnlinePayServiceImpl.java @@ -0,0 +1,127 @@ +package com.github.paicoding.forum.service.pay.service; + +import com.github.paicoding.forum.api.model.enums.pay.PayStatusEnum; +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import com.github.paicoding.forum.api.model.vo.pay.dto.PayInfoDTO; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.core.util.PriceUtil; +import com.github.paicoding.forum.core.util.StrUtil; +import com.github.paicoding.forum.service.article.conveter.PayConverter; +import com.github.paicoding.forum.service.article.repository.entity.ArticlePayRecordDO; +import com.github.paicoding.forum.service.pay.PayService; +import com.github.paicoding.forum.service.pay.model.PayCallbackBo; +import com.github.paicoding.forum.service.pay.model.PrePayInfoResBo; +import com.github.paicoding.forum.service.pay.model.ThirdPayOrderReqBo; +import com.wechat.pay.java.service.refund.model.RefundNotification; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletRequest; +import java.util.Date; +import java.util.function.Function; + +/** + * 在线支付流程 + * + * @author YiHui + * @date 2024/12/9 + */ +@Slf4j +@Service +public class OnlinePayServiceImpl implements PayService { + @Autowired + private ThirdPayHandler thirdPayFacade; + + @Override + public boolean support(ThirdPayWayEnum payWay) { + return payWay != ThirdPayWayEnum.EMAIL; + } + + @Override + public PayInfoDTO toPay(ArticlePayRecordDO record, boolean needRefresh) { + if (!needRefresh) { + // 不需要刷新时,直接根据数据库中缓存的进行返回 + PayInfoDTO payInfo = new PayInfoDTO(); + payInfo.setPrePayId(PayConverter.genQrCode(record.getPrePayId())); + payInfo.setPayWay(record.getPayWay()); + payInfo.setPrePayExpireTime(record.getPrePayExpireTime().getTime()); + payInfo.setPayAmount(PriceUtil.toYuanPrice(record.getPayAmount())); + return payInfo; + } + + // 需要像微信重新创建支付订单,并且将结果反写到支付记录中 + ThirdPayOrderReqBo req = new ThirdPayOrderReqBo(); + req.setTotal(record.getPayAmount()); + req.setOutTradeNo(record.getVerifyCode()); + req.setDescription(StrUtil.pickWxSupportTxt(record.getNotes())); + req.setPayWay(ThirdPayWayEnum.ofPay(record.getPayWay())); + PrePayInfoResBo res = thirdPayFacade.createPayOrder(req); + + PayInfoDTO payInfo = new PayInfoDTO(); + if (res != null) { + // 回写微信支付信息到支付记录中,用于下次唤起支付使用 + record.setPrePayId(res.getPrePayId()); + record.setPrePayExpireTime(new Date(res.getExpireTime())); + + payInfo.setPrePayId(PayConverter.genQrCode(res.getPrePayId())); + payInfo.setPayWay(record.getPayWay()); + payInfo.setPrePayExpireTime(res.getExpireTime()); + payInfo.setPayAmount(PriceUtil.toYuanPrice(record.getPayAmount())); + } + return payInfo; + } + + /** + * 主动查询一下支付状态 + * + * @param dbRecord + * @return + */ + @Override + public boolean paying(ArticlePayRecordDO dbRecord) { + // 主动查询一下支付状态 + try { + PayCallbackBo bo = thirdPayFacade.queryOrder(dbRecord.getVerifyCode(), ThirdPayWayEnum.ofPay(dbRecord.getPayWay())); + if (bo.getPayStatus() == PayStatusEnum.SUCCEED || bo.getPayStatus() == PayStatusEnum.FAIL) { + // 实际结果是支付成功/支付失败时,刷新下record对应的内容 + // 更新原来的支付状态为最新的结果 + dbRecord.setPayStatus(bo.getPayStatus().getStatus()); + dbRecord.setPayCallbackTime(new Date(bo.getSuccessTime())); + dbRecord.setUpdateTime(new Date()); + dbRecord.setThirdTransCode(bo.getThirdTransactionId()); + } + } catch (Exception e) { + log.error("查询三方支付状态出现异常: {}", JsonUtil.toStr(dbRecord), e); + } + + // 依然返回true,将支付状态设置为true + return true; + } + + @Override + public ResponseEntity payCallback(HttpServletRequest request, Function payCallback) { + try { + PayCallbackBo bo = thirdPayFacade.payCallback(request, ThirdPayWayEnum.WX_NATIVE); + boolean ans = payCallback.apply(bo); + if (ans) { + // 处理成功,返回 200 OK 状态码 + return ResponseEntity.status(HttpStatus.OK).build(); + } else { + // 处理异常,返回 500 服务器内部异常 状态码 + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } catch (Exception e) { + log.error("微信支付回调v3java失败={}", e.getMessage(), e); + } + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + + } + + @Override + public ResponseEntity refundCallback(HttpServletRequest request, Function payCallback) { + return thirdPayFacade.refundCallback(request, ThirdPayWayEnum.WX_NATIVE, payCallback); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/ThirdPayHandler.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/ThirdPayHandler.java new file mode 100644 index 000000000..76909290c --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/ThirdPayHandler.java @@ -0,0 +1,53 @@ + +package com.github.paicoding.forum.service.pay.service; + +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.service.pay.model.PayCallbackBo; +import com.github.paicoding.forum.service.pay.model.PrePayInfoResBo; +import com.github.paicoding.forum.service.pay.model.ThirdPayOrderReqBo; +import com.github.paicoding.forum.service.pay.service.integration.ThirdPayIntegrationApi; +import com.github.paicoding.forum.service.pay.service.integration.email.EmailPayIntegration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.function.Function; + +/** + * 与三方支付服务交互的门面类 + * + * @author YiHui + * @date 2024/12/6 + */ +@Service +public class ThirdPayHandler { + @Autowired + private List payServiceList; + + private ThirdPayIntegrationApi getPayService(ThirdPayWayEnum payWay) { + return payServiceList.stream().filter(s -> s.support(payWay)).findFirst() + .orElse(SpringUtil.getBean(EmailPayIntegration.class)); + } + + public PrePayInfoResBo createPayOrder(ThirdPayOrderReqBo payReq) { + return getPayService(payReq.getPayWay()).createOrder(payReq); + } + + public PayCallbackBo queryOrder(String outTradeNo, ThirdPayWayEnum payWay) { + return getPayService(payWay).queryOrder(outTradeNo); + } + + @Transactional + public PayCallbackBo payCallback(HttpServletRequest request, ThirdPayWayEnum payWay) { + return getPayService(payWay).payCallback(request); + } + + @Transactional + public ResponseEntity refundCallback(HttpServletRequest request, ThirdPayWayEnum payWay, Function refundCallback) { + return getPayService(payWay).refundCallback(request, refundCallback); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/ThirdPayIntegrationApi.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/ThirdPayIntegrationApi.java new file mode 100644 index 000000000..b23309f25 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/ThirdPayIntegrationApi.java @@ -0,0 +1,67 @@ +package com.github.paicoding.forum.service.pay.service.integration; + +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import com.github.paicoding.forum.service.pay.model.PayCallbackBo; +import com.github.paicoding.forum.service.pay.model.PrePayInfoResBo; +import com.github.paicoding.forum.service.pay.model.ThirdPayOrderReqBo; +import org.springframework.http.ResponseEntity; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.function.Function; + +/** + * 对接三方支付的API定义 + * + * @author YiHui + * @date 2024/12/6 + */ +public interface ThirdPayIntegrationApi { + + boolean support(ThirdPayWayEnum payWay); + + /** + * 下单 + * + * @param payReq + * @return + */ + PrePayInfoResBo createOrder(ThirdPayOrderReqBo payReq); + + + /** + * 查询订单 + * + * @param outTradeNo + * @return + */ + PayCallbackBo queryOrder(String outTradeNo); + + + /** + * 支付回调 + * + * @param request + * @return + */ + PayCallbackBo payCallback(HttpServletRequest request); + + /** + * 关单 + * + * @param outTradeNo + */ + void closeOrder(String outTradeNo); + + /** + * 退款回调 + * + * @param request 携带回传的请求参数 + * @param refundCallback 退款结果回调执行业务逻辑 + * @return + * @throws IOException + */ + default ResponseEntity refundCallback(HttpServletRequest request, Function refundCallback) { + return ResponseEntity.ok(true); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/email/EmailPayIntegration.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/email/EmailPayIntegration.java new file mode 100644 index 000000000..ec7ca5006 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/email/EmailPayIntegration.java @@ -0,0 +1,65 @@ +package com.github.paicoding.forum.service.pay.service.integration.email; + +import com.github.paicoding.forum.api.model.enums.pay.PayStatusEnum; +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; +import com.github.paicoding.forum.service.pay.model.PayCallbackBo; +import com.github.paicoding.forum.service.pay.model.PrePayInfoResBo; +import com.github.paicoding.forum.service.pay.model.ThirdPayOrderReqBo; +import com.github.paicoding.forum.service.pay.service.integration.ThirdPayIntegrationApi; +import com.github.paicoding.forum.service.user.service.UserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletRequest; +import java.util.Objects; + +/** + * 个人收款码,基于微信的支付方式 + * + * @author YiHui + * @date 2024/12/6 + */ +@Slf4j +@Service +public class EmailPayIntegration implements ThirdPayIntegrationApi { + @Autowired + private UserService userService; + + @Override + public boolean support(ThirdPayWayEnum payWay) { + return payWay == ThirdPayWayEnum.EMAIL; + } + + @Override + public PrePayInfoResBo createOrder(ThirdPayOrderReqBo payReq) { + PrePayInfoResBo resBo = new PrePayInfoResBo(); + resBo.setPayWay(ThirdPayWayEnum.EMAIL); + resBo.setOutTradeNo(payReq.getOutTradeNo()); + + BaseUserInfoDTO receiveUserInfo = userService.queryBasicUserInfo(Long.parseLong(payReq.getOpenId())); + resBo.setPrePayId(receiveUserInfo.getPayCode()); + resBo.setExpireTime(System.currentTimeMillis() + ThirdPayWayEnum.EMAIL.getExpireTimePeriod()); + return resBo; + } + + @Override + public void closeOrder(String outTradeNo) { + } + + @Override + public PayCallbackBo queryOrder(String outTradeNo) { + return new PayCallbackBo().setOutTradeNo(outTradeNo); + } + + + @Override + public PayCallbackBo payCallback(HttpServletRequest request) { + String outTradeNo = request.getParameter("verifyCode"); + Long payId = Long.parseLong(request.getParameter("payId")); + PayStatusEnum payStatus = Objects.equals("true", request.getParameter("succeed")) ? PayStatusEnum.SUCCEED : PayStatusEnum.FAIL; + return new PayCallbackBo().setPayId(payId).setOutTradeNo(outTradeNo).setPayStatus(payStatus) + .setSuccessTime(System.currentTimeMillis()); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/wx/AbsWxPayIntegration.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/wx/AbsWxPayIntegration.java new file mode 100644 index 000000000..b1e33262d --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/wx/AbsWxPayIntegration.java @@ -0,0 +1,167 @@ +package com.github.paicoding.forum.service.pay.service.integration.wx; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.github.paicoding.forum.api.model.enums.pay.PayStatusEnum; +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import com.github.paicoding.forum.core.net.HttpRequestHelper; +import com.github.paicoding.forum.core.util.DateUtil; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.core.util.id.IdUtil; +import com.github.paicoding.forum.service.pay.config.WxPayConfig; +import com.github.paicoding.forum.service.pay.model.PayCallbackBo; +import com.github.paicoding.forum.service.pay.model.PrePayInfoResBo; +import com.github.paicoding.forum.service.pay.model.ThirdPayOrderReqBo; +import com.github.paicoding.forum.service.pay.service.integration.ThirdPayIntegrationApi; +import com.wechat.pay.java.core.RSAAutoCertificateConfig; +import com.wechat.pay.java.core.exception.ValidationException; +import com.wechat.pay.java.core.notification.NotificationConfig; +import com.wechat.pay.java.core.notification.NotificationParser; +import com.wechat.pay.java.core.notification.RequestParam; +import com.wechat.pay.java.service.payments.model.Transaction; +import com.wechat.pay.java.service.refund.model.RefundNotification; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; + +import javax.servlet.http.HttpServletRequest; +import java.util.function.Function; + +/** + * @author YiHui + * @date 2024/12/6 + */ +@Slf4j +public abstract class AbsWxPayIntegration implements ThirdPayIntegrationApi { + public WxPayConfig wxPayConfig; + + public abstract String createPayOrder(ThirdPayOrderReqBo payReq); + + /** + * 补齐支付信息 + * + * @param payReq 支付请求参数 + * @param prePayId 微信返回的支付唤起code + */ + protected PrePayInfoResBo buildPayInfo(ThirdPayOrderReqBo payReq, String prePayId) { + // 结果封装返回 + ThirdPayWayEnum payWay = payReq.getPayWay(); + PrePayInfoResBo prePay = new PrePayInfoResBo(); + prePay.setPayWay(payWay); + prePay.setOutTradeNo(payReq.getOutTradeNo()); + prePay.setAppId(wxPayConfig.getAppId()); + prePay.setPrePayId(prePayId); + prePay.setExpireTime(System.currentTimeMillis() + payWay.getExpireTimePeriod()); + return prePay; + } + + /** + * 唤起支付 + * JSAPI调起支付 + * + * @return + */ + public PrePayInfoResBo createOrder(ThirdPayOrderReqBo payReq) { + log.info("微信支付 >>>>>>>>>>>>>>>>> 请求:{}", JsonUtil.toStr(payReq)); + // 微信下单 + String prePayId = createPayOrder(payReq); + return buildPayInfo(payReq, prePayId); + } + + protected PayCallbackBo toBo(Transaction transaction) { + String outTradeNo = transaction.getOutTradeNo(); + PayStatusEnum payStatus; + switch (transaction.getTradeState()) { + case SUCCESS: + payStatus = PayStatusEnum.SUCCEED; + break; + case NOTPAY: + payStatus = PayStatusEnum.NOT_PAY; + break; + case USERPAYING: + payStatus = PayStatusEnum.PAYING; + break; + default: + payStatus = PayStatusEnum.FAIL; + } + Long payId = IdUtil.getPayIdFromPayCode(outTradeNo); + Long payTime = transaction.getSuccessTime() != null ? DateUtil.wxDayToTimestamp(transaction.getSuccessTime()) : null; + return new PayCallbackBo() + .setPayStatus(payStatus) + .setOutTradeNo(outTradeNo) + .setPayId(payId) + .setThirdTransactionId(transaction.getTransactionId()) + .setSuccessTime(payTime); + } + + @Override + public PayCallbackBo payCallback(HttpServletRequest request) { + RequestParam requestParam = new RequestParam.Builder() + .serialNumber(request.getHeader("Wechatpay-Serial")) + .nonce(request.getHeader("Wechatpay-Nonce")) + .timestamp(request.getHeader("Wechatpay-Timestamp")) + .signature(request.getHeader("Wechatpay-Signature")) + .body(HttpRequestHelper.readReqData(request)) + .build(); + log.info("微信回调v3 >>>>>>>>>>>>>>>>> {}", JSONObject.toJSONString(requestParam)); + + NotificationConfig config = new RSAAutoCertificateConfig.Builder() + .merchantId(wxPayConfig.getMerchantId()) + .privateKey(wxPayConfig.getPrivateKeyContent()) + .merchantSerialNumber(wxPayConfig.getMerchantSerialNumber()) + .apiV3Key(wxPayConfig.getApiV3Key()) + .build(); + + NotificationParser parser = new NotificationParser(config); + // 验签、解密并转换成 Transaction(返回参数对象) + Transaction transaction = parser.parse(requestParam, Transaction.class); + log.info("微信支付回调 成功,解析: {}", JSON.toJSONString(transaction)); + return toBo(transaction); + } + + /** + * 微信退款回调 + * - 技术派目前没有实现退款流程,下面只是实现了回调,没有具体的业务场景 + * + * @param request + * @return + */ + @Transactional + public ResponseEntity refundCallback(HttpServletRequest request, Function refundCallback) { + RequestParam requestParam = new RequestParam.Builder() + .serialNumber(request.getHeader("Wechatpay-Serial")) + .nonce(request.getHeader("Wechatpay-Nonce")) + .timestamp(request.getHeader("Wechatpay-Timestamp")) + .signature(request.getHeader("Wechatpay-Signature")) + .body(HttpRequestHelper.readReqData(request)) + .build(); + log.info("微信退款回调v3 >>>>>>>>>>>>>>>>> {}", JSONObject.toJSONString(requestParam)); + + NotificationConfig config = new RSAAutoCertificateConfig.Builder() + .merchantId(wxPayConfig.getMerchantId()) + .privateKey(wxPayConfig.getPrivateKeyContent()) + .merchantSerialNumber(wxPayConfig.getMerchantSerialNumber()) + .apiV3Key(wxPayConfig.getApiV3Key()) + .build(); + + NotificationParser parser = new NotificationParser(config); + + try { + // 验签、解密并转换成 Transaction(返回参数对象) + RefundNotification refundNotify = parser.parse(requestParam, RefundNotification.class); + log.info("微信退款回调 成功,解析: {}", JSON.toJSONString(refundNotify)); + boolean ans = refundCallback.apply((T) refundNotify); + if (ans) { + // 处理成功,返回 200 OK 状态码 + return ResponseEntity.status(HttpStatus.OK).build(); + } else { + // 处理异常,返回 500 服务器内部异常 状态码 + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } catch (ValidationException e) { + log.error("微信退款回调v3java失败=" + e.getMessage(), e); + } + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/wx/H5WxPayIntegration.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/wx/H5WxPayIntegration.java new file mode 100644 index 000000000..39afdc97f --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/wx/H5WxPayIntegration.java @@ -0,0 +1,96 @@ +package com.github.paicoding.forum.service.pay.service.integration.wx; + +import com.alibaba.fastjson.JSONObject; +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.service.pay.config.WxPayConfig; +import com.github.paicoding.forum.service.pay.model.PayCallbackBo; +import com.github.paicoding.forum.service.pay.model.PrePayInfoResBo; +import com.github.paicoding.forum.service.pay.model.ThirdPayOrderReqBo; +import com.wechat.pay.java.core.Config; +import com.wechat.pay.java.core.RSAAutoCertificateConfig; +import com.wechat.pay.java.service.payments.h5.H5Service; +import com.wechat.pay.java.service.payments.h5.model.*; +import com.wechat.pay.java.service.payments.model.Transaction; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.stereotype.Service; + +/** + * @author YiHui + * @date 2024/12/4 + */ +@Slf4j +@Service +@ConditionalOnBean(WxPayConfig.class) +public class H5WxPayIntegration extends AbsWxPayIntegration { + private H5Service h5Service; + + public H5WxPayIntegration(WxPayConfig wxPayConfig) { + this.wxPayConfig = wxPayConfig; + Config config = new RSAAutoCertificateConfig.Builder() + .merchantId(wxPayConfig.getMerchantId()) + .privateKey(wxPayConfig.getPrivateKeyContent()) + .merchantSerialNumber(wxPayConfig.getMerchantSerialNumber()) + .apiV3Key(wxPayConfig.getApiV3Key()) + .build(); + h5Service = new H5Service.Builder().config(config).build(); + } + + @Override + public boolean support(ThirdPayWayEnum payWay) { + return ThirdPayWayEnum.WX_H5 == payWay; + } + + /** + * h5支付,生成微信支付收银台中间页,适用于拿不到微信给与的用户 OpenId 场景 + * + * @return + */ + public String createPayOrder(ThirdPayOrderReqBo payReq) { + PrepayRequest request = new PrepayRequest(); + request.setAppid(wxPayConfig.getAppId()); + request.setMchid(wxPayConfig.getMerchantId()); + request.setDescription(payReq.getDescription()); + request.setNotifyUrl(wxPayConfig.getPayNotifyUrl()); + request.setOutTradeNo(payReq.getOutTradeNo()); + + Amount amount = new Amount(); + amount.setTotal(payReq.getTotal()); + amount.setCurrency("CNY"); + request.setAmount(amount); + + SceneInfo sceneInfo = new SceneInfo(); + sceneInfo.setPayerClientIp(ReqInfoContext.getReqInfo().getClientIp()); + H5Info h5Info = new H5Info(); + h5Info.setAppName("技术派"); + h5Info.setAppUrl("https://paicoding.com"); + h5Info.setType("PC"); + sceneInfo.setH5Info(h5Info); + request.setSceneInfo(sceneInfo); + + log.info("微信h5下单, 微信请求参数: {}", JsonUtil.toStr(request)); + PrepayResponse response = h5Service.prepay(request); + log.info("微信支付 >>>>>>>>>>>> 返回: {}", response.getH5Url()); + return response.getH5Url(); + } + + @Override + public void closeOrder(String outTradeNo) { + CloseOrderRequest closeRequest = new CloseOrderRequest(); + closeRequest.setMchid(wxPayConfig.getMerchantId()); + closeRequest.setOutTradeNo(outTradeNo); + h5Service.closeOrder(closeRequest); + } + + @Override + public PayCallbackBo queryOrder(String outTradeNo) { + QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest(); + request.setMchid(wxPayConfig.getMerchantId()); + request.setOutTradeNo(outTradeNo); + Transaction transaction = h5Service.queryOrderByOutTradeNo(request); + return toBo(transaction); + } + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/wx/JsapiWxPayIntegration.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/wx/JsapiWxPayIntegration.java new file mode 100644 index 000000000..5a8efb118 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/wx/JsapiWxPayIntegration.java @@ -0,0 +1,133 @@ + +package com.github.paicoding.forum.service.pay.service.integration.wx; + +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.core.util.RandUtil; +import com.github.paicoding.forum.service.pay.config.WxPayConfig; +import com.github.paicoding.forum.service.pay.model.PayCallbackBo; +import com.github.paicoding.forum.service.pay.model.PrePayInfoResBo; +import com.github.paicoding.forum.service.pay.model.ThirdPayOrderReqBo; +import com.wechat.pay.java.core.Config; +import com.wechat.pay.java.core.RSAAutoCertificateConfig; +import com.wechat.pay.java.core.util.PemUtil; +import com.wechat.pay.java.service.payments.jsapi.JsapiService; +import com.wechat.pay.java.service.payments.jsapi.model.Amount; +import com.wechat.pay.java.service.payments.jsapi.model.CloseOrderRequest; +import com.wechat.pay.java.service.payments.jsapi.model.Payer; +import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest; +import com.wechat.pay.java.service.payments.jsapi.model.QueryOrderByOutTradeNoRequest; +import com.wechat.pay.java.service.payments.model.Transaction; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.security.Signature; +import java.util.Base64; + +/** + * @author YiHui + * @date 2024/12/4 + */ +@Slf4j +@Service +@ConditionalOnBean(WxPayConfig.class) +public class JsapiWxPayIntegration extends AbsWxPayIntegration { + private JsapiService jsapiService; + + public JsapiWxPayIntegration(WxPayConfig wxPayConfig) { + this.wxPayConfig = wxPayConfig; + Config config = new RSAAutoCertificateConfig.Builder() + .merchantId(wxPayConfig.getMerchantId()) + .privateKey(wxPayConfig.getPrivateKeyContent()) + .merchantSerialNumber(wxPayConfig.getMerchantSerialNumber()) + .apiV3Key(wxPayConfig.getApiV3Key()) + .build(); + jsapiService = new JsapiService.Builder().config(config).build(); + } + + @Override + public boolean support(ThirdPayWayEnum payWay) { + return ThirdPayWayEnum.WX_JSAPI == payWay; + } + + + /** + * jsApi微信支付 -- 适用于小程序、公众号等方式的支付场景: 需要拿到用户的openId + */ + public String createPayOrder(ThirdPayOrderReqBo payReq) { + PrepayRequest request = new PrepayRequest(); + request.setAppid(wxPayConfig.getAppId()); + request.setMchid(wxPayConfig.getMerchantId()); + request.setDescription(payReq.getDescription()); + request.setNotifyUrl(wxPayConfig.getPayNotifyUrl()); + request.setOutTradeNo(payReq.getOutTradeNo()); + + Amount amount = new Amount(); + amount.setTotal(payReq.getTotal()); + request.setAmount(amount); + + Payer payer = new Payer(); + payer.setOpenid(payReq.getOpenId()); + request.setPayer(payer); + + log.info("微信JsApi下单, 请求参数: {}", JsonUtil.toStr(request)); + com.wechat.pay.java.service.payments.jsapi.model.PrepayResponse response = jsapiService.prepay(request); + log.info("微信支付 >>>>>>>>>>>> 返回: {}", response.getPrepayId()); + return response.getPrepayId(); + } + + @Override + protected PrePayInfoResBo buildPayInfo(ThirdPayOrderReqBo payReq, String prePayId) { + PrePayInfoResBo payRes = super.buildPayInfo(payReq, prePayId); + long now = System.currentTimeMillis(); + // 官方说明有效期为两小时,我们设置为1.8小时之后失效 + payRes.setExpireTime(now + ThirdPayWayEnum.WX_JSAPI.getExpireTimePeriod()); + String timeStamp = String.valueOf(now / 1000); + //随机字符串,要求小于32位 + String nonceStr = RandUtil.random(30); + String packageStr = "prepay_id=" + payRes.getPrePayId(); + + // 不能去除'.append("\n")',否则失败 + String signStr = wxPayConfig.getAppId() + "\n" + + timeStamp + "\n" + + nonceStr + "\n" + + packageStr + "\n"; + + byte[] message = signStr.getBytes(StandardCharsets.UTF_8); + try { + Signature sign = Signature.getInstance("SHA256withRSA"); + sign.initSign(PemUtil.loadPrivateKeyFromString(wxPayConfig.getPrivateKeyContent())); + sign.update(message); + String signStrBase64 = Base64.getEncoder().encodeToString(sign.sign()); + + // 拼装返回结果 + payRes.setNonceStr(nonceStr); + payRes.setPrePackage(packageStr); + payRes.setSignType("RSA"); + payRes.setTimeStamp(timeStamp); + payRes.setPaySign(signStrBase64); + return payRes; + } catch (Exception e) { + log.error("唤醒支付签名异常: {} - {}", payRes.getPrePayId(), payRes.getOutTradeNo(), e); + return null; + } + } + + public void closeOrder(String outTradeNo) { + CloseOrderRequest closeRequest = new CloseOrderRequest(); + closeRequest.setMchid(wxPayConfig.getMerchantId()); + closeRequest.setOutTradeNo(outTradeNo); + jsapiService.closeOrder(closeRequest); + } + + + public PayCallbackBo queryOrder(String outTradeNo) { + QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest(); + request.setMchid(wxPayConfig.getMerchantId()); + request.setOutTradeNo(outTradeNo); + Transaction transaction = jsapiService.queryOrderByOutTradeNo(request); + return toBo(transaction); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/wx/NativeWxPayIntegration.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/wx/NativeWxPayIntegration.java new file mode 100644 index 000000000..ffac6235c --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/pay/service/integration/wx/NativeWxPayIntegration.java @@ -0,0 +1,91 @@ +package com.github.paicoding.forum.service.pay.service.integration.wx; + +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.api.model.enums.pay.ThirdPayWayEnum; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.service.pay.config.WxPayConfig; +import com.github.paicoding.forum.service.pay.model.PayCallbackBo; +import com.github.paicoding.forum.service.pay.model.ThirdPayOrderReqBo; +import com.wechat.pay.java.core.Config; +import com.wechat.pay.java.core.RSAAutoCertificateConfig; +import com.wechat.pay.java.service.payments.model.Transaction; +import com.wechat.pay.java.service.payments.nativepay.NativePayService; +import com.wechat.pay.java.service.payments.nativepay.model.Amount; +import com.wechat.pay.java.service.payments.nativepay.model.CloseOrderRequest; +import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest; +import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse; +import com.wechat.pay.java.service.payments.nativepay.model.QueryOrderByOutTradeNoRequest; +import com.wechat.pay.java.service.payments.nativepay.model.SceneInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.stereotype.Service; + +/** + * @author YiHui + * @date 2024/12/4 + */ +@Slf4j +@Service +@ConditionalOnBean(WxPayConfig.class) +public class NativeWxPayIntegration extends AbsWxPayIntegration { + private NativePayService nativePayService; + + public NativeWxPayIntegration(WxPayConfig wxPayConfig) { + this.wxPayConfig = wxPayConfig; + Config config = new RSAAutoCertificateConfig.Builder() + .merchantId(wxPayConfig.getMerchantId()) + .privateKey(wxPayConfig.getPrivateKeyContent()) + .merchantSerialNumber(wxPayConfig.getMerchantSerialNumber()) + .apiV3Key(wxPayConfig.getApiV3Key()) + .build(); + nativePayService = new NativePayService.Builder().config(config).build(); + } + + @Override + public boolean support(ThirdPayWayEnum payWay) { + return ThirdPayWayEnum.WX_NATIVE == payWay; + } + + /** + * native 支付,生成扫描支付二维码唤起微信支付页面 + * + * @return 形如 wx://xxx 的支付二维码 + */ + public String createPayOrder(ThirdPayOrderReqBo payReq) { + PrepayRequest request = new PrepayRequest(); + request.setAppid(wxPayConfig.getAppId()); + request.setMchid(wxPayConfig.getMerchantId()); + request.setDescription(payReq.getDescription()); + request.setNotifyUrl(wxPayConfig.getPayNotifyUrl()); + request.setOutTradeNo(payReq.getOutTradeNo()); + + Amount amount = new Amount(); + amount.setTotal(payReq.getTotal()); + amount.setCurrency("CNY"); + request.setAmount(amount); + + SceneInfo sceneInfo = new SceneInfo(); + sceneInfo.setPayerClientIp(ReqInfoContext.getReqInfo().getClientIp()); + request.setSceneInfo(sceneInfo); + + log.info("微信native下单, 微信请求参数: {}", JsonUtil.toStr(request)); + PrepayResponse response = nativePayService.prepay(request); + log.info("微信支付 >>>>>>>>>>>> 返回: {}", response.getCodeUrl()); + return response.getCodeUrl(); + } + + public void closeOrder(String outTradeNo) { + CloseOrderRequest closeRequest = new CloseOrderRequest(); + closeRequest.setMchid(wxPayConfig.getMerchantId()); + closeRequest.setOutTradeNo(outTradeNo); + nativePayService.closeOrder(closeRequest); + } + + public PayCallbackBo queryOrder(String outTradeNo) { + QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest(); + request.setMchid(wxPayConfig.getMerchantId()); + request.setOutTradeNo(outTradeNo); + Transaction transaction = nativePayService.queryOrderByOutTradeNo(request); + return toBo(transaction); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/rank/package-info.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/rank/package-info.java new file mode 100644 index 000000000..ee0778f24 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/rank/package-info.java @@ -0,0 +1,7 @@ +/** + * 排行榜 + * + * @author YiHui + * @date 2023/8/19 + */ +package com.github.paicoding.forum.service.rank; \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/rank/service/UserActivityRankService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/rank/service/UserActivityRankService.java new file mode 100644 index 000000000..d000b546e --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/rank/service/UserActivityRankService.java @@ -0,0 +1,40 @@ +package com.github.paicoding.forum.service.rank.service; + +import com.github.paicoding.forum.api.model.enums.rank.ActivityRankTimeEnum; +import com.github.paicoding.forum.api.model.vo.rank.dto.RankItemDTO; +import com.github.paicoding.forum.service.rank.service.model.ActivityScoreBo; + +import java.util.List; + +/** + * 用户活跃排行榜 + * + * @author YiHui + * @date 2023/8/19 + */ +public interface UserActivityRankService { + /** + * 添加活跃分 + * + * @param userId + * @param activityScore + */ + void addActivityScore(Long userId, ActivityScoreBo activityScore); + + /** + * 查询用户的活跃信息 + * + * @param userId + * @param time + * @return + */ + RankItemDTO queryRankInfo(Long userId, ActivityRankTimeEnum time); + + /** + * 查询活跃度排行榜 + * + * @param time + * @return + */ + List queryRankList(ActivityRankTimeEnum time, int size); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/rank/service/impl/UserActivityRankServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/rank/service/impl/UserActivityRankServiceImpl.java new file mode 100644 index 000000000..e28a56941 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/rank/service/impl/UserActivityRankServiceImpl.java @@ -0,0 +1,189 @@ +package com.github.paicoding.forum.service.rank.service.impl; + +import com.github.paicoding.forum.api.model.enums.rank.ActivityRankTimeEnum; +import com.github.paicoding.forum.api.model.vo.rank.dto.RankItemDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.SimpleUserInfoDTO; +import com.github.paicoding.forum.core.cache.RedisClient; +import com.github.paicoding.forum.core.util.DateUtil; +import com.github.paicoding.forum.core.util.NumUtil; +import com.github.paicoding.forum.service.rank.service.UserActivityRankService; +import com.github.paicoding.forum.service.rank.service.model.ActivityScoreBo; +import com.github.paicoding.forum.service.user.service.UserService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * @author YiHui + * @date 2023/8/19 + */ +@Slf4j +@Service +public class UserActivityRankServiceImpl implements UserActivityRankService { + private static final String ACTIVITY_SCORE_KEY = "activity_rank_"; + + @Autowired + private UserService userService; + + /** + * 当天活跃度排行榜 + * + * @return 当天排行榜key + */ + private String todayRankKey() { + return ACTIVITY_SCORE_KEY + DateUtil.format(DateTimeFormatter.ofPattern("yyyyMMdd"), System.currentTimeMillis()); + } + + /** + * 本月排行榜 + * + * @return 月度排行榜key + */ + private String monthRankKey() { + return ACTIVITY_SCORE_KEY + DateUtil.format(DateTimeFormatter.ofPattern("yyyyMM"), System.currentTimeMillis()); + } + + /** + * 添加活跃分 + * + * @param userId 用于更新活跃积分的用户 + * @param activityScore 触发活跃积分的时间类型 + */ + @Override + public void addActivityScore(Long userId, ActivityScoreBo activityScore) { + if (userId == null) { + return; + } + + // 1. 计算活跃度(正为加活跃,负为减活跃) + String field; + int score = 0; + if (activityScore.getPath() != null) { + field = "path_" + activityScore.getPath(); + score = 1; + } else if (activityScore.getArticleId() != null) { + field = activityScore.getArticleId() + "_"; + if (activityScore.getPraise() != null) { + field += "praise"; + score = BooleanUtils.isTrue(activityScore.getPraise()) ? 2 : -2; + } else if (activityScore.getCollect() != null) { + field += "collect"; + score = BooleanUtils.isTrue(activityScore.getCollect()) ? 2 : -2; + } else if (activityScore.getRate() != null) { + // 评论回复 + field += "rate"; + score = BooleanUtils.isTrue(activityScore.getRate()) ? 3 : -3; + } else if (BooleanUtils.isTrue(activityScore.getPublishArticle())) { + // 发布文章 + field += "publish"; + score += 10; + } + } else if (activityScore.getFollowedUserId() != null) { + // 关注添加积分 + field = activityScore.getFollowedUserId() + "_follow"; + score = BooleanUtils.isTrue(activityScore.getFollow()) ? 2 : -2; + } else { + return; + } + + final String todayRankKey = todayRankKey(); + final String monthRankKey = monthRankKey(); + // 2. 幂等:判断之前是否有更新过相关的活跃度信息 + final String userActionKey = ACTIVITY_SCORE_KEY + userId + DateUtil.format(DateTimeFormatter.ofPattern("yyyyMMdd"), System.currentTimeMillis()); + Integer ans = RedisClient.hGet(userActionKey, field, Integer.class); + if (ans == null) { + // 2.1 之前没有加分记录,执行具体的加分 + if (score > 0) { + // 记录加分记录 + RedisClient.hSet(userActionKey, field, score); + // 个人用户的操作记录,保存一个月的有效期,方便用户查询自己最近31天的活跃情况 + RedisClient.expire(userActionKey, 31 * DateUtil.ONE_DAY_SECONDS); + + // 更新当天和当月的活跃度排行榜 + Double newAns = RedisClient.zIncrBy(todayRankKey, String.valueOf(userId), score); + RedisClient.zIncrBy(monthRankKey, String.valueOf(userId), score); + if (log.isDebugEnabled()) { + log.info("活跃度更新加分! key#field = {}#{}, add = {}, newScore = {}", todayRankKey, userId, score, newAns); + } + if (newAns <= score) { + // 由于上面只实现了日/月活跃度的增加,但是没有设置对应的有效期;为了避免持久保存导致redis占用较高;因此这里设定了缓存的有效期 + // 日活跃榜单,保存31天;月活跃榜单,保存1年 + // 为什么是 newAns <= score 才设置有效期呢? + // 因为 newAns 是用户当天的活跃度,如果发现和需要增加的活跃度 scopre 相等,则表明是今天的首次添加记录,此时设置有效期就比较符合预期了 + // 但是请注意,下面的实现有两个缺陷: + // 1. 对于月的有效期,就变成了本月,每天的首次增加活跃度时,都会重新刷一下它的有效期,这样就和预期中的首次添加缓存时,设置有效期不符 + // 2. 若先增加活跃度1,再减少活跃度1,然后再加活跃度1,同样会导致重新算了有效期 + // 严谨一些的写法,应该是 先判断 key 的 ttl, 对于没有设置的才进行设置有效期,如下 + Long ttl = RedisClient.ttl(todayRankKey); + if (!NumUtil.upZero(ttl)) { + RedisClient.expire(todayRankKey, 31 * DateUtil.ONE_DAY_SECONDS); + } + ttl = RedisClient.ttl(monthRankKey); + if (!NumUtil.upZero(ttl)) { + RedisClient.expire(monthRankKey, 12 * DateUtil.ONE_MONTH_SECONDS); + } + } + } + } else if (ans > 0) { + // 2.2 之前已经加过分,因此这次减分可以执行 + if (score < 0) { + // 移除用户的活跃执行记录 --> 即移除用来做防重复添加活跃度的幂等键 + Boolean oldHave = RedisClient.hDel(userActionKey, field); + if (BooleanUtils.isTrue(oldHave)) { + Double newAns = RedisClient.zIncrBy(todayRankKey, String.valueOf(userId), score); + RedisClient.zIncrBy(monthRankKey, String.valueOf(userId), score); + if (log.isDebugEnabled()) { + log.info("活跃度更新减分! key#field = {}#{}, add = {}, newScore = {}", todayRankKey, userId, score, newAns); + } + } + } + } + } + + @Override + public RankItemDTO queryRankInfo(Long userId, ActivityRankTimeEnum time) { + RankItemDTO item = new RankItemDTO(); + item.setUser(userService.querySimpleUserInfo(userId)); + + String rankKey = time == ActivityRankTimeEnum.DAY ? todayRankKey() : monthRankKey(); + ImmutablePair rank = RedisClient.zRankInfo(rankKey, String.valueOf(userId)); + item.setRank(rank.getLeft()); + item.setScore(rank.getRight().intValue()); + return item; + } + + @Override + public List queryRankList(ActivityRankTimeEnum time, int size) { + String rankKey = time == ActivityRankTimeEnum.DAY ? todayRankKey() : monthRankKey(); + // 1. 获取topN的活跃用户 + List> rankList = RedisClient.zTopNScore(rankKey, size); + if (CollectionUtils.isEmpty(rankList)) { + return Collections.emptyList(); + } + + // 2. 查询用户对应的基本信息 + // 构建userId -> 活跃评分的map映射,用于补齐用户信息 + Map userScoreMap = rankList.stream().collect(Collectors.toMap(s -> Long.valueOf(s.getLeft()), s -> s.getRight().intValue())); + List users = userService.batchQuerySimpleUserInfo(userScoreMap.keySet()); + + // 3. 根据评分进行排序 + List rank = users.stream() + .map(user -> new RankItemDTO().setUser(user).setScore(userScoreMap.getOrDefault(user.getUserId(), 0))) + .sorted((o1, o2) -> Integer.compare(o2.getScore(), o1.getScore())) + .collect(Collectors.toList()); + + // 4. 补齐每个用户的排名 + IntStream.range(0, rank.size()).forEach(i -> rank.get(i).setRank(i + 1)); + return rank; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/rank/service/listener/UserActivityListener.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/rank/service/listener/UserActivityListener.java new file mode 100644 index 000000000..530c45799 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/rank/service/listener/UserActivityListener.java @@ -0,0 +1,85 @@ +package com.github.paicoding.forum.service.rank.service.listener; + +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.api.model.enums.ArticleEventEnum; +import com.github.paicoding.forum.api.model.event.ArticleMsgEvent; +import com.github.paicoding.forum.api.model.vo.notify.NotifyMsgEvent; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDO; +import com.github.paicoding.forum.service.comment.repository.entity.CommentDO; +import com.github.paicoding.forum.service.rank.service.UserActivityRankService; +import com.github.paicoding.forum.service.rank.service.model.ActivityScoreBo; +import com.github.paicoding.forum.service.user.repository.entity.UserFootDO; +import com.github.paicoding.forum.service.user.repository.entity.UserRelationDO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * 用户活跃相关的消息监听器 + * + * @author YiHui + * @date 2023/8/19 + */ +@Component +public class UserActivityListener { + @Autowired + private UserActivityRankService userActivityRankService; + + /** + * 用户操作行为,增加对应的积分 + * + * @param msgEvent + */ + @EventListener(classes = NotifyMsgEvent.class) + @Async + public void notifyMsgListener(NotifyMsgEvent msgEvent) { + switch (msgEvent.getNotifyType()) { + case COMMENT: + case REPLY: + CommentDO comment = (CommentDO) msgEvent.getContent(); + userActivityRankService.addActivityScore(ReqInfoContext.getReqInfo().getUserId(), new ActivityScoreBo().setRate(true).setArticleId(comment.getArticleId())); + break; + case COLLECT: + UserFootDO foot = (UserFootDO) msgEvent.getContent(); + userActivityRankService.addActivityScore(ReqInfoContext.getReqInfo().getUserId(), new ActivityScoreBo().setCollect(true).setArticleId(foot.getDocumentId())); + break; + case CANCEL_COLLECT: + foot = (UserFootDO) msgEvent.getContent(); + userActivityRankService.addActivityScore(ReqInfoContext.getReqInfo().getUserId(), new ActivityScoreBo().setCollect(false).setArticleId(foot.getDocumentId())); + break; + case PRAISE: + foot = (UserFootDO) msgEvent.getContent(); + userActivityRankService.addActivityScore(ReqInfoContext.getReqInfo().getUserId(), new ActivityScoreBo().setPraise(true).setArticleId(foot.getDocumentId())); + break; + case CANCEL_PRAISE: + foot = (UserFootDO) msgEvent.getContent(); + userActivityRankService.addActivityScore(ReqInfoContext.getReqInfo().getUserId(), new ActivityScoreBo().setPraise(false).setArticleId(foot.getDocumentId())); + break; + case FOLLOW: + UserRelationDO relation = (UserRelationDO) msgEvent.getContent(); + userActivityRankService.addActivityScore(ReqInfoContext.getReqInfo().getUserId(), new ActivityScoreBo().setFollow(true).setFollowedUserId(relation.getUserId())); + break; + case CANCEL_FOLLOW: + relation = (UserRelationDO) msgEvent.getContent(); + userActivityRankService.addActivityScore(ReqInfoContext.getReqInfo().getUserId(), new ActivityScoreBo().setFollow(false).setFollowedUserId(relation.getUserId())); + break; + default: + } + } + + /** + * 发布文章,更新对应的积分 + * + * @param event + */ + @Async + @EventListener(ArticleMsgEvent.class) + public void publishArticleListener(ArticleMsgEvent event) { + ArticleEventEnum type = event.getType(); + if (type == ArticleEventEnum.ONLINE) { + userActivityRankService.addActivityScore(ReqInfoContext.getReqInfo().getUserId(), new ActivityScoreBo().setPublishArticle(true).setArticleId(event.getContent().getId())); + } + } + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/rank/service/model/ActivityScoreBo.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/rank/service/model/ActivityScoreBo.java new file mode 100644 index 000000000..a4f7f1aa4 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/rank/service/model/ActivityScoreBo.java @@ -0,0 +1,52 @@ +package com.github.paicoding.forum.service.rank.service.model; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @author YiHui + * @date 2023/8/19 + */ +@Data +@Accessors(chain = true) +public class ActivityScoreBo { + /** + * 访问页面增加活跃度 + */ + private String path; + + /** + * 目标文章 + */ + private Long articleId; + + /** + * 评论增加活跃度 + */ + private Boolean rate; + + /** + * 点赞增加活跃度 + */ + private Boolean praise; + + /** + * 收藏增加活跃度 + */ + private Boolean collect; + + /** + * 发布文章增加活跃度 + */ + private Boolean publishArticle; + + /** + * 被关注的用户 + */ + private Long followedUserId; + + /** + * 关注增加活跃度 + */ + private Boolean follow; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/help/ShortCodeGenerator.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/help/ShortCodeGenerator.java new file mode 100644 index 000000000..f83c4f018 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/help/ShortCodeGenerator.java @@ -0,0 +1,62 @@ +package com.github.paicoding.forum.service.shortlink.help; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.TimeUnit; + +public class ShortCodeGenerator { + + private static final String BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + private static final int BASE62_LENGTH = BASE62.length(); + private static final int HASH_LENGTH = 5; + + + private static final Cache existingShortCodes = CacheBuilder.newBuilder() + .maximumSize(10000) + .expireAfterWrite(24, TimeUnit.HOURS) + .build(); + public static String generateShortCode(String longUrl) throws NoSuchAlgorithmException { + String shortCode = generateHash(longUrl); + final int MAX_ATTEMPTS = 10; + int attempts = 0; + while (existingShortCodes.getIfPresent(shortCode) != null) { + if (attempts >= MAX_ATTEMPTS) { + throw new RuntimeException("生成唯一短链接代码失败,已尝试 " + MAX_ATTEMPTS + " 次"); + } + shortCode = generateHash(longUrl + System.nanoTime()); + attempts++; + } + + existingShortCodes.put(shortCode, Boolean.TRUE); + return shortCode; + } + + private static String generateHash(String input) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] hash = md.digest(input.getBytes()); + StringBuilder hashString = new StringBuilder(); + + for (int i = 0; i < hash.length && hashString.length() < ShortCodeGenerator.HASH_LENGTH; i++) { + int index = (hash[i] & 0xFF) % BASE62_LENGTH; + hashString.append(BASE62.charAt(index)); + } + + return hashString.toString(); + } + + public static void main(String[] args) { + try { + String longUrl = "http://example.com"; + String shortCode = generateShortCode(longUrl); + System.out.println("The First Short code for " + longUrl + " is " + shortCode); + + String repeatShortCode = generateShortCode(longUrl); + System.out.println("The Second Short code for " + longUrl + " is " + repeatShortCode); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/help/SourceDetector.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/help/SourceDetector.java new file mode 100644 index 000000000..d5f3ad205 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/help/SourceDetector.java @@ -0,0 +1,89 @@ +package com.github.paicoding.forum.service.shortlink.help; + +import com.github.paicoding.forum.api.model.context.ReqInfoContext; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SourceDetector { + + private static final String MOBILE_PATTERN = "(Android|iPhone|iPad|iPod|Windows Phone|Mobile)"; + private static final String DESKTOP_PATTERN = "(Windows NT|Macintosh|Linux)"; + private static final String BOT_PATTERN = "(bot|spider|crawler|curl|wget)"; + + /** + * 根据 User-Agent 和 Referer 判断请求来源 + * + * @return 来源字符串 (WeChat, QQ, Mobile, Desktop, Bot, Unknown) + */ + public static String detectSource() { + String userAgent = ReqInfoContext.getReqInfo().getUserAgent(); + String referer = ReqInfoContext.getReqInfo().getReferer(); + + // 1. 优先判断 Referer (更可靠,但可能为空) + if (referer != null && !referer.isEmpty()) { + if (referer.contains("servicewechat.com") || referer.contains("weixin")) { + return "WeChat"; + } else if (referer.contains("qq.com") || referer.contains("mobile.qq.com") || referer.contains("connect.qq.com")) { + return "QQ"; + } + } + + // 2. 如果 Referer 无法判断,则根据 User-Agent 判断 + if (userAgent != null && !userAgent.isEmpty()) { + + // 2.1 微信 (User-Agent 中包含 MicroMessenger) + if (userAgent.contains("MicroMessenger")) { + return "WeChat"; + } + + // 2.2 QQ (User-Agent 中包含 QQ 或 MQQBrowser) + Pattern qqPattern = Pattern.compile("(QQ|MQQBrowser)", Pattern.CASE_INSENSITIVE); + Matcher qqMatcher = qqPattern.matcher(userAgent); + if (qqMatcher.find()) { + return "QQ"; + } + + // 2.3 浏览器判断 (常见浏览器 User-Agent 特征) + if (userAgent.contains("Edg")) { //Edge 浏览器需要在 Chrome 之前判断,因为Edge的UA字符串也包含 Chrome + return "Edge"; + } else if (userAgent.contains("Chrome")) { + return "Chrome"; + } else if (userAgent.contains("Safari") && !userAgent.contains("Chrome") && !userAgent.contains("Edg")) { // 排除 Chrome 和 Edge + return "Safari"; + } else if (userAgent.contains("Firefox")) { + return "Firefox"; + } else if (userAgent.contains("MSIE") || userAgent.contains("Trident")) { + return "IE"; // Internet Explorer + } else if (userAgent.contains("Opera") || userAgent.contains("OPR")) { + return "Opera"; + } + + // 2.3 移动设备 (常见移动设备 User-Agent 特征) + Pattern mobilePattern = Pattern.compile(MOBILE_PATTERN, Pattern.CASE_INSENSITIVE); // 增加更多移动设备 + Matcher mobileMatcher = mobilePattern.matcher(userAgent); + if (mobileMatcher.find()) { + return "Mobile"; + } + + + // 2.4 桌面设备 (常见桌面设备 User-Agent 特征) + Pattern desktopPattern = Pattern.compile(DESKTOP_PATTERN, Pattern.CASE_INSENSITIVE); + Matcher desktopMatcher = desktopPattern.matcher(userAgent); + if (desktopMatcher.find()) { + return "Desktop"; + } + + // 2.5 爬虫/机器人 (常见爬虫 User-Agent 特征) + Pattern botPattern = Pattern.compile(BOT_PATTERN, Pattern.CASE_INSENSITIVE); + Matcher botMatcher = botPattern.matcher(userAgent); + if (botMatcher.find()) { + return "Bot"; + } + + } + + // 3. 无法识别 + return "Unknown"; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/repository/entity/ShortLinkDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/repository/entity/ShortLinkDO.java new file mode 100644 index 000000000..3546dbb67 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/repository/entity/ShortLinkDO.java @@ -0,0 +1,47 @@ +package com.github.paicoding.forum.service.shortlink.repository.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 短链接数据库对象 + * + * @author betasecond + * @date 2025-02-13 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@TableName("short_link") +public class ShortLinkDO extends BaseDO { + + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId + private Long id; + + /** + * 原始URL + */ + private String originalUrl; + + /** + * 短链接代码 + */ + private String shortCode; + + /** + * 删除标记 + */ + private Integer deleted; + + +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/repository/entity/ShortLinkRecordDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/repository/entity/ShortLinkRecordDO.java new file mode 100644 index 000000000..4fda16b08 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/repository/entity/ShortLinkRecordDO.java @@ -0,0 +1,52 @@ +package com.github.paicoding.forum.service.shortlink.repository.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 短链接记录数据库对象 + * + * @author betasecond + * @date 2025-02-13 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@TableName("short_link_record") +public class ShortLinkRecordDO extends BaseDO { + + private static final long serialVersionUID = 1L; + + /** + * 短链接代码 + */ + private String shortCode; + + /** + * 用户ID + */ + private String userId; + + /** + * 访问时间 + */ + private Long accessTime; + + /** + * IP地址 + */ + private String ipAddress; + + /** + * 登录方式 (如:微信、QQ、微博等)。 + */ + private String loginMethod; + + /** + * 访问来源(如:网页、移动端等)。 + */ + private String accessSource; +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/repository/mapper/ShortLinkMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/repository/mapper/ShortLinkMapper.java new file mode 100644 index 000000000..d625760f9 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/repository/mapper/ShortLinkMapper.java @@ -0,0 +1,17 @@ +package com.github.paicoding.forum.service.shortlink.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.service.shortlink.repository.entity.ShortLinkDO; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Options; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +public interface ShortLinkMapper extends BaseMapper { + @Select("SELECT * FROM short_link WHERE short_code = #{shortCode} LIMIT 1") + ShortLinkDO getByShortCode(@Param("shortCode") String shortCode); + + @Insert("INSERT INTO short_link (original_url, short_code, deleted, create_time, update_time) VALUES (#{originalUrl}, #{shortCode}, #{deleted}, #{createTime}, #{updateTime})") + @Options(useGeneratedKeys = true, keyProperty = "id") + int getIdAfterInsert(ShortLinkDO shortLinkDO); +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/repository/mapper/ShortLinkRecordMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/repository/mapper/ShortLinkRecordMapper.java new file mode 100644 index 000000000..492fd40cb --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/repository/mapper/ShortLinkRecordMapper.java @@ -0,0 +1,7 @@ +package com.github.paicoding.forum.service.shortlink.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.service.shortlink.repository.entity.ShortLinkRecordDO; + +public interface ShortLinkRecordMapper extends BaseMapper { +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/service/ShortLinkService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/service/ShortLinkService.java new file mode 100644 index 000000000..a97ae3382 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/service/ShortLinkService.java @@ -0,0 +1,27 @@ +package com.github.paicoding.forum.service.shortlink.service; + +import com.github.paicoding.forum.api.model.vo.shortlink.dto.ShortLinkDTO; +import com.github.paicoding.forum.api.model.vo.shortlink.ShortLinkVO; + +import java.security.NoSuchAlgorithmException; + +public interface ShortLinkService { + + + /** + * 创建短链接 + * + * @param shortLinkDTO 包含原始URL和用户信息的数据传输对象 + * @return 包含短链接和原始URL的ShortLinkVO对象 + * @throws NoSuchAlgorithmException 如果生成短码时发生错误 + */ + ShortLinkVO createShortLink(ShortLinkDTO shortLinkDTO) throws NoSuchAlgorithmException; + + /** + * 获取原始URL + * + * @param shortCode 短码 + * @return 包含原始URL的ShortLinkVO对象 + */ + ShortLinkVO getOriginalLink(String shortCode); +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/service/impl/ShortLinkServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/service/impl/ShortLinkServiceImpl.java new file mode 100644 index 000000000..a6a2d4384 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/shortlink/service/impl/ShortLinkServiceImpl.java @@ -0,0 +1,248 @@ +package com.github.paicoding.forum.service.shortlink.service.impl; + +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.service.shortlink.repository.entity.ShortLinkDO; +import com.github.paicoding.forum.api.model.vo.shortlink.dto.ShortLinkDTO; +import com.github.paicoding.forum.service.shortlink.repository.entity.ShortLinkRecordDO; +import com.github.paicoding.forum.api.model.vo.shortlink.ShortLinkVO; +import com.github.paicoding.forum.core.cache.RedisClient; +import com.github.paicoding.forum.service.shortlink.help.ShortCodeGenerator; +import com.github.paicoding.forum.service.shortlink.help.SourceDetector; +import com.github.paicoding.forum.service.shortlink.repository.mapper.ShortLinkMapper; +import com.github.paicoding.forum.service.shortlink.repository.mapper.ShortLinkRecordMapper; +import com.github.paicoding.forum.service.shortlink.service.ShortLinkService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import javax.annotation.Resource; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.NoSuchAlgorithmException; +import java.util.Date; +import java.util.List; + +@Slf4j +@Service +public class ShortLinkServiceImpl implements ShortLinkService { + + + // Redis中短链接的前缀 + private static final String REDIS_SHORT_LINK_PREFIX = "short_link:"; + + @Resource + private ShortLinkMapper shortLinkMapper; + + @Resource + private ShortLinkRecordMapper shortLinkRecordMapper; + + @Value("${view.site.host:https://paicoding.com}") + private String host; + + public ShortLinkServiceImpl(ShortLinkMapper shortLinkMapper, ShortLinkRecordMapper shortLinkRecordMapper) { + this.shortLinkMapper = shortLinkMapper; + this.shortLinkRecordMapper = shortLinkRecordMapper; + } + + + // 域名白名单 + @Value("#{'${short-link.whitelist:}'.split(',')}") + private List domainWhitelist; + + + /** + * 创建短链接 + * + * @param shortLinkDTO 包含原始URL和用户信息的数据传输对象 + * @return 包含短链接和原始URL的ShortLinkVO对象 + * @throws NoSuchAlgorithmException 如果生成短码时发生错误 + */ + @Override + public ShortLinkVO createShortLink(ShortLinkDTO shortLinkDTO) throws NoSuchAlgorithmException { + if (log.isDebugEnabled()) { + log.debug("Creating short link for URL: {}", shortLinkDTO.getOriginalUrl()); + } + + // 验证域名是否在白名单中 + if (!isUrlInWhitelist(shortLinkDTO.getOriginalUrl())) { + log.warn("域名不在白名单中: {}", shortLinkDTO.getOriginalUrl()); + throw new RuntimeException("不允许为该域名创建短链接"); + } + + // 从原始URL中提取路径部分 + // ^(https?://|http://[^/]+) - 匹配URL开头的协议和域名部分 + String path = shortLinkDTO.getOriginalUrl().replaceAll("^(https?://|http://[^/]+)(/.*)?$", "$2"); + + String shortCode = generateUniqueShortCode(path); + + ShortLinkDO shortLinkDO = createShortLinkDO(shortLinkDTO, shortCode); + + // 保存原始链接--短链接映射到DB与Cache + int shortLinkId = shortLinkMapper.getIdAfterInsert(shortLinkDO); + if (log.isDebugEnabled()) { + log.debug("Short link created with ID: {}", shortLinkId); + } + RedisClient.hSet(REDIS_SHORT_LINK_PREFIX + shortCode, shortLinkDO.getOriginalUrl(), String.class); + + // 保存记录到DB + ShortLinkRecordDO shortLinkRecordDO = createShortLinkRecordDO(shortLinkDO.getShortCode(), shortLinkDTO); + shortLinkRecordMapper.insert(shortLinkRecordDO); + + if (log.isDebugEnabled()) { + log.debug("Short link record saved for short code: {}", shortCode); + } + return createShortLinkVO(shortLinkDO); + } + + + /** + * 获取原始URL + * + * @param shortCode 短码 + * @return 包含原始URL的ShortLinkVO对象 + */ + @Override + public ShortLinkVO getOriginalLink(String shortCode) { + if (log.isDebugEnabled()) { + log.debug("Fetching original link for short code: {}", shortCode); + } + + String originalUrl = getOriginalUrlFromCacheOrDb(shortCode); + + if (!StringUtils.hasText(originalUrl)) { + log.error("Short link not found for short code: {}", shortCode); + throw new RuntimeException("Short link not found"); + } + String paramUserId = ((null == ReqInfoContext.getReqInfo().getUserId()) ? "0" : ReqInfoContext.getReqInfo().getUserId().toString()); + log.info("Short link retrieved - shortCode: {}, originalUrl: {}, userId: {}", shortCode, originalUrl, paramUserId); + return new ShortLinkVO(originalUrl, originalUrl); + } + + /** + * 将ShortLinkDO对象转换为ShortLinkVO对象 + * + * @param shortLinkDO ShortLinkDO对象 + * @return ShortLinkVO对象 + */ + private ShortLinkVO createShortLinkVO(ShortLinkDO shortLinkDO) { + ShortLinkVO shortLinkVO = new ShortLinkVO(); + shortLinkVO.setShortUrl(host + "/sol/" + shortLinkDO.getShortCode()); + shortLinkVO.setOriginalUrl(shortLinkDO.getOriginalUrl()); + return shortLinkVO; + } + + + /** + * 生成唯一的短码 + * + * @param path URL路径 + * @return 短码 + * @throws NoSuchAlgorithmException 如果生成短码时发生错误 + */ + private String generateUniqueShortCode(String path) throws NoSuchAlgorithmException { + long generateTime = 0; + String shortCode; + do { + shortCode = ShortCodeGenerator.generateShortCode(path); + generateTime++; + } while (null != shortLinkMapper.getByShortCode(shortCode) && generateTime < 3); + return shortCode; + } + + + /** + * 创建ShortLinkDO对象 + * + * @param shortLinkDTO 短链接数据 + * @param shortCode 生成的短码 + * @return 创建的ShortLinkDO对象 + */ + private ShortLinkDO createShortLinkDO(ShortLinkDTO shortLinkDTO, String shortCode) { + long currentTimeMillis = System.currentTimeMillis(); + Date currentDate = new Date(currentTimeMillis); + ShortLinkDO shortLinkDO = new ShortLinkDO(); + shortLinkDO.setOriginalUrl(shortLinkDTO.getOriginalUrl()); + shortLinkDO.setShortCode(shortCode); + shortLinkDO.setCreateTime(currentDate); + shortLinkDO.setUpdateTime(currentDate); + shortLinkDO.setDeleted(0); + return shortLinkDO; + } + + + /** + * 创建ShortLinkRecordDO对象 + * + * @param shortcode 短链接代码 + * @param shortLinkDTO 短链接数据 + * @return 创建的ShortLinkRecordDO对象 + */ + private ShortLinkRecordDO createShortLinkRecordDO(String shortcode, ShortLinkDTO shortLinkDTO) { + ShortLinkRecordDO shortLinkRecordDO = new ShortLinkRecordDO(); + shortLinkRecordDO.setShortCode(shortcode); + shortLinkRecordDO.setUserId(shortLinkDTO.getUserId()); + shortLinkRecordDO.setAccessTime(System.currentTimeMillis()); + // fixme: 目前没有很好的办法获得用户的登陆方式 因为用户都不一定登录了 + shortLinkRecordDO.setLoginMethod("Unknown"); + shortLinkRecordDO.setIpAddress(ReqInfoContext.getReqInfo().getClientIp()); + shortLinkRecordDO.setAccessSource(SourceDetector.detectSource()); + return shortLinkRecordDO; + } + + + /** + * 从Redis缓存或数据库中获取原始URL + * + * @param shortCode 短码 + * @return 原始URL + */ + private String getOriginalUrlFromCacheOrDb(String shortCode) { + String originalUrl = RedisClient.hGet(REDIS_SHORT_LINK_PREFIX + shortCode, "originalUrl", String.class); + if (!StringUtils.hasText(originalUrl)) { + ShortLinkDO shortLinkDO = shortLinkMapper.getByShortCode(shortCode); + if (shortLinkDO != null) { + originalUrl = shortLinkDO.getOriginalUrl(); + } + } + return originalUrl; + } + + /** + * 检查URL是否在白名单中 + * + * @param url 待检查的URL + * @return 是否在白名单中 + */ + private boolean isUrlInWhitelist(String url) { + if (domainWhitelist == null || domainWhitelist.isEmpty()) { + return true; // 如果白名单为空,则允许所有域名 + } + + try { + URI uri = new URI(url); + String hostRaw = uri.getHost(); + if (hostRaw == null) { + log.error("无效的URL格式,缺少host: {}", url); + return false; + } + + // 去掉URL中的协议部分 + String host = hostRaw.replaceAll("^[a-zA-Z]+://", ""); + String hostWithPort = host + (uri.getPort() != -1 ? ":" + uri.getPort() : ""); + + return domainWhitelist.stream() + // 白名单中无协议名,只有域名 + .map(String::trim) + // 检查域名是否在白名单中 + // 精确匹配域名,避免子域名攻击 + .anyMatch(domain -> + hostWithPort.equals(domain) || // 带端口 + host.equals(domain) // 不带端口 + ); + } catch (URISyntaxException e) { + log.error("无效的URL格式: {}", url, e); + return false; + } + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/sidebar/service/SidebarService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/sidebar/service/SidebarService.java new file mode 100644 index 000000000..f260c5225 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/sidebar/service/SidebarService.java @@ -0,0 +1,36 @@ +package com.github.paicoding.forum.service.sidebar.service; + +import com.github.paicoding.forum.api.model.vo.recommend.SideBarDTO; + +import java.util.List; + +/** + * @author YiHui + * @date 2022/9/6 + */ +public interface SidebarService { + + /** + * 查询首页的侧边栏信息 + * + * @return + */ + List queryHomeSidebarList(); + + /** + * 查询教程的侧边栏信息 + * + * @return + */ + List queryColumnSidebarList(); + + /** + * 查询文章详情的侧边栏信息 + * + * @param author 文章作者id + * @param articleId 文章id + * @return + */ + List queryArticleDetailSidebarList(Long author, Long articleId); + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/sidebar/service/SidebarServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/sidebar/service/SidebarServiceImpl.java new file mode 100644 index 000000000..d32f3eb07 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/sidebar/service/SidebarServiceImpl.java @@ -0,0 +1,252 @@ +package com.github.paicoding.forum.service.sidebar.service; + +import com.github.paicoding.forum.api.model.enums.ConfigTypeEnum; +import com.github.paicoding.forum.api.model.enums.SidebarStyleEnum; +import com.github.paicoding.forum.api.model.enums.rank.ActivityRankTimeEnum; +import com.github.paicoding.forum.api.model.vo.PageListVo; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.article.dto.SimpleArticleDTO; +import com.github.paicoding.forum.api.model.vo.banner.dto.ConfigDTO; +import com.github.paicoding.forum.api.model.vo.rank.dto.RankItemDTO; +import com.github.paicoding.forum.api.model.vo.recommend.RateVisitDTO; +import com.github.paicoding.forum.api.model.vo.recommend.SideBarDTO; +import com.github.paicoding.forum.api.model.vo.recommend.SideBarItemDTO; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.service.article.repository.dao.ArticleDao; +import com.github.paicoding.forum.service.article.service.ArticleReadService; +import com.github.paicoding.forum.service.config.service.ConfigService; +import com.github.paicoding.forum.service.rank.service.UserActivityRankService; +import com.google.common.base.Splitter; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author YiHui + * @date 2022/9/6 + */ +@Service +public class SidebarServiceImpl implements SidebarService { + + @Autowired + private ArticleReadService articleReadService; + + @Autowired + private ConfigService configService; + + @Autowired + private ArticleDao articleDao; + + /** + * 使用caffeine本地缓存,来处理侧边栏不怎么变动的消息 + *

+ * cacheNames -> 类似缓存前缀的概念 + * key -> SpEL 表达式,可以从传参中获取,来构建缓存的key + * cacheManager -> 缓存管理器,如果全局只有一个时,可以省略 + * + * @return + */ + @Override + @Cacheable(key = "'homeSidebar'", cacheManager = "caffeineCacheManager", cacheNames = "home") + public List queryHomeSidebarList() { + List list = new ArrayList<>(); + list.add(noticeSideBar()); + list.add(columnSideBar()); + list.add(hotArticles()); + SideBarDTO bar = rankList(); + if (bar != null) { + list.add(bar); + } + return list; + } + + /** + * 公告信息 + * + * @return + */ + private SideBarDTO noticeSideBar() { + List noticeList = configService.getConfigList(ConfigTypeEnum.NOTICE); + List items = new ArrayList<>(noticeList.size()); + noticeList.forEach(configDTO -> { + List configTags; + if (StringUtils.isBlank(configDTO.getTags())) { + configTags = Collections.emptyList(); + } else { + configTags = Splitter.on(",").splitToStream(configDTO.getTags()).map(s -> Integer.parseInt(s.trim())).collect(Collectors.toList()); + } + items.add(new SideBarItemDTO() + .setName(configDTO.getName()) + .setTitle(configDTO.getContent()) + .setUrl(configDTO.getJumpUrl()) + .setTime(configDTO.getCreateTime().getTime()) + .setTags(configTags) + ); + }); + return new SideBarDTO() + .setTitle("关于技术派") + // TODO 知识星球的 + .setImg("https://cdn.tobebetterjavaer.com/paicoding/main/paicoding-zsxq.jpg") + .setUrl("https://paicoding.com/article/detail/2422000009961473") + .setItems(items) + .setStyle(SidebarStyleEnum.NOTICE.getStyle()); + } + + + /** + * 推荐教程的侧边栏 + * + * @return + */ + private SideBarDTO columnSideBar() { + List columnList = configService.getConfigList(ConfigTypeEnum.COLUMN); + List items = new ArrayList<>(columnList.size()); + columnList.forEach(configDTO -> { + SideBarItemDTO item = new SideBarItemDTO(); + item.setName(configDTO.getName()); + item.setTitle(configDTO.getContent()); + item.setUrl(configDTO.getJumpUrl()); + item.setImg(configDTO.getBannerUrl()); + items.add(item); + }); + return new SideBarDTO().setTitle("精选教程").setItems(items).setStyle(SidebarStyleEnum.COLUMN.getStyle()); + } + + + /** + * 热门文章 + * + * @return + */ + private SideBarDTO hotArticles() { + PageListVo vo = articleReadService.queryHotArticlesForRecommend(PageParam.newPageInstance(1, 8)); + List items = vo.getList().stream().map(s -> new SideBarItemDTO().setTitle(s.getTitle()).setUrl("/article/detail/" + s.getId()).setTime(s.getCreateTime().getTime())).collect(Collectors.toList()); + return new SideBarDTO().setTitle("热门文章").setItems(items).setStyle(SidebarStyleEnum.ARTICLES.getStyle()); + } + + + /** + * 以用户 + 文章维度进行缓存设置 + * + * @param author 文章作者id + * @param articleId 文章id + * @return + */ + @Override + @Cacheable(key = "'sideBar_' + #articleId", cacheManager = "caffeineCacheManager", cacheNames = "article") + public List queryArticleDetailSidebarList(Long author, Long articleId) { + List list = new ArrayList<>(2); + // 不能直接使用 pdfSideBar()的方式调用,会导致缓存不生效 + list.add(SpringUtil.getBean(SidebarServiceImpl.class).pdfSideBar()); + list.add(recommendByAuthor(author, articleId, PageParam.DEFAULT_PAGE_SIZE)); + return list; + } + + /** + * PDF 优质资源 + * + * @return + */ + @Cacheable(key = "'sideBar'", cacheManager = "caffeineCacheManager", cacheNames = "article") + public SideBarDTO pdfSideBar() { + List pdfList = configService.getConfigList(ConfigTypeEnum.PDF); + List items = new ArrayList<>(pdfList.size()); + pdfList.forEach(configDTO -> { + SideBarItemDTO dto = new SideBarItemDTO(); + dto.setName(configDTO.getName()); + dto.setUrl(configDTO.getJumpUrl()); + dto.setImg(configDTO.getBannerUrl()); + RateVisitDTO visit; + if (StringUtils.isNotBlank(configDTO.getExtra())) { + visit = (JsonUtil.toObj(configDTO.getExtra(), RateVisitDTO.class)); + } else { + visit = new RateVisitDTO(); + } + visit.incrVisit(); + // 更新阅读计数 + configService.updateVisit(configDTO.getId(), JsonUtil.toStr(visit)); + dto.setVisit(visit); + items.add(dto); + }); + return new SideBarDTO().setTitle("优质PDF").setItems(items).setStyle(SidebarStyleEnum.PDF.getStyle()); + } + + + /** + * 作者的文章列表推荐 + * + * @param authorId + * @param size + * @return + */ + public SideBarDTO recommendByAuthor(Long authorId, Long articleId, long size) { + List list = articleDao.listAuthorHotArticles(authorId, PageParam.newPageInstance(PageParam.DEFAULT_PAGE_NUM, size)); + List items = list.stream().filter(s -> !s.getId().equals(articleId)) + .map(s -> new SideBarItemDTO() + .setTitle(s.getTitle()).setUrl("/article/detail/" + s.getId()) + .setTime(s.getCreateTime().getTime())) + .collect(Collectors.toList()); + return new SideBarDTO().setTitle("相关文章").setItems(items).setStyle(SidebarStyleEnum.ARTICLES.getStyle()); + } + + + /** + * 查询教程的侧边栏信息 + * + * @return + */ + @Override + @Cacheable(key = "'columnSidebar'", cacheManager = "caffeineCacheManager", cacheNames = "column") + public List queryColumnSidebarList() { + List list = new ArrayList<>(); + list.add(subscribeSideBar()); + return list; + } + + + /** + * 订阅公众号 + * + * @return + */ + private SideBarDTO subscribeSideBar() { + return new SideBarDTO().setTitle("订阅").setSubTitle("楼仔") + .setImg("//cdn.tobebetterjavaer.com/paicoding/a768cfc54f59d4a056f79d1c959dcae9.jpg") + .setContent("10本校招必刷八股文") + .setStyle(SidebarStyleEnum.SUBSCRIBE.getStyle()); + } + + + @Autowired + private UserActivityRankService userActivityRankService; + + /** + * 排行榜 + * + * @return + */ + private SideBarDTO rankList() { + List list = userActivityRankService.queryRankList(ActivityRankTimeEnum.MONTH, 8); + if (list.isEmpty()) { + return null; + } + SideBarDTO sidebar = new SideBarDTO().setTitle("月度活跃排行榜").setStyle(SidebarStyleEnum.ACTIVITY_RANK.getStyle()); + List itemList = new ArrayList<>(); + for (RankItemDTO item : list) { + SideBarItemDTO sideItem = new SideBarItemDTO().setName(item.getUser().getName()) + .setUrl(String.valueOf(item.getUser().getUserId())) + .setImg(item.getUser().getAvatar()) + .setTime(item.getScore().longValue()); + itemList.add(sideItem); + } + sidebar.setItems(itemList); + return sidebar; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/constants/SitemapConstants.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/constants/SitemapConstants.java new file mode 100644 index 000000000..3d5b0fc5d --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/constants/SitemapConstants.java @@ -0,0 +1,18 @@ +package com.github.paicoding.forum.service.sitemap.constants; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * 站点相关地图 + * + * @author YiHui + * @date 2023/8/22 + */ +public class SitemapConstants { + public static final String SITE_VISIT_KEY = "visit_info"; + + public static String day(LocalDate day) { + return DateTimeFormatter.ofPattern("yyyyMMdd").format(day); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/model/SiteCntVo.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/model/SiteCntVo.java new file mode 100644 index 000000000..56e16aa0a --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/model/SiteCntVo.java @@ -0,0 +1,32 @@ +package com.github.paicoding.forum.service.sitemap.model; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 站点计数 + * + * @author YiHui + * @date 2023/8/22 + */ +@Data +public class SiteCntVo implements Serializable { + private static final long serialVersionUID = 8747459624770066661L; + /** + * 日期 + */ + private String day; + /** + * 路径,全站时,path为null + */ + private String path; + /** + * 站点page view 点击数 + */ + private Integer pv; + /** + * 站点unique view 点击数 + */ + private Integer uv; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/model/SiteMapVo.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/model/SiteMapVo.java new file mode 100644 index 000000000..2b1adc37e --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/model/SiteMapVo.java @@ -0,0 +1,45 @@ +package com.github.paicoding.forum.service.sitemap.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author YiHui + * @date 2023/2/13 + */ +@Data +@JacksonXmlRootElement(localName = "urlset", namespace = "http://www.sitemaps.org/schemas/sitemap/0.9") +public class SiteMapVo { + + @JacksonXmlProperty(isAttribute = true, localName = "xmlns:news") + private String xmlnsNews = "http://www.google.com/schemas/sitemap-news/0.9"; + + @JacksonXmlProperty(isAttribute = true, localName = "xmlns:xhtml") + private String xmlnsXhtml = "http://www.w3.org/1999/xhtml"; + + @JacksonXmlProperty(isAttribute = true, localName = "xmlns:image") + private String xmlnsImage = "http://www.google.com/schemas/sitemap-image/1.1"; + + @JacksonXmlProperty(isAttribute = true, localName = "xmlns:video") + private String xmlnsVideo = "http://www.google.com/schemas/sitemap-video/1.1"; + + /** + * 将列表数据转为XML节点, useWrapping = false 表示不要外围标签名 + */ + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "url") + private List url; + + public SiteMapVo() { + url = new ArrayList<>(); + } + + public void addUrl(SiteUrlVo xmlUrl) { + url.add(xmlUrl); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/model/SiteUrlVo.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/model/SiteUrlVo.java new file mode 100644 index 000000000..157cbd8b7 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/model/SiteUrlVo.java @@ -0,0 +1,25 @@ +package com.github.paicoding.forum.service.sitemap.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author YiHui + * @date 2023/2/13 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@JacksonXmlRootElement(localName = "url") +public class SiteUrlVo { + + @JacksonXmlProperty(localName = "loc") + private String loc; + + @JacksonXmlProperty(localName = "lastmod") + private String lastMod; + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/service/SitemapService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/service/SitemapService.java new file mode 100644 index 000000000..c540e75af --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/service/SitemapService.java @@ -0,0 +1,47 @@ +package com.github.paicoding.forum.service.sitemap.service; + +import com.github.paicoding.forum.service.sitemap.model.SiteCntVo; +import com.github.paicoding.forum.service.sitemap.model.SiteMapVo; + +import java.time.LocalDate; + +/** + * 站点统计相关服务: + * - 站点地图 + * - pv/uv + * + * @author YiHui + * @date 2023/2/13 + */ +public interface SitemapService { + + /** + * 查询站点地图 + * + * @return + */ + SiteMapVo getSiteMap(); + + /** + * 刷新站点地图 + */ + void refreshSitemap(); + + /** + * 保存用户访问信息 + * + * @param visitIp 访问者ip + * @param path 访问的资源路径 + */ + void saveVisitInfo(String visitIp, String path); + + + /** + * 查询站点某一天or总的访问信息 + * + * @param date 日期,为空时,表示查询所有的站点信息 + * @param path 访问路径,为空时表示查站点信息 + * @return + */ + SiteCntVo querySiteVisitInfo(LocalDate date, String path); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/service/impl/SitemapServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/service/impl/SitemapServiceImpl.java new file mode 100644 index 000000000..f252a0e4a --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/sitemap/service/impl/SitemapServiceImpl.java @@ -0,0 +1,269 @@ +package com.github.paicoding.forum.service.sitemap.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.github.paicoding.forum.api.model.enums.ArticleEventEnum; +import com.github.paicoding.forum.api.model.event.ArticleMsgEvent; +import com.github.paicoding.forum.api.model.vo.article.dto.SimpleArticleDTO; +import com.github.paicoding.forum.core.cache.RedisClient; +import com.github.paicoding.forum.core.util.DateUtil; +import com.github.paicoding.forum.service.article.repository.dao.ArticleDao; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDO; +import com.github.paicoding.forum.service.sitemap.constants.SitemapConstants; +import com.github.paicoding.forum.service.sitemap.model.SiteCntVo; +import com.github.paicoding.forum.service.sitemap.model.SiteMapVo; +import com.github.paicoding.forum.service.sitemap.model.SiteUrlVo; +import com.github.paicoding.forum.service.sitemap.service.SitemapService; +import com.github.paicoding.forum.service.statistics.service.CountService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author YiHui + * @date 2023/2/13 + */ +@Slf4j +@Service +public class SitemapServiceImpl implements SitemapService { + @Value("${view.site.host:https://paicoding.com}") + private String host; + private static final int SCAN_SIZE = 100; + + private static final String SITE_MAP_CACHE_KEY = "sitemap"; + + @Resource + private ArticleDao articleDao; + @Resource + private CountService countService; + + /** + * 查询站点地图 + * @return 返回站点地图 + */ + public SiteMapVo getSiteMap() { + // key = 文章id, value = 最后更新时间 + Map siteMap = RedisClient.hGetAll(SITE_MAP_CACHE_KEY, Long.class); + if (CollectionUtils.isEmpty(siteMap)) { + // 首次访问时,没有数据,全量初始化 + initSiteMap(); + } + siteMap = RedisClient.hGetAll(SITE_MAP_CACHE_KEY, Long.class); + SiteMapVo vo = initBasicSite(); + if (CollectionUtils.isEmpty(siteMap)) { + return vo; + } + + for (Map.Entry entry : siteMap.entrySet()) { + vo.addUrl(new SiteUrlVo(host + "/article/detail/" + entry.getKey(), DateUtil.time2utc(entry.getValue()))); + } + return vo; + } + + /** + * fixme: 加锁初始化,更推荐的是采用分布式锁 + */ + private synchronized void initSiteMap() { + long lastId = 0L; + RedisClient.del(SITE_MAP_CACHE_KEY); + while (true) { + List list = articleDao.getBaseMapper().listArticlesOrderById(lastId, SCAN_SIZE); + // 刷新文章的统计信息 + list.forEach(s -> countService.refreshArticleStatisticInfo(s.getId())); + + // 刷新站点地图信息 + Map map = list.stream().collect(Collectors.toMap(s -> String.valueOf(s.getId()), s -> s.getCreateTime().getTime(), (a, b) -> a)); + RedisClient.hMSet(SITE_MAP_CACHE_KEY, map); + if (list.size() < SCAN_SIZE) { + break; + } + lastId = list.get(list.size() - 1).getId(); + } + } + + private SiteMapVo initBasicSite() { + SiteMapVo vo = new SiteMapVo(); + String time = DateUtil.time2utc(System.currentTimeMillis()); + vo.addUrl(new SiteUrlVo(host + "/", time)); + vo.addUrl(new SiteUrlVo(host + "/column", time)); + vo.addUrl(new SiteUrlVo(host + "/admin-view", time)); + return vo; + } + + /** + * 重新刷新站点地图 + */ + @Override + public void refreshSitemap() { + initSiteMap(); + } + + /** + * 基于文章的上下线,自动更新站点地图 + * + * @param event + */ + @EventListener(ArticleMsgEvent.class) + public void autoUpdateSiteMap(ArticleMsgEvent event) { + ArticleEventEnum type = event.getType(); + if (type == ArticleEventEnum.ONLINE) { + addArticle(event.getContent().getId()); + } else if (type == ArticleEventEnum.OFFLINE || type == ArticleEventEnum.DELETE) { + rmArticle(event.getContent().getId()); + } + } + + /** + * 新增文章并上线 + * + * @param articleId + */ + private void addArticle(Long articleId) { + RedisClient.hSet(SITE_MAP_CACHE_KEY, String.valueOf(articleId), System.currentTimeMillis()); + } + + /** + * 删除文章、or文章下线 + * + * @param articleId + */ + private void rmArticle(Long articleId) { + RedisClient.hDel(SITE_MAP_CACHE_KEY, String.valueOf(articleId)); + } + + + /** + * 采用定时器方案,每天5:15分刷新站点地图,确保数据的一致性 + */ + @Scheduled(cron = "0 15 5 * * ?") + public void autoRefreshCache() { + log.info("开始刷新sitemap.xml的url地址,避免出现数据不一致问题!"); + refreshSitemap(); + log.info("刷新完成!"); + } + + + /** + * 保存站点数据模型 + *

+ * 站点统计hash: + * - visit_info: + * ---- pv: 站点的总pv + * ---- uv: 站点的总uv + * ---- pv_path: 站点某个资源的总访问pv + * ---- uv_path: 站点某个资源的总访问uv + * - visit_info_ip: + * ---- pv: 用户访问的站点总次数 + * ---- path_pv: 用户访问的路径总次数 + * - visit_info_20230822每日记录, 一天一条记录 + * ---- pv: 12 # field = 月日_pv, pv的计数 + * ---- uv: 5 # field = 月日_uv, uv的计数 + * ---- pv_path: 2 # 资源的当前访问计数 + * ---- uv_path: # 资源的当天访问uv + * ---- pv_ip: # 用户当天的访问次数 + * ---- pv_path_ip: # 用户对资源的当天访问次数 + * + * @param visitIp 访问者ip + * @param path 访问的资源路径 + */ + @Override + public void saveVisitInfo(String visitIp, String path) { + String globalKey = SitemapConstants.SITE_VISIT_KEY; + String day = SitemapConstants.day(LocalDate.now()); + + String todayKey = globalKey + "_" + day; + + // 用户的全局访问计数+1 + Long globalUserVisitCnt = RedisClient.hIncr(globalKey + "_" + visitIp, "pv", 1); + // 用户的当日访问计数+1 + Long todayUserVisitCnt = RedisClient.hIncr(todayKey, "pv_" + visitIp, 1); + + RedisClient.PipelineAction pipelineAction = RedisClient.pipelineAction(); + if (globalUserVisitCnt == 1) { + // 站点新用户 + // 今日的uv + 1 + pipelineAction.add(todayKey, "uv" + , (connection, key, field) -> { + connection.hIncrBy(key, field, 1); + }); + pipelineAction.add(todayKey, "uv_" + path + , (connection, key, field) -> connection.hIncrBy(key, field, 1)); + + // 全局站点的uv + pipelineAction.add(globalKey, "uv", (connection, key, field) -> connection.hIncrBy(key, field, 1)); + pipelineAction.add(globalKey, "uv_" + path, (connection, key, field) -> connection.hIncrBy(key, field, 1)); + } else if (todayUserVisitCnt == 1) { + // 判断是今天的首次访问,更新今天的uv+1 + pipelineAction.add(todayKey, "uv", (connection, key, field) -> connection.hIncrBy(key, field, 1)); + if (RedisClient.hIncr(todayKey, "pv_" + path + "_" + visitIp, 1) == 1) { + // 判断是否为今天首次访问这个资源,若是,则uv+1 + pipelineAction.add(todayKey, "uv_" + path, (connection, key, field) -> connection.hIncrBy(key, field, 1)); + } + + // 判断是否是用户的首次访问这个path,若是,则全局的path uv计数需要+1 + if (RedisClient.hIncr(globalKey + "_" + visitIp, "pv_" + path, 1) == 1) { + pipelineAction.add(globalKey, "uv_" + path, (connection, key, field) -> connection.hIncrBy(key, field, 1)); + } + } + + + // 更新pv 以及 用户的path访问信息 + // 今天的相关信息 pv + pipelineAction.add(todayKey, "pv", (connection, key, field) -> connection.hIncrBy(key, field, 1)); + pipelineAction.add(todayKey, "pv_" + path, (connection, key, field) -> connection.hIncrBy(key, field, 1)); + if (todayUserVisitCnt > 1) { + // 非当天首次访问,则pv+1; 因为首次访问时,在前面更新uv时,已经计数+1了 + pipelineAction.add(todayKey, "pv_" + path + "_" + visitIp, (connection, key, field) -> connection.hIncrBy(key, field, 1)); + } + + + // 全局的 PV + pipelineAction.add(globalKey, "pv", (connection, key, field) -> connection.hIncrBy(key, field, 1)); + pipelineAction.add(globalKey, "pv" + "_" + path, (connection, key, field) -> connection.hIncrBy(key, field, 1)); + + // 保存访问信息 + pipelineAction.execute(); + if (log.isDebugEnabled()) { + log.info("用户访问信息更新完成! 当前用户总访问: {},今日访问: {}", globalUserVisitCnt, todayUserVisitCnt); + } + } + + /** + * 查询站点某一天or总的访问信息 + * + * @param date 日期,为空时,表示查询所有的站点信息 + * @param path 访问路径,为空时表示查站点信息 + * @return + */ + @Override + public SiteCntVo querySiteVisitInfo(LocalDate date, String path) { + String globalKey = SitemapConstants.SITE_VISIT_KEY; + String day = null, todayKey = globalKey; + if (date != null) { + day = SitemapConstants.day(date); + todayKey = globalKey + "_" + day; + } + + String pvField = "pv", uvField = "uv"; + if (path != null) { + // 表示查询对应路径的访问信息 + pvField += "_" + path; + uvField += "_" + path; + } + + Map map = RedisClient.hMGet(todayKey, Arrays.asList(pvField, uvField), Integer.class); + SiteCntVo siteInfo = new SiteCntVo(); + siteInfo.setDay(day); + siteInfo.setPv(map.getOrDefault(pvField, 0)); + siteInfo.setUv(map.getOrDefault(uvField, 0)); + return siteInfo; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/constants/CountConstants.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/constants/CountConstants.java new file mode 100644 index 000000000..2801db1e9 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/constants/CountConstants.java @@ -0,0 +1,53 @@ +package com.github.paicoding.forum.service.statistics.constants; + +/** + * 用户相关的常量信息 + * + * @author YiHui + * @date 2023/8/25 + */ +public interface CountConstants { + + /** + * 用户相关统计信息 + */ + String USER_STATISTIC_INFO = "user_statistic_"; + /** + * 文章相关统计信息 + */ + String ARTICLE_STATISTIC_INFO = "article_statistic_"; + /** + * 关注数 + */ + String FOLLOW_COUNT = "followCount"; + + /** + * 粉丝数 + */ + String FANS_COUNT = "fansCount"; + + /** + * 已发布文章数 + */ + String ARTICLE_COUNT = "articleCount"; + + /** + * 文章点赞数 + */ + String PRAISE_COUNT = "praiseCount"; + + /** + * 文章被阅读数 + */ + String READ_COUNT = "readCount"; + + /** + * 文章被收藏数 + */ + String COLLECTION_COUNT = "collectionCount"; + + /** + * 评论数 + */ + String COMMENT_COUNT = "commentCount"; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/converter/StatisticsConverter.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/converter/StatisticsConverter.java new file mode 100644 index 000000000..0fad10502 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/converter/StatisticsConverter.java @@ -0,0 +1,39 @@ +package com.github.paicoding.forum.service.statistics.converter; + +import com.github.paicoding.forum.api.model.vo.statistics.dto.StatisticsDayDTO; +import com.github.paicoding.forum.service.statistics.repository.entity.RequestCountDO; +import com.github.paicoding.forum.service.statistics.repository.entity.RequestCountExcelDO; +import com.github.paicoding.forum.service.statistics.repository.entity.StatisticsDayExcelDO; + +import java.util.List; +import java.util.stream.Collectors; + +public class StatisticsConverter { + public static StatisticsDayExcelDO convertToExcelDO(StatisticsDayDTO dto) { + StatisticsDayExcelDO excelDO = new StatisticsDayExcelDO(); + excelDO.setDate(dto.getDate()); + excelDO.setUvCount(dto.getUvCount()); + excelDO.setPvCount(dto.getPvCount()); + return excelDO; + } + + public static RequestCountExcelDO ConvertToRequestCountDO(RequestCountDO requestCountDO) { + RequestCountExcelDO excelDO = new RequestCountExcelDO(); + excelDO.setHost(requestCountDO.getHost()); + excelDO.setCnt(requestCountDO.getCnt()); + excelDO.setDate(requestCountDO.getDate()); + return excelDO; + } + + public static List convertToRequestCountExcelDOList(List requestCountDOList) { + return requestCountDOList.stream() + .map(StatisticsConverter::ConvertToRequestCountDO) + .collect(Collectors.toList()); + } + + public static List convertToExcelDOList(List dtoList) { + return dtoList.stream() + .map(StatisticsConverter::convertToExcelDO) + .collect(Collectors.toList()); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/listener/UserStatisticEventListener.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/listener/UserStatisticEventListener.java new file mode 100644 index 000000000..e777a00f5 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/listener/UserStatisticEventListener.java @@ -0,0 +1,102 @@ +package com.github.paicoding.forum.service.statistics.listener; + +import com.github.paicoding.forum.api.model.enums.ArticleEventEnum; +import com.github.paicoding.forum.api.model.event.ArticleMsgEvent; +import com.github.paicoding.forum.api.model.vo.notify.NotifyMsgEvent; +import com.github.paicoding.forum.core.cache.RedisClient; +import com.github.paicoding.forum.service.article.repository.dao.ArticleDao; +import com.github.paicoding.forum.service.article.repository.entity.ArticleDO; +import com.github.paicoding.forum.service.comment.repository.entity.CommentDO; +import com.github.paicoding.forum.service.user.repository.entity.UserFootDO; +import com.github.paicoding.forum.service.user.repository.entity.UserRelationDO; +import com.github.paicoding.forum.service.statistics.constants.CountConstants; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 用户活跃相关的消息监听器 + * + * @author YiHui + * @date 2023/8/19 + */ +@Component +public class UserStatisticEventListener { + @Resource + private ArticleDao articleDao; + + /** + * 用户操作行为,增加对应的积分 + * + * @param msgEvent + */ + @EventListener(classes = NotifyMsgEvent.class) + @Async + public void notifyMsgListener(NotifyMsgEvent msgEvent) { + switch (msgEvent.getNotifyType()) { + case COMMENT: + case REPLY: + CommentDO comment = (CommentDO) msgEvent.getContent(); + RedisClient.hIncr(CountConstants.ARTICLE_STATISTIC_INFO + comment.getArticleId(), CountConstants.COMMENT_COUNT, 1); + break; + case DELETE_COMMENT: + case DELETE_REPLY: + comment = (CommentDO) msgEvent.getContent(); + RedisClient.hIncr(CountConstants.ARTICLE_STATISTIC_INFO + comment.getArticleId(), CountConstants.COMMENT_COUNT, -1); + break; + case COLLECT: + UserFootDO foot = (UserFootDO) msgEvent.getContent(); + RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + foot.getDocumentUserId(), CountConstants.COLLECTION_COUNT, 1); + RedisClient.hIncr(CountConstants.ARTICLE_STATISTIC_INFO + foot.getDocumentId(), CountConstants.COLLECTION_COUNT, 1); + break; + case CANCEL_COLLECT: + foot = (UserFootDO) msgEvent.getContent(); + RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + foot.getDocumentUserId(), CountConstants.COLLECTION_COUNT, -1); + RedisClient.hIncr(CountConstants.ARTICLE_STATISTIC_INFO + foot.getDocumentId(), CountConstants.COLLECTION_COUNT, -1); + break; + case PRAISE: + foot = (UserFootDO) msgEvent.getContent(); + RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + foot.getDocumentUserId(), CountConstants.PRAISE_COUNT, 1); + RedisClient.hIncr(CountConstants.ARTICLE_STATISTIC_INFO + foot.getDocumentId(), CountConstants.PRAISE_COUNT, 1); + break; + case CANCEL_PRAISE: + foot = (UserFootDO) msgEvent.getContent(); + RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + foot.getDocumentUserId(), CountConstants.PRAISE_COUNT, -1); + RedisClient.hIncr(CountConstants.ARTICLE_STATISTIC_INFO + foot.getDocumentId(), CountConstants.PRAISE_COUNT, -1); + break; + case FOLLOW: + UserRelationDO relation = (UserRelationDO) msgEvent.getContent(); + // 主用户粉丝数 + 1 + RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + relation.getUserId(), CountConstants.FANS_COUNT, 1); + // 粉丝的关注数 + 1 + RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + relation.getFollowUserId(), CountConstants.FOLLOW_COUNT, 1); + break; + case CANCEL_FOLLOW: + relation = (UserRelationDO) msgEvent.getContent(); + // 主用户粉丝数 + 1 + RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + relation.getUserId(), CountConstants.FANS_COUNT, -1); + // 粉丝的关注数 + 1 + RedisClient.hIncr(CountConstants.USER_STATISTIC_INFO + relation.getFollowUserId(), CountConstants.FOLLOW_COUNT, -1); + break; + default: + } + } + + /** + * 发布文章,更新对应的文章计数 + * + * @param event + */ + @Async + @EventListener(ArticleMsgEvent.class) + public void publishArticleListener(ArticleMsgEvent event) { + ArticleEventEnum type = event.getType(); + if (type == ArticleEventEnum.ONLINE || type == ArticleEventEnum.OFFLINE || type == ArticleEventEnum.DELETE) { + Long userId = event.getContent().getUserId(); + int count = articleDao.countArticleByUser(userId); + RedisClient.hSet(CountConstants.USER_STATISTIC_INFO + userId, CountConstants.ARTICLE_COUNT, count); + } + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/repository/dao/RequestCountDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/repository/dao/RequestCountDao.java new file mode 100644 index 000000000..f5f46047f --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/repository/dao/RequestCountDao.java @@ -0,0 +1,69 @@ +package com.github.paicoding.forum.service.statistics.repository.dao; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.paicoding.forum.api.model.enums.PushStatusEnum; +import com.github.paicoding.forum.api.model.enums.YesOrNoEnum; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.article.dto.TagDTO; +import com.github.paicoding.forum.api.model.vo.statistics.dto.StatisticsDayDTO; +import com.github.paicoding.forum.service.article.conveter.ArticleConverter; +import com.github.paicoding.forum.service.article.repository.entity.TagDO; +import com.github.paicoding.forum.service.statistics.repository.entity.RequestCountDO; +import com.github.paicoding.forum.service.statistics.repository.mapper.RequestCountMapper; +import org.springframework.stereotype.Repository; + +import java.sql.Date; +import java.util.List; + +/** + * 请求计数 + * + * @author louzai + * @date 2022-10-1 + */ +@Repository +public class RequestCountDao extends ServiceImpl { + + public Long getPvTotalCount() { + return baseMapper.getPvTotalCount(); + } + + /** + * 获取请求数据 + * + * @param host + * @param date + * @return + */ + public RequestCountDO getRequestCount(String host, Date date) { + return lambdaQuery() + .eq(RequestCountDO::getHost, host) + .eq(RequestCountDO::getDate, date) + .one(); + } + + public List listRequestCount(PageParam pageParam) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.orderByDesc(RequestCountDO::getId); + if (pageParam != null) { + query.last(PageParam.getLimitSql(pageParam)); + } + return baseMapper.selectList(query); + } + + /** + * 获取 PV UV 数据列表 + * @param day + * @return + */ + public List getPvUvDayList(Integer day) { + return baseMapper.getPvUvDayList(day); + } + + public void incrementCount(Long id) { + baseMapper.incrementCount(id); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/repository/entity/RequestCountDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/repository/entity/RequestCountDO.java new file mode 100644 index 000000000..6ce31d1d5 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/repository/entity/RequestCountDO.java @@ -0,0 +1,37 @@ +package com.github.paicoding.forum.service.statistics.repository.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * 请求计数表 + * + * @author louzai + * @date 2022-10-1 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("request_count") +public class RequestCountDO extends BaseDO { + + private static final long serialVersionUID = 1L; + + /** + * 机器IP + */ + private String host; + + /** + * 访问计数 + */ + private Integer cnt; + + /** + * 当前日期 + */ + private Date date; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/repository/entity/RequestCountExcelDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/repository/entity/RequestCountExcelDO.java new file mode 100644 index 000000000..b73162c58 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/repository/entity/RequestCountExcelDO.java @@ -0,0 +1,19 @@ +package com.github.paicoding.forum.service.statistics.repository.entity; + +import cn.idev.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.util.Date; + +@Data +public class RequestCountExcelDO { + + @ExcelProperty("机器IP") + private String host; + + @ExcelProperty("访问计数") + private Integer cnt; + + @ExcelProperty("当前日期") + private Date date; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/repository/entity/StatisticsDayExcelDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/repository/entity/StatisticsDayExcelDO.java new file mode 100644 index 000000000..f4eb5ab1e --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/repository/entity/StatisticsDayExcelDO.java @@ -0,0 +1,26 @@ +package com.github.paicoding.forum.service.statistics.repository.entity; + +import cn.idev.excel.annotation.ExcelProperty; +import lombok.Data; + +@Data +public class StatisticsDayExcelDO { + + /** + * 日期 + */ + @ExcelProperty("日期") + private String date; + + /** + * 数量 + */ + @ExcelProperty("PV") + private Long pvCount; + + /** + * UV数量 + */ + @ExcelProperty("UV") + private Long uvCount; +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/repository/mapper/RequestCountMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/repository/mapper/RequestCountMapper.java new file mode 100644 index 000000000..556c95893 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/repository/mapper/RequestCountMapper.java @@ -0,0 +1,42 @@ +package com.github.paicoding.forum.service.statistics.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.api.model.vo.statistics.dto.StatisticsDayDTO; +import com.github.paicoding.forum.service.statistics.repository.entity.RequestCountDO; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +/** + * 请求计数mapper接口 + * + * @author louzai + * @date 2022-10-1 + */ +public interface RequestCountMapper extends BaseMapper { + + /** + * 获取 PV 总数 + * + * @return + */ + @Select("select sum(cnt) from request_count") + Long getPvTotalCount(); + + /** + * 获取 PV UV 数据列表 + * @param day + * @return + */ + List getPvUvDayList(@Param("day") Integer day); + + /** + * 增加计数 + * + * @param id + */ + @Update("update request_count set cnt = cnt + 1 where id = #{id}") + void incrementCount(Long id); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/CountService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/CountService.java new file mode 100644 index 000000000..90449f86d --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/CountService.java @@ -0,0 +1,82 @@ +package com.github.paicoding.forum.service.statistics.service; + +import com.github.paicoding.forum.api.model.vo.user.dto.ArticleFootCountDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.UserStatisticInfoDTO; + +/** + * 计数统计相关 + * + * @author YiHui + * @date 2022/9/2 + */ +public interface CountService { + /** + * 根据文章ID查询文章计数 + * 本方法直接基于db进行查询相关信息,改用下面的 queryArticleStatisticInfo() 方法进行替换 + * + * @param articleId + * @return + */ + @Deprecated + ArticleFootCountDTO queryArticleCountInfoByArticleId(Long articleId); + + + /** + * 查询用户总阅读相关计数(当前未返回评论数) + * 本方法直接基于db进行查询相关信息,改用下面的 queryUserStatisticInfo() 方法进行替换 + * + * @param userId + * @return + */ + @Deprecated + ArticleFootCountDTO queryArticleCountInfoByUserId(Long userId); + + /** + * 获取评论点赞数量 + * + * @param commentId + * @return + */ + Long queryCommentPraiseCount(Long commentId); + + + /** + * 查询用户的相关统计信息 + * + * @param userId + * @return 返回用户的 收藏、点赞、文章、粉丝、关注,总的文章阅读数 + */ + UserStatisticInfoDTO queryUserStatisticInfo(Long userId); + + /** + * 查询文章相关的统计信息 + * + * @param articleId + * @return 返回文章的 收藏、点赞、评论、阅读数 + */ + ArticleFootCountDTO queryArticleStatisticInfo(Long articleId); + + + /** + * 文章计数+1 + * + * @param authorUserId 作者 + * @param articleId 文章 + * @return 计数器 + */ + void incrArticleReadCount(Long authorUserId, Long articleId); + + /** + * 刷新用户的统计信息 + * + * @param userId + */ + void refreshUserStatisticInfo(Long userId); + + /** + * 刷新文章的统计信息 + * + * @param articleId + */ + void refreshArticleStatisticInfo(Long articleId); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/RequestCountService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/RequestCountService.java new file mode 100644 index 000000000..1f2b54aed --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/RequestCountService.java @@ -0,0 +1,30 @@ +package com.github.paicoding.forum.service.statistics.service; + +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.statistics.dto.StatisticsDayDTO; +import com.github.paicoding.forum.service.statistics.repository.entity.RequestCountDO; + +import java.util.List; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 5/24/23 + */ +public interface RequestCountService { + RequestCountDO getRequestCount(String host); + + void insert(String host); + + void incrementCount(Long id); + + Long getPvTotalCount(); + + List getPvUvDayList(Integer day); + + long count(); + + // 分页返回 RequestCountDO + List listRequestCount(PageParam pageParam); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/StatisticsSettingService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/StatisticsSettingService.java new file mode 100644 index 000000000..5b29d3c7b --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/StatisticsSettingService.java @@ -0,0 +1,41 @@ +package com.github.paicoding.forum.service.statistics.service; + +import com.github.paicoding.forum.api.model.vo.statistics.dto.StatisticsCountDTO; +import com.github.paicoding.forum.api.model.vo.statistics.dto.StatisticsDayDTO; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 数据统计后台接口 + * + * @author louzai + * @date 2022-09-19 + */ +public interface StatisticsSettingService { + + /** + * 保存计数 + * + * @param host + */ + void saveRequestCount(String host); + + /** + * 获取总数 + * + * @return + */ + StatisticsCountDTO getStatisticsCount(); + + /** + * 获取每天的PV UV统计数据 + * + * @param day + * @return + */ + List getPvUvDayList(Integer day); + + void download2Excel(Integer day, HttpServletResponse response); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/UserStatisticService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/UserStatisticService.java new file mode 100644 index 000000000..f09eb640c --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/UserStatisticService.java @@ -0,0 +1,25 @@ +package com.github.paicoding.forum.service.statistics.service; + +/** + * 用户统计服务 + * + * @author YiHui + * @date 2023/3/26 + */ +public interface UserStatisticService { + /** + * 添加在线人数 + * + * @param add 正数,表示添加在线人数;负数,表示减少在线人数 + * @return + */ + int incrOnlineUserCnt(int add); + + /** + * 查询在线用户人数 + * + * @return + */ + int getOnlineUserCnt(); + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/impl/CountServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/impl/CountServiceImpl.java new file mode 100644 index 000000000..bdc5e7053 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/impl/CountServiceImpl.java @@ -0,0 +1,184 @@ +package com.github.paicoding.forum.service.statistics.service.impl; + +import com.github.paicoding.forum.api.model.vo.user.dto.ArticleFootCountDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.UserStatisticInfoDTO; +import com.github.paicoding.forum.core.cache.RedisClient; +import com.github.paicoding.forum.core.util.MapUtils; +import com.github.paicoding.forum.service.article.repository.dao.ArticleDao; +import com.github.paicoding.forum.service.comment.service.CommentReadService; +import com.github.paicoding.forum.service.statistics.constants.CountConstants; +import com.github.paicoding.forum.service.statistics.service.CountService; +import com.github.paicoding.forum.service.user.repository.dao.UserDao; +import com.github.paicoding.forum.service.user.repository.dao.UserFootDao; +import com.github.paicoding.forum.service.user.repository.dao.UserRelationDao; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +/** + * 计数服务,后续计数相关的可以考虑基于redis来做 + * + * @author YiHui + * @date 2022/9/2 + */ +@Slf4j +@Service +public class CountServiceImpl implements CountService { + private final UserFootDao userFootDao; + public CountServiceImpl(UserFootDao userFootDao) { + this.userFootDao = userFootDao; + } + + @Resource + private UserRelationDao userRelationDao; + + @Resource + private ArticleDao articleDao; + + @Resource + private CommentReadService commentReadService; + + @Resource + private UserDao userDao; + + @Override + public ArticleFootCountDTO queryArticleCountInfoByArticleId(Long articleId) { + ArticleFootCountDTO res = userFootDao.countArticleByArticleId(articleId); + if (res == null) { + res = new ArticleFootCountDTO(); + } else { + res.setCommentCount(commentReadService.queryCommentCount(articleId)); + } + return res; + } + + + @Override + public ArticleFootCountDTO queryArticleCountInfoByUserId(Long userId) { + return userFootDao.countArticleByUserId(userId); + } + + /** + * 查询评论的点赞数 + * + * @param commentId + * @return + */ + @Override + public Long queryCommentPraiseCount(Long commentId) { + return userFootDao.countCommentPraise(commentId); + } + + @Override + public UserStatisticInfoDTO queryUserStatisticInfo(Long userId) { + Map ans = RedisClient.hGetAll(CountConstants.USER_STATISTIC_INFO + userId, Integer.class); + UserStatisticInfoDTO info = new UserStatisticInfoDTO(); + info.setFollowCount(ans.getOrDefault(CountConstants.FOLLOW_COUNT, 0)); + info.setArticleCount(ans.getOrDefault(CountConstants.ARTICLE_COUNT, 0)); + info.setPraiseCount(ans.getOrDefault(CountConstants.PRAISE_COUNT, 0)); + info.setCollectionCount(ans.getOrDefault(CountConstants.COLLECTION_COUNT, 0)); + info.setReadCount(ans.getOrDefault(CountConstants.READ_COUNT, 0)); + info.setFansCount(ans.getOrDefault(CountConstants.FANS_COUNT, 0)); + return info; + } + + @Override + public ArticleFootCountDTO queryArticleStatisticInfo(Long articleId) { + Map ans = RedisClient.hGetAll(CountConstants.ARTICLE_STATISTIC_INFO + articleId, Integer.class); + ArticleFootCountDTO info = new ArticleFootCountDTO(); + info.setPraiseCount(ans.getOrDefault(CountConstants.PRAISE_COUNT, 0)); + info.setCollectionCount(ans.getOrDefault(CountConstants.COLLECTION_COUNT, 0)); + info.setCommentCount(ans.getOrDefault(CountConstants.COMMENT_COUNT, 0)); + info.setReadCount(ans.getOrDefault(CountConstants.READ_COUNT, 0)); + return info; + } + + @Override + public void incrArticleReadCount(Long authorUserId, Long articleId) { + // db层的计数+1 + articleDao.incrReadCount(articleId); + // redis计数器 +1 + RedisClient.pipelineAction() + .add(CountConstants.ARTICLE_STATISTIC_INFO + articleId, CountConstants.READ_COUNT, + (connection, key, value) -> connection.hIncrBy(key, value, 1)) + .add(CountConstants.USER_STATISTIC_INFO + authorUserId, CountConstants.READ_COUNT, + (connection, key, value) -> connection.hIncrBy(key, value, 1)) + .execute(); + } + + /** + * 每天4:15分执行定时任务,全量刷新用户的统计信息 + */ + @Scheduled(cron = "0 15 4 * * ?") + public void autoRefreshAllUserStatisticInfo() { + Long now = System.currentTimeMillis(); + log.info("开始自动刷新用户统计信息"); + Long userId = 0L; + int batchSize = 20; + while (true) { + List userIds = userDao.scanUserId(userId, batchSize); + userIds.forEach(this::refreshUserStatisticInfo); + if (userIds.size() < batchSize) { + userId = userIds.get(userIds.size() - 1); + break; + } else { + userId = userIds.get(batchSize - 1); + } + } + log.info("结束自动刷新用户统计信息,共耗时: {}ms, maxUserId: {}", System.currentTimeMillis() - now, userId); + } + + + /** + * 更新用户的统计信息 + * + * @param userId + */ + @Override + public void refreshUserStatisticInfo(Long userId) { + // 用户的文章点赞数,收藏数,阅读计数 + ArticleFootCountDTO count = userFootDao.countArticleByUserId(userId); + if (count == null) { + count = new ArticleFootCountDTO(); + } + + // 获取关注数 + Long followCount = userRelationDao.queryUserFollowCount(userId); + // 粉丝数 + Long fansCount = userRelationDao.queryUserFansCount(userId); + + // 查询用户发布的文章数 + Integer articleNum = articleDao.countArticleByUser(userId); + + String key = CountConstants.USER_STATISTIC_INFO + userId; + RedisClient.hMSet(key, MapUtils.create(CountConstants.PRAISE_COUNT, count.getPraiseCount(), + CountConstants.COLLECTION_COUNT, count.getCollectionCount(), + CountConstants.READ_COUNT, count.getReadCount(), + CountConstants.FANS_COUNT, fansCount, + CountConstants.FOLLOW_COUNT, followCount, + CountConstants.ARTICLE_COUNT, articleNum)); + + } + + + public void refreshArticleStatisticInfo(Long articleId) { + ArticleFootCountDTO res = userFootDao.countArticleByArticleId(articleId); + if (res == null) { + res = new ArticleFootCountDTO(); + } else { + res.setCommentCount(commentReadService.queryCommentCount(articleId)); + } + + RedisClient.hMSet(CountConstants.ARTICLE_STATISTIC_INFO + articleId, + MapUtils.create(CountConstants.COLLECTION_COUNT, res.getCollectionCount(), + CountConstants.PRAISE_COUNT, res.getPraiseCount(), + CountConstants.READ_COUNT, res.getReadCount(), + CountConstants.COMMENT_COUNT, res.getCommentCount() + ) + ); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/impl/RequestCountServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/impl/RequestCountServiceImpl.java new file mode 100644 index 000000000..0ae5f92bc --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/impl/RequestCountServiceImpl.java @@ -0,0 +1,75 @@ +package com.github.paicoding.forum.service.statistics.service.impl; + +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.statistics.dto.StatisticsDayDTO; +import com.github.paicoding.forum.service.statistics.repository.dao.RequestCountDao; +import com.github.paicoding.forum.service.statistics.repository.entity.RequestCountDO; +import com.github.paicoding.forum.service.statistics.service.RequestCountService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.sql.Date; +import java.time.LocalDate; +import java.util.Collections; +import java.util.List; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 5/24/23 + */ +@Slf4j +@Service +public class RequestCountServiceImpl implements RequestCountService { + @Autowired + private RequestCountDao requestCountDao; + + @Override + public RequestCountDO getRequestCount(String host) { + return requestCountDao.getRequestCount(host, Date.valueOf(LocalDate.now())); + } + + @Override + public void insert(String host) { + RequestCountDO requestCountDO = null; + try { + requestCountDO = new RequestCountDO(); + requestCountDO.setHost(host); + requestCountDO.setCnt(1); + requestCountDO.setDate(Date.valueOf(LocalDate.now())); + requestCountDao.save(requestCountDO); + } catch (Exception e) { + // fixme 非数据库原因得异常,则大概率是0点的并发访问,导致同一天写入多条数据的问题; 可以考虑使用分布式锁来避免 + // todo 后续考虑使用redis自增来实现pv计数统计 + log.error("save requestCount error: {}", requestCountDO, e); + } + } + + @Override + public void incrementCount(Long id) { + requestCountDao.incrementCount(id); + } + + @Override + public Long getPvTotalCount() { + return requestCountDao.getPvTotalCount(); + } + + @Override + public List getPvUvDayList(Integer day) { + return requestCountDao.getPvUvDayList(day); + } + + @Override + public long count() { + return requestCountDao.count(); + } + + @Override + public List listRequestCount(PageParam pageParam) { + return requestCountDao.listRequestCount(pageParam); + } + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/impl/StatisticsSettingServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/impl/StatisticsSettingServiceImpl.java new file mode 100644 index 000000000..d5c61f919 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/impl/StatisticsSettingServiceImpl.java @@ -0,0 +1,108 @@ +package com.github.paicoding.forum.service.statistics.service.impl; + +import cn.idev.excel.FastExcel; +import com.github.paicoding.forum.api.model.vo.statistics.dto.StatisticsCountDTO; +import com.github.paicoding.forum.api.model.vo.statistics.dto.StatisticsDayDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.UserFootStatisticDTO; +import com.github.paicoding.forum.service.article.service.ArticleReadService; +import com.github.paicoding.forum.service.article.service.ColumnService; +import com.github.paicoding.forum.service.statistics.converter.StatisticsConverter; +import com.github.paicoding.forum.service.statistics.repository.entity.RequestCountDO; +import com.github.paicoding.forum.service.statistics.repository.entity.StatisticsDayExcelDO; +import com.github.paicoding.forum.service.statistics.service.RequestCountService; +import com.github.paicoding.forum.service.statistics.service.StatisticsSettingService; +import com.github.paicoding.forum.service.user.service.UserFootService; +import com.github.paicoding.forum.service.user.service.UserService; +import com.github.paicoding.forum.service.user.service.conf.AiConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + * 数据统计后台接口 + * + * @author louzai + * @date 2022-09-19 + */ +@Slf4j +@Service +public class StatisticsSettingServiceImpl implements StatisticsSettingService { + + @Autowired + private RequestCountService requestCountService; + + @Autowired + private UserService userService; + + @Autowired + private ColumnService columnService; + + @Autowired + private UserFootService userFootService; + + @Autowired + private ArticleReadService articleReadService; + + @Resource + private AiConfig aiConfig; + + @Override + public void saveRequestCount(String host) { + RequestCountDO requestCountDO = requestCountService.getRequestCount(host); + if (requestCountDO == null) { + requestCountService.insert(host); + } else { + // 改为数据库直接更新 + requestCountService.incrementCount(requestCountDO.getId()); + } + } + + @Override + public StatisticsCountDTO getStatisticsCount() { + // 从 user_foot 表中查询点赞数、收藏数、留言数、阅读数 + UserFootStatisticDTO userFootStatisticDTO = userFootService.getFootCount(); + if (userFootStatisticDTO == null) { + userFootStatisticDTO = new UserFootStatisticDTO(); + } + return StatisticsCountDTO.builder() + .userCount(userService.getUserCount()) + .articleCount(articleReadService.getArticleCount()) + .pvCount(requestCountService.getPvTotalCount()) + .tutorialCount(columnService.getTutorialCount()) + .commentCount(userFootStatisticDTO.getCommentCount()) + .collectCount(userFootStatisticDTO.getCollectionCount()) + .likeCount(userFootStatisticDTO.getPraiseCount()) + .readCount(userFootStatisticDTO.getReadCount()) + .starPayCount(aiConfig.getMaxNum().getStarNumber()) + .build(); + } + + @Override + public List getPvUvDayList(Integer day) { + return requestCountService.getPvUvDayList(day); + } + + @Override + public void download2Excel(Integer day, HttpServletResponse response) { + List pvDayList = requestCountService.getPvUvDayList(day); + // StatisticsDayDTO 转 StatisticsDayExcelDTO + List excelDTOList = StatisticsConverter.convertToExcelDOList(pvDayList); + + // 使用 FastExcel 导出 Excel + // TODO 这里可以用一个大文件,比如说 10万条数据测试一下,看看 FastExcel 的性能 + try { + FastExcel.write(response.getOutputStream(), StatisticsDayExcelDO.class) + .sheet(day + "天统计") + .doWrite(excelDTOList); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/impl/UserStatisticServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/impl/UserStatisticServiceImpl.java new file mode 100644 index 000000000..731299d8f --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/statistics/service/impl/UserStatisticServiceImpl.java @@ -0,0 +1,44 @@ +package com.github.paicoding.forum.service.statistics.service.impl; + +import com.github.paicoding.forum.service.statistics.service.UserStatisticService; +import org.springframework.stereotype.Service; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 用户统计服务 + * + * @author YiHui + * @date 2023/3/26 + */ +@Service +public class UserStatisticServiceImpl implements UserStatisticService { + + /** + * 对于单机的场景,可以直接使用本地局部变量来实现计数 + * 对于集群的场景,可考虑借助 redis的zset 来实现集群的在线用户人数统计 + */ + private AtomicInteger onlineUserCnt = new AtomicInteger(0); + + /** + * 添加在线人数 + * + * @param add 正数,表示添加在线人数;负数,表示减少在线人数 + * @return + */ + @Override + public int incrOnlineUserCnt(int add) { + return onlineUserCnt.addAndGet(add); + } + + /** + * 查询在线用户人数 + * + * @return + */ + @Override + public int getOnlineUserCnt() { + return onlineUserCnt.get(); + } + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/converter/UserAiConverter.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/converter/UserAiConverter.java new file mode 100644 index 000000000..163a54551 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/converter/UserAiConverter.java @@ -0,0 +1,41 @@ +package com.github.paicoding.forum.service.user.converter; + +import com.github.paicoding.forum.api.model.enums.user.StarSourceEnum; +import com.github.paicoding.forum.api.model.enums.user.UserAIStatEnum; +import com.github.paicoding.forum.service.user.repository.entity.UserAiDO; +import com.github.paicoding.forum.service.user.service.help.UserRandomGenHelper; +import org.apache.commons.lang3.StringUtils; + +/** + * @author YiHui + * @date 2023/6/27 + */ +public class UserAiConverter { + + + public static UserAiDO initAi(Long userId) { + return initAi(userId, null); + } + + public static UserAiDO initAi(Long userId, String starNumber) { + UserAiDO userAiDO = new UserAiDO(); + userAiDO.setUserId(userId); + userAiDO.setStarType(0); + userAiDO.setInviterUserId(0L); + userAiDO.setStrategy(0); + userAiDO.setInviteNum(0); + userAiDO.setDeleted(0); + userAiDO.setInviteCode(UserRandomGenHelper.genInviteCode(userId)); + if (StringUtils.isBlank(starNumber)) { + userAiDO.setStarNumber(""); + userAiDO.setState(UserAIStatEnum.IGNORE.getCode()); + } else { + userAiDO.setStarNumber(starNumber); + userAiDO.setState(UserAIStatEnum.TRYING.getCode()); + // 先只支持Java进阶之路的星球绑定 + userAiDO.setStarType(StarSourceEnum.JAVA_GUIDE.getSource()); + } + return userAiDO; + } + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/converter/UserConverter.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/converter/UserConverter.java new file mode 100644 index 000000000..b51480207 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/converter/UserConverter.java @@ -0,0 +1,106 @@ +package com.github.paicoding.forum.service.user.converter; + +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.api.model.enums.FollowStateEnum; +import com.github.paicoding.forum.api.model.enums.RoleEnum; +import com.github.paicoding.forum.api.model.enums.user.UserAIStatEnum; +import com.github.paicoding.forum.api.model.vo.user.UserInfoSaveReq; +import com.github.paicoding.forum.api.model.vo.user.UserRelationReq; +import com.github.paicoding.forum.api.model.vo.user.UserSaveReq; +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.SimpleUserInfoDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.UserStatisticInfoDTO; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.service.user.repository.entity.UserAiDO; +import com.github.paicoding.forum.service.user.repository.entity.UserDO; +import com.github.paicoding.forum.service.user.repository.entity.UserInfoDO; +import com.github.paicoding.forum.service.user.repository.entity.UserRelationDO; +import io.netty.util.internal.StringUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.util.CollectionUtils; + +/** + * 用户转换 + * + * @author louzai + * @date 2022-07-20 + */ +public class UserConverter { + + public static UserDO toDO(UserSaveReq req) { + if (req == null) { + return null; + } + UserDO userDO = new UserDO(); + userDO.setId(req.getUserId()); + userDO.setThirdAccountId(req.getThirdAccountId()); + userDO.setLoginType(req.getLoginType()); + return userDO; + } + + public static UserInfoDO toDO(UserInfoSaveReq req) { + if (req == null) { + return null; + } + UserInfoDO userInfoDO = new UserInfoDO(); + userInfoDO.setUserId(req.getUserId()); + userInfoDO.setUserName(req.getUserName()); + userInfoDO.setPhoto(req.getPhoto()); + userInfoDO.setPosition(req.getPosition()); + userInfoDO.setCompany(req.getCompany()); + userInfoDO.setProfile(req.getProfile()); + userInfoDO.setEmail(req.getEmail()); + if (!CollectionUtils.isEmpty(req.getPayCode())) { + userInfoDO.setPayCode(JsonUtil.toStr(req.getPayCode())); + } + return userInfoDO; + } + + public static BaseUserInfoDTO toDTO(UserInfoDO info, UserAiDO userAiDO) { + BaseUserInfoDTO user = toDTO(info); + if (userAiDO != null) { + user.setStarStatus(UserAIStatEnum.fromCode(userAiDO.getState())); + } + return user; + } + + public static BaseUserInfoDTO toDTO(UserInfoDO info) { + if (info == null) { + return null; + } + BaseUserInfoDTO user = new BaseUserInfoDTO(); + // todo 知识点,bean属性拷贝的几种方式, 直接get/set方式,使用BeanUtil工具类(spring, cglib, apache, objectMapper),序列化方式等 + BeanUtils.copyProperties(info, user); + // 设置用户最新登录地理位置 + user.setRegion(info.getIp().getLatestRegion()); + // 设置用户角色 + user.setRole(RoleEnum.role(info.getUserRole())); + return user; + } + + public static SimpleUserInfoDTO toSimpleInfo(UserInfoDO info) { + return new SimpleUserInfoDTO().setUserId(info.getUserId()) + .setName(info.getUserName()) + .setAvatar(info.getPhoto()) + .setProfile(info.getProfile()); + } + + public static UserRelationDO toDO(UserRelationReq req) { + if (req == null) { + return null; + } + UserRelationDO userRelationDO = new UserRelationDO(); + userRelationDO.setUserId(req.getUserId()); + userRelationDO.setFollowUserId(ReqInfoContext.getReqInfo().getUserId()); + userRelationDO.setFollowState(req.getFollowed() ? FollowStateEnum.FOLLOW.getCode() : FollowStateEnum.CANCEL_FOLLOW.getCode()); + return userRelationDO; + } + + public static UserStatisticInfoDTO toUserHomeDTO(UserStatisticInfoDTO userHomeDTO, BaseUserInfoDTO baseUserInfoDTO) { + if (baseUserInfoDTO == null) { + return null; + } + BeanUtils.copyProperties(baseUserInfoDTO, userHomeDTO); + return userHomeDTO; + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/converter/UserStructMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/converter/UserStructMapper.java new file mode 100644 index 000000000..9b384e9dd --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/converter/UserStructMapper.java @@ -0,0 +1,23 @@ +package com.github.paicoding.forum.service.user.converter; + +import com.github.paicoding.forum.api.model.vo.user.SearchZsxqUserReq; +import com.github.paicoding.forum.service.user.repository.params.SearchZsxqWhiteParams; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 6/29/23 + */ +@Mapper +public interface UserStructMapper { + UserStructMapper INSTANCE = Mappers.getMapper( UserStructMapper.class ); + // req to params + @Mapping(source = "pageNumber", target = "pageNum") + // state to status + @Mapping(source = "state", target = "status") + SearchZsxqWhiteParams toSearchParams(SearchZsxqUserReq req); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/package-info.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/package-info.java new file mode 100644 index 000000000..516edd61d --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/package-info.java @@ -0,0 +1,7 @@ +/** + * 用户相关包 + * + * @author YiHui + * @date 2022/7/6 + */ +package com.github.paicoding.forum.service.user; \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/dao/UserAiDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/dao/UserAiDao.java new file mode 100644 index 000000000..70c41fe3c --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/dao/UserAiDao.java @@ -0,0 +1,164 @@ +package com.github.paicoding.forum.service.user.repository.dao; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.paicoding.forum.api.model.enums.YesOrNoEnum; +import com.github.paicoding.forum.api.model.enums.user.StarSourceEnum; +import com.github.paicoding.forum.api.model.enums.user.UserAiStrategyEnum; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.user.dto.ZsxqUserInfoDTO; +import com.github.paicoding.forum.service.user.converter.UserAiConverter; +import com.github.paicoding.forum.service.user.repository.entity.UserAiDO; +import com.github.paicoding.forum.service.user.repository.entity.UserDO; +import com.github.paicoding.forum.service.user.repository.mapper.UserAiMapper; +import com.github.paicoding.forum.service.user.repository.params.SearchZsxqWhiteParams; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @author YiHui + * @date 2022/9/2 + */ +@Repository +public class UserAiDao extends ServiceImpl { + + @Resource + private UserAiMapper userAiMapper; + + @Resource + private UserDao userDao; + + /** + * 根据星球编号反查用户 + * + * @param starNumber + * @return + */ + public UserAiDO getByStarNumber(String starNumber) { + LambdaQueryWrapper queryUserAi = Wrappers.lambdaQuery(); + + queryUserAi.eq(UserAiDO::getStarNumber, starNumber) + .eq(UserAiDO::getDeleted, YesOrNoEnum.NO.getCode()) + .last("limit 1"); + return userAiMapper.selectOne(queryUserAi); + } + + public UserAiDO getByUserId(Long userId) { + LambdaQueryWrapper queryUserAi = Wrappers.lambdaQuery(); + + queryUserAi.eq(UserAiDO::getUserId, userId) + .eq(UserAiDO::getDeleted, YesOrNoEnum.NO.getCode()); + return userAiMapper.selectOne(queryUserAi); + } + + + /** + * 查询用户的ai信息,若不存在,则初始化一个,主要用于存量的账号已存在的场景 + * + * @param userId + */ + public UserAiDO getOrInitAiInfo(Long userId) { + UserAiDO ai = getByUserId(userId); + if (ai != null) { + return ai; + } + + // 当不存在时,初始化一个 + ai = UserAiConverter.initAi(userId); + saveOrUpdateAiBindInfo(ai, null); + return ai; + } + + /** + * 根据邀请码,查找对应的邀请人 + * + * @param inviteCode 邀请码 + * @return + */ + public UserAiDO getByInviteCode(String inviteCode) { + LambdaQueryWrapper queryUserAi = Wrappers.lambdaQuery(); + + queryUserAi.eq(UserAiDO::getInviteCode, inviteCode) + .eq(UserAiDO::getDeleted, YesOrNoEnum.NO.getCode()); + return userAiMapper.selectOne(queryUserAi); + } + + /** + * 更新用户的邀请人数 + * + * @param id + * @param incr + */ + private void updateInviteCnt(Long id, int incr) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(UserAiDO::getId, id).setSql("invite_num = invite_num + " + incr); + userAiMapper.update(null, updateWrapper); + } + + public void saveOrUpdateAiBindInfo(UserAiDO ai) { + saveOrUpdateAiBindInfo(ai, null); + } + + /** + * 更新userAi绑定信息 + * + * @param ai + * @param inviteCode + */ + public void saveOrUpdateAiBindInfo(UserAiDO ai, String inviteCode) { + int strategy = ai.getStrategy(); + if (StringUtils.isNotBlank(inviteCode)) { + // todo 待支持更新邀请绑定 + // 对于绑定邀请码的用户,需要将邀请他的用户找出来,计数 + 1 + UserAiDO inviteUser = getByInviteCode(inviteCode); + if (inviteUser != null) { + ai.setInviterUserId(inviteUser.getUserId()); + updateInviteCnt(inviteUser.getId(), 1); + strategy = UserAiStrategyEnum.INVITE_USER.updateCondition(strategy); + } + } + + // 这里有点问题 + // 用户名密码注册的时候,还没有审核通过,所以即使有星球编号,也无法绑定 AI 策略 + // 去掉用户审核通过的判断,如果用户绑定了星球,就直接更新策略,默认为进阶之路 + // 后面获取册数的时候会根据用户的审核状态,计算次数 + if (StringUtils.isNotBlank(ai.getStarNumber())) { + // 绑定了星球,且审核通过 + if (ai.getStarType() == StarSourceEnum.TECH_PAI.getSource()) { + strategy = UserAiStrategyEnum.STAR_TECH_PAI.updateCondition(strategy); + } else { + strategy = UserAiStrategyEnum.STAR_JAVA_GUIDE.updateCondition(strategy); + } + } else { + // 有星球编号就直接走上面的判断,不走这里公众号的判断了 + // 如果绑定了微信公众号 + UserDO user = userDao.getUserByUserId(ai.getUserId()); + if (StringUtils.isNotBlank(user.getThirdAccountId())) { + strategy = UserAiStrategyEnum.WECHAT.updateCondition(strategy); + } + } + + ai.setStrategy(strategy); + this.saveOrUpdate(ai); + } + + public List listZsxqUsersByParams(SearchZsxqWhiteParams params) { + return userAiMapper.listZsxqUsersByParams(params, + PageParam.newPageInstance(params.getPageNum(), params.getPageSize())); + } + + public Long countZsxqUserByParams(SearchZsxqWhiteParams params) { + return userAiMapper.countZsxqUsersByParams(params); + } + + public void batchUpdateState(List ids, Integer code) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.in(UserAiDO::getId, ids).set(UserAiDO::getState, code); + userAiMapper.update(null, updateWrapper); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/dao/UserAiHistoryDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/dao/UserAiHistoryDao.java new file mode 100644 index 000000000..4d67421ef --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/dao/UserAiHistoryDao.java @@ -0,0 +1,16 @@ +package com.github.paicoding.forum.service.user.repository.dao; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.paicoding.forum.service.user.repository.entity.UserAiHistoryDO; +import com.github.paicoding.forum.service.user.repository.mapper.UserAiHistoryMapper; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; + +@Repository +public class UserAiHistoryDao extends ServiceImpl { + + @Resource + private UserAiHistoryMapper userAiHistoryMapper; +} + diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/dao/UserDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/dao/UserDao.java new file mode 100644 index 000000000..8bad97c7f --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/dao/UserDao.java @@ -0,0 +1,125 @@ +package com.github.paicoding.forum.service.user.repository.dao; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.paicoding.forum.api.model.enums.YesOrNoEnum; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.service.user.repository.entity.UserDO; +import com.github.paicoding.forum.service.user.repository.entity.UserInfoDO; +import com.github.paicoding.forum.service.user.repository.mapper.UserInfoMapper; +import com.github.paicoding.forum.service.user.repository.mapper.UserMapper; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +/** + * UserDao + * + * @author YiHui + * @date 2022/9/2 + */ +@Repository +public class UserDao extends ServiceImpl { + + @Resource + private UserMapper userMapper; + + public List scanUserId(Long userId, Integer size) { + return userMapper.getUserIdsOrderByIdAsc(userId, size == null ? PageParam.DEFAULT_PAGE_SIZE : size); + } + + /** + * 三方账号登录方式 + * + * @param accountId + * @return + */ + public UserDO getByThirdAccountId(String accountId) { + return userMapper.getByThirdAccountId(accountId); + } + + /** + * 根据用户名来查询 + * + * @param userName + * @return + */ + public List getByUserNameLike(String userName) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.select(UserInfoDO::getUserId, UserInfoDO::getUserName, UserInfoDO::getPhoto, UserInfoDO::getProfile) + .and(!StringUtils.isEmpty(userName), + v -> v.like(UserInfoDO::getUserName, userName) + ) + .eq(UserInfoDO::getDeleted, YesOrNoEnum.NO.getCode()); + return baseMapper.selectList(query); + } + + public void saveUser(UserDO user) { + if (user.getId() == null) { + userMapper.insert(user); + } else { + userMapper.updateById(user); + } + } + + public UserInfoDO getByUserId(Long userId) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(UserInfoDO::getUserId, userId) + .eq(UserInfoDO::getDeleted, YesOrNoEnum.NO.getCode()); + return baseMapper.selectOne(query); + } + + public List getByUserIds(Collection userIds) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.in(UserInfoDO::getUserId, userIds) + .eq(UserInfoDO::getDeleted, YesOrNoEnum.NO.getCode()); + return baseMapper.selectList(query); + } + + public Long getUserCount() { + return lambdaQuery() + .eq(UserInfoDO::getDeleted, YesOrNoEnum.NO.getCode()) + .count(); + } + + public void updateUserInfo(UserInfoDO user) { + UserInfoDO record = getByUserId(user.getUserId()); + if (record.equals(user)) { + return; + } + if (StringUtils.isEmpty(user.getPhoto())) { + user.setPhoto(null); + } + if (StringUtils.isEmpty(user.getUserName())) { + user.setUserName(null); + } + if (StringUtils.isEmpty(user.getEmail())) { + user.setEmail(null); + } + if (StringUtils.isBlank(user.getPayCode())) { + user.setPayCode(null); + } + user.setId(record.getId()); + updateById(user); + } + + public UserDO getUserByUserName(String userName) { + LambdaQueryWrapper queryUser = Wrappers.lambdaQuery(); + queryUser.eq(UserDO::getUserName, userName) + .eq(UserDO::getDeleted, YesOrNoEnum.NO.getCode()) + .last("limit 1"); + return userMapper.selectOne(queryUser); + } + + public UserDO getUserByUserId(Long userId) { + return userMapper.selectById(userId); + } + + public void updateUser(UserDO userDO) { + userMapper.updateById(userDO); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/dao/UserFootDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/dao/UserFootDao.java new file mode 100644 index 000000000..f619e10a3 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/dao/UserFootDao.java @@ -0,0 +1,101 @@ +package com.github.paicoding.forum.service.user.repository.dao; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.paicoding.forum.api.model.enums.DocumentTypeEnum; +import com.github.paicoding.forum.api.model.enums.PraiseStatEnum; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.user.dto.ArticleFootCountDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.SimpleUserInfoDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.UserFootStatisticDTO; +import com.github.paicoding.forum.service.user.repository.entity.UserFootDO; +import com.github.paicoding.forum.service.user.repository.mapper.UserFootMapper; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * @author YiHui + * @date 2022/9/2 + */ +@Repository +public class UserFootDao extends ServiceImpl { + public UserFootDO getByDocumentAndUserId(Long documentId, Integer type, Long userId) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(UserFootDO::getDocumentId, documentId) + .eq(UserFootDO::getDocumentType, type) + .eq(UserFootDO::getUserId, userId); + return baseMapper.selectOne(query); + } + + public List listDocumentPraisedUsers(Long documentId, Integer type, int size) { + return baseMapper.listSimpleUserInfosByArticleId(documentId, type, size); + } + + /** + * 查询用户收藏的文章列表 + * + * @param userId + * @param pageParam + * @return + */ + public List listCollectedArticlesByUserId(Long userId, PageParam pageParam) { + return baseMapper.listCollectedArticlesByUserId(userId, pageParam); + } + + + /** + * 查询用户阅读的文章列表 + * + * @param userId + * @param pageParam + * @return + */ + public List listReadArticleByUserId(Long userId, PageParam pageParam) { + return baseMapper.listReadArticleByUserId(userId, pageParam); + } + + /** + * 查询文章计数信息 + * + * @param articleId + * @return + */ + public ArticleFootCountDTO countArticleByArticleId(Long articleId) { + return baseMapper.countArticleByArticleId(articleId); + } + + /** + * 查询作者的文章统计 + * + * @param author + * @return + */ + public ArticleFootCountDTO countArticleByUserId(Long author) { + // 统计收藏、点赞数 + ArticleFootCountDTO count = baseMapper.countArticleByUserId(author); + Optional.ofNullable(count).ifPresent(s -> s.setReadCount(baseMapper.countArticleReadsByUserId(author))); + return count; + } + + /** + * 查询评论的点赞数 + * + * @param commentId + * @return + */ + public Long countCommentPraise(Long commentId) { + return lambdaQuery() + .eq(UserFootDO::getDocumentId, commentId) + .eq(UserFootDO::getDocumentType, DocumentTypeEnum.COMMENT.getCode()) + .eq(UserFootDO::getPraiseStat, PraiseStatEnum.PRAISE.getCode()) + .count(); + } + + public UserFootStatisticDTO getFootCount() { + return baseMapper.getFootCount(); + + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/dao/UserRelationDao.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/dao/UserRelationDao.java new file mode 100644 index 000000000..cbfb23bac --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/dao/UserRelationDao.java @@ -0,0 +1,104 @@ +package com.github.paicoding.forum.service.user.repository.dao; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.paicoding.forum.api.model.enums.FollowStateEnum; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.user.dto.FollowUserInfoDTO; +import com.github.paicoding.forum.service.user.repository.entity.UserRelationDO; +import com.github.paicoding.forum.service.user.repository.mapper.UserRelationMapper; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.List; + +/** + * 用户相关DB操作 + * + * @author louzai + * @date 2022-07-18 + */ +@Repository +public class UserRelationDao extends ServiceImpl { + + /** + * 查询用户的关注列表 + * + * @param followUserId + * @param pageParam + * @return + */ + public List listUserFollows(Long followUserId, PageParam pageParam) { + return baseMapper.queryUserFollowList(followUserId, pageParam); + } + + /** + * 查询用户的粉丝列表,即关注userId的用户 + * + * @param userId + * @param pageParam + * @return + */ + public List listUserFans(Long userId, PageParam pageParam) { + return baseMapper.queryUserFansList(userId, pageParam); + } + + /** + * 查询followUserId与给定的用户列表的关联关旭 + * + * @param followUserId 粉丝用户id + * @param targetUserId 关注者用户id列表 + * @return + */ + public List listUserRelations(Long followUserId, Collection targetUserId) { + return lambdaQuery().eq(UserRelationDO::getFollowUserId, followUserId) + .in(UserRelationDO::getUserId, targetUserId).list(); + } + + public Long queryUserFollowCount(Long userId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(UserRelationDO::getFollowUserId, userId) + .eq(UserRelationDO::getFollowState, FollowStateEnum.FOLLOW.getCode()); + return baseMapper.selectCount(queryWrapper); + } + + public Long queryUserFansCount(Long userId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(UserRelationDO::getUserId, userId) + .eq(UserRelationDO::getFollowState, FollowStateEnum.FOLLOW.getCode()); + return baseMapper.selectCount(queryWrapper); + } + + /** + * 获取关注信息 + * + * @param userId 登录用户 + * @param followUserId 关注的用户 + * @return + */ + public UserRelationDO getUserRelationByUserId(Long userId, Long followUserId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(UserRelationDO::getUserId, userId) + .eq(UserRelationDO::getFollowUserId, followUserId) + .eq(UserRelationDO::getFollowState, FollowStateEnum.FOLLOW.getCode()); + return baseMapper.selectOne(queryWrapper); + } + + /** + * 获取关注记录 + * + * @param userId 登录用户 + * @param followUserId 关注的用户 + * @return + */ + public UserRelationDO getUserRelationRecord(Long userId, Long followUserId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(UserRelationDO::getUserId, userId) + .eq(UserRelationDO::getFollowUserId, followUserId); + return baseMapper.selectOne(queryWrapper); + } +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/IpInfo.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/IpInfo.java new file mode 100644 index 000000000..f6360bb8d --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/IpInfo.java @@ -0,0 +1,24 @@ +package com.github.paicoding.forum.service.user.repository.entity; + +import lombok.Data; + +import java.io.Serializable; + +/** + * ip信息 + * + * @author YiHui + * @date 2022-12-29 + */ +@Data +public class IpInfo implements Serializable { + private static final long serialVersionUID = -4612222921661930429L; + + private String firstIp; + + private String firstRegion; + + private String latestIp; + + private String latestRegion; +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserAiDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserAiDO.java new file mode 100644 index 000000000..0751bcab3 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserAiDO.java @@ -0,0 +1,72 @@ +package com.github.paicoding.forum.service.user.repository.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * ai用户表 + * + * @ClassName: UserAiDO + * @Author: ygl + * @Date: 2023/6/25 21:38 + * @Version: 1.0 + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +@TableName("user_ai") +public class UserAiDO extends BaseDO { + + /** + * 用户id + */ + private Long userId; + + /** + * 知识星球编号 + */ + private String starNumber; + + /** + * 星球来源 1=java进阶之路 2=技术派 + */ + private Integer starType; + + /** + * 当前用户绑定的邀请者 + */ + private Long inviterUserId; + + /** + * 邀请码 + */ + private String inviteCode; + + /** + * 当前用户邀请的人数 + */ + private Integer inviteNum; + + /** + * 二进制使用姿势
+ * 第0位: = 1 表示已绑定微信公众号
+ * 第1位: = 1 表示绑定了邀请用户
+ * 第2位: = 1 表示绑定了java星球
+ * 第3位: = 1 表示绑定了技术派星球 + */ + private Integer strategy; + + /** + * 0 审核中 1 试用中 2 审核通过 3 审核拒绝 + */ + private Integer state; + + /** + * 是否删除 + */ + private Integer deleted; + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserAiHistoryDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserAiHistoryDO.java new file mode 100644 index 000000000..4423d4b1c --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserAiHistoryDO.java @@ -0,0 +1,43 @@ +package com.github.paicoding.forum.service.user.repository.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * AI 历史消息表 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("user_ai_history") +public class UserAiHistoryDO extends BaseDO { + + /** + * 用户id + */ + private Long userId; + + /** + * 问题 + */ + private String question; + + /** + * 答案 + */ + private String answer; + + /** + * AI 类型 + * + * @see AISourceEnum#getCode() + */ + private Integer aiType; + + /** + * 会话id + */ + private String chatId; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserDO.java new file mode 100644 index 000000000..5222f63ef --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserDO.java @@ -0,0 +1,46 @@ +package com.github.paicoding.forum.service.user.repository.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 用户登录表 + * + * @author louzai + * @date 2022-07-18 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("user") +public class UserDO extends BaseDO { + + private static final long serialVersionUID = 1L; + + /** + * 第三方用户ID + */ + private String thirdAccountId; + + /** + * 登录方式: 0-微信登录,1-账号密码登录 + */ + private Integer loginType; + + /** + * 删除标记 + */ + private Integer deleted; + + /** + * 登录用户名 + */ + private String userName; + + /** + * 登录密码,密文存储 + */ + private String password; + +} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/entity/UserFootDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserFootDO.java similarity index 89% rename from forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/entity/UserFootDO.java rename to paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserFootDO.java index 463f726f9..be9b8afd7 100644 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/entity/UserFootDO.java +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserFootDO.java @@ -1,7 +1,7 @@ -package com.github.liuyueyi.forum.service.user.repository.entity; +package com.github.paicoding.forum.service.user.repository.entity; import com.baomidou.mybatisplus.annotation.TableName; -import com.github.liueyueyi.forum.api.model.entity.BaseDO; +import com.github.paicoding.forum.api.model.entity.BaseDO; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserInfoDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserInfoDO.java new file mode 100644 index 000000000..795fbf0ec --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserInfoDO.java @@ -0,0 +1,92 @@ +package com.github.paicoding.forum.service.user.repository.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.github.paicoding.forum.api.model.entity.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 用户个人信息表 + * + * @author louzai + * @date 2022-07-18 + */ +@Data +@EqualsAndHashCode(callSuper = true) +// autoResultMap 必须存在,否则ip对象无法正确获取 +@TableName(value = "user_info", autoResultMap = true) +public class UserInfoDO extends BaseDO { + + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String userName; + + /** + * 用户图像 + */ + private String photo; + + /** + * 职位 + */ + private String position; + + /** + * 公司 + */ + private String company; + + /** + * 个人简介 + */ + private String profile; + + /** + * 扩展字段 + */ + private String extend; + + /** + * 删除标记 + */ + private Integer deleted; + + /** + * 0 普通用户 + * 1 超级管理员 + */ + private Integer userRole; + + /** + * ip信息 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private IpInfo ip; + + /** + * 用户的邮箱 + */ + private String email; + + /** + * 收款码信息 + */ + private String payCode; + + public IpInfo getIp() { + if (ip == null) { + ip = new IpInfo(); + } + return ip; + } +} diff --git a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/entity/UserRelationDO.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserRelationDO.java similarity index 76% rename from forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/entity/UserRelationDO.java rename to paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserRelationDO.java index 8d7a8e2f2..2dfc52efb 100644 --- a/forum-service/src/main/java/com/github/liuyueyi/forum/service/user/repository/entity/UserRelationDO.java +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/entity/UserRelationDO.java @@ -1,7 +1,7 @@ -package com.github.liuyueyi.forum.service.user.repository.entity; +package com.github.paicoding.forum.service.user.repository.entity; import com.baomidou.mybatisplus.annotation.TableName; -import com.github.liueyueyi.forum.api.model.entity.BaseDO; +import com.github.paicoding.forum.api.model.entity.BaseDO; import lombok.Data; import lombok.EqualsAndHashCode; @@ -19,12 +19,12 @@ public class UserRelationDO extends BaseDO { private static final long serialVersionUID = 1L; /** - * 用户ID + * 主用户ID */ private Long userId; /** - * 关注用户ID + * 粉丝用户ID */ private Long followUserId; diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserAiHistoryMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserAiHistoryMapper.java new file mode 100644 index 000000000..e6dc63373 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserAiHistoryMapper.java @@ -0,0 +1,8 @@ +package com.github.paicoding.forum.service.user.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.service.user.repository.entity.UserAiHistoryDO; + +public interface UserAiHistoryMapper extends BaseMapper { + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserAiMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserAiMapper.java new file mode 100644 index 000000000..75b12a350 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserAiMapper.java @@ -0,0 +1,24 @@ +package com.github.paicoding.forum.service.user.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.user.dto.ZsxqUserInfoDTO; +import com.github.paicoding.forum.service.user.repository.entity.UserAiDO; +import com.github.paicoding.forum.service.user.repository.params.SearchZsxqWhiteParams; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * ai用户登录mapper接口 + * + * @author ygl + * @date 2022-07-18 + */ +public interface UserAiMapper extends BaseMapper { + + Long countZsxqUsersByParams(@Param("searchParams") SearchZsxqWhiteParams params); + + List listZsxqUsersByParams(@Param("searchParams") SearchZsxqWhiteParams params, + @Param("pageParam") PageParam newPageInstance); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserFootMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserFootMapper.java new file mode 100644 index 000000000..b0f96f713 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserFootMapper.java @@ -0,0 +1,78 @@ +package com.github.paicoding.forum.service.user.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.user.dto.ArticleFootCountDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.SimpleUserInfoDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.UserFootStatisticDTO; +import com.github.paicoding.forum.service.user.repository.entity.UserFootDO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 用户足迹mapper接口 + * + * @author louzai + * @date 2022-07-18 + */ +public interface UserFootMapper extends BaseMapper { + + /** + * 查询文章计数信息 + * + * @param articleId + * @return + */ + ArticleFootCountDTO countArticleByArticleId(@Param("articleId") Long articleId); + + /** + * 查询作者的文章统计 + * + * @param author + * @return + */ + ArticleFootCountDTO countArticleByUserId(@Param("userId") Long author); + + /** + * 查询作者的所有文章阅读计数 + * + * @param author + * @return + */ + Integer countArticleReadsByUserId(@Param("userId") Long author); + + /** + * 查询用户收藏的文章列表 + * + * @param userId + * @param pageParam + * @return + */ + List listCollectedArticlesByUserId(@Param("userId") Long userId, @Param("pageParam") PageParam pageParam); + + + /** + * 查询用户阅读的文章列表 + * + * @param userId + * @param pageParam + * @return + */ + List listReadArticleByUserId(@Param("userId") Long userId, @Param("pageParam") PageParam pageParam); + + /** + * 查询文章的点赞列表 + * + * @param documentId + * @param type + * @param size + * @return + */ + List listSimpleUserInfosByArticleId(@Param("documentId") Long documentId, + @Param("type") Integer type, + @Param("size") int size); + + + UserFootStatisticDTO getFootCount(); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserInfoMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserInfoMapper.java new file mode 100644 index 000000000..e7684ed1b --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserInfoMapper.java @@ -0,0 +1,13 @@ +package com.github.paicoding.forum.service.user.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.service.user.repository.entity.UserInfoDO; + +/** + * 用户个人信息mapper接口 + * + * @author louzai + * @date 2022-07-18 + */ +public interface UserInfoMapper extends BaseMapper { +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserMapper.java new file mode 100644 index 000000000..f8642ef0f --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserMapper.java @@ -0,0 +1,36 @@ +package com.github.paicoding.forum.service.user.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.service.user.repository.entity.UserDO; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 用户登录mapper接口 + * + * @author louzai + * @date 2022-07-18 + */ +public interface UserMapper extends BaseMapper { + /** + * 根据三方唯一id进行查询 + * + * @param accountId + * @return + */ + @Select("select * from user where third_account_id = #{account_id} limit 1") + UserDO getByThirdAccountId(@Param("account_id") String accountId); + + + /** + * 遍历用户id + * + * @param offsetUserId + * @param size + * @return + */ + @Select("select id from user where id > #{offsetUserId} order by id asc limit #{size}") + List getUserIdsOrderByIdAsc(@Param("offsetUserId") Long offsetUserId, @Param("size") Long size); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserRelationMapper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserRelationMapper.java new file mode 100644 index 000000000..578edcd60 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/mapper/UserRelationMapper.java @@ -0,0 +1,34 @@ +package com.github.paicoding.forum.service.user.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.user.dto.FollowUserInfoDTO; +import com.github.paicoding.forum.service.user.repository.entity.UserRelationDO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 用户关系mapper接口 + * + * @author louzai + * @date 2022-07-18 + */ +public interface UserRelationMapper extends BaseMapper { + + /** + * 我关注的用户 + * @param followUserId + * @param pageParam + * @return + */ + List queryUserFollowList(@Param("followUserId") Long followUserId, @Param("pageParam") PageParam pageParam); + + /** + * 关注我的粉丝 + * @param userId + * @param pageParam + * @return + */ + List queryUserFansList(@Param("userId") Long userId, @Param("pageParam") PageParam pageParam); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/params/SearchZsxqWhiteParams.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/params/SearchZsxqWhiteParams.java new file mode 100644 index 000000000..e3a4819f9 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/repository/params/SearchZsxqWhiteParams.java @@ -0,0 +1,37 @@ +package com.github.paicoding.forum.service.user.repository.params; + +import com.github.paicoding.forum.api.model.vo.PageParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 6/29/23 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class SearchZsxqWhiteParams extends PageParam { + + /** + * 审核状态 + */ + private Integer status; + + /** + * 星球编号 + */ + private String starNumber; + + /** + * 登录用户名 + */ + private String name; + + /** + * 用户编号 + */ + private String userCode; + +} \ No newline at end of file diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/AuthorWhiteListService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/AuthorWhiteListService.java new file mode 100644 index 000000000..561882a87 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/AuthorWhiteListService.java @@ -0,0 +1,43 @@ +package com.github.paicoding.forum.service.user.service; + +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; + +import java.util.List; + +/** + * @author YiHui + * @date 2023/4/9 + */ +public interface AuthorWhiteListService { + + /** + * 判断作者是否再文章发布的白名单中; + * 这个白名单主要是用于控制作者发文章之后是否需要进行审核 + * + * @param authorId + * @return + */ + boolean authorInArticleWhiteList(Long authorId); + + /** + * 获取所有的白名单用户 + * + * @return + */ + List queryAllArticleWhiteListAuthors(); + + /** + * 将用户添加到白名单中 + * + * @param userId + */ + void addAuthor2ArticleWhitList(Long userId); + + /** + * 从白名单中移除用户 + * + * @param userId + */ + void removeAuthorFromArticleWhiteList(Long userId); + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/LoginService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/LoginService.java new file mode 100644 index 000000000..af5807c99 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/LoginService.java @@ -0,0 +1,53 @@ +package com.github.paicoding.forum.service.user.service; + +import com.github.paicoding.forum.api.model.vo.user.UserPwdLoginReq; + +/** + * @author YiHui + * @date 2022/8/15 + */ +public interface LoginService { + String SESSION_KEY = "f-session"; + String USER_DEVICE_KEY = "f-device"; + + + /** + * 适用于微信公众号登录场景下,自动注册一个用户 + * + * @param uuid 微信唯一标识 + * @return userId 用户主键 + */ + Long autoRegisterWxUserInfo(String uuid); + + /** + * 登出 + * + * @param session 用户会话 + */ + void logout(String session); + + /** + * 给微信公众号的用户生成一个用于登录的会话 + * + * @param userId 用户主键id + * @return + */ + String loginByWx(Long userId); + + /** + * 用户名密码方式登录 + * + * @param username 用户名 + * @param password 密码 + * @return + */ + String loginByUserPwd(String username, String password); + + /** + * 注册登录,并绑定对应的星球、邀请码 + * + * @param loginReq 登录信息 + * @return + */ + String registerByUserPwd(UserPwdLoginReq loginReq); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/RegisterService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/RegisterService.java new file mode 100644 index 000000000..7382a015f --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/RegisterService.java @@ -0,0 +1,38 @@ +package com.github.paicoding.forum.service.user.service; + +import com.github.paicoding.forum.api.model.vo.user.UserPwdLoginReq; + +/** + * 用户注册服务 + * + * @author YiHui + * @date 2023/6/26 + */ +public interface RegisterService { + + /** + * 注册系统用户 + * + * @param loginUser + * @param nickUser + * @param avatar + * @return + */ + Long registerSystemUser(String loginUser, String nickUser, String avatar); + + /** + * 通过用户名/密码进行注册 + * + * @param loginReq + * @return + */ + Long registerByUserNameAndPassword(UserPwdLoginReq loginReq); + + /** + * 通过微信公众号进行注册 + * + * @param thirdAccount + * @return + */ + Long registerByWechat(String thirdAccount); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/UserAiService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/UserAiService.java new file mode 100644 index 000000000..4837d9676 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/UserAiService.java @@ -0,0 +1,32 @@ +package com.github.paicoding.forum.service.user.service; + +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.user.UserPwdLoginReq; + +public interface UserAiService { + /** + * 保存聊天历史记录 + * + * @param source + * @param user + * @param item + */ + void pushChatItem(AISourceEnum source, Long user, ChatItemVo item); + + /** + * 获取用户的最大聊天次数 + * + * @param userId + * @return + */ + int getMaxChatCnt(Long userId); + + /** + * 重建用户绑定的星球编号 + * + * @param loginReq + */ + + void initOrUpdateAiInfo(UserPwdLoginReq loginReq); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/UserFootService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/UserFootService.java new file mode 100644 index 000000000..8c5593809 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/UserFootService.java @@ -0,0 +1,103 @@ +package com.github.paicoding.forum.service.user.service; + +import com.github.paicoding.forum.api.model.enums.DocumentTypeEnum; +import com.github.paicoding.forum.api.model.enums.OperateTypeEnum; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.user.dto.SimpleUserInfoDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.UserFootStatisticDTO; +import com.github.paicoding.forum.service.comment.repository.entity.CommentDO; +import com.github.paicoding.forum.service.user.repository.entity.UserFootDO; + +import java.util.List; + +/** + * 用户足迹Service接口 + * + * @author louzai + * @date 2022-07-20 + */ +public interface UserFootService { + /** + * 保存或更新状态信息 + * + * @param documentType 文档类型:博文 + 评论 + * @param documentId 文档id + * @param authorId 作者 + * @param userId 操作人 + * @param operateTypeEnum 操作类型:点赞,评论,收藏等 + * @return + */ + UserFootDO saveOrUpdateUserFoot(DocumentTypeEnum documentType, Long documentId, Long authorId, Long userId, OperateTypeEnum operateTypeEnum); + + /** + * 文章/评论点赞、取消点赞、收藏、取消收藏 + * + * @param documentType 文档类型:博文 + 评论 + * @param documentId 文档id + * @param authorId 作者 + * @param userId 操作人 + * @param operateTypeEnum 操作类型:点赞,评论,收藏等 + */ + void favorArticleComment(DocumentTypeEnum documentType, Long documentId, Long authorId, Long userId, OperateTypeEnum operateTypeEnum); + + + /** + * 保存评论足迹 + * 1. 用户文章记录上,设置为已评论 + * 2. 若改评论为回复别人的评论,则针对父评论设置为已评论 + * + * @param comment 保存评论入参 + * @param articleAuthor 文章作者 + * @param parentCommentAuthor 父评论作者 + */ + void saveCommentFoot(CommentDO comment, Long articleAuthor, Long parentCommentAuthor); + + /** + * 删除评论足迹 + * + * @param comment 保存评论入参 + * @param articleAuthor 文章作者 + * @param parentCommentAuthor 父评论作者 + */ + void removeCommentFoot(CommentDO comment, Long articleAuthor, Long parentCommentAuthor); + + + /** + * 查询已读文章列表 + * + * @param userId + * @param pageParam + * @return + */ + List queryUserReadArticleList(Long userId, PageParam pageParam); + + /** + * 查询收藏文章列表 + * + * @param userId + * @param pageParam + * @return + */ + List queryUserCollectionArticleList(Long userId, PageParam pageParam); + + /** + * 查询文章的点赞用户信息 + * + * @param articleId + * @return + */ + List queryArticlePraisedUsers(Long articleId); + + + /** + * 查询用户记录,用于判断是否点过赞、是否评论、是否收藏过 + * + * @param documentId + * @param type + * @param userId + * @return + */ + UserFootDO queryUserFoot(Long documentId, Integer type, Long userId); + + UserFootStatisticDTO getFootCount(); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/UserRelationService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/UserRelationService.java new file mode 100644 index 000000000..fdf0e0f19 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/UserRelationService.java @@ -0,0 +1,62 @@ +package com.github.paicoding.forum.service.user.service; + +import com.github.paicoding.forum.api.model.vo.PageListVo; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.user.UserRelationReq; +import com.github.paicoding.forum.api.model.vo.user.dto.FollowUserInfoDTO; + +import java.util.List; +import java.util.Set; + +/** + * 用户关系Service接口 + * + * @author louzai + * @date 2022-07-20 + */ +public interface UserRelationService { + + /** + * 我关注的用户 + * + * @param userId + * @param pageParam + * @return + */ + PageListVo getUserFollowList(Long userId, PageParam pageParam); + + + /** + * 关注我的粉丝 + * + * @param userId + * @param pageParam + * @return + */ + PageListVo getUserFansList(Long userId, PageParam pageParam); + + /** + * 更新当前登录用户与列表中的用户的关注关系 + * + * @param followList + * @param loginUserId + */ + void updateUserFollowRelationId(PageListVo followList, Long loginUserId); + + /** + * 根据登录用户从给定用户列表中,找出已关注的用户id + * + * @param userIds + * @param loginUserId + * @return + */ + Set getFollowedUserId(List userIds, Long loginUserId); + + /** + * 保存用户关系: 关注or取消关注 + * + * @param req + * @throws Exception + */ + void saveUserRelation(UserRelationReq req); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/UserService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/UserService.java new file mode 100644 index 000000000..faa4bf95a --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/UserService.java @@ -0,0 +1,115 @@ +package com.github.paicoding.forum.service.user.service; + +import com.github.paicoding.forum.api.model.vo.user.UserInfoSaveReq; +import com.github.paicoding.forum.api.model.vo.user.UserPwdLoginReq; +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.SimpleUserInfoDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.UserStatisticInfoDTO; +import com.github.paicoding.forum.service.user.repository.entity.UserDO; + +import java.util.Collection; +import java.util.List; + +/** + * 用户Service接口 + * + * @author louzai + * @date 2022-07-20 + */ +public interface UserService { + /** + * 判断微信用户是否注册过 + * + * @param wxuuid + * @return + */ + UserDO getWxUser(String wxuuid); + + /** + * 根据用户名模糊搜索用户 + * + * @param userName 用户名 + * @return + */ + List searchUser(String userName); + + /** + * 保存用户详情 + * + * @param req + */ + void saveUserInfo(UserInfoSaveReq req); + + /** + * 获取登录的用户信息,并更行丢对应的ip信息 + * + * @param session 用户会话 + * @param clientIp 用户最新的登录ip + * @return 返回用户基本信息 + */ + BaseUserInfoDTO getAndUpdateUserIpInfoBySessionId(String session, String clientIp); + + /** + * 查询极简的用户信息 + * + * @param userId + * @return + */ + SimpleUserInfoDTO querySimpleUserInfo(Long userId); + + /** + * 查询用户基本信息 + * todo: 可以做缓存优化 + * + * @param userId + * @return + */ + BaseUserInfoDTO queryBasicUserInfo(Long userId); + + + /** + * 批量查询用户基本信息 + * + * @param userIds + * @return + */ + List batchQuerySimpleUserInfo(Collection userIds); + + /** + * 批量查询用户基本信息 + * + * @param userIds + * @return + */ + List batchQueryBasicUserInfo(Collection userIds); + + /** + * 查询用户主页信息 + * + * @param userId + * @return + * @throws Exception + */ + UserStatisticInfoDTO queryUserInfoWithStatistic(Long userId); + + /** + * 用户计数 + * + * @return + */ + Long getUserCount(); + + /** + * 绑定用户信息 + */ + void bindUserInfo(UserPwdLoginReq loginReq); + + + /** + * 根据登录用户名,查询用户信息 + * + * @param uname + * @return + */ + BaseUserInfoDTO queryUserByLoginName(String uname); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/ZsxqWhiteListService.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/ZsxqWhiteListService.java new file mode 100644 index 000000000..cd4c6e068 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/ZsxqWhiteListService.java @@ -0,0 +1,27 @@ +package com.github.paicoding.forum.service.user.service; + +import com.github.paicoding.forum.api.model.enums.user.UserAIStatEnum; +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.user.SearchZsxqUserReq; +import com.github.paicoding.forum.api.model.vo.user.ZsxqUserPostReq; +import com.github.paicoding.forum.api.model.vo.user.dto.ZsxqUserInfoDTO; + +import java.util.List; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 6/29/23 + */ +public interface ZsxqWhiteListService { + PageVo getList(SearchZsxqUserReq req); + + void operate(Long id, UserAIStatEnum operate); + + void update(ZsxqUserPostReq req); + + void batchOperate(List ids, UserAIStatEnum operate); + + void reset(Integer authorId); +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/ai/UserAiServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/ai/UserAiServiceImpl.java new file mode 100644 index 000000000..cc1ea67bb --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/ai/UserAiServiceImpl.java @@ -0,0 +1,122 @@ +package com.github.paicoding.forum.service.user.service.ai; + +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import com.github.paicoding.forum.api.model.enums.user.UserAIStatEnum; +import com.github.paicoding.forum.api.model.enums.user.UserAiStrategyEnum; +import com.github.paicoding.forum.api.model.vo.chat.ChatItemVo; +import com.github.paicoding.forum.api.model.vo.user.UserPwdLoginReq; +import com.github.paicoding.forum.service.chatai.bot.AiBots; +import com.github.paicoding.forum.service.user.converter.UserAiConverter; +import com.github.paicoding.forum.service.user.repository.dao.UserAiDao; +import com.github.paicoding.forum.service.user.repository.dao.UserAiHistoryDao; +import com.github.paicoding.forum.service.user.repository.entity.UserAiDO; +import com.github.paicoding.forum.service.user.repository.entity.UserAiHistoryDO; +import com.github.paicoding.forum.service.user.service.UserAiService; +import com.github.paicoding.forum.service.user.service.conf.AiConfig; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Objects; + +@Service +public class UserAiServiceImpl implements UserAiService { + @Resource + private UserAiHistoryDao userAiHistoryDao; + + @Resource + private UserAiDao userAiDao; + + @Resource + private AiConfig aiConfig; + + @Resource + private AiBots aiBots; + + @Override + public void pushChatItem(AISourceEnum source, Long user, ChatItemVo item) { + UserAiHistoryDO userAiHistoryDO = new UserAiHistoryDO(); + userAiHistoryDO.setAiType(source.getCode()); + userAiHistoryDO.setUserId(user); + userAiHistoryDO.setQuestion(item.getQuestion()); + userAiHistoryDO.setAnswer(item.getAnswer()); + userAiHistoryDO.setChatId(ReqInfoContext.getReqInfo().getChatId()); + userAiHistoryDao.save(userAiHistoryDO); + } + + /** + * 获取用户的最大使用次数 + * + * @param userId + * @return + */ + public int getMaxChatCnt(Long userId) { + // 对于系统AI机器人,不进行次数限制 + if (aiBots.aiBots(userId)) { + return Integer.MAX_VALUE; + } + + UserAiDO ai = userAiDao.getOrInitAiInfo(userId); + int strategy = ai.getStrategy(); + int cnt = 0; + + // 星球用户 +100 + if (UserAiStrategyEnum.STAR_JAVA_GUIDE.match(strategy) || UserAiStrategyEnum.STAR_TECH_PAI.match(strategy)) { + if (Objects.equals(ai.getState(), UserAIStatEnum.FORMAL.getCode())) { + // 审核通过 + cnt += aiConfig.getMaxNum().getStar(); + } else if (Objects.equals(ai.getState(), UserAIStatEnum.TRYING.getCode())) { + // 试用中 + cnt += aiConfig.getMaxNum().getStarTry(); + } + } else { + // 有星球走星球,无星球再走公众号 + // 微信公众号登录用户 +5次 + if (UserAiStrategyEnum.WECHAT.match(strategy)) { + cnt += aiConfig.getMaxNum().getWechat(); + } + } + + // 推荐机制,如果绑定了邀请码,则总次数 + 10% + if (UserAiStrategyEnum.INVITE_USER.match(strategy)) { + cnt = (int) (cnt + cnt * aiConfig.getMaxNum().getInvited()); + } + + // 根据推荐的人数,来进行增加 + if (ai.getInviteNum() > 0) { + cnt = cnt + ai.getInviteNum() * ((int) (cnt * aiConfig.getMaxNum().getInviteNum())); + } + + if (cnt == 0) { + // 对于登录用户,给五次使用机会 + cnt = aiConfig.getMaxNum().getBasic(); + } + return cnt; + } + + @Override + public void initOrUpdateAiInfo(UserPwdLoginReq loginReq) { + // 之前已经检查过编号是否已经被绑定过了,那我们直接进行绑定 + Long userId = loginReq.getUserId(); + UserAiDO userAiDO = userAiDao.getByUserId(userId); + if (userAiDO == null) { + // 初始化新的ai信息 + userAiDO = UserAiConverter.initAi(userId, loginReq.getStarNumber()); + } else if (StringUtils.isBlank(loginReq.getStarNumber()) && StringUtils.isBlank(loginReq.getInvitationCode())) { + // 没有传递星球和邀请码时,直接返回,不用更新ai信息 + return; + } else if (StringUtils.isNotBlank(loginReq.getStarNumber())) { + // 之前有绑定信息,检查到与之前的不一致,则执行更新星球编号流程 + if (!Objects.equals(loginReq.getStarNumber(), userAiDO.getStarNumber())) { + userAiDO.setStarNumber(loginReq.getStarNumber()); + } + // 并设置为试用 + userAiDO.setState(UserAIStatEnum.TRYING.getCode()); + if (ReqInfoContext.getReqInfo().getUser() != null) { + ReqInfoContext.getReqInfo().getUser().setStarStatus(UserAIStatEnum.TRYING); + } + } + userAiDao.saveOrUpdateAiBindInfo(userAiDO, loginReq.getInvitationCode()); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/conf/AiConfig.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/conf/AiConfig.java new file mode 100644 index 000000000..696f1b3fc --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/conf/AiConfig.java @@ -0,0 +1,64 @@ +package com.github.paicoding.forum.service.user.service.conf; + +import com.github.paicoding.forum.api.model.enums.ai.AISourceEnum; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @author YiHui + * @date 2023/6/29 + */ +@Data +@Component +@ConfigurationProperties(prefix = "ai") +public class AiConfig { + @Data + public static class AiMaxChatNumStrategyConf { + /** + * 默认的策略 + */ + private Integer basic; + /** + * 公众号用户 AI交互次数 + */ + private Integer wechat; + /** + * 星球用户 AI交互次数 + */ + private Integer star; + + // 星球最大编号 + private Integer starNumber; + /** + * 星球试用用户 AI交互次数 + */ + private Integer starTry; + /** + * 绑定了邀请者,再当前次数基础上新增的策略, 默认增加 10% + */ + private Float invited; + + /** + * 根据邀请的人数,增加的聊天次数策略,默认增加 20% + */ + private Float inviteNum; + + /** + * 多轮对话上下文的条数,默认最多给10条 + */ + private Integer historyContextCnt; + } + + /** + * 用户的最大使用次数配置项 + */ + private AiMaxChatNumStrategyConf maxNum; + + /** + * 当前支持的AI模型 + */ + private List source; +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/help/StarNumberHelper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/help/StarNumberHelper.java new file mode 100644 index 000000000..188b5440d --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/help/StarNumberHelper.java @@ -0,0 +1,24 @@ +package com.github.paicoding.forum.service.user.service.help; + +import com.github.paicoding.forum.service.user.service.conf.AiConfig; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 密码加密器,后续接入SpringSecurity之后,可以使用 PasswordEncoder 进行替换 + * + * @author YiHui + * @date 2022/12/5 + */ +@Component +public class StarNumberHelper { + @Resource + private AiConfig aiConfig; + + public Boolean checkStarNumber(String starNumber) { + // 判断编号是否在 0 - maxStarNumber 之间 + return Integer.parseInt(starNumber) >= 0 && Integer.parseInt(starNumber) <= aiConfig.getMaxNum().getStarNumber(); + } + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/help/UserPwdEncoder.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/help/UserPwdEncoder.java new file mode 100644 index 000000000..6c18a357a --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/help/UserPwdEncoder.java @@ -0,0 +1,46 @@ +package com.github.paicoding.forum.service.user.service.help; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.DigestUtils; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +/** + * 密码加密器,后续接入SpringSecurity之后,可以使用 PasswordEncoder 进行替换 + * + * @author YiHui + * @date 2022/12/5 + */ +@Component +public class UserPwdEncoder { + /** + * 密码加盐,更推荐的做法是每个用户都使用独立的盐,提高安全性 + */ + @Value("${security.salt}") + private String salt; + + @Value("${security.salt-index}") + private Integer saltIndex; + + public boolean match(String plainPwd, String encPwd) { + return Objects.equals(encPwd(plainPwd), encPwd); + } + + /** + * 明文密码处理 + * + * @param plainPwd + * @return + */ + public String encPwd(String plainPwd) { + if (plainPwd.length() > saltIndex) { + plainPwd = plainPwd.substring(0, saltIndex) + salt + plainPwd.substring(saltIndex); + } else { + plainPwd = plainPwd + salt; + } + return DigestUtils.md5DigestAsHex(plainPwd.getBytes(StandardCharsets.UTF_8)); + } + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/help/UserRandomGenHelper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/help/UserRandomGenHelper.java new file mode 100644 index 000000000..0293d63f7 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/help/UserRandomGenHelper.java @@ -0,0 +1,90 @@ +package com.github.paicoding.forum.service.user.service.help; + +import java.util.Random; + +/** + * 用户名生成器 + * + * @author YiHui + * @date 2022/9/27 + */ +public class UserRandomGenHelper { + public static final String[] name_decorate = new String[]{ + "迷你的", "鲜艳的", "飞快的", "真实的", "清新的", "幸福的", "可耐的", "快乐的", "冷静的", "醉熏的", "潇洒的", "糊涂的", "积极的", "冷酷的", "深情的", "粗暴的", + "温柔的", "可爱的", "愉快的", "义气的", "认真的", "威武的", "帅气的", "传统的", "潇洒的", "漂亮的", "自然的", "专一的", "听话的", "昏睡的", "狂野的", "等待的", "搞怪的", + "幽默的", "魁梧的", "活泼的", "开心的", "高兴的", "超帅的", "留胡子的", "坦率的", "直率的", "轻松的", "痴情的", "完美的", "精明的", "无聊的", "有魅力的", "丰富的", "繁荣的", + "饱满的", "炙热的", "暴躁的", "碧蓝的", "俊逸的", "英勇的", "健忘的", "故意的", "无心的", "土豪的", "朴实的", "兴奋的", "幸福的", "淡定的", "不安的", "阔达的", "孤独的", + "独特的", "疯狂的", "时尚的", "落后的", "风趣的", "忧伤的", "大胆的", "爱笑的", "矮小的", "健康的", "合适的", "玩命的", "沉默的", "斯文的", "香蕉", "苹果", "鲤鱼", "鳗鱼", + "任性的", "细心的", "粗心的", "大意的", "甜甜的", "酷酷的", "健壮的", "英俊的", "霸气的", "阳光的", "默默的", "大力的", "孝顺的", "忧虑的", "着急的", "紧张的", "善良的", + "凶狠的", "害怕的", "重要的", "危机的", "欢喜的", "欣慰的", "满意的", "跳跃的", "诚心的", "称心的", "如意的", "怡然的", "娇气的", "无奈的", "无语的", "激动的", "愤怒的", + "美好的", "感动的", "激情的", "激昂的", "震动的", "虚拟的", "超级的", "寒冷的", "精明的", "明理的", "犹豫的", "忧郁的", "寂寞的", "奋斗的", "勤奋的", "现代的", "过时的", + "稳重的", "热情的", "含蓄的", "开放的", "无辜的", "多情的", "纯真的", "拉长的", "热心的", "从容的", "体贴的", "风中的", "曾经的", "追寻的", "儒雅的", "优雅的", "开朗的", + "外向的", "内向的", "清爽的", "文艺的", "长情的", "平常的", "单身的", "伶俐的", "高大的", "懦弱的", "柔弱的", "爱笑的", "乐观的", "耍酷的", "酷炫的", "神勇的", "年轻的", + "唠叨的", "瘦瘦的", "无情的", "包容的", "顺心的", "畅快的", "舒适的", "靓丽的", "负责的", "背后的", "简单的", "谦让的", "彩色的", "缥缈的", "欢呼的", "生动的", "复杂的", + "慈祥的", "仁爱的", "魔幻的", "虚幻的", "淡然的", "受伤的", "雪白的", "高高的", "糟糕的", "顺利的", "闪闪的", "羞涩的", "缓慢的", "迅速的", "优秀的", "聪明的", "含糊的", + "俏皮的", "淡淡的", "坚强的", "平淡的", "欣喜的", "能干的", "灵巧的", "友好的", "机智的", "机灵的", "正直的", "谨慎的", "俭朴的", "殷勤的", "虚心的", "辛勤的", "自觉的", + "无私的", "无限的", "踏实的", "老实的", "现实的", "可靠的", "务实的", "拼搏的", "个性的", "粗犷的", "活力的", "成就的", "勤劳的", "单纯的", "落寞的", "朴素的", "悲凉的", + "忧心的", "洁净的", "清秀的", "自由的", "小巧的", "单薄的", "贪玩的", "刻苦的", "干净的", "壮观的", "和谐的", "文静的", "调皮的", "害羞的", "安详的", "自信的", "端庄的", + "坚定的", "美满的", "舒心的", "温暖的", "专注的", "勤恳的", "美丽的", "腼腆的", "优美的", "甜美的", "甜蜜的", "整齐的", "动人的", "典雅的", "尊敬的", "舒服的", "妩媚的", + "秀丽的", "喜悦的", "甜美的", "彪壮的", "强健的", "大方的", "俊秀的", "聪慧的", "迷人的", "陶醉的", "悦耳的", "动听的", "明亮的", "结实的", "魁梧的", "标致的", "清脆的", + "敏感的", "光亮的", "大气的", "老迟到的", "知性的", "冷傲的", "呆萌的", "野性的", "隐形的", "笑点低的", "微笑的", "笨笨的", "难过的", "沉静的", "火星上的", "失眠的", + "安静的", "纯情的", "要减肥的", "迷路的", "烂漫的", "哭泣的", "贤惠的", "苗条的", "温婉的", "发嗲的", "会撒娇的", "贪玩的", "执着的", "眯眯眼的", "花痴的", "想人陪的", + "眼睛大的", "高贵的", "傲娇的", "心灵美的", "爱撒娇的", "细腻的", "天真的", "怕黑的", "感性的", "飘逸的", "怕孤独的", "忐忑的", "高挑的", "傻傻的", "冷艳的", "爱听歌的", + "还单身的", "怕孤单的", "懵懂的" + }; + public static final String[] name_body = new String[]{ + "嚓茶", "皮皮虾", "皮卡丘", "马里奥", "小霸王", "凉面", "便当", "毛豆", "花生", "可乐", "灯泡", "哈密瓜", "野狼", "背包", "眼神", "缘分", "雪碧", "人生", "牛排", + "蚂蚁", "飞鸟", "灰狼", "斑马", "汉堡", "悟空", "巨人", "绿茶", "自行车", "保温杯", "大碗", "墨镜", "魔镜", "煎饼", "月饼", "月亮", "星星", "芝麻", "啤酒", "玫瑰", + "大叔", "小伙", "哈密瓜,数据线", "太阳", "树叶", "芹菜", "黄蜂", "蜜粉", "蜜蜂", "信封", "西装", "外套", "裙子", "大象", "猫咪", "母鸡", "路灯", "蓝天", "白云", + "星月", "彩虹", "微笑", "摩托", "板栗", "高山", "大地", "大树", "电灯胆", "砖头", "楼房", "水池", "鸡翅", "蜻蜓", "红牛", "咖啡", "机器猫", "枕头", "大船", "诺言", + "钢笔", "刺猬", "天空", "飞机", "大炮", "冬天", "洋葱", "春天", "夏天", "秋天", "冬日", "航空", "毛衣", "豌豆", "黑米", "玉米", "眼睛", "老鼠", "白羊", "帅哥", "美女", + "季节", "鲜花", "服饰", "裙子", "白开水", "秀发", "大山", "火车", "汽车", "歌曲", "舞蹈", "老师", "导师", "方盒", "大米", "麦片", "水杯", "水壶", "手套", "鞋子", "自行车", + "鼠标", "手机", "电脑", "书本", "奇迹", "身影", "香烟", "夕阳", "台灯", "宝贝", "未来", "皮带", "钥匙", "心锁", "故事", "花瓣", "滑板", "画笔", "画板", "学姐", "店员", + "电源", "饼干", "宝马", "过客", "大白", "时光", "石头", "钻石", "河马", "犀牛", "西牛", "绿草", "抽屉", "柜子", "往事", "寒风", "路人", "橘子", "耳机", "鸵鸟", "朋友", + "苗条", "铅笔", "钢笔", "硬币", "热狗", "大侠", "御姐", "萝莉", "毛巾", "期待", "盼望", "白昼", "黑夜", "大门", "黑裤", "钢铁侠", "哑铃", "板凳", "枫叶", "荷花", "乌龟", + "仙人掌", "衬衫", "大神", "草丛", "早晨", "心情", "茉莉", "流沙", "蜗牛", "战斗机", "冥王星", "猎豹", "棒球", "篮球", "乐曲", "电话", "网络", "世界", "中心", "鱼", "鸡", "狗", + "老虎", "鸭子", "雨", "羽毛", "翅膀", "外套", "火", "丝袜", "书包", "钢笔", "冷风", "八宝粥", "烤鸡", "大雁", "音响", "招牌", "胡萝卜", "冰棍", "帽子", "菠萝", "蛋挞", "香水", + "泥猴桃", "吐司", "溪流", "黄豆", "樱桃", "小鸽子", "小蝴蝶", "爆米花", "花卷", "小鸭子", "小海豚", "日记本", "小熊猫", "小懒猪", "小懒虫", "荔枝", "镜子", "曲奇", "金针菇", + "小松鼠", "小虾米", "酒窝", "紫菜", "金鱼", "柚子", "果汁", "百褶裙", "项链", "帆布鞋", "火龙果", "奇异果", "煎蛋", "唇彩", "小土豆", "高跟鞋", "戒指", "雪糕", "睫毛", "铃铛", + "手链", "香氛", "红酒", "月光", "酸奶", "银耳汤", "咖啡豆", "小蜜蜂", "小蚂蚁", "蜡烛", "棉花糖", "向日葵", "水蜜桃", "小蝴蝶", "小刺猬", "小丸子", "指甲油", "康乃馨", "糖豆", + "薯片", "口红", "超短裙", "乌冬面", "冰淇淋", "棒棒糖", "长颈鹿", "豆芽", "发箍", "发卡", "发夹", "发带", "铃铛", "小馒头", "小笼包", "小甜瓜", "冬瓜", "香菇", "小兔子", + "含羞草", "短靴", "睫毛膏", "小蘑菇", "跳跳糖", "小白菜", "草莓", "柠檬", "月饼", "百合", "纸鹤", "小天鹅", "云朵", "芒果", "面包", "海燕", "小猫咪", "龙猫", "唇膏", "鞋垫", + "羊", "黑猫", "白猫", "万宝路", "金毛", "山水", "音响", "纸飞机", "烧鹅" + }; + + private static final Random RANDOM = new Random(); + + private static final int AVATAR_NUM = 92; + + private static final String AVATAR_TEMPLATE = "https://cdn.tobebetterjavaer.com/paicoding/avatar/%04d.png"; + + /** + * 昵称自动生成器 + * + * @return + */ + public static String genNickName() { + int decorateIndex = RANDOM.nextInt(name_decorate.length); + int bodyIndex = RANDOM.nextInt(name_body.length); + return name_decorate[decorateIndex] + name_body[bodyIndex]; + } + + /** + * 头像自动选择 + * + * @return + */ + public static String genAvatar() { + return String.format(AVATAR_TEMPLATE, RANDOM.nextInt(AVATAR_NUM) + 1); + } + + /** + * 生成用户邀请码 + * 规则:前缀 + 年月日转十六进制 + * + * @return + */ + public static String genInviteCode(Long prefix) { + return String.format("%03x%04x", prefix, System.currentTimeMillis() / 1000 / 60 / 60 / 24).toUpperCase(); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/help/UserSessionHelper.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/help/UserSessionHelper.java new file mode 100644 index 000000000..5418a2391 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/help/UserSessionHelper.java @@ -0,0 +1,101 @@ +package com.github.paicoding.forum.service.user.service.help; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.github.paicoding.forum.core.cache.RedisClient; +import com.github.paicoding.forum.core.mdc.SelfTraceIdGenerator; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.core.util.MapUtils; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.util.Base64Utils; + +import java.util.Date; +import java.util.HashMap; +import java.util.Objects; + +/** + * 使用jwt来存储用户token,则不需要后端来存储session了 + * + * @author YiHui + * @date 2022/12/5 + */ +@Slf4j +@Component +public class UserSessionHelper { + @Component + @Data + @ConfigurationProperties("paicoding.jwt") + public static class JwtProperties { + /** + * 签发人 + */ + private String issuer; + /** + * 密钥 + */ + private String secret; + /** + * 有效期,毫秒时间戳 + */ + private Long expire; + } + + private final JwtProperties jwtProperties; + + private Algorithm algorithm; + private JWTVerifier verifier; + + public UserSessionHelper(JwtProperties jwtProperties) { + this.jwtProperties = jwtProperties; + algorithm = Algorithm.HMAC256(jwtProperties.getSecret()); + verifier = JWT.require(algorithm).withIssuer(jwtProperties.getIssuer()).build(); + } + + public String genSession(Long userId) { + // 1.生成jwt格式的会话,内部持有有效期,用户信息 + String session = JsonUtil.toStr(MapUtils.create("s", SelfTraceIdGenerator.generate(), "u", userId)); + String token = JWT.create().withIssuer(jwtProperties.getIssuer()).withExpiresAt(new Date(System.currentTimeMillis() + jwtProperties.getExpire())) + .withPayload(session) + .sign(algorithm); + + // 2.使用jwt生成的token时,后端可以不存储这个session信息, 完全依赖jwt的信息 + // 但是需要考虑到用户登出,需要主动失效这个token,而jwt本身无状态,所以再这里的redis做一个简单的token -> userId的缓存,用于双重判定 + RedisClient.setStrWithExpire(token, String.valueOf(userId), jwtProperties.getExpire() / 1000); + return token; + } + + public void removeSession(String session) { + RedisClient.del(session); + } + + /** + * 根据会话获取用户信息 + * + * @param session + * @return + */ + public Long getUserIdBySession(String session) { + // jwt的校验方式,如果token非法或者过期,则直接验签失败 + try { + DecodedJWT decodedJWT = verifier.verify(session); + String pay = new String(Base64Utils.decodeFromString(decodedJWT.getPayload())); + // jwt验证通过,获取对应的userId + String userId = String.valueOf(JsonUtil.toObj(pay, HashMap.class).get("u")); + + // 从redis中获取userId,解决用户登出,后台失效jwt token的问题 + String user = RedisClient.getStr(session); + if (user == null || !Objects.equals(userId, user)) { + return null; + } + return Long.valueOf(user); + } catch (Exception e) { + log.info("jwt token校验失败! token: {}, msg: {}", session, e.getMessage()); + return null; + } + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/relation/UserRelationServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/relation/UserRelationServiceImpl.java new file mode 100644 index 000000000..76c37b587 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/relation/UserRelationServiceImpl.java @@ -0,0 +1,124 @@ +package com.github.paicoding.forum.service.user.service.relation; + +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.api.model.enums.FollowStateEnum; +import com.github.paicoding.forum.api.model.enums.NotifyTypeEnum; +import com.github.paicoding.forum.api.model.vo.PageListVo; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.notify.NotifyMsgEvent; +import com.github.paicoding.forum.api.model.vo.user.UserRelationReq; +import com.github.paicoding.forum.api.model.vo.user.dto.FollowUserInfoDTO; +import com.github.paicoding.forum.core.util.MapUtils; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.service.user.converter.UserConverter; +import com.github.paicoding.forum.service.user.repository.dao.UserRelationDao; +import com.github.paicoding.forum.service.user.repository.entity.UserRelationDO; +import com.github.paicoding.forum.service.user.service.UserRelationService; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 用户关系Service + * + * @author louzai + * @date 2022-07-20 + */ +@Service +public class UserRelationServiceImpl implements UserRelationService { + @Resource + private UserRelationDao userRelationDao; + + + /** + * 查询用户的关注列表 + * + * @param userId + * @param pageParam + * @return + */ + @Override + public PageListVo getUserFollowList(Long userId, PageParam pageParam) { + List userRelationList = userRelationDao.listUserFollows(userId, pageParam); + return PageListVo.newVo(userRelationList, pageParam.getPageSize()); + } + + @Override + public PageListVo getUserFansList(Long userId, PageParam pageParam) { + List userRelationList = userRelationDao.listUserFans(userId, pageParam); + return PageListVo.newVo(userRelationList, pageParam.getPageSize()); + } + + @Override + public void updateUserFollowRelationId(PageListVo followList, Long loginUserId) { + if (loginUserId == null) { + followList.getList().forEach(r -> { + r.setRelationId(null); + r.setFollowed(false); + }); + return; + } + + // 判断登录用户与给定的用户列表的关注关系 + Set userIds = followList.getList().stream().map(FollowUserInfoDTO::getUserId).collect(Collectors.toSet()); + if (CollectionUtils.isEmpty(userIds)) { + return; + } + + List relationList = userRelationDao.listUserRelations(loginUserId, userIds); + Map relationMap = MapUtils.toMap(relationList, UserRelationDO::getUserId, r -> r); + followList.getList().forEach(follow -> { + UserRelationDO relation = relationMap.get(follow.getUserId()); + if (relation == null) { + follow.setRelationId(null); + follow.setFollowed(false); + } else if (Objects.equals(relation.getFollowState(), FollowStateEnum.FOLLOW.getCode())) { + follow.setRelationId(relation.getId()); + follow.setFollowed(true); + } else { + follow.setRelationId(relation.getId()); + follow.setFollowed(false); + } + }); + } + + /** + * 根据登录用户从给定用户列表中,找出已关注的用户id + * + * @param userIds 主用户列表 + * @param fansUserId 粉丝用户id + * @return 返回fansUserId已经关注过的用户id列表 + */ + @Override + public Set getFollowedUserId(List userIds, Long fansUserId) { + if (CollectionUtils.isEmpty(userIds)) { + return Collections.emptySet(); + } + + List relationList = userRelationDao.listUserRelations(fansUserId, userIds); + Map relationMap = MapUtils.toMap(relationList, UserRelationDO::getUserId, r -> r); + return relationMap.values().stream().filter(s -> s.getFollowState().equals(FollowStateEnum.FOLLOW.getCode())).map(UserRelationDO::getUserId).collect(Collectors.toSet()); + } + + @Override + public void saveUserRelation(UserRelationReq req) { + // 查询是否存在 + UserRelationDO userRelationDO = userRelationDao.getUserRelationRecord(req.getUserId(), ReqInfoContext.getReqInfo().getUserId()); + if (userRelationDO == null) { + userRelationDO = UserConverter.toDO(req); + userRelationDao.save(userRelationDO); + // 发布关注事件 + SpringUtil.publishEvent(new NotifyMsgEvent<>(this, NotifyTypeEnum.FOLLOW, userRelationDO)); + return; + } + + // 将是否关注状态重置 + userRelationDO.setFollowState(req.getFollowed() ? FollowStateEnum.FOLLOW.getCode() : FollowStateEnum.CANCEL_FOLLOW.getCode()); + userRelationDao.updateById(userRelationDO); + // 发布关注、取消关注事件 + SpringUtil.publishEvent(new NotifyMsgEvent<>(this, req.getFollowed() ? NotifyTypeEnum.FOLLOW : NotifyTypeEnum.CANCEL_FOLLOW, userRelationDO)); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/user/LoginServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/user/LoginServiceImpl.java new file mode 100644 index 000000000..460054d24 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/user/LoginServiceImpl.java @@ -0,0 +1,204 @@ +package com.github.paicoding.forum.service.user.service.user; + +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.api.model.exception.ExceptionUtil; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import com.github.paicoding.forum.api.model.vo.user.UserPwdLoginReq; +import com.github.paicoding.forum.api.model.vo.user.UserSaveReq; +import com.github.paicoding.forum.service.user.repository.dao.UserAiDao; +import com.github.paicoding.forum.service.user.repository.dao.UserDao; +import com.github.paicoding.forum.service.user.repository.entity.UserAiDO; +import com.github.paicoding.forum.service.user.repository.entity.UserDO; +import com.github.paicoding.forum.service.user.service.LoginService; +import com.github.paicoding.forum.service.user.service.RegisterService; +import com.github.paicoding.forum.service.user.service.UserAiService; +import com.github.paicoding.forum.service.user.service.UserService; +import com.github.paicoding.forum.service.user.service.help.StarNumberHelper; +import com.github.paicoding.forum.service.user.service.help.UserPwdEncoder; +import com.github.paicoding.forum.service.user.service.help.UserSessionHelper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 基于验证码、用户名密码的登录方式 + * + * @author YiHui + * @date 2022/8/15 + */ +@Service +@Slf4j +public class LoginServiceImpl implements LoginService { + @Autowired + private UserDao userDao; + + @Autowired + private UserAiDao userAiDao; + + @Autowired + private UserSessionHelper userSessionHelper; + @Autowired + private StarNumberHelper starNumberHelper; + + @Autowired + private RegisterService registerService; + + @Autowired + private UserPwdEncoder userPwdEncoder; + + @Autowired + private UserService userService; + + @Autowired + private UserAiService userAiService; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long autoRegisterWxUserInfo(String uuid) { + UserSaveReq req = new UserSaveReq().setLoginType(0).setThirdAccountId(uuid); + Long userId = registerOrGetUserInfo(req); + ReqInfoContext.getReqInfo().setUserId(userId); + return userId; + } + + /** + * 没有注册时,先注册一个用户;若已经有,则登录 + * + * @param req + */ + private Long registerOrGetUserInfo(UserSaveReq req) { + UserDO user = userDao.getByThirdAccountId(req.getThirdAccountId()); + if (user == null) { + return registerService.registerByWechat(req.getThirdAccountId()); + } + return user.getId(); + } + + @Override + public void logout(String session) { + userSessionHelper.removeSession(session); + } + + /** + * 给微信公众号的用户生成一个用于登录的会话 + * + * @param userId 用户id + * @return + */ + @Override + public String loginByWx(Long userId) { + return userSessionHelper.genSession(userId); + } + + /** + * 用户名密码方式登录 + * + * @param username 用户名 + * @param password 密码 + * @return + */ + @Override + public String loginByUserPwd(String username, String password) { + UserDO user = userDao.getUserByUserName(username); + if (user == null) { + throw ExceptionUtil.of(StatusEnum.USER_NOT_EXISTS, "userName=" + username); + } + + // passwordEncoder.matches(password, user.getPassword()); + + if (!userPwdEncoder.match(password, user.getPassword())) { + throw ExceptionUtil.of(StatusEnum.USER_PWD_ERROR); + } + + Long userId = user.getId(); + // 1. 为了兼容历史数据,对于首次登录成功的用户,初始化ai信息 + userAiService.initOrUpdateAiInfo(new UserPwdLoginReq().setUserId(userId).setUsername(username).setPassword(password)); + + // 登录成功,返回对应的session + ReqInfoContext.getReqInfo().setUserId(userId); + return userSessionHelper.genSession(userId); + } + + + /** + * 用户名密码方式登录,若用户不存在,则进行注册 + * + * @param loginReq 登录信息 + * @return + */ + @Override + public String registerByUserPwd(UserPwdLoginReq loginReq) { + // 1. 前置校验 + registerPreCheck(loginReq); + + // 2. 判断当前用户是否登录,若已经登录,则直接走绑定流程 + Long userId = ReqInfoContext.getReqInfo().getUserId(); + loginReq.setUserId(userId); + if (userId != null) { + // 2.1 如果用户已经登录,则走绑定用户信息流程 + userService.bindUserInfo(loginReq); + return ReqInfoContext.getReqInfo().getSession(); + } + + + // 3. 尝试使用用户名进行登录,若成功,则依然走绑定流程 + UserDO user = userDao.getUserByUserName(loginReq.getUsername()); + if (user != null) { + if (!userPwdEncoder.match(loginReq.getPassword(), user.getPassword())) { + // 3.1 用户名已经存在 + throw ExceptionUtil.of(StatusEnum.USER_LOGIN_NAME_REPEAT, loginReq.getUsername()); + } + + // 3.2 用户存在,尝试走绑定流程 + userId = user.getId(); + loginReq.setUserId(userId); + userAiService.initOrUpdateAiInfo(loginReq); + } else { + //4. 走用户注册流程 + userId = registerService.registerByUserNameAndPassword(loginReq); + } + ReqInfoContext.getReqInfo().setUserId(userId); + return userSessionHelper.genSession(userId); + } + + + /** + * 注册前置校验 + * + * @param loginReq + */ + private void registerPreCheck(UserPwdLoginReq loginReq) { + if (StringUtils.isBlank(loginReq.getUsername()) || StringUtils.isBlank(loginReq.getPassword())) { + throw ExceptionUtil.of(StatusEnum.USER_PWD_ERROR); + } + + String starNumber = loginReq.getStarNumber(); + // 若传了星球信息,首先进行校验 + if (StringUtils.isNotBlank(starNumber)) { + if (Boolean.FALSE.equals(starNumberHelper.checkStarNumber(starNumber))) { + // 星球编号校验不通过,直接抛异常 + throw ExceptionUtil.of(StatusEnum.USER_STAR_NOT_EXISTS, "星球编号=" + starNumber); + } + + UserAiDO userAi = userAiDao.getByStarNumber(starNumber); + + // 如果星球编号已经被绑定了 + if (userAi != null) { + // 判断星球是否已经被绑定了 + throw ExceptionUtil.of(StatusEnum.USER_STAR_REPEAT, starNumber); + } + } + + String invitationCode = loginReq.getInvitationCode(); + if (StringUtils.isNotBlank(invitationCode) && userAiDao.getByInviteCode(invitationCode) == null) { + // 填写的邀请码不对, 找不到对应的用户 + throw ExceptionUtil.of(StatusEnum.UNEXPECT_ERROR, "非法的邀请码【" + starNumber + "】"); + } + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/user/RegisterServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/user/RegisterServiceImpl.java new file mode 100644 index 000000000..250d9f4d1 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/user/RegisterServiceImpl.java @@ -0,0 +1,137 @@ +package com.github.paicoding.forum.service.user.service.user; + +import com.github.paicoding.forum.api.model.enums.NotifyTypeEnum; +import com.github.paicoding.forum.api.model.enums.user.LoginTypeEnum; +import com.github.paicoding.forum.api.model.exception.ExceptionUtil; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import com.github.paicoding.forum.api.model.vo.notify.NotifyMsgEvent; +import com.github.paicoding.forum.api.model.vo.user.UserPwdLoginReq; +import com.github.paicoding.forum.core.util.RandUtil; +import com.github.paicoding.forum.core.util.SpringUtil; +import com.github.paicoding.forum.core.util.TransactionUtil; +import com.github.paicoding.forum.service.user.converter.UserAiConverter; +import com.github.paicoding.forum.service.user.repository.dao.UserAiDao; +import com.github.paicoding.forum.service.user.repository.dao.UserDao; +import com.github.paicoding.forum.service.user.repository.entity.UserAiDO; +import com.github.paicoding.forum.service.user.repository.entity.UserDO; +import com.github.paicoding.forum.service.user.repository.entity.UserInfoDO; +import com.github.paicoding.forum.service.user.service.RegisterService; +import com.github.paicoding.forum.service.user.service.help.UserPwdEncoder; +import com.github.paicoding.forum.service.user.service.help.UserRandomGenHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 用户注册服务 + * + * @author YiHui + * @date 2023/6/26 + */ +@Service +public class RegisterServiceImpl implements RegisterService { + @Autowired + private UserPwdEncoder userPwdEncoder; + @Autowired + private UserDao userDao; + + @Autowired + private UserAiDao userAiDao; + + + @Override + @Transactional(rollbackFor = Exception.class) + public Long registerSystemUser(String loginUser, String nickUser, String avatar) { + UserDO dbUser = userDao.getUserByUserName(loginUser); + if (dbUser != null) { + return dbUser.getId(); + } + + // 注册系统账号 + UserDO user = new UserDO(); + user.setUserName(loginUser); + user.setThirdAccountId("system_" + RandUtil.random(16)); + user.setLoginType(LoginTypeEnum.WECHAT.getType()); + userDao.saveUser(user); + + UserInfoDO userInfo = new UserInfoDO(); + userInfo.setUserId(user.getId()); + userInfo.setUserName(nickUser); + userInfo.setPhoto(avatar); + userDao.save(userInfo); + return user.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long registerByUserNameAndPassword(UserPwdLoginReq loginReq) { + // 1. 判断用户名是否准确 + UserDO user = userDao.getUserByUserName(loginReq.getUsername()); + if (user != null) { + throw ExceptionUtil.of(StatusEnum.USER_LOGIN_NAME_REPEAT, loginReq.getUsername()); + } + + // 2. 保存用户登录信息 + user = new UserDO(); + user.setUserName(loginReq.getUsername()); + user.setPassword(userPwdEncoder.encPwd(loginReq.getPassword())); + user.setThirdAccountId(""); + // 用户名密码注册 + user.setLoginType(LoginTypeEnum.USER_PWD.getType()); + userDao.saveUser(user); + + // 3. 保存用户信息 + UserInfoDO userInfo = new UserInfoDO(); + userInfo.setUserId(user.getId()); + userInfo.setUserName(loginReq.getUsername()); + userInfo.setPhoto(UserRandomGenHelper.genAvatar()); + userDao.save(userInfo); + + // 4. 保存ai相互信息 + UserAiDO userAiDO = UserAiConverter.initAi(user.getId(), loginReq.getStarNumber()); + userAiDao.saveOrUpdateAiBindInfo(userAiDO, loginReq.getInvitationCode()); + processAfterUserRegister(user.getId()); + return user.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long registerByWechat(String thirdAccount) { + // 用户不存在,则需要注册 + // 1. 保存用户登录信息 + UserDO user = new UserDO(); + user.setThirdAccountId(thirdAccount); + user.setLoginType(LoginTypeEnum.WECHAT.getType()); + userDao.saveUser(user); + + + // 2. 初始化用户信息,随机生成用户昵称 + 头像 + UserInfoDO userInfo = new UserInfoDO(); + userInfo.setUserId(user.getId()); + userInfo.setUserName(UserRandomGenHelper.genNickName()); + userInfo.setPhoto(UserRandomGenHelper.genAvatar()); + userDao.save(userInfo); + + // 3. 保存ai相互信息 + UserAiDO userAiDO = UserAiConverter.initAi(user.getId()); + userAiDao.saveOrUpdateAiBindInfo(userAiDO, null); + processAfterUserRegister(user.getId()); + return user.getId(); + } + + + /** + * 用户注册完毕之后触发的动作 + * + * @param userId + */ + private void processAfterUserRegister(Long userId) { + TransactionUtil.registryAfterCommitOrImmediatelyRun(new Runnable() { + @Override + public void run() { + // 用户注册事件 + SpringUtil.publishEvent(new NotifyMsgEvent<>(this, NotifyTypeEnum.REGISTER, userId)); + } + }); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/user/UserServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/user/UserServiceImpl.java new file mode 100644 index 000000000..7a51c7c90 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/user/UserServiceImpl.java @@ -0,0 +1,249 @@ +package com.github.paicoding.forum.service.user.service.user; + +import com.github.paicoding.forum.api.model.context.ReqInfoContext; +import com.github.paicoding.forum.api.model.exception.ExceptionUtil; +import com.github.paicoding.forum.api.model.vo.article.dto.YearArticleDTO; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import com.github.paicoding.forum.api.model.vo.user.UserInfoSaveReq; +import com.github.paicoding.forum.api.model.vo.user.UserPwdLoginReq; +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.SimpleUserInfoDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.UserStatisticInfoDTO; +import com.github.paicoding.forum.core.util.IpUtil; +import com.github.paicoding.forum.service.article.repository.dao.ArticleDao; +import com.github.paicoding.forum.service.statistics.service.CountService; +import com.github.paicoding.forum.service.user.converter.UserConverter; +import com.github.paicoding.forum.service.user.repository.dao.UserAiDao; +import com.github.paicoding.forum.service.user.repository.dao.UserDao; +import com.github.paicoding.forum.service.user.repository.dao.UserRelationDao; +import com.github.paicoding.forum.service.user.repository.entity.IpInfo; +import com.github.paicoding.forum.service.user.repository.entity.UserAiDO; +import com.github.paicoding.forum.service.user.repository.entity.UserDO; +import com.github.paicoding.forum.service.user.repository.entity.UserInfoDO; +import com.github.paicoding.forum.service.user.repository.entity.UserRelationDO; +import com.github.paicoding.forum.service.user.service.UserAiService; +import com.github.paicoding.forum.service.user.service.UserService; +import com.github.paicoding.forum.service.user.service.help.UserPwdEncoder; +import com.github.paicoding.forum.service.user.service.help.UserSessionHelper; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 用户Service + * + * @author louzai + * @date 2022-07-20 + */ +@Service +public class UserServiceImpl implements UserService { + + @Resource + private UserDao userDao; + + @Resource + private UserAiDao userAiDao; + + @Resource + private UserRelationDao userRelationDao; + + @Autowired + private CountService countService; + + @Autowired + private ArticleDao articleDao; + + @Autowired + private UserSessionHelper userSessionHelper; + + @Autowired + private UserPwdEncoder userPwdEncoder; + + @Autowired + private UserAiService userAiService; + + @Override + public UserDO getWxUser(String wxuuid) { + return userDao.getByThirdAccountId(wxuuid); + } + + @Override + public List searchUser(String userName) { + List users = userDao.getByUserNameLike(userName); + if (CollectionUtils.isEmpty(users)) { + return Collections.emptyList(); + } + return users.stream().map(s -> new SimpleUserInfoDTO() + .setUserId(s.getUserId()) + .setName(s.getUserName()) + .setAvatar(s.getPhoto()) + .setProfile(s.getProfile()) + ) + .collect(Collectors.toList()); + } + + @Override + public void saveUserInfo(UserInfoSaveReq req) { + UserInfoDO userInfoDO = UserConverter.toDO(req); + userDao.updateUserInfo(userInfoDO); + } + + @Override + public BaseUserInfoDTO getAndUpdateUserIpInfoBySessionId(String session, String clientIp) { + if (StringUtils.isBlank(session)) { + return null; + } + + Long userId = userSessionHelper.getUserIdBySession(session); + if (userId == null) { + return null; + } + + // 查询用户信息,并更新最后一次使用的ip + UserInfoDO user = userDao.getByUserId(userId); + if (user == null) { + // 常见于:session中记录的用户被删除了,直接移除缓存中的session,走重新登录流程 + userSessionHelper.removeSession(session); + return null; + } + + IpInfo ip = user.getIp(); + if (clientIp != null && !Objects.equals(ip.getLatestIp(), clientIp)) { + // ip不同,需要更新 + ip.setLatestIp(clientIp); + ip.setLatestRegion(IpUtil.getLocationByIp(clientIp).toRegionStr()); + + if (ip.getFirstIp() == null) { + ip.setFirstIp(clientIp); + ip.setFirstRegion(ip.getLatestRegion()); + } + userDao.updateById(user); + } + + // 查询 user_ai信息,标注用户是否为星球专属用户 + UserAiDO userAiDO = userAiDao.getByUserId(userId); + return UserConverter.toDTO(user, userAiDO); + } + + @Override + public SimpleUserInfoDTO querySimpleUserInfo(Long userId) { + UserInfoDO user = userDao.getByUserId(userId); + if (user == null) { + throw ExceptionUtil.of(StatusEnum.USER_NOT_EXISTS, "userId=" + userId); + } + return UserConverter.toSimpleInfo(user); + } + + @Override + public BaseUserInfoDTO queryBasicUserInfo(Long userId) { + UserInfoDO user = userDao.getByUserId(userId); + if (user == null) { + throw ExceptionUtil.of(StatusEnum.USER_NOT_EXISTS, "userId=" + userId); + } + return UserConverter.toDTO(user); + } + + @Override + public List batchQuerySimpleUserInfo(Collection userIds) { + List users = userDao.getByUserIds(userIds); + if (CollectionUtils.isEmpty(users)) { + return Collections.emptyList(); + } + return users.stream().map(UserConverter::toSimpleInfo).collect(Collectors.toList()); + } + + @Override + public List batchQueryBasicUserInfo(Collection userIds) { + List users = userDao.getByUserIds(userIds); + if (CollectionUtils.isEmpty(users)) { + throw ExceptionUtil.of(StatusEnum.USER_NOT_EXISTS, "userId=" + userIds); + } + return users.stream().map(UserConverter::toDTO).collect(Collectors.toList()); + } + + @Override + public UserStatisticInfoDTO queryUserInfoWithStatistic(Long userId) { + BaseUserInfoDTO userInfoDTO = queryBasicUserInfo(userId); + UserStatisticInfoDTO userHomeDTO = countService.queryUserStatisticInfo(userId); + userHomeDTO = UserConverter.toUserHomeDTO(userHomeDTO, userInfoDTO); + + // 用户资料完整度 + int cnt = 0; + if (StringUtils.isNotBlank(userHomeDTO.getCompany())) { + ++cnt; + } + if (StringUtils.isNotBlank(userHomeDTO.getPosition())) { + ++cnt; + } + if (StringUtils.isNotBlank(userHomeDTO.getProfile())) { + ++cnt; + } + userHomeDTO.setInfoPercent(cnt * 100 / 3); + + // 是否关注 + Long followUserId = ReqInfoContext.getReqInfo().getUserId(); + if (followUserId != null) { + UserRelationDO userRelationDO = userRelationDao.getUserRelationByUserId(userId, followUserId); + userHomeDTO.setFollowed((userRelationDO == null) ? Boolean.FALSE : Boolean.TRUE); + } else { + userHomeDTO.setFollowed(Boolean.FALSE); + } + + // 加入天数 + int joinDayCount = (int) ((System.currentTimeMillis() - userHomeDTO.getCreateTime() + .getTime()) / (1000 * 3600 * 24)); + userHomeDTO.setJoinDayCount(Math.max(1, joinDayCount)); + + // 创作历程 + List yearArticleDTOS = articleDao.listYearArticleByUserId(userId); + userHomeDTO.setYearArticleList(yearArticleDTOS); + return userHomeDTO; + } + + @Override + public Long getUserCount() { + return this.userDao.getUserCount(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void bindUserInfo(UserPwdLoginReq loginReq) { + // 0. 绑定用户名 & 密码 前置校验 + UserDO user = userDao.getUserByUserName(loginReq.getUsername()); + if (user == null) { + // 用户名不存在,则标识当前登录用户可以使用这个用户名 + user = new UserDO(); + user.setId(loginReq.getUserId()); + } else if (!Objects.equals(loginReq.getUserId(), user.getId())) { + // 登录用户名已经存在了 + throw ExceptionUtil.of(StatusEnum.USER_LOGIN_NAME_REPEAT, loginReq.getUsername()); + } + + // 1. 更新用户名密码 + user.setUserName(loginReq.getUsername()); + user.setPassword(userPwdEncoder.encPwd(loginReq.getPassword())); + userDao.saveUser(user); + + // 2. 更新ai相关信息 + userAiService.initOrUpdateAiInfo(loginReq); + } + + @Override + public BaseUserInfoDTO queryUserByLoginName(String uname) { + UserDO user = userDao.getUserByUserName(uname); + if (user == null) { + return null; + } + + return queryBasicUserInfo(user.getId()); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/userfoot/UserFootServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/userfoot/UserFootServiceImpl.java new file mode 100644 index 000000000..0d2b6f02c --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/userfoot/UserFootServiceImpl.java @@ -0,0 +1,223 @@ +package com.github.paicoding.forum.service.user.service.userfoot; + +import com.github.paicoding.forum.api.model.enums.DocumentTypeEnum; +import com.github.paicoding.forum.api.model.enums.NotifyTypeEnum; +import com.github.paicoding.forum.api.model.enums.OperateTypeEnum; +import com.github.paicoding.forum.api.model.vo.PageParam; +import com.github.paicoding.forum.api.model.vo.ResVo; +import com.github.paicoding.forum.api.model.vo.user.dto.SimpleUserInfoDTO; +import com.github.paicoding.forum.api.model.vo.user.dto.UserFootStatisticDTO; +import com.github.paicoding.forum.core.common.CommonConstants; +import com.github.paicoding.forum.core.util.JsonUtil; +import com.github.paicoding.forum.service.article.service.ArticleReadService; +import com.github.paicoding.forum.service.comment.repository.entity.CommentDO; +import com.github.paicoding.forum.service.comment.service.CommentReadService; +import com.github.paicoding.forum.service.notify.help.MsgNotifyHelper; +import com.github.paicoding.forum.service.notify.service.RabbitmqService; +import com.github.paicoding.forum.service.user.repository.dao.UserFootDao; +import com.github.paicoding.forum.service.user.repository.entity.UserFootDO; +import com.github.paicoding.forum.service.user.service.UserFootService; +import com.rabbitmq.client.BuiltinExchangeType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * 用户足迹Service + * + * @author louzai + * @date 2022-07-20 + */ +@Service +public class UserFootServiceImpl implements UserFootService { + private final UserFootDao userFootDao; + + @Autowired + private ArticleReadService articleReadService; + + @Autowired + private CommentReadService commentReadService; + + @Autowired + private RabbitmqService rabbitmqService; + + public UserFootServiceImpl(UserFootDao userFootDao) { + this.userFootDao = userFootDao; + } + + /** + * 保存或更新状态信息 + * + * @param documentType 文档类型:博文 + 评论 + * @param documentId 文档id + * @param authorId 作者 + * @param userId 操作人 + * @param operateTypeEnum 操作类型:点赞,评论,收藏等 + */ + @Override + public UserFootDO saveOrUpdateUserFoot(DocumentTypeEnum documentType, Long documentId, Long authorId, Long userId, OperateTypeEnum operateTypeEnum) { + // 查询是否有该足迹;有则更新,没有则插入 + UserFootDO readUserFootDO = userFootDao.getByDocumentAndUserId(documentId, documentType.getCode(), userId); + if (readUserFootDO == null) { + readUserFootDO = new UserFootDO(); + readUserFootDO.setUserId(userId); + readUserFootDO.setDocumentId(documentId); + readUserFootDO.setDocumentType(documentType.getCode()); + readUserFootDO.setDocumentUserId(authorId); + setUserFootStat(readUserFootDO, operateTypeEnum); + userFootDao.save(readUserFootDO); + } else if (setUserFootStat(readUserFootDO, operateTypeEnum)) { + readUserFootDO.setUpdateTime(new Date()); + userFootDao.updateById(readUserFootDO); + } + return readUserFootDO; + } + + /** + * 文章/评论点赞、取消点赞、收藏、取消收藏 + * + * @param documentType 文档类型:博文 + 评论 + * @param documentId 文档id + * @param authorId 作者 + * @param userId 操作人 + * @param operateTypeEnum 操作类型:点赞,评论,收藏等 + */ + @Override + public void favorArticleComment(DocumentTypeEnum documentType, Long documentId, Long authorId, Long userId, OperateTypeEnum operateTypeEnum) { + // fixme 这里没有做并发控制,在大并发场景下,可能出现查询出来的数据,与db中数据不一致的场景 + // fixme 解决方案:自旋等待的分布式锁 or 事务 + 悲观锁 + // fixme 考虑到这个足迹的准确性影响并不大,留待有缘人进行修正 + + // 查询是否有该足迹;有则更新,没有则插入 + UserFootDO readUserFootDO = userFootDao.getByDocumentAndUserId(documentId, documentType.getCode(), userId); + boolean dbChanged = false; + if (readUserFootDO == null) { + readUserFootDO = new UserFootDO(); + readUserFootDO.setUserId(userId); + readUserFootDO.setDocumentId(documentId); + readUserFootDO.setDocumentType(documentType.getCode()); + readUserFootDO.setDocumentUserId(authorId); + setUserFootStat(readUserFootDO, operateTypeEnum); + userFootDao.save(readUserFootDO); + dbChanged = true; + } else if (setUserFootStat(readUserFootDO, operateTypeEnum)) { + readUserFootDO.setUpdateTime(new Date()); + userFootDao.updateById(readUserFootDO); + dbChanged = true; + } + + if (!dbChanged) { + // 幂等,直接返回 + return; + } + + + // 点赞、收藏两种操作时,需要发送异步消息,用于生成消息通知、更新文章/评论的相关计数统计、更新用户的活跃积分 + NotifyTypeEnum notifyType = OperateTypeEnum.getNotifyType(operateTypeEnum); + if (notifyType == null) { + // 不需要发送通知的场景,直接返回 + return; + } + + // 点赞消息走 RabbitMQ,其它走 Java 内置消息机制 + if (notifyType.equals(NotifyTypeEnum.PRAISE) && rabbitmqService.enabled()) { + rabbitmqService.publishMsg( + CommonConstants.EXCHANGE_NAME_DIRECT, + BuiltinExchangeType.DIRECT, + CommonConstants.QUERE_KEY_PRAISE, + JsonUtil.toStr(readUserFootDO)); + } else { + MsgNotifyHelper.publish(notifyType, readUserFootDO); + } + } + + @Override + public void saveCommentFoot(CommentDO comment, Long articleAuthor, Long parentCommentAuthor) { + // 保存文章对应的评论足迹 + saveOrUpdateUserFoot(DocumentTypeEnum.ARTICLE, comment.getArticleId(), articleAuthor, comment.getUserId(), OperateTypeEnum.COMMENT); + // 如果是子评论,则找到父评论的记录,然后设置为已评 + if (comment.getParentCommentId() != null && comment.getParentCommentId() != 0) { + // 如果需要展示父评论的子评论数量,authorId 需要传父评论的 userId + saveOrUpdateUserFoot(DocumentTypeEnum.COMMENT, comment.getParentCommentId(), parentCommentAuthor, comment.getUserId(), OperateTypeEnum.COMMENT); + } + } + + @Override + public void removeCommentFoot(CommentDO comment, Long articleAuthor, Long parentCommentAuthor) { + saveOrUpdateUserFoot(DocumentTypeEnum.ARTICLE, comment.getArticleId(), articleAuthor, comment.getUserId(), OperateTypeEnum.DELETE_COMMENT); + if (comment.getParentCommentId() != null) { + // 如果需要展示父评论的子评论数量,authorId 需要传父评论的 userId + saveOrUpdateUserFoot(DocumentTypeEnum.COMMENT, comment.getParentCommentId(), parentCommentAuthor, comment.getUserId(), OperateTypeEnum.DELETE_COMMENT); + } + } + + + private boolean setUserFootStat(UserFootDO userFootDO, OperateTypeEnum operate) { + switch (operate) { + case READ: + // 设置为已读 + userFootDO.setReadStat(1); + // 需要更新时间,用于浏览记录 + return true; + case PRAISE: + case CANCEL_PRAISE: + return compareAndUpdate(userFootDO::getPraiseStat, userFootDO::setPraiseStat, operate.getDbStatCode()); + case COLLECTION: + case CANCEL_COLLECTION: + return compareAndUpdate(userFootDO::getCollectionStat, userFootDO::setCollectionStat, operate.getDbStatCode()); + case COMMENT: + case DELETE_COMMENT: + return compareAndUpdate(userFootDO::getCommentStat, userFootDO::setCommentStat, operate.getDbStatCode()); + default: + return false; + } + } + + /** + * 相同则直接返回false不用更新;不同则更新,返回true + * + * @param supplier + * @param consumer + * @param input + * @param + * @return + */ + private boolean compareAndUpdate(Supplier supplier, Consumer consumer, T input) { + if (Objects.equals(supplier.get(), input)) { + return false; + } + consumer.accept(input); + return true; + } + + @Override + public List queryUserReadArticleList(Long userId, PageParam pageParam) { + return userFootDao.listReadArticleByUserId(userId, pageParam); + } + + @Override + public List queryUserCollectionArticleList(Long userId, PageParam pageParam) { + return userFootDao.listCollectedArticlesByUserId(userId, pageParam); + } + + @Override + public List queryArticlePraisedUsers(Long articleId) { + return userFootDao.listDocumentPraisedUsers(articleId, DocumentTypeEnum.ARTICLE.getCode(), 10); + } + + @Override + public UserFootDO queryUserFoot(Long documentId, Integer type, Long userId) { + return userFootDao.getByDocumentAndUserId(documentId, type, userId); + } + + @Override + public UserFootStatisticDTO getFootCount() { + return userFootDao.getFootCount(); + } + +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/whitelist/AuthorWhiteListServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/whitelist/AuthorWhiteListServiceImpl.java new file mode 100644 index 000000000..fa1706630 --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/whitelist/AuthorWhiteListServiceImpl.java @@ -0,0 +1,58 @@ +package com.github.paicoding.forum.service.user.service.whitelist; + +import com.github.paicoding.forum.api.model.vo.user.dto.BaseUserInfoDTO; +import com.github.paicoding.forum.core.cache.RedisClient; +import com.github.paicoding.forum.service.user.service.AuthorWhiteListService; +import com.github.paicoding.forum.service.user.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * @author YiHui + * @date 2023/4/9 + */ +@Service +public class AuthorWhiteListServiceImpl implements AuthorWhiteListService { + /** + * 实用 redis - set 来存储允许直接发文章的白名单 + */ + private static final String ARTICLE_WHITE_LIST = "auth_article_white_list"; + + @Autowired + private UserService userService; + + @Override + public boolean authorInArticleWhiteList(Long authorId) { + return RedisClient.sIsMember(ARTICLE_WHITE_LIST, authorId); + } + + /** + * 获取所有的白名单用户 + * + * @return + */ + @Override + public List queryAllArticleWhiteListAuthors() { + Set users = RedisClient.sGetAll(ARTICLE_WHITE_LIST, Long.class); + if (CollectionUtils.isEmpty(users)) { + return Collections.emptyList(); + } + List userInfos = userService.batchQueryBasicUserInfo(users); + return userInfos; + } + + @Override + public void addAuthor2ArticleWhitList(Long userId) { + RedisClient.sPut(ARTICLE_WHITE_LIST, userId); + } + + @Override + public void removeAuthorFromArticleWhiteList(Long userId) { + RedisClient.sDel(ARTICLE_WHITE_LIST, userId); + } +} diff --git a/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/whitelist/ZsxqWhiteListServiceImpl.java b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/whitelist/ZsxqWhiteListServiceImpl.java new file mode 100644 index 000000000..d3ce6706b --- /dev/null +++ b/paicoding-service/src/main/java/com/github/paicoding/forum/service/user/service/whitelist/ZsxqWhiteListServiceImpl.java @@ -0,0 +1,150 @@ +package com.github.paicoding.forum.service.user.service.whitelist; + +import com.github.paicoding.forum.api.model.enums.user.LoginTypeEnum; +import com.github.paicoding.forum.api.model.enums.user.UserAIStatEnum; +import com.github.paicoding.forum.api.model.exception.ExceptionUtil; +import com.github.paicoding.forum.api.model.vo.PageVo; +import com.github.paicoding.forum.api.model.vo.constants.StatusEnum; +import com.github.paicoding.forum.api.model.vo.user.SearchZsxqUserReq; +import com.github.paicoding.forum.api.model.vo.user.ZsxqUserPostReq; +import com.github.paicoding.forum.api.model.vo.user.dto.ZsxqUserInfoDTO; +import com.github.paicoding.forum.service.user.converter.UserAiConverter; +import com.github.paicoding.forum.service.user.converter.UserStructMapper; +import com.github.paicoding.forum.service.user.repository.dao.UserAiDao; +import com.github.paicoding.forum.service.user.repository.dao.UserDao; +import com.github.paicoding.forum.service.user.repository.entity.UserAiDO; +import com.github.paicoding.forum.service.user.repository.entity.UserDO; +import com.github.paicoding.forum.service.user.repository.entity.UserInfoDO; +import com.github.paicoding.forum.service.user.repository.params.SearchZsxqWhiteParams; +import com.github.paicoding.forum.service.user.service.ZsxqWhiteListService; +import com.github.paicoding.forum.service.user.service.help.UserPwdEncoder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 微信搜索「沉默王二」,回复 Java + * + * @author 沉默王二 + * @date 6/29/23 + */ +@Service +public class ZsxqWhiteListServiceImpl implements ZsxqWhiteListService { + @Autowired + private UserAiDao userAiDao; + @Autowired + private UserDao userDao; + + @Autowired + private UserPwdEncoder userPwdEncoder; + + @Override + public PageVo getList(SearchZsxqUserReq req) { + SearchZsxqWhiteParams params = UserStructMapper.INSTANCE.toSearchParams(req); + // 查询知识星球用户 + List zsxqUserInfoDTOs = userAiDao.listZsxqUsersByParams(params); + Long totalCount = userAiDao.countZsxqUserByParams(params); + return PageVo.build(zsxqUserInfoDTOs, req.getPageSize(), req.getPageNumber(), totalCount); + } + + @Override + public void operate(Long id, UserAIStatEnum operate) { + // 根据id获取用户信息 + UserAiDO userAiDO = userAiDao.getById(id); + // 为空则抛出异常 + if (userAiDO == null) { + throw ExceptionUtil.of(StatusEnum.USER_NOT_EXISTS, id, "用户不存在"); + } + + // 更新用户状态 + userAiDO.setState(operate.getCode()); + + // 审核通过的时候调整用户的策略 + userAiDao.updateById(userAiDO); + } + + @Override + // 加事务 + @Transactional(rollbackFor = Exception.class) + public void update(ZsxqUserPostReq req) { + // 根据id获取用户信息 + UserAiDO userAiDO = userAiDao.getById(req.getId()); + // 为空则抛出异常 + if (userAiDO == null) { + throw ExceptionUtil.of(StatusEnum.USER_NOT_EXISTS, req.getId(), "用户不存在"); + } + + // 星球编号不能重复 + UserAiDO userAiDOByStarNumber = userAiDao.getByStarNumber(req.getStarNumber()); + if (userAiDOByStarNumber != null && !userAiDOByStarNumber.getId().equals(req.getId())) { + throw ExceptionUtil.of(StatusEnum.USER_STAR_REPEAT, req.getStarNumber(), "星球编号已存在"); + } + + // 用户登录名不能重复 + UserDO userDO = userDao.getUserByUserName(req.getUserCode()); + if (userDO != null && !userDO.getId().equals(userAiDO.getUserId())) { + throw ExceptionUtil.of(StatusEnum.USER_LOGIN_NAME_REPEAT, req.getUserCode(), "用户登录名已存在"); + } + + // 更新用户登录名 + userDO = new UserDO(); + userDO.setId(userAiDO.getUserId()); + userDO.setUserName(req.getUserCode()); + userDao.updateUser(userDO); + + // 更新用户昵称 + UserInfoDO userInfoDO = new UserInfoDO(); + userInfoDO.setId(userAiDO.getUserId()); + userInfoDO.setUserName(req.getName()); + userDao.updateById(userInfoDO); + + // 更新星球编号 + userAiDO.setStarNumber(req.getStarNumber()); + // 更新 AI 策略 + userAiDO.setStrategy(req.getStrategy()); + + userAiDao.updateById(userAiDO); + } + + @Override + public void batchOperate(List ids, UserAIStatEnum operate) { + // 批量更新用户状态 + userAiDao.batchUpdateState(ids, operate.getCode()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void reset(Integer authorId) { + // 根据id获取用户信息 + UserAiDO userAiDO = userAiDao.getById(authorId); + // 为空则抛出异常 + if (userAiDO == null) { + throw ExceptionUtil.of(StatusEnum.USER_NOT_EXISTS, authorId, "该星球用户不存在"); + } + + // 获取用户,看是微信还是用户名密码注册用户 + UserDO userDO = userDao.getUserByUserId(userAiDO.getUserId()); + if (userDO == null) { + throw ExceptionUtil.of(StatusEnum.USER_NOT_EXISTS, userAiDO.getUserId(), "该用户不存在"); + } + + // 不能直接删除,要初始化用户的 AI 信息 + UserAiDO initUserAiDO = UserAiConverter.initAi(userAiDO.getUserId()); + initUserAiDO.setId(userAiDO.getId()); + userAiDao.updateById(initUserAiDO); + + UserDO user = new UserDO(); + user.setId(userAiDO.getUserId()); + // 如果是微信注册用户 + if (LoginTypeEnum.WECHAT.getType() == userDO.getLoginType()) { + // 用户登录名也重置 + user.setUserName(""); + } + + // 密码重置为 + user.setPassword(userPwdEncoder.encPwd("paicoding")); + userDao.saveUser(user); + } +} diff --git a/paicoding-service/src/main/resources/META-INF/spring.factories b/paicoding-service/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..2160cae5a --- /dev/null +++ b/paicoding-service/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configure +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.github.paicoding.forum.service.ServiceAutoConfig \ No newline at end of file diff --git a/paicoding-service/src/main/resources/mapper/ArticleMapper.xml b/paicoding-service/src/main/resources/mapper/ArticleMapper.xml new file mode 100644 index 000000000..724328f2d --- /dev/null +++ b/paicoding-service/src/main/resources/mapper/ArticleMapper.xml @@ -0,0 +1,108 @@ + + + + + + + limit #{pageParam.offset}, #{pageParam.limit} + + + + + + + + + + + + + + + where a.deleted = ${@com.github.paicoding.forum.api.model.enums.YesOrNoEnum@NO.code} + + and a.title like concat('%', #{searchParams.title}, '%') + + + and u.user_name like concat('%', #{searchParams.userName}, '%') + + + and a.offical_stat = #{searchParams.officalStat} + + + and a.topping_stat = #{searchParams.toppingStat} + + + and a.status = #{searchParams.status} + + + + + + + diff --git a/paicoding-service/src/main/resources/mapper/ArticleTagMapper.xml b/paicoding-service/src/main/resources/mapper/ArticleTagMapper.xml new file mode 100644 index 000000000..a801abb06 --- /dev/null +++ b/paicoding-service/src/main/resources/mapper/ArticleTagMapper.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/paicoding-service/src/main/resources/mapper/ColumnArticleMapper.xml b/paicoding-service/src/main/resources/mapper/ColumnArticleMapper.xml new file mode 100644 index 000000000..16676aafc --- /dev/null +++ b/paicoding-service/src/main/resources/mapper/ColumnArticleMapper.xml @@ -0,0 +1,67 @@ + + + + + + + limit #{pageParam.offset}, #{pageParam.limit} + + + + + + + + + + + + + diff --git a/paicoding-service/src/main/resources/mapper/CommentMapper.xml b/paicoding-service/src/main/resources/mapper/CommentMapper.xml new file mode 100644 index 000000000..5f56acfd1 --- /dev/null +++ b/paicoding-service/src/main/resources/mapper/CommentMapper.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/paicoding-service/src/main/resources/mapper/NotifyMsgMapper.xml b/paicoding-service/src/main/resources/mapper/NotifyMsgMapper.xml new file mode 100644 index 000000000..c6e360f85 --- /dev/null +++ b/paicoding-service/src/main/resources/mapper/NotifyMsgMapper.xml @@ -0,0 +1,56 @@ + + + + + + limit #{pageParam.offset}, #{pageParam.limit} + + + + + + + + + + update notify_msg set `state` = 1 where `id` in + + #{id} + + + + + diff --git a/paicoding-service/src/main/resources/mapper/QueryCountMapper.xml b/paicoding-service/src/main/resources/mapper/QueryCountMapper.xml new file mode 100644 index 000000000..4e0a154b3 --- /dev/null +++ b/paicoding-service/src/main/resources/mapper/QueryCountMapper.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/paicoding-service/src/main/resources/mapper/ShortLinkMapper.xml b/paicoding-service/src/main/resources/mapper/ShortLinkMapper.xml new file mode 100644 index 000000000..5af44d97d --- /dev/null +++ b/paicoding-service/src/main/resources/mapper/ShortLinkMapper.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/paicoding-service/src/main/resources/mapper/UserAiMapper.xml b/paicoding-service/src/main/resources/mapper/UserAiMapper.xml new file mode 100644 index 000000000..ce067a4e1 --- /dev/null +++ b/paicoding-service/src/main/resources/mapper/UserAiMapper.xml @@ -0,0 +1,47 @@ + + + + + + + limit #{pageParam.offset}, #{pageParam.limit} + + + + + where a.deleted = ${@com.github.paicoding.forum.api.model.enums.YesOrNoEnum@NO.code} + + and u.user_name like concat('%', #{searchParams.userCode}, '%') + + + and ui.user_name like concat('%', #{searchParams.name}, '%') + + + and a.star_number like concat('%', #{searchParams.starNumber}, '%') + + + and a.state = #{searchParams.status} + + + + + + + diff --git a/paicoding-service/src/main/resources/mapper/UserFootMapper.xml b/paicoding-service/src/main/resources/mapper/UserFootMapper.xml new file mode 100644 index 000000000..326d21fc6 --- /dev/null +++ b/paicoding-service/src/main/resources/mapper/UserFootMapper.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/paicoding-service/src/main/resources/mapper/UserRelationMapper.xml b/paicoding-service/src/main/resources/mapper/UserRelationMapper.xml new file mode 100644 index 000000000..1dd8097eb --- /dev/null +++ b/paicoding-service/src/main/resources/mapper/UserRelationMapper.xml @@ -0,0 +1,47 @@ + + + + + + limit #{pageParam.offset}, #{pageParam.limit} + + + + + + + + + + diff --git a/paicoding-ui/README.md b/paicoding-ui/README.md new file mode 100644 index 000000000..e1675abf1 --- /dev/null +++ b/paicoding-ui/README.md @@ -0,0 +1,4 @@ +forum-ui +=== + +存储前端资源文件 \ No newline at end of file diff --git a/paicoding-ui/pom.xml b/paicoding-ui/pom.xml new file mode 100644 index 000000000..80e98298a --- /dev/null +++ b/paicoding-ui/pom.xml @@ -0,0 +1,21 @@ + + + + paicoding-forum + com.github.paicoding.forum + 0.0.1-SNAPSHOT + + 4.0.0 + + paicoding-ui + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/admonition/admonition.css b/paicoding-ui/src/main/resources/static/admonition/admonition.css new file mode 100644 index 000000000..9ec268acf --- /dev/null +++ b/paicoding-ui/src/main/resources/static/admonition/admonition.css @@ -0,0 +1,265 @@ +.adm-block { + display: block; + width: 99%; + border-radius: 6px; + padding-left: 10px; + margin-bottom: 1em; + border: 1px solid; + border-left-width: 4px; + box-shadow: 2px 2px 6px #cdcdcd; +} + +.adm-heading { + display: block; + font-weight: bold; + font-size: 0.9em; + height: 1.8em; + padding-top: 0.3em; + padding-bottom: 2em; + border-bottom: solid 1px; + padding-left: 10px; + margin-left: -10px; +} + +.adm-body { + display: block; + padding-bottom: 0.5em; + padding-top: 0.5em; + margin-left: 1.5em; + margin-right: 1.5em; +} + +.adm-heading > span { + color: initial; +} + +.adm-icon { + height: 1.6em; + width: 1.6em; + display: inline-block; + vertical-align: middle; + margin-right: 0.25em; + margin-left: -0.25em; +} + +.adm-hidden { + display: none !important; +} + +.adm-block.adm-collapsed > .adm-heading, .adm-block.adm-open > .adm-heading { + position: relative; + cursor: pointer; +} + +.adm-block.adm-collapsed > .adm-heading { + margin-bottom: 0; +} + +.adm-block.adm-collapsed .adm-body { + display: none !important; +} + +.adm-block.adm-open > .adm-heading:after, +.adm-block.adm-collapsed > .adm-heading:after { + display: inline-block; + position: absolute; + top:calc(50% - .65em); + right: 0.5em; + font-size: 1.3em; + content: '▼'; +} + +.adm-block.adm-collapsed > .adm-heading:after { + right: 0.50em; + top:calc(50% - .75em); + transform: rotate(90deg); +} + +/* default scheme */ + +.adm-block { + border-color: #ebebeb; + border-bottom-color: #bfbfbf; +} + +.adm-block.adm-abstract { + border-left-color: #48C4FF; +} + +.adm-block.adm-abstract .adm-heading { + background: #E8F7FF; + color: #48C4FF; + border-bottom-color: #dbf3ff; +} + +.adm-block.adm-abstract.adm-open > .adm-heading:after, +.adm-block.adm-abstract.adm-collapsed > .adm-heading:after { + color: #80d9ff; +} + + +.adm-block.adm-bug { + border-left-color: #F50057; +} + +.adm-block.adm-bug .adm-heading { + background: #FEE7EE; + color: #F50057; + border-bottom-color: #fcd9e4; +} + +.adm-block.adm-bug.adm-open > .adm-heading:after, +.adm-block.adm-bug.adm-collapsed > .adm-heading:after { + color: #f57aab; +} + +.adm-block.adm-danger { + border-left-color: #FE1744; +} + +.adm-block.adm-danger .adm-heading { + background: #FFE9ED; + color: #FE1744; + border-bottom-color: #ffd9e0; +} + +.adm-block.adm-danger.adm-open > .adm-heading:after, +.adm-block.adm-danger.adm-collapsed > .adm-heading:after { + color: #fc7e97; +} + +.adm-block.adm-example { + border-left-color: #7940ff; +} + +.adm-block.adm-example .adm-heading { + background: #EFEBFF; + color: #7940ff; + border-bottom-color: #e0d9ff; +} + +.adm-block.adm-example.adm-open > .adm-heading:after, +.adm-block.adm-example.adm-collapsed > .adm-heading:after { + color: #b199ff; +} + +.adm-block.adm-fail { + border-left-color: #FE5E5E; +} + +.adm-block.adm-fail .adm-heading { + background: #FFEEEE; + color: #Fe5e5e; + border-bottom-color: #ffe3e3; +} + +.adm-block.adm-fail.adm-open > .adm-heading:after, +.adm-block.adm-fail.adm-collapsed > .adm-heading:after { + color: #fcb1b1; +} + +.adm-block.adm-faq { + border-left-color: #5ED116; +} + +.adm-block.adm-faq .adm-heading { + background: #EEFAE8; + color: #5ED116; + border-bottom-color: #e6fadc; +} + +.adm-block.adm-faq.adm-open > .adm-heading:after, +.adm-block.adm-faq.adm-collapsed > .adm-heading:after { + color: #98cf72; +} + +.adm-block.adm-info { + border-left-color: #00B8D4; +} + +.adm-block.adm-info .adm-heading { + background: #E8F7FA; + color: #00B8D4; + border-bottom-color: #dcf5fa; +} + +.adm-block.adm-info.adm-open > .adm-heading:after, +.adm-block.adm-info.adm-collapsed > .adm-heading:after { + color: #83ced6; +} + +.adm-block.adm-note { + border-left-color: #448AFF; +} + +.adm-block.adm-note .adm-heading { + background: #EDF4FF; + color: #448AFF; + border-bottom-color: #e0edff; +} + +.adm-block.adm-note.adm-open > .adm-heading:after, +.adm-block.adm-note.adm-collapsed > .adm-heading:after { + color: #8cb8ff; +} + +.adm-block.adm-quote { + border-left-color: #9E9E9E; +} + +.adm-block.adm-quote .adm-heading { + background: #F4F4F4; + color: #9E9E9E; + border-bottom-color: #e8e8e8; +} + +.adm-block.adm-quote.adm-open > .adm-heading:after, +.adm-block.adm-quote.adm-collapsed > .adm-heading:after { + color: #b3b3b3; +} + +.adm-block.adm-success { + border-left-color: #1DCD63; +} + +.adm-block.adm-success .adm-heading { + background: #E9F8EE; + color: #1DCD63; + border-bottom-color: #dcf7e5; +} + +.adm-block.adm-success.adm-open > .adm-heading:after, +.adm-block.adm-success.adm-collapsed > .adm-heading:after { + color: #7acc98; +} + +.adm-block.adm-tip { + border-left-color: #01BFA5; +} + +.adm-block.adm-tip .adm-heading { + background: #E9F9F6; + color: #01BFA5; + border-bottom-color: #dcf7f2; +} + +.adm-block.adm-tip.adm-open > .adm-heading:after, +.adm-block.adm-tip.adm-collapsed > .adm-heading:after { + color: #7dd1c0; +} + +.adm-block.adm-warning { + border-left-color: #FF9001; +} + +.adm-block.adm-warning .adm-heading { + background: #FEF3E8; + color: #FF9001; + border-bottom-color: #Fef3e8; +} + +.adm-block.adm-warning.adm-open > .adm-heading:after, +.adm-block.adm-warning.adm-collapsed > .adm-heading:after { + color: #fcbb6a; +} + diff --git a/paicoding-ui/src/main/resources/static/admonition/admonition.js b/paicoding-ui/src/main/resources/static/admonition/admonition.js new file mode 100644 index 000000000..31478adef --- /dev/null +++ b/paicoding-ui/src/main/resources/static/admonition/admonition.js @@ -0,0 +1,25 @@ +(() => { + let divs = document.getElementsByClassName("adm-block"); + for (let i = 0; i < divs.length; i++) { + let div = divs[i]; + if (div.classList.contains("adm-collapsed") || div.classList.contains("adm-open")) { + let headings = div.getElementsByClassName("adm-heading"); + if (headings.length > 0) { + headings[0].addEventListener("click", event => { + let el = div; + event.preventDefault(); + event.stopImmediatePropagation(); + if (el.classList.contains("adm-collapsed")) { + console.debug("Admonition Open", event.srcElement); + el.classList.remove("adm-collapsed"); + el.classList.add("adm-open"); + } else { + console.debug("Admonition Collapse", event.srcElement); + el.classList.add("adm-collapsed"); + el.classList.remove("adm-open"); + } + }); + } + } + } +})(); diff --git a/paicoding-ui/src/main/resources/static/admonition/images/adm-abstract.svg b/paicoding-ui/src/main/resources/static/admonition/images/adm-abstract.svg new file mode 100644 index 000000000..2757f1bfd --- /dev/null +++ b/paicoding-ui/src/main/resources/static/admonition/images/adm-abstract.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/paicoding-ui/src/main/resources/static/admonition/images/adm-bug.svg b/paicoding-ui/src/main/resources/static/admonition/images/adm-bug.svg new file mode 100644 index 000000000..d6a8fb7ad --- /dev/null +++ b/paicoding-ui/src/main/resources/static/admonition/images/adm-bug.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/paicoding-ui/src/main/resources/static/admonition/images/adm-danger.svg b/paicoding-ui/src/main/resources/static/admonition/images/adm-danger.svg new file mode 100644 index 000000000..056d60e4d --- /dev/null +++ b/paicoding-ui/src/main/resources/static/admonition/images/adm-danger.svg @@ -0,0 +1,3 @@ + + + diff --git a/paicoding-ui/src/main/resources/static/admonition/images/adm-example.svg b/paicoding-ui/src/main/resources/static/admonition/images/adm-example.svg new file mode 100644 index 000000000..13f1834d1 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/admonition/images/adm-example.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/paicoding-ui/src/main/resources/static/admonition/images/adm-fail.svg b/paicoding-ui/src/main/resources/static/admonition/images/adm-fail.svg new file mode 100644 index 000000000..0ecf9f7c4 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/admonition/images/adm-fail.svg @@ -0,0 +1,3 @@ + + + diff --git a/paicoding-ui/src/main/resources/static/admonition/images/adm-faq.svg b/paicoding-ui/src/main/resources/static/admonition/images/adm-faq.svg new file mode 100644 index 000000000..e3a836c2e --- /dev/null +++ b/paicoding-ui/src/main/resources/static/admonition/images/adm-faq.svg @@ -0,0 +1,3 @@ + + + diff --git a/paicoding-ui/src/main/resources/static/admonition/images/adm-info.svg b/paicoding-ui/src/main/resources/static/admonition/images/adm-info.svg new file mode 100644 index 000000000..27677adde --- /dev/null +++ b/paicoding-ui/src/main/resources/static/admonition/images/adm-info.svg @@ -0,0 +1,3 @@ + + + diff --git a/paicoding-ui/src/main/resources/static/admonition/images/adm-note.svg b/paicoding-ui/src/main/resources/static/admonition/images/adm-note.svg new file mode 100644 index 000000000..5f8f0fcf3 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/admonition/images/adm-note.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/paicoding-ui/src/main/resources/static/admonition/images/adm-quote.svg b/paicoding-ui/src/main/resources/static/admonition/images/adm-quote.svg new file mode 100644 index 000000000..e41cf5ce9 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/admonition/images/adm-quote.svg @@ -0,0 +1,3 @@ + + + diff --git a/paicoding-ui/src/main/resources/static/admonition/images/adm-success.svg b/paicoding-ui/src/main/resources/static/admonition/images/adm-success.svg new file mode 100644 index 000000000..583a8e9b0 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/admonition/images/adm-success.svg @@ -0,0 +1,3 @@ + + + diff --git a/paicoding-ui/src/main/resources/static/admonition/images/adm-tip.svg b/paicoding-ui/src/main/resources/static/admonition/images/adm-tip.svg new file mode 100644 index 000000000..0b8ae7f04 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/admonition/images/adm-tip.svg @@ -0,0 +1,3 @@ + + + diff --git a/paicoding-ui/src/main/resources/static/admonition/images/adm-warning.svg b/paicoding-ui/src/main/resources/static/admonition/images/adm-warning.svg new file mode 100644 index 000000000..5348eec73 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/admonition/images/adm-warning.svg @@ -0,0 +1,3 @@ + + + diff --git a/paicoding-ui/src/main/resources/static/css/common.css b/paicoding-ui/src/main/resources/static/css/common.css new file mode 100644 index 000000000..9f16f3aa3 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/common.css @@ -0,0 +1,16 @@ +/* 页面公共组件样式 */ + +/* 导航栏 */ +@import "./components/navbar.css"; + +/* 底部 */ +@import "./components/footer.css"; + +/* 文章样式 */ +@import "./components/article-item.css"; + +/* 侧边栏 */ +@import "./components/side-column.css"; + +/* 文章底部 */ +@import "./components/article-footer.css"; diff --git a/paicoding-ui/src/main/resources/static/css/components/article-footer.css b/paicoding-ui/src/main/resources/static/css/components/article-footer.css new file mode 100644 index 000000000..fb4263d3b --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/components/article-footer.css @@ -0,0 +1,440 @@ +/*文章底部的点赞*/ +.article-heart { + width: 100%; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + margin-top: 30px; + border-top: 1px solid var(--pai-hr-color-1); + padding-top: 30px; +} + +.article-heart .praise-box.active { + color: var(--pai-brand-3-click); + border: 1px solid var(--pai-brand-3-click); +} + +.article-heart .praise-box { + font-size: 27px; + width: 3rem; + height: 3rem; + text-align: center; + border-radius: 30px; + cursor: pointer; + border: 1px solid var(--pai-color-3-gray); + color: var(--pai-color-3-gray); + margin-bottom: 7.5px; +} + +.article-heart .approval-tips-line { + position: relative; + margin-bottom: 15px; + color: var(--pai-color-999-gray); + font-size: 14px; +} + +.article-heart .approval-tips-line:before { + content: ""; + position: absolute; + top: 10px; + left: -2rem; + height: 2px; + width: 25%; + background: linear-gradient( + 270deg, + var(--pai-brand-1-normal), + var(--pai-color-fff-normal) + ); +} + +.article-heart .approval-tips-line:after { + content: ""; + position: absolute; + top: 10px; + right: -3rem; + height: 2px; + width: 25%; + background: linear-gradient( + 90deg, + var(--pai-brand-1-normal), + var(--pai-color-fff-normal) + ); +} + +.article-heart .approval-img { + display: inline-block; + width: 25px; + height: 25px; + margin-right: 7.5px; + border-radius: 50%; + cursor: pointer; + text-align: center; +} + +.praise-photos { + text-align: center; +} + +.article-heart .approval-img:last-child { + margin-right: 0; +} + +.article-heart .approval-img img { + width: 100%; + height: 100%; + border-radius: 50%; +} + +/* 评论列表 */ +.common-item-content { + margin-left: 10px; + flex: 1; + padding-top: 8px; +} + +.common-item-content-head { + flex: 1; + display: flex; + justify-content: space-between; + font-size: 12px; + line-height: 14px; + color: var(--pai-color-999-gray); +} + +.common-item-content-value { + font-size: 14px; + line-height: 24px; + color: #333; + margin: 10px 0 0; + word-break: break-all; + white-space: pre-wrap; +} + +.comment-write-wrap { + display: flex; + justify-content: flex-start; + padding: 20px; +} + +.comment-write-img { + font-size: 40px; + width: 40px; + height: 40px; + margin-right: 15px; + border-radius: 50%; +} + +.common-write-content { + display: flex; + flex-direction: column; + align-items: flex-end; + flex: 1; +} + +.comment-write-textarea { + width: 100%; + border: 1px solid var(--pai-color-999-gray); + border-radius: 2px; + padding: 8px 16px; + margin-bottom: 10px; +} + +.comment-write-btn { + border-radius: 2px; +} + +.c-btn-disabled, +.c-btn-disabled:hover { + background-color: var(--pai-color-5-gray); + border-color: var(--pai-color-5-gray); + color: var(--pai-color-999-gray); + cursor: pointer; +} + +.comment-item-wrap, +.comment-item-wrap-second { + /* display: flex; */ + margin-bottom: 10px; + padding: 12px 0; + border-bottom: 1px solid var(--pai-hr-color-1); +} + +.comment-item-top { + display: flex; +} + +.comment-item-wrap-second { + margin-left: 30px; + background-color: #f7f8fa; + border-radius: 4px; + padding: 12px; +} + +.comment-item-img { + width: 24px; + height: 24px; + border-radius: 50%; + box-sizing: border-box; + border: 1px solid var(--pai-color-5-gray); +} + +/* 评论 */ +.hf-con { + display: flex; + flex-direction: column; + margin-top: 8px; + width: 100%; +} + +.hf-pl { + border: 0; + flex: 0 0 auto; + margin-left: auto; + width: 92px; + text-align: center; + border-radius: 2px; + font-size: 14px; + line-height: 36px; + background: var(--pai-brand-1-normal); + color: #fff; + padding: 0; + cursor: pointer; + margin-top: 4px; +} + +.hf-pl--disabled { + background-color: #999; + cursor: not-allowed; +} + +.hf-input { + padding: 8px 12px; + border-radius: 2px; +} + +.reply-comment-text-none { + display: none; +} + +.ui-message { + box-shadow: inset 0 0 0 1px #a9d5de, 0 0 0 0 transparent; + background: #f8f8f9; + border-radius: 0.28571429rem; + color: rgba(0, 0, 0, 0.87); + line-height: 1.4285em; + margin: 1em 0; + min-height: 1em; + padding: 1em 1.5em; + position: relative; + transition: opacity 0.1s ease, color 0.1s ease, background 0.1s ease, + box-shadow 0.1s ease; + color: #276f86; + font-size: 0.8em; +} + +/*全部评论*/ +.all-comment, +.hot-comment { + padding: 20px; +} + +.all-comment-title, +.hot-comment-title { + font-size: 18px; + border-bottom: 1px solid var(--pai-hr-color-1); + padding-bottom: 12px; +} + +.all-comment-title em { + font-weight: 500; + color: var(--pai-brand-1-normal); +} + +/* 评论的点赞回复 */ +.action-box { + margin-top: 8px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.action-box .item, +.action-box { + display: flex; + align-items: center; +} + +.action-box .item { + margin-right: 16px; + line-height: 22px; + font-size: 12px; + cursor: pointer; + color: var(--pai-color-999-gray); +} + +.action-box .item svg { + fill: #8a919f; + margin-right: 4px; +} + +.action-box .item:hover { + color: var(--pai-brand-2-hover); +} + +.action-box .item:hover svg { + fill: var(--pai-brand-2-hover); +} + +.action-box .item.active { + color: var(--pai-brand-2-hover); +} + +.action-box .item.active svg { + fill: var(--pai-brand-2-hover); +} + +/*文章评论*/ +.correlation-article { + padding: 20px; + margin-top: 20px; +} + +.correlation-article-title { + font-size: 24px; + font-weight: 400; +} + +.panel-btn { + position: relative; + margin-bottom: 1.667rem; + width: 3rem; + height: 3rem; + background-color: #fff; + background-position: 50%; + background-repeat: no-repeat; + border-radius: 50%; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.04); + cursor: pointer; + text-align: center; + font-size: 1.67rem; +} + +.panel-btn .sprite-icon { + color: var(--pai-color-3-gray); + height: 100%; +} + +.panel-btn:hover .sprite-icon { + color: var(--pai-color-4-gray); +} + +.panel-btn:not(.share-btn).active .sprite-icon.icon-collect { + color: var(--pai-brand-3-click); +} + +.panel-btn:not(.share-btn).active .sprite-icon { + color: var(--pai-brand-3-click); +} + +.panel-btn:not(.share-btn).active.with-badge:after { + background-color: var(--pai-brand-6-mq); +} + +.panel-btn.with-badge:after { + content: attr(badge); + position: absolute; + top: 0; + left: 75%; + height: 17px; + line-height: 17px; + padding: 0 5px; + border-radius: 9px; + font-size: 11px; + text-align: center; + white-space: nowrap; + background-color: #c2c8d1; + color: #fff; +} + +.panel-btn.share-btn:after { + display: block; + content: " "; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 50%; +} + +.panel-btn.share-btn:hover .share-popup { + display: flex; +} + +.panel-btn.share-btn .share-popup { + display: none; + position: absolute; + top: 0; + flex-direction: column; + left: calc(100% + 14px); + z-index: 30; + background: #fff; + border-radius: 4px; + padding: 9px 0; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + box-shadow: 0 8px 24px rgba(81, 87, 103, 0.16); +} + +.panel-btn.share-btn .share-popup:after { + position: absolute; + width: 0; + height: 0; + content: " "; + right: 100%; + top: 14px; + border: 12px solid transparent; + border-right-color: #fff; +} + +.panel-btn.share-btn .share-popup .share-item { + display: flex; + align-items: center; + height: 44px; + padding: 0 15px; +} + +.panel-btn.share-btn .share-popup .share-item:hover { + background-color: #f2f3f5; +} + +.panel-btn.share-btn .share-popup .share-item:hover.wechat .wechat-qrcode { + display: flex; +} + +.panel-btn.share-btn .share-popup .share-item:hover .share-icon { + color: #515767; +} + +.panel-btn.share-btn .share-popup .share-item .share-item-title { + margin-left: 8px; + font-size: 14px; + color: #515767; +} + +.panel-btn.share-btn .share-popup .share-item .share-icon { + color: #8a919f; + width: 20px; + height: 20px; + font-size: 1.67rem; +} + +.sprite-icon { + width: 20px; + height: 20px; + fill: currentColor; + vertical-align: middle; + transition: all 0.15s linear; +} diff --git a/paicoding-ui/src/main/resources/static/css/components/article-item.css b/paicoding-ui/src/main/resources/static/css/components/article-item.css new file mode 100644 index 000000000..6e02d2f7f --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/components/article-item.css @@ -0,0 +1,327 @@ +/*首页文章列表*/ +.cdc-article-panel__list .cdc-article-panel { + margin-bottom: 4px; +} + +/*首页文章*/ +.cdc-article-panel { + box-sizing: border-box; + padding: 16px 0; + border-bottom: 1px solid #d6dbe3; + position: relative; + cursor: pointer; + max-width: 872px; + padding-right: 20px; +} + +/*首页文章跳转详情链接*/ +.cdc-article-panel__link { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 1; +} + +/*首页文章(除去 a 链接的部分)*/ +.cdc-article-panel__inner { + position: relative; + display: flex; + align-items: flex-start; + flex-flow: row nowrap; +} +/*再套一层*/ +.cdc-article-panel__main { + min-width: 0; + flex: 1 1 auto; +} + +/*文章标题+推荐图标*/ +.user-article-item-title-wrap { + display: flex; + align-items: center; +} + +/*文章上的推荐图标*/ +.article-card-top-img { + margin-bottom: 12px; + margin-right: 12px; +} + +/*文章标题*/ +.user-article-item-title { + word-break: break-word; + font-weight: 500; + font-size: 18px; + line-height: 26px; + color: var(--pai-color-3-black); + margin-bottom: 12px; + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; +} + +/*文章状态tag*/ +.user-article-item-tag { + word-break: break-word; + font-weight: 500; + font-size: 14px; + line-height: 26px; + color: var(--pai-brand-6-mq); + margin-bottom: 12px; + margin-left: 1em; + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; +} + +.cdc-tag-links__item:not(:first-child):not(:last-child) { + margin: 0; +} + +/*文章的简介描述*/ +.cdc-article-panel__media { + display: flex; + align-items: center; +} + +/*下一层*/ +.cdc-article-panel__desc { + font-size: 14px; + line-height: 22px; + color: var(--pai-color-4-gray); + min-height: 44px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + word-break: break-word; +} + +/*文章列表底部的数据(作者、日期、阅读、留言、点赞、文章标签)*/ +.cdc-article-panel__infos { + margin-top: 10px; + display: flex; + align-items: center; + font-size: 12px; + line-height: 20px; +} + +/*作者*/ +.cdc-article-panel__user { + position: relative; + display: flex; + align-items: center; + z-index: 2; +} + +/*作者头像 圆角*/ +.cdc-avatar.circle, +.cdc-avatar.circle .cdc-avatar__inner { + border-radius: 50%; +} + +/*作者头像大小*/ +.cdc-avatar.large { + width: 32px; + height: 32px; +} + +/*作者头像边距*/ +.cdc-article-panel__user-avatar { + flex-shrink: 0; + margin-right: 8px; +} + +/*作者头像*/ +.cdc-avatar { + display: inline-block; + vertical-align: middle; + position: relative; + width: 28px; + height: 28px; + border-radius: 50%; + border-bottom-left-radius: 0; + background-color: #d8d8d8; +} + +/*头像的位置*/ +.cdc-avatar__inner, +.cdc-avatar__level { + box-sizing: border-box; + position: absolute; + bottom: 0; +} + +/*头像*/ +.cdc-avatar.large .cdc-avatar__inner { + font-size: 16px; + line-height: 30px; +} + +/*头像*/ +.cdc-avatar__inner { + display: block; + width: 100%; + height: 100%; + background-position: bottom; + background-size: auto 100%; + background-repeat: no-repeat; + border-radius: 50%; + border-bottom-left-radius: 0; + text-align: center; + font-size: 14px; + line-height: 26px; + left: 50%; + transform: translateX(-50%); +} + +/*作者名*/ +.cdc-article-panel__user-name { + font-weight: 500; + color: var(--pai-color-3-black); + line-height: 20px; +} +/*文章发布时间*/ +.cdc-article-panel__user + .cdc-article-panel__date { + margin-left: 12px; +} +/*文章发布时间的颜色位置*/ +.cdc-article-panel__date { + color: #97a3b7; + position: relative; +} + +/*发布时间前的显示方式*/ +.cdc-article-panel__user + .cdc-article-panel__date:before { + display: block; +} + +/*发布时间前的小圆点*/ +.cdc-article-panel__date:before { + content: ""; + position: absolute; + top: 9px; + left: -7px; + width: 2px; + height: 2px; + border-radius: 50%; + background-color: rgba(151, 163, 183, 0.9); + display: none; +} + +/*时间留言点赞*/ +.cdc-icon__list { + margin-right: -24px; +} + +/*时间留言点赞*/ +.cdc-article-panel__operate { + margin-left: 24px; +} +/*时间留言点赞大小*/ +.read-comment-praise { + height: 16px; + width: 16px; +} + +/*数量的大小颜色*/ +.cdc-icon__number { + vertical-align: middle; + font-size: 12px; + line-height: 20px; + color: #97a3b7; + margin-left: 6px; +} + +/*时间留言点赞*/ +.article-show-wrap { + display: flex; + align-items: center; + justify-content: flex-start; + color: var(--pai-color-999-gray); + cursor: pointer; + position: relative; + margin-right: 24px; +} + +/*文章标签*/ +.cdc-article-panel__tags { + position: absolute; + right: 0; + bottom: 5px; + z-index: 2; +} + +/*标签的对齐方式*/ +.cdc-tag-links { + display: flex; + align-items: center; +} + +/*标签的图标*/ +.cdc-tag-links__icon { + display: block; + margin-right: 8px; +} + +/*标签的位置*/ +.cdc-tag-links__items { + display: flex; + align-items: center; + margin: 0 -12px; +} + +/*标签的大小*/ +.cdc-tag-links__item { + display: block; + margin: 0 12px; + font-size: 12px; + line-height: 20px; + color: #97a3b7; +} + +/*文章封面图*/ +.cdc-article-panel__object { + flex-shrink: 0; + margin-left: 20px; + width: 180px; + position: relative; +} + +.cdc-article-panel__object-thumbnail { + display: block; + background-size: cover; + background-repeat: no-repeat; + background-position: 50%; + width: 100%; + height: auto; + padding-top: 54.6667%; +} + +.cdc-article-panel:hover .user-article-item-title { + color: var(--pai-brand-2-hover); +} + +.cdc-article-panel__user-name:hover { + color: var(--pai-color-3-black); +} + +/* home 适配 */ +@media screen and (max-width: 768px) { + .article-title-wrap, + .cdc-article-panel__date { + display: none; + } + + .cdc-article-panel__title { + + } + +} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/css/components/footer.css b/paicoding-ui/src/main/resources/static/css/components/footer.css new file mode 100644 index 000000000..9db116435 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/components/footer.css @@ -0,0 +1,61 @@ +.foot { + height: 70px; + display: flex; + align-items: center; + justify-content: center; + background-color: #000; + color: #fff; + flex-direction: column; + font-size: 14px; +} + +.body-404 footer { + position: absolute; + bottom: 0; + width: 100%; + height: 70px; +} + +.foot-link { + list-style: none; + padding: 25px 0; + width: 80%; + margin: 0 auto; + text-align: left; + font-size: 0; +} + +.foot li { + font-size: 14px; + padding: 0 10px; + display: inline-block; + vertical-align: middle; + line-height: 1em; +} + +.foot li:last-child { + border-left: none; + float: right; + padding-right: 0; +} + +.foot .visit_cnt { + color: #e96900; +} + +/* 宽屏布局 */ +.stats-container { + display: flex; + justify-content: space-between; /* 使两个 .stats-row 分布在容器两端 */ +} + +/* 媒体查询,针对小屏幕设备 */ +@media screen and (max-width: 768px) { + .stats-row { + white-space: nowrap; /* 防止内容换行 */ + text-align: center; /* 使内容居中 */ + } + .stats-container { + display: block; /* 使 .stats-row 堆叠显示 */ + } +} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/css/components/navbar.css b/paicoding-ui/src/main/resources/static/css/components/navbar.css new file mode 100644 index 000000000..a6c423e17 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/components/navbar.css @@ -0,0 +1,257 @@ +/* 覆盖框架默认样式 */ +.navbar { + padding: 0.5rem 4rem; +} + +.navbar-count-msg-box { + position: relative; +} + +.navbar-count-msg { + position: absolute; + top: 4px; + right: 2px; + width: 15px; + height: 15px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background-color: var(--pai-brand-6-mq); + color: #f1f1f1; + transform: scale(0.8); + font-size: 12px; + font-weight: 500; + background: #f03535; +} + +.nav-item { + clear: both; + margin: 0; + padding: 5px 10px; + color: rgba(0, 0, 0, 0.65); + font-weight: 600; + font-size: 1.1em; + line-height: 22px; + white-space: nowrap; + cursor: pointer; + transition: all 0.3s; +} + +.navbar-light .navbar-nav .nav-link { + color: rgba(0, 0, 0, 0.85); +} + +/*navbar*/ +.navbar { + height: 60px; + /* border-bottom: 1px solid #dee2e6; */ + position: sticky; + --tw-bg-opacity: 1; + background-color: rgba(36, 41, 47, var(--tw-bg-opacity)); +} +.nav-right { + display: flex; + align-items: center; +} + +.nav-body { + display: flex; + width: 100%; + justify-content: space-between; + max-width: 1200px; + margin: 0 auto; +} + +.nav-link { + font-size: 18px; + font-weight: 500; +} + +.nav-link:hover { + color: var(--pai-brand-2-hover); +} + +.nav-article { + font-weight: 500; + height: 32px; + font-size: 14px; + display: flex; + align-items: center; + border-radius: 2px; +} + +.nav-notice { + display: flex; + align-items: center; +} + +.dropdown-toggle::after { + display: none !important; +} + +.nav-login-img { + height: 36px; + width: 36px; + border-radius: 50%; + cursor: pointer; +} + +.nav-login-menu { + left: -20px; + box-shadow: 0 0 24px rgb(81 87 103 / 16%); + width: 224px; + border-radius: 4px; + padding: 20px; + left: -200px; +} + +.nav-login-head { + display: flex; + align-items: center; + padding: 0; +} + +.nav-link { + color: #fff; +} +.navbar-logo-wrap { + display: flex; + align-items: center; +} + +.logo-lg { + height: 30px; + width: 30px; + display: none; +} + +.nav-right-user { + display: flex; +} + +.nav-menu-lg-btn { + color: #fff; + font-weight: 500; + font-size: 18px; +} +/*categories*/ + +.home-nav-classify { + background-color: #fff; +} + +.nav-menu-lg { + display: none; +} + +.nav-logo-wrap-lg { + display: flex; + padding-right: 36px; +} + +/* navbar right user */ +.nav-right-user { + position: relative; +} + +.nav-user-avatar { + display: flex; + align-items: center; + position: relative; +} + +.nav-login-img { + width: 36px; + height: 36px; + cursor: pointer; +} + +.nav-user-dropdown { + position: absolute; + top: 50px; + right: 0; + z-index: 9999; + display: none; + background-color: #fff; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + border-radius: 4px; +} + +/* 头像侧边箭头 */ +.nav-user-arrow { + cursor: pointer; + position: absolute; + top: calc(50% - 3px); + left: 42px; + width: 0; + height: 0; + border-style: solid; + border-width: 6px 6px 0 6px; + border-color: #fff transparent transparent transparent; +} + +.nav-user-dropdown-inner { + padding: 10px; +} + +/* 下落框向上箭头 */ +.nav-user-dropdown::before { + content: ""; + position: absolute; + top: -6px; + left: 87%; + margin-left: -6px; + border-width: 0 7px 7px; + border-style: solid; + border-color: #fff transparent; +} + + +/* home 适配 */ +@media screen and (max-width: 768px) { + .nav-article { + display: none; + } + .logo { + display: none; + } + .logo-lg { + display: block; + } + .navbar { + padding-left: 18px; + padding-right: 32px; + } + .collapse:not(.show) { + display: none; + } + .nav-menu-lg { + display: flex; + margin-left: 20px; + align-items: center; + } +} + +.vip span { + color: var(--pai-brand-1-normal); + padding: 0; + height: 48px; + line-height: 48px; + font-size: 14px; +} + +.vip img { + margin-left: 10px; + position: relative; + vertical-align: middle; + width: 14px; + height: 24px; + top: 11px; + left: 0; + display: inline-block; +} + +.vip { + display: flex; +} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/css/components/side-column.css b/paicoding-ui/src/main/resources/static/css/components/side-column.css new file mode 100644 index 000000000..08ca0b0cc --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/components/side-column.css @@ -0,0 +1,841 @@ +/* 右侧列表 */ +.home-right-item-wrap { + background-color: var(--pai-bg-white-fff); + overflow: hidden; + padding: 20px; + margin-bottom: 20px; + } + .home-right-item-icon { + height: 18px; + width: 18px; + position: relative; + } + .home-right-item-first { + padding-top: 16px; + } + .home-right-item-title { + margin-bottom: 24px; + left: 20px; + top: 16px; + } + + .home-right-item-article-item, + .home-right-item-post-item { + display: flex; + align-items: center; + margin-bottom: 16px; + } + + .home-right-item-post-title { + flex: 1 1; + } + .home-right-item-post-time { + color: #adb5bd; + font-size: 14px; + } + .home-right-item-article-title-txt { + flex: 1 1; + font-size: 14px; + font-weight: 400; + line-height: 22px; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + -webkit-line-clamp: 2; + } + + .home-right-item-article-item i { + flex-shrink: 0; + font-size: 16px; + color: rgba(0, 0, 0, 0.06); + margin-right: 8px; + } + .home-right-item-about-content { + text-align: justify; + font-size: 14px; + line-height: 180%; + color: #333; + } + .home-QR-title { + display: flex; + border-bottom: 1px var(--pai-hr-color-1) solid; + margin-bottom: 20px; + padding-bottom: 20px; + } + .home-QR-title-img { + height: 24px; + width: 24px; + float: left; + margin-right: 6px; + } + .home-QR-title-code { + width: 57px; + height: 57px; + } + .home-QR-title-wrap { + flex: 1; + } + .home-QR-title-content { + font-size: 20px; + height: 24px; + line-height: 120%; + color: #333; + font-weight: 600; + margin-bottom: 12px; + } + .home-QR-title-content-other { + font-size: 14px; + color: #333; + } + .home-QR-content { + display: flex; + flex-direction: column; + } + .home-QR-content span { + font-size: 14px; + line-height: 25px; + color: #333; + } + + /*侧边栏的背景图*/ + .home-right-item-wrap.hot-article, + .home-right-item-wrap.notice, + .home-right-item-wrap.column, + .home-right-item-wrap.pdf { + padding: 0; + --tw-bg-opacity: 1; + background-color: rgba(255, 255, 255, var(--tw-bg-opacity)); + } + + .hot-article-bg, + .notice-bg, + .column-bg, + .pdf-bg { + height: 74px; + margin-top: -14px; + } + + .hot-article + .home-right-item-article-list + .home-right-item-article-item:nth-child(1) + i { + background: var(--pai-brand-6-mq); + } + + .hot-article + .home-right-item-article-list + .home-right-item-article-item:nth-child(2) + i { + background: var(--pai-brand-5-bak); + } + + .hot-article + .home-right-item-article-list + .home-right-item-article-item:nth-child(3) + i { + background: var(--pai-brand-3-click); + } + + .hot-article .home-right-item-article-list .home-right-item-article-item i { + height: 18px; + width: 18px; + line-height: 18px; + background: #ccd0d7; + color: #fff; + display: inline-block; + text-align: center; + } + + .hot-article-content, + .notice-content, + .column-content { + padding: 20px; + margin-top: -20px; + } + + .home-right-item-post-list a:hover, + .home-right-item-article-list a:hover { + color: var(--pai-brand-2-hover); + } + + .com-2-side-activity-desc { + margin-top: 4px; + font-size: 14px; + line-height: 24px; + color: var(--pai-color-999-gray); + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + max-height: 48px; + } + + .c-btn-small, + .c-btn.s, + .c-btn.small { + height: 28px; + line-height: 26px; + min-width: 66px; + padding: 0 5px; + font-size: 12px; + } + + .com-2-side-activity-title { + font-size: 14px; + line-height: 24px; + font-weight: 400; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + /*星球海报*/ + .com-2-panel-subhd { + position: relative; + margin-top: 20px; + margin-bottom: 12px; + line-height: 20px; + color: var(--pai-color-999-gray); + } + + .com-2-panel-subhd:before { + content: ""; + position: absolute; + left: 0; + top: 50%; + width: 100%; + height: 1px; + background-color: #e5e5e5; + } + .com-2-panel-subtitle { + font-size: 12px; + position: relative; + display: inline-block; + box-sizing: border-box; + max-width: 100%; + font-weight: 400; + padding-right: 8px; + background-color: #fff; + } + + .com-event-panel.without-margin { + margin-bottom: 0; + } + .com-event-panel-inner { + box-sizing: border-box; + display: table; + width: 100%; + table-layout: fixed; + } + .com-event-panel-l .com-event-panel-object { + display: block; + width: auto; + } + .com-event-panel-l.theme2 .com-event-panel-img { + width: 100%; + height: auto; + } + + /*精选教程*/ + .com-media { + margin-bottom: 20px; + display: table; + table-layout: fixed; + width: 100%; + box-sizing: border-box; + } + + .com-2-side-topics.without-margin > li:last-child .com-side-topic { + margin-bottom: 0; + } + + .com-2-side-activity .com-media-object { + width: 60px; + vertical-align: middle; + } + + .column-content .com-side-topic-title { + font-weight: 500; + font-size: 14px; + line-height: 22px; + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + } + + .com-media-object { + display: table-cell; + vertical-align: middle; + box-sizing: content-box; + width: 40px; + } + + .com-side-topic .com-media-object { + width: 60px; + vertical-align: middle; + padding-right: 20px; + } + + .com-media-body { + display: table-cell; + vertical-align: top; + box-sizing: border-box; + } + + .com-thumbnail { + display: block; + width: 236px; + height: 177px; + background-size: cover; + background-repeat: no-repeat; + background-position: 50%; + } + + .com-side-topic .com-thumbnail { + width: 71px; + height: 100px; + border-radius: 2px; + } + + .com-side-topic-title { + font-size: 14px; + line-height: 24px; + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + } + + .com-side-topic-desc { + margin-top: 4px; + font-size: 14px; + line-height: 24px; + color: var(--pai-color-999-gray); + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + max-height: 48px; + } + + .column .vip-free-tag { + width: 30px; + height: 20px; + line-height: 20px; + display: inline-block; + vertical-align: middle; + margin-right: 3px; + transform: translateY(-1px); + background: linear-gradient(90deg, #fde8c3, #edd3a7); + border-radius: 2px; + font-size: 12px; + text-align: center; + font-weight: 500; + color: #7e5d25; + white-space: nowrap; + } + + .column .column-title { + margin-right: 30px; + } + + /*侧边栏的 title*/ + .home-right-item-wrap .com-2-panel-title { + font-size: 18px; + } + + /*侧边栏*/ + .col-body { + box-sizing: border-box; + margin: 30px auto 60px; + padding: 0 10px; + max-width: 1200px; + } + .pg-2-article { + margin-top: 20px; + } + + .com-3-layout { + display: table; + table-layout: fixed; + margin-bottom: 60px; + box-sizing: border-box; + width: 100%; + } + + .com-3-layout > .layout-main { + display: table-cell; + vertical-align: top; + padding-left: 20px; + } + + .com-3-layout > .layout-main:first-child { + padding-right: 20px; + padding-left: 0; + } + + .pg-2-article .com-3-layout > .layout-main, + .pg-2-article .com-crumb { + padding-left: 60px; + } + + .com-3-layout > .layout-side { + display: table-cell; + vertical-align: top; + width: 300px; + } + .com-2-panel { + margin-bottom: 20px; + box-sizing: border-box; + background-color: var(--pai-color-fff-normal); + padding: 32px; + box-shadow: 0 2px 4px 0 rgb(3 27 78 / 6%); + } + + + + /*作者介绍*/ + .com-2-panel.side { + padding: 20px; + } + + .com-2-panel.side .com-2-panel-hd { + margin-bottom: 20px; + } + .com-2-panel.side .com-2-panel-title { + font-size: 16px; + line-height: 26px; + font-weight: 500; + } + + .com-author-intro { + text-align: center; + } + + .com-author-intro-object { + margin-bottom: 20px; + } + + .com-author-intro-name { + margin-bottom: 8px; + font-size: 16px; + line-height: 26px; + } + .com-author-intro-btns { + margin-top: 20px; + font-size: 0; + } + + .com-author-intro-infos { + margin-top: 28px; + padding-top: 15px; + border-top: 1px solid #e5e5e5; + font-size: 0; + } + /*头像*/ + .com-author-intro-avatar:only-child { + margin-bottom: 0; + } + .com-author-intro-avatar { + display: block; + margin: 0 auto -11px; + width: 80px; + height: 80px; + border-radius: 50%; + background-position: 50%; + background-repeat: repeat; + background-size: cover; + background-color: #d1d4db; + box-sizing: border-box; + border: 1px solid #d1d4db; + } + + .join-days { + font-size: 12px; + color: var(--pai-color-999-gray); + } + + a:hover .join-days { + color: inherit; + } + + /*作者介绍区域的一个小背景*/ + .com-2-panel.internal:before { + content: ""; + position: absolute; + width: 111px; + height: 138px; + background: url(../../img/icon-decorate_edc.svg) 50% no-repeat; + top: -2px; + right: -20px; + } + + .com-2-panel.internal { + position: relative; + overflow: hidden; + } + + /*加入天数*/ + .com-author-intro-desc { + position: relative; + display: inline-block; + vertical-align: middle; + box-sizing: border-box; + max-width: 100%; + padding-right: 20px; + font-size: 12px; + line-height: 18px; + color: var(--pai-color-999-gray); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .com-verification { + position: relative; + top: -1px; + display: inline-block; + width: 12px; + height: 12px; + font-size: 0; + vertical-align: middle; + margin: -1px 3px 0; + line-height: 12px; + } + + .com-author-intro-desc .com-verification { + position: absolute; + right: 0; + top: 3px; + } + + .com-verification .verified { + display: inline-block; + width: 100%; + height: 100%; + background-image: url(../../img/icon-verified_804.svg); + } + + /*取消关注*/ + .com-author-intro-btns .c-btn { + min-width: 88px; + margin: 0 5px; + } + + .c-btn { + height: 32px; + min-width: 88px; + padding: 0 16px; + background-color: var(--pai-brand-1-normal); + border: 1px solid transparent; + color: #fff; + font-size: 14px; + line-height: 30px; + text-align: center; + display: inline-block; + cursor: pointer; + outline: 0 none; + box-sizing: border-box; + border-radius: 0; + } + + .c-btn:hover { + text-decoration: none; + background-color: var(--pai-brand-2-hover); + } + + /*教程*/ + .c-btn-hole, + .c-btn-hole:hover { + border: 1px solid var(--pai-brand-2-hover); + color: var(--pai-brand-2-hover); + } + + .c-btn-hole { + background-color: transparent; + line-height: 30px; + } + + .c-btn-hole:hover { + background-color: var(--pai-brand-7-light); + } + + /*阅读和点赞*/ + .com-author-intro-info { + display: inline-block; + vertical-align: top; + width: 24%; + text-align: center; + font-size: 12px; + line-height: 20px; + color: var(--pai-color-999-gray); + padding-top: 5px; + padding-bottom: 5px; + } + + .com-author-intro-info-link { + display: block; + color: inherit; + margin: -5px 0; + padding: 5px 0; + } + + .com-author-intro-info-num { + margin-top: 4px; + font-size: 16px; + line-height: 28px; + height: 28px; + font-weight: 500; + } + + .com-author-intro-info-link:hover { + background-color: #f3f5f9; + } + + .col-2-article { + position: relative; + word-wrap: break-word; + } + + /* 文章部分 */ + .detail-head-img { + width: 100%; + max-height: 432px; + object-fit: cover; + margin-bottom: 20px; + } + + /*文章标题*/ + .article-info-title { + font-size: 26px; + line-height: 31px; + vertical-align: bottom; + margin-bottom: 22px; + } + /*原创标签*/ + .com-2-mark-triangle { + position: relative; + width: 48px; + height: 48px; + line-height: 18px; + font-weight: 500; + color: #ff7800; + font-size: 12px; + } + + .com-2-mark-triangle:before { + content: ""; + position: absolute; + left: 0; + top: 0; + width: 0; + height: 0; + border-color: #faece0 #faece0 transparent transparent; + border-style: solid; + border-width: 24px; + } + + .col-2-article .article-mark { + position: absolute; + right: 0; + top: 0; + } + + .com-2-mark-triangle .mark-cnt { + position: absolute; + right: 0; + top: 50%; + z-index: 2; + margin-top: -16px; + margin-right: -8px; + width: 100%; + text-align: center; + -webkit-transform: rotate(45deg); + transform: rotate(45deg); + } + + /*电子书的星标*/ + .ebook-home-stars { + align-items: center; + } + + .ebook-def-star { + height: 16px; + width: 92px; + background: url(../../img/star.png) no-repeat 0 / auto 100%; + position: relative; + } + + .ebook-home-stars .ebook-star-count { + font-size: 16px; + line-height: 22px; + margin-left: 6px; + } + + .ebook-home-stars .ebook-def-star .ebook-cur-star { + height: 16px; + background: url(../../img/star-lighten.png) no-repeat 0 / auto 100%; + position: absolute; + left: 0; + top: 0; + } + + .ebook-home-info .ebook-home-view { + font-size: 13px; + color: var(--pai-color-999-gray); + line-height: 18px; + padding-left: 20px; + background: url(../../img/eye.png) no-repeat 0/16px auto; + } + + .ebook-home-info .ebook-home-download { + font-size: 13px; + color: var(--pai-color-999-gray); + line-height: 18px; + padding-left: 16px; + margin-left: 16px; + background: url(../../img/download.png) no-repeat 0/12.5px auto; + } + + .rank-box-item-right { + flex: 1; + overflow: hidden; + height: 100px; + display: flex; + flex-direction: column; + justify-content: space-between; + } + + .pdf .com-thumbnail:after { + content: "立即下载"; + width: 60px; + height: 20px; + background-image: linear-gradient(270deg, #ffa400, #ff791a); + font-size: 12px; + color: var(--pai-color-fff-normal); + font-weight: 500; + text-align: center; + line-height: 20px; + position: absolute; + } + +.cdc-card { + background: linear-gradient(1turn,var(--pai-bg-white-fff),var(--pai-color-6-gray)); + border: 2px solid var(--pai-bg-white-fff); + box-shadow: 8px 8px 20px rgb(55 99 170 / 10%); +} + +.cdc-card__inner { + box-sizing: border-box; + padding: 20px; +} + +.cdc-card__hd { + display: flex; + align-items: center; + justify-content: space-between; + box-sizing: border-box; + padding: 2px 0 12px; + border-bottom: 1px solid var(--pai-border-color-1); +} + +.mod-subscribe .cdc-card__hd { + border-bottom: none; + padding-bottom: 16px; +} + +.cdc-card__title { + flex: 1 1; + position: relative; + font-weight: 500; + font-size: 18px; + line-height: 26px; + color: var(--pai-color-3-black); +} + +.cdc-card__title:before { + content: ""; + position: absolute; + display: block; + top: 4px; + left: -12px; + width: 3px; + height: 18px; + background: var(--pai-brand-1-normal); +} + +.mod-subscribe__title { + font-weight: 500; + font-size: 18px; + line-height: 28px; + color: var(--pai-color-3-black); + margin-bottom: 16px; +} + +.mod-subscribe__content { + position: relative; + background: var(--pai-color-7-gray); + border-radius: 2px; + box-sizing: border-box; + padding: 14px 16px; +} + +.mod-subscribe__title em { + font-weight: 500; + font-style: normal; + padding: 0 2px; + color: var(--pai-brand-5-bak); +} + +.mod-subscribe__content-tag { + position: absolute; + left: -4px; + top: 8px; + width: 64px; + height: 26px; + line-height: 26px; + background: url(../../img/you-will-get.png) 50% no-repeat; + background-size: cover; + box-sizing: border-box; + padding-left: 8px; + font-size: 14px; + color: var(--pai-color-fff-normal); +} + +.mod-subscribe__content-inner { + display: flex; + align-items: center; + justify-content: space-between; +} + +.mod-subscribe__content-detail { + margin-top: 14px; +} + +.mod-subscribe__content-qr { + width: 100px; + height: 100px; + flex-shrink: 0; + margin-left: 12px; + background: linear-gradient(180deg,var(--pai-color-5-gray),var(--pai-color-fff-normal)); + border: 2px solid var(--pai-color-fff-normal); + box-shadow: 8px 8px 20px rgb(55 99 170 / 10%), -8px -8px 20px hsl(0deg 0% 100% / 40%); + border-radius: 4px; + box-sizing: border-box; + padding: 2px; +} + +.mod-subscribe__content-qr>img { + width: 100%; + height: 100%; +} + +.mod-subscribe__content-prize { + font-weight: 500; + font-size: 14px; + line-height: 22px; + color: var(--pai-brand-6-mq); + margin-bottom: 8px; +} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/css/global.css b/paicoding-ui/src/main/resources/static/css/global.css new file mode 100644 index 000000000..e4a4037ef --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/global.css @@ -0,0 +1,1058 @@ +/*技术派的整体色系*/ +body { + /*正常色*/ + --pai-brand-1-normal: #ff6900; + /*鼠标放上去后的颜色*/ + --pai-brand-2-hover: #ff8721; + /*点击后激活后的颜色*/ + --pai-brand-3-click: #f59e2f; + /*不可用的颜色*/ + --pai-brand-4-disable: #d1a278; + /*备用色 非常深的颜色*/ + --pai-brand-5-bak: #fe6617; + /*消息提醒点*/ + --pai-brand-6-mq: #f85959; + --pai-brand-7-light: rgba(255, 105, 0, 0.15); + /*浅色时由上一个过度到下一个*/ + --pai-brand-8-light: rgba(255, 105, 0, 0.35); + /*颜色白色*/ + --pai-color-fff-normal: #fff; + /*浅灰色*/ + --pai-color-999-gray: #999; + /*另外一种灰*/ + --pai-color-3-gray: #8a919f; + /*另外一种灰 深一点*/ + --pai-color-4-gray: #515767; + /*disable 的灰*/ + --pai-color-5-gray: #ddd; + /*非常浅的灰*/ + --pai-color-6-gray: #f3f5f8; + /*比上面更灰一点*/ + --pai-color-7-gray: #eff3f9; + /*偏黑色*/ + --pai-color-3-black: #212529; + /*背景色*/ + --pai-bg-white-fff: #fff; + /*淡黄色*/ + --pai-bg-normal-1: #fff3db; + --pai-bg-light-1: #eeeeee; + /*浅一点的颜色*/ + --pai-bg-light-2: #f7f8fa; + /*背景色浅一点*/ + --pai-bg-dark-1: #373d41; + /*主题色深一点*/ + --pai-bg-dark-2: #2c3134; + /*主题色更深一点*/ + --pai-bg-dark-3: #000000; + /*讯飞星火的左侧聊天背景色*/ + --pai-bg-smart-chat: #f5f5f5; + /*横线的颜色*/ + --pai-hr-color-1: #f0f0f0; + /*边框颜色*/ + --pai-border-color-1: #d2d9e7; + /*字体*/ + --pai-font-2: #515767; + + /*边框*/ + --color-canvas-default: transparent; + /*消息的宽度*/ + --message-max-width: 80%; + /*左边宽度*/ + --pai-sidebar-width: 360px; + /*卡片的阴影*/ + --card-shadow: 0px 2px 4px 0px rgba(0,0,0,.05); + --window-height: calc(100vh - 170px); + --window-width: 90vw; + --window-content-width: calc(100% - var(--pai-sidebar-width)); + --second: #e7f8ff; + /*外边距*/ + margin: 0; +} + +/*全局的色调往前面放*/ +ul { + margin: 0; + padding: 0; +} + +li { + list-style: none; +} + +a { + color: var(--pai-color-3-black); +} + +a.underline { + color: var(--pai-brand-1-normal); +} + +a:hover.underline { + text-decoration: underline; + color: var(--pai-brand-2-hover); +} + +a:hover { + text-decoration: none; + color: var(--pai-brand-2-hover); +} + +input:focus,.form-control:focus { + border: 1px solid var(--pai-brand-3-click); + outline: none; + box-shadow: none; +} + +textarea { + resize: none; + font-size: 14px; +} + +input::placeholder, +textarea::placeholder { + font-size: 14px; +} + +textarea:focus { + border: 1px solid var(--pai-brand-3-click); + outline: none; +} + +button, +button:focus { + outline: none; + box-shadow: none !important; +} + +/*接下来是 ID 的*/ +#scrollUp { + bottom: 82px; + right: 20px; + width: 46px; + height: 44px; + position: absolute; + border-color: transparent; + background-color: var(--pai-color-fff-normal); + transition: all 0.2s ease-in-out; + outline: none; + padding-top: 8px; + padding-left: 12px; +} + +#scrollUp:before { + content: ""; + display: inline-block; + vertical-align: middle; + width: 22px; + height: 12px; + background-image: url(../img/widget-top_2e4.svg); +} + +#scrollUp:hover { + background-color: var(--pai-brand-7-light); +} + +#scrollUp-active { + display: none; +} + +/* 登录、注册弹窗 */ + +.modal .modal-dialog { + max-width: 600px; +} + +.modal .title { + font-size: 0.8rem; + font-weight: 500; + margin: 0 0 1rem; +} + +.login-main, .register-main { + width: 32rem; +} + +.modal .mdnice-user-dialog-footer { + display: flex; + justify-content: space-between; + padding: 10px 20px; + font-size: 0.8rem; +} + +.modal .modal-footer { + justify-content: center; +} + +.modal a:not([href]) { + color: var(--pai-brand-1-normal); + cursor: pointer; +} + +.mdnice-user-dialog-footer a { + color: var(--pai-brand-1-normal); +} + +.mdnice-user-dialog-footer a:hover { + color: var(--pai-brand-2-hover); +} + +.modal a:not([href]):hover { + color: var(--pai-brand-2-hover); +} + +.modal .mdnice-user-dialog-footer p { + margin: 0; +} + +.modal .close { + z-index: 10; + padding: 0; + color: rgba(0, 0, 0, 0.45); + font-weight: 700; + line-height: 1; + text-decoration: none; + background: transparent; + border: 0; + outline: 0; + cursor: pointer; + transition: color 0.3s; +} + +.modal .close span { + display: block; + width: 54px; + height: 54px; + line-height: 54px; + text-align: center; + text-transform: none; + text-rendering: auto; +} + +.modal .modal-content { + border-radius: 8px; +} + +.modal .modal-content h2 { + text-align: center; + margin-bottom: 10px; +} + +.other-login-box { + display: flex; + margin-top: 1rem; + color: var(--pai-font-2); + flex-direction: row; + align-items: center; + justify-content: space-between; +} + +.auth-body { + display: flex; +} + +.oauth-box, .oauth { + display: flex; + align-items: center; + flex-direction: row; + font-size: 0.8rem; +} + +.oauth-bg { + border-radius: 50%; + background-color: var(--pai-bg-light-1); + justify-content: center; + margin-right: 8px; +} + +.clickable { + cursor: pointer; + font-size: 0.8rem; +} + + +.modal .modal-content .modal-body .tabpane-container { + border-left: 1px solid var(--pai-border-color-1); + margin-left: 2rem; + display: flex; + flex-direction: column; + justify-content: center; + width: 80%; +} + +.modal input, .modal input::placeholder { + font-size: 0.8rem; +} + +.modal .first { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.modal .first .signin-qrcode { + width: 140px; +} + +.modal .explain { + text-align: center; + font-size: 12px; +} + +.modal #code { + color: var(--pai-brand-6-mq); + font-size: 20px; +} + +/*接下来是 class 的*/ + +/*需要背景色主动添加,一种浅灰色,腾讯云社区的背景色*/ +.bg-color { + background-color: var(--pai-bg-light-2); +} + +/*纯白色*/ +.bg-color-white { + background-color: var(--pai-bg-white-fff); +} + +.dropdown-item { + color: var(--pai-color-3-black); +} + +/*下拉菜单的颜色覆盖*/ +.dropdown-item.active, +.dropdown-item:active { + color: var(--pai-color-3-black); + background-color: var(--pai-brand-3-click); +} + +.dropdown-item:focus, +.dropdown-item:hover { + background-color: var(--pai-bg-normal-1); +} + +/*覆盖 bootstrap*/ +.card { + border: none; + margin-bottom: 20px; +} + +/*右侧贴片*/ +.home-right, +.custom-home-right { + width: 24%; + border-radius: 20px; + position: relative; +} + +.custom-home-right { + width: 28%; +} + +/*评论*/ +.custom-empty { + text-align: center; + margin-top: 20px; + width: 100%; + font-size: 1rem; +} + +/*页面整体的背景色*/ +.posts-comment-input-box, +.posts-author-box, +.posts-box, +.page-box, +.user-info-box { + background-color: #fff; +} + +.btn-outline-primary:hover, +.page-item.active .page-link, +.current-page { + color: #fff !important; +} + +.bottom-line, +.list-group, +.editor-title { + border-bottom: 1px solid rgba(0, 0, 0, 0.125); +} + +.faq-solution-box, +.posts-comment-input-box { + background-color: #fafbfc; +} + +.posts-comment-input-box { + margin-top: -20px; +} + +.posts-comment-box, +.posts-author-box, +.posts-box, +.editor-form-box, +.editor-title, +.card-body, +.card-header { + padding: 20px; +} + +.type-box { + padding: 10px; +} + +.tag-box, +.no-comment-box, +.posts-author-box, +.posts-box, +.user-info-box, +.page-box, +.carousel { + margin-bottom: 0px; +} + +/*按钮*/ +.custom-theme-bg-color, +.btn-outline-primary:hover, +.page-item.active .page-link, +.current-page, +.btn-primary, +.btn-primary:active, +.btn-primary:focus { + border-color: var(--pai-brand-1-normal); + background-color: var(--pai-brand-1-normal); +} + +.btn-primary:hover { + border-color: var(--pai-brand-2-hover); + background-color: var(--pai-brand-2-hover); +} + +.btn-primary:focus { + border-color: var(--pai-brand-2-hover); + box-shadow: none; +} + +.btn-primary:not(:disabled):not(.disabled).active, +.btn-primary:not(:disabled):not(.disabled):active, +.show > .btn-primary.dropdown-toggle { + background-color: var(--pai-brand-2-hover); + border-color: var(--pai-brand-2-hover); +} + +.page-link, +.page-link:hover, +.btn-outline-primary, +.posts-admin-tag-official, +.custom-font-color { + color: var(--pai-bg-normal-1); +} + +.dropdown-menu { + border: 0; +} + +.input-group-text, +.navbar-toggler, +.modal-content { + margin-bottom: 10px; +} + +.form-control, +.btn, +.dropdown-menu, +.list-group-item:first-child, +.list-group-item:last-child, +.pagination { + border-radius: 0; +} + +.posts-list-desc { + color: rgba(0, 0, 0, 0.87); +} + +.custom-by-both { + padding-left: 10px; + padding-right: 10px; +} + +.carousel-inner img { + width: 100%; + height: 100%; +} + +.posts-list-desc { + display: inline; + max-height: 48px; + text-overflow: -o-ellipsis-lastline; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; +} + +.posts-list-title { + font-size: 18px; + font-weight: 700; + line-height: 1.5; + margin-bottom: 5px; + color: rgba(0, 0, 0, 0.85); +} + +.posts-list-payload-item, +.posts-list-payload-item a, +.posts-list-payload-box-author { + color: #6c757d !important; +} + +.posts-list-payload-item { + padding: 3px 8px; +} + +.page-box { + padding: 10px; +} + +.faq-solution-box { + margin-top: 10px; + padding: 15px; +} + +.faq-solution-box, +.posts-list-desc { + font-size: 13px; + line-height: 24px; +} + +.posts-admin-tag { + margin-top: 4px; + height: 16px; + padding: 2px; + border-radius: 2px; + line-height: 1; + font-size: 12px; + margin-right: 6px; + vertical-align: middle; + -webkit-transform: translateY(1px); + -ms-transform: translateY(1px); + transform: translateY(1px); +} + +.posts-admin-tag-official { + background: rgba(101, 212, 117, 0.1); +} + +.posts-admin-tag-top { + color: #f85959; + background: rgba(248, 89, 89, 0.1); +} + +.posts-admin-tag-marrow { + color: #3c8cff; + background: rgba(60, 140, 255, 0.1); +} + +.selected-domain { + border-bottom: 1px solid var(--pai-brand-1-normal); +} + +.selected-domain a { + /*color: var(--pai-brand-1-normal);*/ +} + +.user-info-box { + height: 200px; + width: 100%; + margin-left: 0; + margin-right: 0; +} + +.user-info-date-box { + height: 80px; + padding-top: 40px; + padding-left: 40px; +} + +.user-info-date-box > p { + display: inline-block; +} + +.user-info-desc-box { + margin-top: 15px; + padding-left: 40px; + padding-right: 40px; +} + +.input-icon { + position: relative; +} + +.input-icon input { + border-radius: 26px; +} + +.input-icon-addon { + position: absolute; + top: 0; + bottom: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + min-width: 2.5rem; + color: rgb(179 174 174 / 60%); + pointer-events: none; + font-size: 1.2em; +} + +.input-icon .form-control:not(:first-child), +.input-icon .form-select:not(:last-child) { + padding-left: 2.5rem; +} + +.list-group-item { + border: none; + padding: 0.75rem 1.25rem; +} + +.btn { + padding-left: 25px; + padding-right: 25px; +} + +.btn-sm { + padding-left: 15px; + padding-right: 15px; +} + +.page-item:first-child .page-link, +.page-item:last-child .page-link { + border-radius: 0; +} + +.comment-avatar-box { + width: 40px; + float: left; +} + +.posts-comment-input-box-btn { + width: 100%; + display: none; +} + +.posts-comment-input-box-textarea { + padding: 4px 10px; + font-size: 13px; + line-height: 1.7; +} + +.posts-comment-input-box-textarea, +.comment-content-box { + width: calc(100% - 40px); + float: right; +} + +.best-answer { + margin-left: 20px; +} + +.best-answer:hover, +.reply-comment:hover { + cursor: pointer; +} + +.comment-content-box-title { + font-size: 16px; + color: #3d464d; + font-weight: 300; +} + +.comment-content-box-content { + color: #505050; + font-size: 14px; + margin: 12px 0; +} + +.comment-content-box-foot { + color: #b2b2b2; + font-size: 14px; +} + +.message-block { + margin-right: 10px; +} + +.third-oauth-login-box::before { + content: "三方账号登录"; + position: absolute; + left: 50%; + bottom: 55px; + font-size: 10px; + transform: translateX(-50%); + -webkit-transform: translate(-50%, -50%); + padding: 0 10px; + background-color: #fff; +} + +.third-oauth-login-box { + display: block; + text-align: center; +} + +.hidden { + display: none; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.block { + display: block; +} + +.z-10 { + z-index: 10; +} + +.left-0 { + left: 0; +} + +.opacity-100 { + opacity: 1; +} + +.opacity-0 { + opacity: 0; +} + +.z-50 { + z-index: 50; +} + +.w-full { + width: 100%; +} + +.w-0 { + width: 0; +} + +.-ml-2 { + margin-left: -0.5rem; +} + +.justify-center { + justify-content: center; +} + +.lg\:max-w-lg { + max-width: 32rem; +} + +.lg\:text-primary-900 { + --tw-text-opacity: 1; + color: #007fff; +} + +.lg\:py-1 { + padding-bottom: 0.25rem; + padding-top: 0.25rem; +} + +.lg\:bg-transparent { + background-color: transparent; +} + +.text-sm { + font-size: 1rem; + line-height: 2rem; +} + +.px-2\.5 { + padding-left: 0.625rem; + padding-right: 0.625rem; +} + +.border-search { + border-width: 1px; +} + +.svg-inline--fa.fa-w-10 { + width: 0.625em; +} + +.border-gray-400 { + --tw-border-opacity: 1; + border-color: var(--pai-bg-dark-1); +} + +.border-gray-500 { + --tw-border-opacity: 1; + border-color: var(--pai-bg-dark-2); +} + +.text-gray-100 { + --tw-text-opacity: 1; + color: var(--pai-bg-dark-1); +} + +.text-primary-300 { + color: var(--pai-brand-1-normal); +} + +.bg-gray-500 { + --tw-bg-opacity: 1; + background-color: var(--pai-bg-dark-1); +} + +.border-b { + border-bottom-width: 1px; +} + +.border-w-1 { + border-width: 1px; +} + +.font-bold { + font-weight: 700; +} + +.search-no-result p { + line-height: 2.6rem; +} + +.inline-block { + display: inline-block; +} + +/*文章详情*/ +.article-content h1, +.article-content h2, +.article-content h3, +.article-content h4, +.article-content h5 { + color: var(--pai-brand-1-normal); + margin-bottom: 10px; + padding-bottom: 7px; +} + +.article-detail .home-right-item-wrap .com-2-panel-title { + font-size: 16px; +} + +.article-content img { + max-width: 100%; + box-sizing: content-box; + background-color: #fff; + margin: 0 auto; + display: block; +} + +.article-content h1 { + border-bottom: 1px solid #eaecef; + font-size: 1.7em; +} + +.article-content h2 { + font-size: 1.5em; +} + +.article-content h3 { + font-size: 1.3em; +} + +.article-content h4 { + font-size: 1.1em; +} + +.article-content h5 { + font-size: 1em; +} + +.article-content strong { + color: var(--pai-brand-1-normal); +} + +.article-content p, +.article-content ol, +.article-content ul, +.article-content table, +.article-content pre, +.article-content blockquote { + /* font-weight: 400; */ + line-height: 1.8; + margin-bottom: 15px; +} + +.article-content blockquote { + padding: 0 1em; + color: #6a737d; + border-left: 0.25em solid #dfe2e5; +} + +.article-content ol, +.article-content ul { + padding-left: 20px; +} + +.article-content table { + display: table; + border-collapse: separate; + border-spacing: 2px; + border-color: grey; + border-spacing: 0; + border-collapse: collapse; + font-size: 14px; +} + +.article-content table th, +.article-content table tr, +.article-content table td { + padding: 6px 13px; + border: 1px solid #dfe2e5; +} + +.article-content pre { + padding: 5px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: #fafafa; + border-radius: 3px; + word-wrap: normal; +} + +.article-content pre div { + background-color: #fafafa; +} + +.article-content li { + list-style: disc; + line-height: 1.4; + font-size: 15px; + margin-bottom: 5px; +} + +.article-content p a,.article-content li a { + color: var(--pai-brand-1-normal); + text-decoration: underline; +} + +.article-content p a:hover,.article-content li a:hover { + color: var(--pai-brand-2-hover); +} + +.article-content .hljs-center { + text-align: center; +} + +.article-content .hljs-left { + text-align: left; +} + +.article-content .hljs-right { + text-align: right; +} + +.article-suspended-panel { + position: fixed; + margin-left: -80px; + top: 140px; + z-index: 2; +} + +.article-suspended-panel-md { + position: fixed; + width: 100%; + bottom: 0; + z-index: 2; + display: flex; + justify-content: space-around; + background: #fff; + padding: 12px 0; + visibility: hidden; +} + +.article-suspended-panel-md .panel-btn { + margin-bottom: 0; +} + + +.article-content pre, .article-content pre>code.hljs { + color: #333; + background: #f8f8f8; +} + +.article-content pre>code { + font-size: 12px; + padding: 15px 12px; + margin: 0; + word-break: normal; + display: block; + overflow-x: auto; + color: #333; + background: #f8f8f8; +} + +.article-content code, .article-content pre { + font-family: Menlo,Monaco,Consolas,Courier New,monospace; +} + +.article-content pre>code.copyable.hljs[lang]:before { + right: 70px; +} + +.article-content pre>code.hljs[lang]:before { + content: attr(lang); + position: absolute; + right: 15px; + top: 3px; + color: hsla(0,0%,54.9%,.8); +} + +.article-content pre .copy-code-btn { + position: absolute; + top: 6px; + right: 15px; + font-size: 12px; + line-height: 1; + cursor: pointer; + color: hsla(0,0%,54.9%,.8); + transition: color .1s; +} + +.article-content pre .copy-code-btn:hover { + color: #8c8c8c; +} + +.article-content pre { + position: relative; +} + +@media (max-width: 768px) { + #registerModal .modal-content .modal-body .tabpane-container, + #loginModal .modal-content .modal-body .login-main, #loginModal .modal-footer { + display: none; + } + + .modal input, .modal input::placeholder,.modal .title { + font-size: 1rem; + } + + #loginModal .modal-content .modal-body .tabpane-container { + border-left: none; + } + + #loginModal .first .signin-qrcode { + width: 180px; + } +} \ No newline at end of file diff --git a/forum-ui/src/main/resources/static/css/bootstrap.min.css b/paicoding-ui/src/main/resources/static/css/three/bootstrap.min.css similarity index 100% rename from forum-ui/src/main/resources/static/css/bootstrap.min.css rename to paicoding-ui/src/main/resources/static/css/three/bootstrap.min.css diff --git a/paicoding-ui/src/main/resources/static/css/three/highlightjs.github.min.css b/paicoding-ui/src/main/resources/static/css/three/highlightjs.github.min.css new file mode 100644 index 000000000..275239a7a --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/three/highlightjs.github.min.css @@ -0,0 +1,10 @@ +pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*! + Theme: GitHub + Description: Light theme as seen on github.com + Author: github.com + Maintainer: @Hirse + Updated: 2021-05-15 + + Outdated base version: https://github.com/primer/github-syntax-light + Current colors taken from GitHub's CSS +*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/css/three/index.css b/paicoding-ui/src/main/resources/static/css/three/index.css new file mode 100644 index 000000000..9f2880953 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/three/index.css @@ -0,0 +1,8 @@ +/* 第三方样式 */ + +@import "./bootstrap.min.css"; + +@import "./toastr.min.css"; + +@import "./select2.min.css"; + diff --git a/paicoding-ui/src/main/resources/static/css/three/nprogress.css b/paicoding-ui/src/main/resources/static/css/three/nprogress.css new file mode 100644 index 000000000..6752d7f4b --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/three/nprogress.css @@ -0,0 +1,74 @@ +/* Make clicks pass-through */ +#nprogress { + pointer-events: none; +} + +#nprogress .bar { + background: #29d; + + position: fixed; + z-index: 1031; + top: 0; + left: 0; + + width: 100%; + height: 2px; +} + +/* Fancy blur effect */ +#nprogress .peg { + display: block; + position: absolute; + right: 0px; + width: 100px; + height: 100%; + box-shadow: 0 0 10px #29d, 0 0 5px #29d; + opacity: 1.0; + + -webkit-transform: rotate(3deg) translate(0px, -4px); + -ms-transform: rotate(3deg) translate(0px, -4px); + transform: rotate(3deg) translate(0px, -4px); +} + +/* Remove these to get rid of the spinner */ +#nprogress .spinner { + display: block; + position: fixed; + z-index: 1031; + top: 15px; + right: 15px; +} + +#nprogress .spinner-icon { + width: 18px; + height: 18px; + box-sizing: border-box; + + border: solid 2px transparent; + border-top-color: #29d; + border-left-color: #29d; + border-radius: 50%; + + -webkit-animation: nprogress-spinner 400ms linear infinite; + animation: nprogress-spinner 400ms linear infinite; +} + +.nprogress-custom-parent { + overflow: hidden; + position: relative; +} + +.nprogress-custom-parent #nprogress .spinner, +.nprogress-custom-parent #nprogress .bar { + position: absolute; +} + +@-webkit-keyframes nprogress-spinner { + 0% { -webkit-transform: rotate(0deg); } + 100% { -webkit-transform: rotate(360deg); } +} +@keyframes nprogress-spinner { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + diff --git a/paicoding-ui/src/main/resources/static/css/three/select2.min.css b/paicoding-ui/src/main/resources/static/css/three/select2.min.css new file mode 100644 index 000000000..d493fe0a0 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/three/select2.min.css @@ -0,0 +1,540 @@ +.select2-container { + box-sizing: border-box; + display: inline-block; + margin: 0; + position: relative; + vertical-align: middle; } +.select2-container .select2-selection--single { + box-sizing: border-box; + cursor: pointer; + display: block; + height: 28px; + user-select: none; + -webkit-user-select: none; } +.select2-container .select2-selection--single .select2-selection__rendered { + display: block; + padding-left: 8px; + padding-right: 20px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } +.select2-container .select2-selection--single .select2-selection__clear { + background-color: transparent; + border: none; + font-size: 1em; } +.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered { + padding-right: 8px; + padding-left: 20px; } +.select2-container .select2-selection--multiple { + box-sizing: border-box; + cursor: pointer; + display: block; + min-height: 32px; + user-select: none; + -webkit-user-select: none; } +.select2-container .select2-selection--multiple .select2-selection__rendered { + display: inline; + list-style: none; + padding: 0; } +.select2-container .select2-selection--multiple .select2-selection__clear { + background-color: transparent; + border: none; + font-size: 1em; } +.select2-container .select2-search--inline .select2-search__field { + box-sizing: border-box; + border: none; + font-size: 100%; + margin-top: 5px; + margin-left: 5px; + padding: 0; + max-width: 100%; + resize: none; + height: 18px; + vertical-align: bottom; + font-family: sans-serif; + overflow: hidden; + word-break: keep-all; } +.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button { + -webkit-appearance: none; } + +.select2-dropdown { + background-color: white; + border: 1px solid var(--pai-border-color-1); + border-radius: 2px; + box-sizing: border-box; + display: block; + position: absolute; + left: -100000px; + width: 100%; + z-index: 1051; } + +.select2-results { + display: block; } + +.select2-results__options { + list-style: none; + margin: 0; + padding: 0; } + +.select2-results__option { + font-size: 14px; + padding: 6px; + user-select: none; + -webkit-user-select: none; } + +.select2-results__option--selectable { + cursor: pointer; } + +.select2-container--open .select2-dropdown { + left: 0; } + +.select2-container--open .select2-dropdown--above { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--open .select2-dropdown--below { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-search--dropdown { + display: block; + padding: 4px; } +.select2-search--dropdown .select2-search__field { + padding: 4px; + width: 100%; + box-sizing: border-box; } +.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button { + -webkit-appearance: none; } +.select2-search--dropdown.select2-search--hide { + display: none; } + +.select2-close-mask { + border: 0; + margin: 0; + padding: 0; + display: block; + position: fixed; + left: 0; + top: 0; + min-height: 100%; + min-width: 100%; + height: auto; + width: auto; + opacity: 0; + z-index: 99; + background-color: #fff; + filter: alpha(opacity=0); } + +.select2-hidden-accessible { + border: 0 !important; + clip: rect(0 0 0 0) !important; + -webkit-clip-path: inset(50%) !important; + clip-path: inset(50%) !important; + height: 1px !important; + overflow: hidden !important; + padding: 0 !important; + position: absolute !important; + width: 1px !important; + white-space: nowrap !important; } + +.select2-container--default .select2-selection--single { + background-color: #fff; + border: 1px solid #aaa; + border-radius: 4px; } +.select2-container--default .select2-selection--single .select2-selection__rendered { + color: #444; + line-height: 28px; } +.select2-container--default .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + height: 26px; + margin-right: 20px; + padding-right: 0px; } +.select2-container--default .select2-selection--single .select2-selection__placeholder { + color: #999; } +.select2-container--default .select2-selection--single .select2-selection__arrow { + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; } +.select2-container--default .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; } + +.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; } + +.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow { + left: 1px; + right: auto; } + +.select2-container--default.select2-container--disabled .select2-selection--single { + background-color: #eee; + cursor: default; } +.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear { + display: none; } + +.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; } + +.select2-container--default .select2-selection--multiple { + background-color: white; + border: 1px solid var(--pai-border-color-1); + border-radius: 2px; + cursor: text; + padding-bottom: 5px; + padding-right: 5px; + position: relative; } +.select2-container--default .select2-selection--multiple.select2-selection--clearable { + padding-right: 25px; } +.select2-container--default .select2-selection--multiple .select2-selection__clear { + cursor: pointer; + font-weight: bold; + height: 20px; + margin-right: 10px; + margin-top: 5px; + position: absolute; + right: 0; + padding: 1px; } +.select2-container--default .select2-selection--multiple .select2-selection__clear:hover { + color: var(--pai-brand-2-hover); +} +.select2-container--default .select2-selection--multiple .select2-selection__choice { + background-color: var(--pai-brand-7-light); + border: 1px solid var(--pai-brand-7-light); + border-radius: 4px; + box-sizing: border-box; + display: inline-block; + margin-left: 5px; + margin-top: 5px; + padding: 0; + padding-left: 20px; + position: relative; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: bottom; + white-space: nowrap; } +.select2-container--default .select2-selection--multiple .select2-selection__choice__display { + color: var(--pai-brand-1-normal); + cursor: default; + padding-left: 2px; + padding-right: 5px; } +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove { + background-color: transparent; + border: none; + border-right: 1px solid var(--pai-brand-7-light); + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + color: var(--pai-brand-1-normal); + cursor: pointer; + font-size: 1em; + font-weight: bold; + padding: 0 4px; + position: absolute; + left: 0; + top: 0; } +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover, .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:focus { + background-color: var(--pai-brand-8-light); + outline: none; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + margin-left: 5px; + margin-right: auto; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__display { + padding-left: 5px; + padding-right: 2px; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + border-left: 1px solid #aaa; + border-right: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__clear { + float: left; + margin-left: 10px; + margin-right: auto; } + +.select2-container--default.select2-container--focus .select2-selection--multiple { + border: solid var(--pai-brand-3-click) 1px; + outline: 0; } + +.select2-container--default.select2-container--disabled .select2-selection--multiple { + background-color: #eee; + cursor: default; } + +.select2-container--default.select2-container--disabled .select2-selection__choice__remove { + display: none; } + +.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple { + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--default .select2-search--dropdown .select2-search__field { + border: 1px solid #aaa; } + +.select2-container--default .select2-search--inline .select2-search__field { + background: transparent; + border: none; + outline: 0; + box-shadow: none; + -webkit-appearance: textfield; } + +.select2-container--default .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; } + +.select2-container--default .select2-results__option .select2-results__option { + padding-left: 1em; } +.select2-container--default .select2-results__option .select2-results__option .select2-results__group { + padding-left: 0; } +.select2-container--default .select2-results__option .select2-results__option .select2-results__option { + margin-left: -1em; + padding-left: 2em; } +.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -2em; + padding-left: 3em; } +.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -3em; + padding-left: 4em; } +.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -4em; + padding-left: 5em; } +.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -5em; + padding-left: 6em; } + +.select2-container--default .select2-results__option--group { + padding: 0; } + +.select2-container--default .select2-results__option--disabled { + color: #999; } + +.select2-container--default .select2-results__option--selected { + color: var(--pai-brand-1-normal); } + +.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable { + background-color: var(--pai-brand-7-light);} + +.select2-container--default .select2-results__group { + cursor: default; + display: block; + padding: 6px; } + +.select2-container--classic .select2-selection--single { + background-color: #f7f7f7; + border: 1px solid #aaa; + border-radius: 4px; + outline: 0; + background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%); + background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%); + background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); } +.select2-container--classic .select2-selection--single:focus { + border: 1px solid #5897fb; } +.select2-container--classic .select2-selection--single .select2-selection__rendered { + color: #444; + line-height: 28px; } +.select2-container--classic .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + height: 26px; + margin-right: 20px; } +.select2-container--classic .select2-selection--single .select2-selection__placeholder { + color: #999; } +.select2-container--classic .select2-selection--single .select2-selection__arrow { + background-color: #ddd; + border: none; + border-left: 1px solid #aaa; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; + background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%); + background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%); + background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); } +.select2-container--classic .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; } + +.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; } + +.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow { + border: none; + border-right: 1px solid #aaa; + border-radius: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + left: 1px; + right: auto; } + +.select2-container--classic.select2-container--open .select2-selection--single { + border: 1px solid #5897fb; } +.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow { + background: transparent; + border: none; } +.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; } + +.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; + background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); } + +.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%); + background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%); + background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); } + +.select2-container--classic .select2-selection--multiple { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + cursor: text; + outline: 0; + padding-bottom: 5px; + padding-right: 5px; } +.select2-container--classic .select2-selection--multiple:focus { + border: 1px solid #5897fb; } +.select2-container--classic .select2-selection--multiple .select2-selection__clear { + display: none; } +.select2-container--classic .select2-selection--multiple .select2-selection__choice { + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + display: inline-block; + margin-left: 5px; + margin-top: 5px; + padding: 0; } +.select2-container--classic .select2-selection--multiple .select2-selection__choice__display { + cursor: default; + padding-left: 2px; + padding-right: 5px; } +.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove { + background-color: transparent; + border: none; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + color: #888; + cursor: pointer; + font-size: 1em; + font-weight: bold; + padding: 0 4px; } +.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #555; + outline: none; } + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + margin-left: 5px; + margin-right: auto; } + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__display { + padding-left: 5px; + padding-right: 2px; } + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; } + +.select2-container--classic.select2-container--open .select2-selection--multiple { + border: 1px solid #5897fb; } + +.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--classic .select2-search--dropdown .select2-search__field { + border: 1px solid #aaa; + outline: 0; } + +.select2-container--classic .select2-search--inline .select2-search__field { + outline: 0; + box-shadow: none; } + +.select2-container--classic .select2-dropdown { + background-color: white; + border: 1px solid transparent; } + +.select2-container--classic .select2-dropdown--above { + border-bottom: none; } + +.select2-container--classic .select2-dropdown--below { + border-top: none; } + +.select2-container--classic .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; } + +.select2-container--classic .select2-results__option--group { + padding: 0; } + +.select2-container--classic .select2-results__option--disabled { + color: grey; } + +.select2-container--classic .select2-results__option--highlighted.select2-results__option--selectable { + background-color: #3875d7; + color: white; } + +.select2-container--classic .select2-results__group { + cursor: default; + display: block; + padding: 6px; } + +.select2-container--classic.select2-container--open .select2-dropdown { + border-color: #5897fb; } diff --git a/paicoding-ui/src/main/resources/static/css/three/simplemde.min.css b/paicoding-ui/src/main/resources/static/css/three/simplemde.min.css new file mode 100644 index 000000000..6f8a120ce --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/three/simplemde.min.css @@ -0,0 +1,750 @@ +/** + * simplemde v1.11.2 + * Copyright Next Step Webs, Inc. + * @link https://github.com/NextStepWebs/simplemde-markdown-editor + * @license MIT + */ +.CodeMirror { + color: #000 +} + +.CodeMirror-lines { + padding: 4px 0 +} + +.CodeMirror pre { + padding: 0 4px +} + +.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler { + background-color: #fff +} + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap +} + +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + white-space: nowrap +} + +.CodeMirror-guttermarker { + color: #000 +} + +.CodeMirror-guttermarker-subtle { + color: #999 +} + +.CodeMirror-cursor { + border-left: 1px solid #000; + border-right: none; + width: 0 +} + +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver +} + +.cm-fat-cursor .CodeMirror-cursor { + width: auto; + border: 0!important; + background: #7e7 +} + +.cm-fat-cursor div.CodeMirror-cursors { + z-index: 1 +} + +.cm-animate-fat-cursor { + width: auto; + border: 0; + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; + background-color: #7e7 +} + +@-moz-keyframes blink { + 50% { + background-color: transparent + } +} + +@-webkit-keyframes blink { + 50% { + background-color: transparent + } +} + +@keyframes blink { + 50% { + background-color: transparent + } +} + +.cm-tab { + display: inline-block; + text-decoration: inherit +} + +.CodeMirror-ruler { + border-left: 1px solid #ccc; + position: absolute +} + +.cm-s-default .cm-header { + color: #00f +} + +.cm-s-default .cm-quote { + color: #090 +} + +.cm-negative { + color: #d44 +} + +.cm-positive { + color: #292 +} + +.cm-header,.cm-strong { + font-weight: 700 +} + +.cm-em { + font-style: italic +} + +.cm-link { + text-decoration: underline +} + +.cm-strikethrough { + text-decoration: line-through +} + +.cm-s-default .cm-keyword { + color: #708 +} + +.cm-s-default .cm-atom { + color: #219 +} + +.cm-s-default .cm-number { + color: #164 +} + +.cm-s-default .cm-def { + color: #00f +} + +.cm-s-default .cm-variable-2 { + color: #05a +} + +.cm-s-default .cm-variable-3 { + color: #085 +} + +.cm-s-default .cm-comment { + color: #a50 +} + +.cm-s-default .cm-string { + color: #a11 +} + +.cm-s-default .cm-string-2 { + color: #f50 +} + +.cm-s-default .cm-meta,.cm-s-default .cm-qualifier { + color: #555 +} + +.cm-s-default .cm-builtin { + color: #30a +} + +.cm-s-default .cm-bracket { + color: #997 +} + +.cm-s-default .cm-tag { + color: #170 +} + +.cm-s-default .cm-attribute { + color: #00c +} + +.cm-s-default .cm-hr { + color: #999 +} + +.cm-s-default .cm-link { + color: #00c +} + +.cm-invalidchar,.cm-s-default .cm-error { + color: red +} + +.CodeMirror-composing { + border-bottom: 2px solid +} + +div.CodeMirror span.CodeMirror-matchingbracket { + color: #0f0 +} + +div.CodeMirror span.CodeMirror-nonmatchingbracket { + color: #f22 +} + +.CodeMirror-matchingtag { + background: rgba(255,150,0,.3) +} + +.CodeMirror-activeline-background { + background: #e8f2ff +} + +.CodeMirror { + position: relative; + overflow: hidden; + background: #fff +} + +.CodeMirror-scroll { + overflow: scroll!important; + margin-bottom: -30px; + margin-right: -30px; + padding-bottom: 30px; + height: 100%; + outline: 0; + position: relative +} + +.CodeMirror-sizer { + position: relative; + border-right: 30px solid transparent +} + +.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar { + position: absolute; + z-index: 6; + display: none +} + +.CodeMirror-vscrollbar { + right: 0; + top: 0; + overflow-x: hidden; + overflow-y: scroll +} + +.CodeMirror-hscrollbar { + bottom: 0; + left: 0; + overflow-y: hidden; + overflow-x: scroll +} + +.CodeMirror-scrollbar-filler { + right: 0; + bottom: 0 +} + +.CodeMirror-gutter-filler { + left: 0; + bottom: 0 +} + +.CodeMirror-gutters { + position: absolute; + left: 0; + top: 0; + min-height: 100%; + z-index: 3 +} + +.CodeMirror-gutter { + white-space: normal; + height: 100%; + display: inline-block; + vertical-align: top; + margin-bottom: -30px +} + +.CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + background: 0 0!important; + border: none!important; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none +} + +.CodeMirror-gutter-background { + position: absolute; + top: 0; + bottom: 0; + z-index: 4 +} + +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4 +} + +.CodeMirror-lines { + cursor: text; + min-height: 1px +} + +.CodeMirror pre { + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; + border-width: 0; + background: 0 0; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + -webkit-tap-highlight-color: transparent; + -webkit-font-variant-ligatures: none; + font-variant-ligatures: none +} + +.CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal +} + +.CodeMirror-linebackground { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + z-index: 0 +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + overflow: auto +} + +.CodeMirror-code { + outline: 0 +} + +.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer { + -moz-box-sizing: content-box; + box-sizing: content-box +} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden +} + +.CodeMirror-cursor { + position: absolute +} + +.CodeMirror-measure pre { + position: static +} + +div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3 +} + +.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors { + visibility: visible +} + +.CodeMirror-selected { + background: #d9d9d9 +} + +.CodeMirror-focused .CodeMirror-selected,.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection { + background: #d7d4f0 +} + +.CodeMirror-crosshair { + cursor: crosshair +} + +.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection { + background: #d7d4f0 +} + +.cm-searching { + background: #ffa; + background: rgba(255,255,0,.4) +} + +.cm-force-border { + padding-right: .1px +} + +@media print { + .CodeMirror div.CodeMirror-cursors { + visibility: hidden + } +} + +.cm-tab-wrap-hack:after { + content: '' +} + +span.CodeMirror-selectedtext { + background: 0 0 +} + +.CodeMirror { + height: auto; + min-height: 300px; + border: 1px solid #ddd; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + padding: 10px; + font: inherit; + z-index: 1 +} + +.CodeMirror-scroll { + min-height: 300px +} + +.CodeMirror-sided { + width: 50%!important +} + +.editor-toolbar { + position: relative; + opacity: .6; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; + padding: 0 10px; + border-top: 1px solid #bbb; + border-left: 1px solid #bbb; + border-right: 1px solid #bbb; + border-top-left-radius: 4px; + border-top-right-radius: 4px +} + +.editor-toolbar:after,.editor-toolbar:before { + display: block; + content: ' '; + height: 1px +} + +.editor-toolbar:before { + margin-bottom: 8px +} + +.editor-toolbar:after { + margin-top: 8px +} + +.editor-toolbar:hover,.editor-wrapper input.title:focus,.editor-wrapper input.title:hover { + opacity: .8 +} + +.editor-toolbar.fullscreen { + width: 100%; + height: 50px; + overflow-x: auto; + overflow-y: hidden; + white-space: nowrap; + padding-top: 10px; + padding-bottom: 10px; + box-sizing: border-box; + background: #fff; + border: 0; + position: fixed; + top: 0; + left: 0; + opacity: 1; + z-index: 9 +} + +.editor-toolbar.fullscreen::before { + width: 20px; + height: 50px; + background: -moz-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%); + background: -webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,1)),color-stop(100%,rgba(255,255,255,0))); + background: -webkit-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%); + background: -o-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%); + background: -ms-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%); + background: linear-gradient(to right,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%); + position: fixed; + top: 0; + left: 0; + margin: 0; + padding: 0 +} + +.editor-toolbar.fullscreen::after { + width: 20px; + height: 50px; + background: -moz-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%); + background: -webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,0)),color-stop(100%,rgba(255,255,255,1))); + background: -webkit-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%); + background: -o-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%); + background: -ms-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%); + background: linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%); + position: fixed; + top: 0; + right: 0; + margin: 0; + padding: 0 +} + +.editor-toolbar a { + display: inline-block; + text-align: center; + text-decoration: none!important; + color: #2c3e50!important; + width: 30px; + height: 30px; + margin: 0; + border: 1px solid transparent; + border-radius: 3px; + cursor: pointer +} + +.editor-toolbar a.active,.editor-toolbar a:hover { + background: #fcfcfc; + border-color: #95a5a6 +} + +.editor-toolbar a:before { + line-height: 30px +} + +.editor-toolbar i.separator { + display: inline-block; + width: 0; + border-left: 1px solid #d9d9d9; + border-right: 1px solid #fff; + color: transparent; + text-indent: -10px; + margin: 0 6px +} + +.editor-toolbar a.fa-header-x:after { + font-family: Arial,"Helvetica Neue",Helvetica,sans-serif; + font-size: 65%; + vertical-align: text-bottom; + position: relative; + top: 2px +} + +.editor-toolbar a.fa-header-1:after { + content: "1" +} + +.editor-toolbar a.fa-header-2:after { + content: "2" +} + +.editor-toolbar a.fa-header-3:after { + content: "3" +} + +.editor-toolbar a.fa-header-bigger:after { + content: "▲" +} + +.editor-toolbar a.fa-header-smaller:after { + content: "▼" +} + +.editor-toolbar.disabled-for-preview a:not(.no-disable) { + pointer-events: none; + background: #fff; + border-color: transparent; + text-shadow: inherit +} + +@media only screen and (max-width: 700px) { + .editor-toolbar a.no-mobile { + display:none + } +} + +.editor-statusbar { + padding: 8px 10px; + font-size: 12px; + color: #959694; + text-align: right; + position: fixed; + bottom: 0; +} + +.editor-statusbar span { + display: inline-block; + min-width: 4em; + margin-left: 1em +} + +.editor-preview,.editor-preview-side { + padding: 10px; + background: #fafafa; + overflow: auto; + display: none; + box-sizing: border-box +} + +.editor-statusbar .lines:before { + content: 'lines: ' +} + +.editor-statusbar .words:before { + content: 'words: ' +} + +.editor-statusbar .characters:before { + content: 'characters: ' +} + +.editor-preview { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 7 +} + +.editor-preview-side { + position: fixed; + bottom: 34px; + width: 50%; + top: 84px; + right: 0; + z-index: 9; + border: 1px solid #ddd +} + +.editor-preview-active,.editor-preview-active-side { + display: block +} + +.editor-preview-side>p,.editor-preview>p { + margin-top: 0 +} + +.editor-preview pre,.editor-preview-side pre { + background: #eee; + margin-bottom: 10px +} + +.editor-preview table td,.editor-preview table th,.editor-preview-side table td,.editor-preview-side table th { + border: 1px solid #ddd; + padding: 5px +} + +.CodeMirror .CodeMirror-code .cm-tag { + color: #63a35c +} + +.CodeMirror .CodeMirror-code .cm-attribute { + color: #795da3 +} + +.CodeMirror .CodeMirror-code .cm-string { + color: #183691 +} + +.CodeMirror .CodeMirror-selected { + background: #d9d9d9 +} + +.CodeMirror .CodeMirror-code .cm-header-1 { + font-size: 200%; + line-height: 200% +} + +.CodeMirror .CodeMirror-code .cm-header-2 { + font-size: 160%; + line-height: 160% +} + +.CodeMirror .CodeMirror-code .cm-header-3 { + font-size: 125%; + line-height: 125% +} + +.CodeMirror .CodeMirror-code .cm-header-4 { + font-size: 110%; + line-height: 110% +} + +.CodeMirror .CodeMirror-code .cm-comment { + background: rgba(0,0,0,.05); + border-radius: 2px +} + +.CodeMirror .CodeMirror-code .cm-link { + color: #7f8c8d +} + +.CodeMirror .CodeMirror-code .cm-url { + color: #aab2b3 +} + +.CodeMirror .CodeMirror-code .cm-strikethrough { + text-decoration: line-through +} + +.CodeMirror .CodeMirror-placeholder { + opacity: .5 +} + +.CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word) { + background: rgba(255,0,0,.15) +} + +.CodeMirror-fullscreen { + position: fixed!important; + top: 84px; + left: 0; + right: 0; + bottom: 34px; + height: auto; + z-index: 9; +} \ No newline at end of file diff --git a/forum-ui/src/main/resources/static/css/toastr.min.css b/paicoding-ui/src/main/resources/static/css/three/toastr.min.css similarity index 100% rename from forum-ui/src/main/resources/static/css/toastr.min.css rename to paicoding-ui/src/main/resources/static/css/three/toastr.min.css diff --git a/paicoding-ui/src/main/resources/static/css/views/article-detail.css b/paicoding-ui/src/main/resources/static/css/views/article-detail.css new file mode 100644 index 000000000..c9cf67ba0 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/views/article-detail.css @@ -0,0 +1,303 @@ + +.detail-content-title { + color: rgba(0, 0, 0, 0.85); + font-size: 26px; + line-height: 31px; + vertical-align: bottom; + margin-bottom: 12px; +} + +.detail-content-title-other-wrap { + font-size: 14px; + display: flex; + align-items: center; + color: var(--pai-color-999-gray); + padding-bottom: 16px; + margin-bottom: 16px; + border-bottom: 1px solid var(--pai-hr-color-1); +} + +.detail-content-title-other-img { + height: 22px; + width: 22px; + border-radius: 50%; +} +.detail-content-title-other-name { + color: #62749f; + margin-left: 8px; + margin-right: 16px; +} +.detail-content-title-other-time { + margin-right: 16px; +} + +.detail-content-title-other-look { + display: flex; + align-items: center; + justify-content: center; + margin-left: 28px; + padding: 0 11px; + height: 20px; + border-radius: 20px; + font-size: 12px; + color: #3973ff; + border: 1px solid #3973ff; + cursor: pointer; +} + +.com-opt-link, +.com-opt-text { + display: inline-block; + vertical-align: middle; + color: var(--pai-color-999-gray); +} + +[class*="com-i-"], +[class^="com-i-"] { + display: inline-block; + vertical-align: middle; + width: 20px; + height: 20px; + background-size: 100% auto; +} + +.com-opt-link:hover .com-i-edit, +.com-opt-link:hover .com-i-delete { + fill: var(--pai-brand-2-hover); +} + +.com-opt-link .com-i-edit, +.com-opt-link .com-i-delete { + fill: var(--pai-color-999-gray); +} + +.detail-content-title-edit [class*="com-i-"], +.detail-content-title-edit [class^="com-i-"] { + position: relative; +} + +.detail-content-title-edit { + position: absolute; + right: 0; + top: 0; + line-height: 20px; +} + +/*文章目录*/ +.com-nav-bar { + max-height: none; + overflow-y: auto; + margin-bottom: 20px; +} + +.com-nav-bar-menu { + border-left: 2px solid #e5e5e5; + padding-left: 15px; +} + +.toc { + height: 100%; + word-wrap: break-word; +} + +.com-nav-bar-title { + font-size: 16px; + line-height: 26px; + font-weight: 500; + margin-bottom: 20px; +} + +.com-nav-bar-menu .h2 a, +.com-nav-bar-menu .h3 a { + position: relative; + display: block; + font-size: 14px; + line-height: 24px; + font-weight: 400; +} + +.com-nav-bar-menu .h3 a { + padding-left: 17px; + color: var(--pai-color-4-gray); +} + +.com-nav-bar-menu .h3 a:hover { + padding-left: 17px; + color: var(--pai-brand-2-hover); +} + +.com-nav-bar-menu .h3.active > a, +.com-nav-bar-menu .h3.active > a:hover { + color: var(--pai-brand-2-hover); +} + +.com-nav-bar-menu .h3.active > a:before { + content: ""; + position: absolute; + left: -17px; + top: 0; + width: 2px; + height: 24px; + background-color: var(--pai-brand-2-hover); +} + +.com-nav-bar-menu .h3 > a:before { + left: -34px; +} + +.arCatalog { + position: relative; + margin: 20px 0 0 0; +} + +.arCatalog .arCatalog-line { + position: absolute; + left: 7px; + top: -5px; + width: 0; + border: 1px solid #eaeaea; + border-top: 0; + border-bottom: 0; + background-color: #eaeaea; +} + +.arCatalog .arCatalog-body { + margin-left: -2px; + padding-left: 27px; + overflow: hidden; + font-size: 14px; +} + +.arCatalog .arCatalog-body dl { + margin: 0; + transition: 0.3s all; +} + +.arCatalog-body .arCatalog-tack1 .arCatalog-index { + position: absolute; + left: 2px; + top: 0; + color: #bbb; +} + +.arCatalog-body dd.on a { + color: var(--pai-brand-1-normal); +} + +.arCatalog-body .arCatalog-tack1 a { + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 260px; +} + +.arCatalog-body .arCatalog-tack1 .arCatalog-dot { + position: absolute; + left: -21px; + top: 11px; + width: 8px; + height: 8px; + background-color: #eaeaea; + border: 1px solid #fff; + border-radius: 2em; +} + +.arCatalog-body dd.on .arCatalog-dot { + position: absolute; + left: -23px; + top: 8px; + width: 12px; + height: 12px; + background-color: #fd7013; + box-shadow: 0px 0px 2px 2px rgb(253 112 19 / 50%); + border-radius: 2em; + border-width: 0; +} + +.arCatalog .arCatalog-body dd { + position: relative; + margin: 0; + font-size: 15px; + line-height: 28px; +} + +.arCatalog-body .arCatalog-tack2 { + padding-left: 17px; +} + +.arCatalog-body .arCatalog-tack2 .arCatalog-index { + position: absolute; + left: 27px; + top: 0; + color: #bbb; +} + +.arCatalog-body .arCatalog-tack2 a { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--pai-color-3-gray); + max-width: 240px; +} + +.arCatalog .arCatalog-line:before { + content: ""; + position: absolute; + top: -10px; + left: -5px; + display: block; + width: 10px; + height: 10px; + border: 1px solid #d2d2d2; + border-radius: 10px; +} + +.arCatalog .arCatalog-line:after { + content: ""; + position: absolute; + bottom: -10px; + left: -5px; + display: block; + width: 10px; + height: 10px; + border: 1px solid #d2d2d2; + border-radius: 10px; +} +.toc-container { + /* position: sticky; + z-index: 10; + top: 30px; */ + position: fixed; + z-index: 10; + width: 230px; + top: 90px; + right: 0; +} + +.widget { + margin-bottom: 30px; +} + +.comment-list-wrap { + padding: 20px; +} + +/* home 适配 */ +@media screen and (max-width: 768px) { + .layout-side, + .article-suspended-panel { + display: none !important; + } + .layout-main { + padding: 0 !important; + } + .article-suspended-panel-md { + visibility: visible; + } + .foot { + margin-bottom: 72px; + } +} + diff --git a/paicoding-ui/src/main/resources/static/css/views/article-edit.css b/paicoding-ui/src/main/resources/static/css/views/article-edit.css new file mode 100644 index 000000000..06d659ceb --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/views/article-edit.css @@ -0,0 +1,440 @@ +.edit-nav { + background-color: #fff; + height: 50px; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: 0 3px 6px rgb(0 0 0 / 5%); + z-index: 3; + padding: 0 10px; +} + +.edit-title-input::placeholder{ + font-size: 24px; +} + +.form-control:focus { + border-color: var(--pai-brand-3-click); + box-shadow: none; +} + +.edit-save { + background: var(--pai-brand-4-disable); + border-color: #d9d9d9; + text-shadow: none; + box-shadow: none; + width: 66px; + height: 28px; + border-radius: 2px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + color: #ccc; + cursor: not-allowed; + margin-right: 20px; +} + +.edit-save--active { + background-color: var(--pai-brand-1-normal); + color: var(--pai-color-fff-normal); + cursor: pointer; +} + +.edit-save--active:hover { + background-color: var(--pai-brand-2-hover); +} + +.edit-title-input { + border: none; + font-size: 24px; + height: 100%; + width: 100%; +} + +.edit-title-input:focus-visible { + outline: none !important; +} + +.edit-title-form { + height: 80%; + flex: 1; + overflow-x: auto; + margin-right: 20px; +} + +.edit-title-input:active { + border: none; +} + +/* 编辑器样式 */ +.editor-toolbar::before, +.editor-toolbar::after { + margin: 0 !important; +} + +.editor-toolbar { + border-bottom: 1px solid #e1e4e8 !important; + border-top: 1px solid #e1e4e8 !important; + background-color: #fafbfc !important; +} + +.editor-preview-active { + background-color: #fff !important; +} + +.editor-preview-side .editor-preview-active-side .editor-statusbar { + text-align: left !important; + border-top: 1px solid #fff !important; + font-size: 12px !important; + background-color: #fff !important; +} + +.editor-preview-active-side { + background-color: #fff !important; +} + +blockquote { + color: #666; + padding: 1px 23px; + margin: 22px 0; + border-left: 4px solid #cbcbcb; + background-color: #f8f8f8; +} + +.modal-content { + width: 630px; + font-size: 14px; +} + +.modal-title { + font-size: 18px; +} + +.required .form-label:before { + content: "*"; + color: var(--pai-brand-6-mq); + vertical-align: -2px; + padding-right: 2px; +} + +.category .form-label { + padding-top: 5px; +} + +.input-group { + display: flex; + flex-wrap: nowrap; + align-items: center; + position: relative; + margin-bottom: 10px; +} + +.form-group { + margin-bottom: 10px; +} + +.cover { + margin: 16px 0 20px 0; +} + +.input-group input, +.input-group textarea { + border-radius: 2px !important; +} + +.edit-sort-wrap { + display: flex; +} + +.edit-sort-wrap label { + flex: none; +} + +.edit-tag-wrap { + align-items: baseline; +} + +.form-selectgroup-item { + width: 100px; + height: 32px; + line-height: 32px; + text-align: center; + margin-right: 10px; + background-color: var(--pai-bg-light-2); + color: var(--pai-color-4-gray); + cursor: pointer; +} +.r-form-selectgroup-item { + width: 100px; + height: 32px; + line-height: 32px; + text-align: center; + margin-right: 10px; + background-color: var(--pai-bg-light-2); + color: var(--pai-color-4-gray); + cursor: pointer; +} + +.form-check { + display: flex; + align-items: center; + justify-content: center; + min-width: 70px; + height: 30px; + border-radius: 4px; + /* margin-right: 10px; */ + background-color: #f8f9fa; + color: #919aa6; + cursor: pointer; + position: relative; + padding-left: 0; +} + +.form-check-input { + margin: 0; + left: 0; + top: 0; + height: 30px; + min-width: 70px; + opacity: 0; + cursor: pointer; +} +.form-check-label { + padding: 10px; +} +.form-selectgroup-item--active, +.form-check--active { + background-color: var(--pai-brand-7-light); + color: var(--pai-brand-1-normal); +} + +.r-form-selectgroup-item:hover, +.form-check:hover { + background-color: var(--pai-brand-7-light); +} + +.form-selectgroup-input { + position: absolute; + visibility: hidden; +} + +.form-textarea { + height: 100px !important; + background-color: #f8f9fa; + border: 1px solid #e9ecef; + resize: none; + line-height: 22px; +} + +.form-textarea-limit { + position: absolute; + bottom: 5px; + right: 90px; + color: var(--pai-color-999-gray); + font-size: 12px; + z-index: 10; +} + +.btn-getdistill { + position: absolute; + bottom: 5px; + right: 12px; + font-size: 12px; + z-index: 10; + display: inline-block; + white-space: nowrap; + background-color: var(--pai-color-6-gray); + border: 1px solid var(--pai-border-color-1); + color: var(--pai-color-4-gray); + border-radius: 20px; + padding: 0 10px; +} + +.form-label { + width: 80px; + text-align: right; + margin-right: 8px; + padding: 0; + align-items: flex-start; + flex-shrink: 0; +} + +.input-textarea { + align-items: flex-start !important; +} + +.person-img-wrap { + display: flex; + flex-direction: column; + align-items: center; + width: 112px; + margin-left: 74px; +} +.person-img-inter-wrap { + width: 150px; + height: 80px; + position: relative; + border: 1px #e9ecef solid; +} +.person-img { + width: 100%; + height: 100%; +} +.person-upload-text { + color: #1d2129; + font-weight: 500; + font-size: 14px; + margin-top: 10px; + margin-bottom: 8px; +} +.person-upload-limit { + color: #86909c; + font-size: 12px; + line-height: 17px; + font-weight: 400; +} +.person-img-inter-wrap-img { + position: relative; +} +.upload-icon-up { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} +.cancel-title { + color: var(--pai-brand-1-normal); + cursor: pointer; +} +.click-cover { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + color: #fff; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: rgba(29, 33, 41, 0.5); + z-index: 2; + visibility: hidden; + cursor: pointer; +} +.click-text { + font-size: 12px; + margin-top: 7px; + line-height: 17px; + font-weight: 400; +} +.click-input { + display: none; +} + +.custom-file { + position: relative; + height: 80px; +} + +.article-tag-wrap { + display: flex; + flex-wrap: wrap; + min-height: 40px; + align-items: center; + gap: 8px; +} + +.editor-preview-side a { + color: var(--pai-brand-1-normal); + text-decoration: none; + border-bottom: 1px solid var(--pai-brand-1-normal); +} + +.editor-preview-side p>img { + max-width: 100%; + margin: 0; +} + +.editor-preview-side ol, .editor-preview-side ul { + margin: 0 0 24px; + padding: 0; + font-size: 16px; + overflow: hidden; + overflow-x: auto; +} + +.editor-preview-side li { + margin: 10px 0; +} + +.editor-preview-side strong { + color: var(--pai-brand-1-normal); +} + +.editor-preview-side h2 { + margin-left: -10px; + display: inline-block; + width: auto; + height: 40px; + background-color: var(--pai-brand-1-normal); + border-bottom-right-radius: 100px; + color: rgb(255, 255, 255); + padding-right: 30px; + padding-left: 30px; + line-height: 40px; + font-size: 16px; +} + +.summary { + font-size: 14px; + padding: 8px 8px 0; +} + +.btn-getdistill:hover { + background-color: var(--pai-brand-7-light); + color: var(--pai-brand-2-hover); +} + +.person-img-inter-wrap .close_icon { + z-index: 9; + position: absolute; + background: var(--pai-color-999-gray); + color: var(--pai-color-fff-normal); + line-height: 20px; + right: -8px; + top: -8px; + display: none; + width: 20px; + height: 20px; + font-size: 14px; + text-align: center; + background-size: contain; + border-radius: 50%; + cursor: pointer; +} + +.edit-mask { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #fff; + z-index: 100; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + font-size: 20px; + font-weight: 500; + display: none; +} + +.edit-mask-img { + width: 70px; + height: 70px; + margin-bottom: 12px; +} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/css/views/article-tag.css b/paicoding-ui/src/main/resources/static/css/views/article-tag.css new file mode 100644 index 000000000..7e5cdf30d --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/views/article-tag.css @@ -0,0 +1,16 @@ +.tag-wrap-out { + height: calc(100vh - 60px); + overflow-y: auto; +} + +.tag-wrap { + width: 1200px; + margin: 0 auto; + padding: 20px; +} + +.tag-list { + padding: 20px; + background-color: #fff; + border-radius: 8px; +} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/css/views/chat-home.css b/paicoding-ui/src/main/resources/static/css/views/chat-home.css new file mode 100644 index 000000000..51cae7d5c --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/views/chat-home.css @@ -0,0 +1,675 @@ +.chat-wrap { + display: flex; + margin: 20px; + border-radius: 5px; + min-height: 480px; + height: var(--window-height); +} + +.chat-sidebar { + overflow-y: auto; + top: 0; + width: var(--pai-sidebar-width); + box-sizing: border-box; + display: flex; + flex-direction: column; + position: relative; + transition: width .05s ease; +} + +.name .annotation { + font-size: small; + padding-left: 6px; + color: var(--pai-color-3-gray); +} + +.window-header-title .name { + display: flex; +} + +.window-header-title .name .com-verification { + top: 2px; +} + +.chat-sidebar .uno-buy-card-foot { + margin-right: 20px; +} + +.chat-sidebar .uno-buy-card-wrap { + display: flex; + background-color: #fff; + padding: 20px; + align-items: center; +} + +.chat-sidebar .uno-buy-card-wrap .qrcode { + width: 200px; +} + +.chat-sidebar .uno-buy-card-foot-prices { +} + +.chat-sidebar .uno-buy-card-foot-tag-list { + height: 22px; + font-size: 14px; + margin-bottom: 8px; + white-space: nowrap; +} + +.chat-sidebar .uno-buy-card-foot-tag-type1 { + padding: 4px; + background: var(--pai-brand-1-normal); + color: var(--pai-color-fff-normal); + border: none; +} + +.chat-sidebar .uno-buy-card-foot-tag-type3 { + padding: 4px; + line-height: 20px; + background: transparent; + color: #6f7a94; + border: 1px solid rgba(111,122,148,.5); +} + +.chat-sidebar .uno-buy-card-foot-price-num { + color: var(--pai-brand-1-normal); + font-size: 22px; + line-height: 36px; + font-weight: 500; + display: inline-block; +} + +.chat-sidebar .uno-buy-card-foot-tag-item { + font-size: 12px; +} + +.chat-sidebar .uno-buy-card-foot-price-unit { + color: var(--pai-brand-5-bak); + font-size: 14px; + line-height: 22px; + font-weight: 500; + margin-left: 4px; +} + +.chat-sidebar .uno-buy-card-foot-price-average { + font-size: 12px; + line-height: 18px; + color: var(--pai-color-3-gray); + margin-right: 8px; +} + +.chat-sidebar .uno-buy-card-foot-price-original { + font-size: 12px; + line-height: 18px; + color: var(--pai-color-3-gray); + text-decoration: line-through; +} + +.chat-sidebar .uno-buy-card-foot-price-detail { + margin-bottom: 10px; +} + +.chat-sidebar .login-guide-wrap { + margin-bottom: 20px; + padding: 20px; + background-color: #fff; +} + + +.chat-main { + width: var(--window-content-width); + padding: 0 20px; + display: flex; + flex-direction: column; + position: relative; + height: 100%; + background-color: #fff; + margin-left: 20px; +} + +.chat-main a { + color: var(--pai-brand-1-normal); +} + +.chat-main a:hover { + color: var(--pai-brand-2-hover); +} + +.server-msg .markdown-body pre { + padding: 0; +} + +.server-msg .home_chat-message-item__hDEOq { + margin-top: 0; +} + +.bg-black { + --tw-bg-opacity: 1; + background-color: rgba(0,0,0,var(--tw-bg-opacity)); +} + +#chatCnt { + color: var(--pai-brand-6-mq); +} + +#chat-content { + overflow: auto; + flex: 1 1; + padding: 20px 20px 40px; + position: relative; + overscroll-behavior: none; +} + +.chat-input { + display: flex; + position: relative; +} + +#input-field { + height: 60px; + resize: none; /* 禁止手动改变大小 */ + padding: 10px 90px 10px 10px; + font-size: 14px; + border: 1px solid #ccc; + border-radius: 4px; +} + +#input-field:focus { + border: 1px solid var(--pai-brand-3-click); + outline: none; + box-shadow: none; +} + +#send-btn { + background-color: var(--pai-brand-1-normal); + color: #fff; + position: absolute; + right: 16px; + bottom: 12px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + padding: 10px; + cursor: pointer; + transition: all .3s ease; + overflow: hidden; + user-select: none; + outline: none; + border: none; +} + +#send-btn:hover { + background-color: var(--pai-brand-2-hover); +} + +/*send-btn disabled*/ +#send-btn[disabled] { + background-color: var(--pai-brand-4-disable); + cursor: not-allowed; +} + +.button_icon-button-icon__qlUH3 { + width: 16px; + height: 16px; + display: flex; + justify-content: center; + align-items: center; +} + +.button_icon-button-text__k3vob { + margin-left: 5px; + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.home_chat-message__rdH_g { + display: flex; + flex-direction: row; +} + +.home_chat-message-item__hDEOq { + box-sizing: border-box; + max-width: 100%; + margin-top: 10px; + border-radius: 10px; + background-color: rgba(0,0,0,.05); + padding: 10px; + font-size: 14px; + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; + word-break: break-word; + border: var(--pai-border-color-1); + position: relative; +} + +.home_chat-message-item__hDEOq img { + width: 100%; +} + +.home_chat-message-actions__nrHd1, .home_chat-message-actions__loading { + display: flex; + flex-direction: row-reverse; + width: 100%; + padding-top: 5px; + box-sizing: border-box; + font-size: 12px; +} + +.markdown-body { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + margin: 0; + color: #24292f; + background-color: var(--color-canvas-default); + font-size: 14px; + line-height: 1.5; + word-wrap: break-word; +} + +.markdown-body p { + white-space: pre-wrap; +} + +.markdown-body>:last-child { + margin-bottom: 0!important; +} + +.markdown-body>:first-child { + margin-top: 0!important; +} + +.home_chat-message-action-date__6ToUp,.home_chat-message-action-loading__6ToUp { + color: var(--pai-color-3-gray); +} + +.home_chat-message-user__WsuiB { + display: flex; + flex-direction: row-reverse; +} +.home_chat-message-container__plj_e { + max-width: var(--message-max-width); + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.home_chat-message-user__WsuiB>.home_chat-message-container__plj_e { + align-items: flex-end; +} + +.home_chat-message-avatar__611lI { + margin-top: 20px; +} + +.home_chat-message-user__WsuiB>.home_chat-message-container__plj_e>.home_chat-message-item__hDEOq { + background-color: var(--pai-brand-7-light); +} + +.user-avatar { + height: 35px; + min-height: 35px; + width: 35px; + min-width: 35px; + display: flex; + align-items: center; + justify-content: center; + border: var(--pai-border-color-1); + box-shadow: var(--card-shadow); + border-radius: 10px; +} + +.user-avatar img { + height: 100%; + width: 100%; + border-radius: 10px; +} + +.lds-ellipsis { + display: flex; + justify-content: space-between; + align-items: center; + height: 20px; +} + +.lds-ellipsis div { + width: 8px; + height: 8px; + border-radius: 50%; + background: currentColor; + animation: lds-ellipsis 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite; +} + +.lds-ellipsis div:nth-child(2) { + animation-delay: 0.2s; +} + +.lds-ellipsis div:nth-child(3) { + animation-delay: 0.4s; +} + +.lds-ellipsis div:nth-child(4) { + animation-delay: 0.6s; +} + +@keyframes lds-ellipsis { + 0% { + transform: scale(0); + } + 100% { + transform: scale(1); + } + 80%, 90% { + transform: scale(0); + } +} + +.home_chat-message-top-actions__PfOzb { + min-width: 120px; + font-size: 12px; + position: absolute; + right: 20px; + top: -26px; + left: 30px; + transition: all .3s ease; + opacity: 0; + pointer-events: none; + display: flex; + flex-direction: row-reverse; +} + +.home_chat-message-container__plj_e:hover .home_chat-message-top-actions__PfOzb { + opacity: 1; + transform: translateX(10px); + pointer-events: all; +} + +.home_chat-message-top-actions__PfOzb .home_chat-message-top-action__wXKmA { + opacity: .5; + color: var(--pai-color-3-gray); + white-space: nowrap; + cursor: pointer; +} + +.home_chat-message-top-actions__PfOzb .home_chat-message-top-action__wXKmA:not(:first-child) { + margin-right: 10px; +} + +.home_chat-message-top-actions__PfOzb .home_chat-message-top-action__wXKmA:hover { + opacity: 1; +} + +.window-header { + padding: 14px 20px; + border-bottom: 1px solid rgba(0,0,0,.1); + position: relative; + display: flex; + justify-content: space-between; + align-items: center; +} + +.window-header-title { + max-width: calc(100% - 100px); + overflow: hidden; +} + +.window-header-title .window-header-main-title { + font-size: 20px; + font-weight: bolder; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; + max-width: 50vw; +} + +.window-header-title .window-header-sub-title { + font-size: 14px; + margin-top: 5px; +} + +.home_chat-body-title__5S8w4:hover { + text-decoration: underline; + color: var(--pai-brand-2-hover); +} + +.home_chat-body-title__5S8w4 { + cursor: pointer; + color: var(--pai-brand-1-normal); +} + +.home_sidebar-header__b5asC { + position: relative; + background-color: #fff; + padding: 20px; + margin-bottom: 20px; +} + +.home_sidebar-title__d8_c_ { + font-size: 20px; + font-weight: 700; + margin-bottom: 12px; + animation: home_slide-in__gYZA0 .3s ease; +} + +.home_sidebar-sub-title__IS2Or { + font-size: 12px; + font-weight: 400; + animation: home_slide-in__gYZA0 .3s ease; + color: var(--pai-color-4-gray); +} + +.home_sidebar-logo__FFdBS { + position: absolute; + right: 0; + bottom: 18px; +} + +.login-guide-list { + display: flex; + flex-direction: row; + flex: 1 0 auto; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + color: #515767; + font-size: 12px; + line-height: 22px; +} + +.login-guide-list-item { + width: 100%; + margin-bottom: 8px; + display: flex; + flex-direction: row; + align-items: center; +} + +.login-guide-icon-wrap { + width: 28px; + height: 28px; + border-radius: 14px; + background-color: var(--pai-brand-7-light); + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.login-guide-icon { + color: var(--pai-brand-1-normal); + width: 16px; + height: 16px; +} + +.login-guide-text { + width: 80%; + margin-left: 8px; + color: var(--pai-color-4-gray); +} + +.chat-sidebar img { + width: 100%; + object-fit: cover; +} + +.chat_split_hr { + height:0; + border-top:1px solid #888686; + text-align:center; + margin-top: 1em; + margin-bottom: 1em; +} + +.chat_split_txt { + position:relative; + top:-14px; + color: #888686; + font-size: 0.8em; + background-color:#fff; +} + +pre code.hljs { + display: block; + overflow-x: auto; + padding: 1em +} + +code.hljs { + padding: 3px 5px +} + +.hljs { + background: #1e1e1e; + color: #dcdcdc +} + +.hljs-keyword,.hljs-literal,.hljs-name,.hljs-symbol { + color: #569cd6 +} + +.hljs-link { + color: #569cd6; + text-decoration: underline +} + +.hljs-built_in,.hljs-type { + color: #4ec9b0 +} + +.hljs-class,.hljs-number { + color: #b8d7a3 +} + +.hljs-meta .hljs-string,.hljs-string { + color: #d69d85 +} + +.hljs-regexp,.hljs-template-tag { + color: #9a5334 +} + +.hljs-formula,.hljs-function,.hljs-params,.hljs-subst,.hljs-title { + color: #dcdcdc +} + +.hljs-comment,.hljs-quote { + color: #57a64a; + font-style: italic +} + +.hljs-doctag { + color: #608b4e +} + +.hljs-meta,.hljs-meta .hljs-keyword,.hljs-tag { + color: #9b9b9b +} + +.hljs-template-variable,.hljs-variable { + color: #bd63c5 +} + +.hljs-attr,.hljs-attribute { + color: #9cdcfe +} + +.hljs-section { + color: gold +} + +.hljs-emphasis { + font-style: italic +} + +.hljs-strong { + font-weight: 700 +} + +.hljs-bullet,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-selector-tag { + color: #d7ba7d +} + +.hljs-addition { + background-color: #144212; + display: inline-block; + width: 100% +} + +.hljs-deletion { + background-color: #600; + display: inline-block; + width: 100% +} + +select.styled-dropdown { + width: 120px; + font-size: 16px; + border: 1px solid #aaa; + padding: 6px; + border-radius: 4px; + appearance: none; /* removes the default arrow in some browsers */ + background: #fff url('data:image/svg+xml;utf8,') no-repeat 90% center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + cursor: pointer; + transition: border-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out; +} + +select.styled-dropdown:focus { + border-color: #ff6900; + box-shadow: 0 0 5px rgba(255, 105, 0, 0.5); + outline: none; +} + +select.styled-dropdown option { + font-size: 16px; + padding: 8px 10px; +} + +/*手机端适配*/ +@media screen and (max-width: 768px) { + .chat-sidebar, .chat-annotation, .window-header-sub-title .info { + display: none; + } + + .chat-main { + width: 100%; + padding: 0; + margin-left: 0; + } + + .window-header-title .window-header-main-title { + font-size: 15px; + } +} + +.chat-main .markdown-body ol li { + list-style-type: decimal; +} + +.chat-main .markdown-body h3 { + font-size: 1.17em; +} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/css/views/column-detail.css b/paicoding-ui/src/main/resources/static/css/views/column-detail.css new file mode 100644 index 000000000..1ba1a75ff --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/views/column-detail.css @@ -0,0 +1,557 @@ +.article-wrap { + height: calc(100vh - 60px); + display: flex; +} + +.article-content-wrap { + flex: 1; + display: flex; + flex-direction: column; + overflow-y: auto; + padding: 20px 0; +} +.article-content-inter-wrap { + max-width: 800px; + margin: 0 auto; +} + +.article-content { + word-break: break-word; +} + +.book-directory-comp { + color: #000; + padding: 18px 0; +} + +.book-directory-comp .section-list .section { + position: relative; + display: flex; + justify-content: flex-start; + transition: all 0.2s; + padding: 9px 16px; + cursor: pointer; +} + +.book-directory-comp .section-list .section:hover { + background-color: hsla(0, 0%, 84.7%, 0.2); +} + +.book-directory-comp .section-list .section.unfinished { + cursor: not-allowed; +} + +.book-directory-comp + .section-list + .section.route-active + .center + .main-line + .title + .icon-camera + path, +.book-directory-comp + .section-list + .section.route-active + .left + .index + .icon-camera + path { + fill: currentColor; +} + +.book-directory-comp .section-list .section .left .index { + font-weight: 600; + font-size: 16px; + line-height: 24px; + color: var(--pai-color-999-gray); + padding: 0 6px; + min-width: 26px; + text-align: center; +} + +.book-directory-comp .section-list .section .center { + flex-grow: 1; +} + +.book-directory-comp .section-list .section .center .main-line { + font-size: 15px; + line-height: 24px; + display: flex; + max-width: 90%; +} + +.book-directory-comp .section-list .section .center .main-line .title { + font-size: 0; + flex: 1; + color: #252933; +} + +.book-directory-comp + .section-list + .section + .center + .main-line + .title + .icon-camera { + vertical-align: middle; + display: inline-block; + margin-right: 6px; +} + +.book-directory-comp + .section-list + .section + .center + .main-line + .title + .icon-camera + path { + fill: #8a919f; +} + +.book-directory-comp + .section-list + .section + .center + .main-line + .title + .title-text { + vertical-align: bottom; + font-size: 16px; + line-height: 24px; +} + +.book-directory-comp .section-list .section .center .main-line .right { + margin-left: 15px; +} + +.book-directory-comp .section-list .section .center .main-line .right .lock { + width: 40px; + text-align: center; +} + +.book-directory-comp .section-list .section .center .main-line .right .label { + height: 24px; + background: #fff3db; + line-height: 24px; + border-radius: 12px; + padding: 0 8px; + color: #ff8412; + font-size: 12px; + white-space: nowrap; +} + +.book-directory-comp .section-list .section .center .sub-line { + display: flex; + align-items: center; + margin-top: 6px; + font-size: 13px; + color: var(--pai-color-999-gray); + line-height: 24px; +} + +.book-directory-comp .section-list .section .center .sub-line .label { + background: #eaf2ff; + border-radius: 2px; + line-height: 20px; + padding: 0 6px; + color: #1e80ff; + margin-right: 12px; + font-size: 12px; + min-width: 40px; + white-space: nowrap; + flex-shrink: 0; +} + +.book-summary { + height: 100%; + cursor: default; + flex-shrink: 0; + z-index: 11; + border-right: 1px solid #ddd; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + will-change: left; + background-color: #fff; +} + +.book-summary .book-summary-masker { + display: none; + position: fixed; + left: 0; + top: 0; + z-index: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.3); +} + +.book-summary .book-summary-inner { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + z-index: 1; + height: 100%; +} + +.book-summary .book-summary-inner .book-summary__header { + height: 60px; + display: flex; + padding-left: 16px; + align-items: center; + background-color: #fff; + border-bottom: 1px solid #ddd; +} + +.book-summary .book-summary-inner .book-summary__header .logo { + height: 24px; +} + +.book-summary .book-summary-inner .book-summary__header .logo img { + height: 100%; +} + +.book-summary .book-summary-inner .book-summary__header .label { + margin-left: 13px; + margin-right: 25px; + padding-left: 10px; + padding-right: 10px; + height: 24px; + line-height: 24px; + font-size: 15px; + font-weight: 500; + color: #007fff; + position: relative; + background-color: rgba(0, 127, 255, 0.1); +} + +.book-summary .book-summary-inner .book-summary__header .label:after { + content: ""; + position: absolute; + bottom: 0; + right: 0; + width: 0; + height: 0; + border-color: rgba(0, 127, 255, 0.2) #fff #fff rgba(0, 127, 255, 0.2); + border-style: solid; + border-width: 5px; +} + +.book-summary .book-summary-inner .book-summary__header .audit { + color: #71777c; + font-size: 15px; + opacity: 0.6; +} + +.book-summary .book-summary-inner .buy-sticky { + display: flex; + justify-content: center; + align-items: center; + height: 60px; + background: #fff; + padding: 0 10px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); +} + +.book-summary .book-summary-inner .buy-sticky .section-buy { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + height: 40px; + flex-grow: 1; + cursor: pointer; + border-radius: 4px; + color: #fff; + font-size: 14px; + text-align: center; +} + +.book-summary .book-summary-inner .book-directory { + box-sizing: border-box; + -webkit-overflow-scrolling: touch; + height: calc(100% - 60px); +} + +.book-summary .book-summary-inner .book-directory.bought { + height: calc(100% - 120px); +} + +.book-summary__footer { + position: absolute; + left: 0; + bottom: 0; + right: 0; + height: 60px; + padding-top: 20px; + padding-left: 20px; + box-sizing: border-box; + z-index: 1; +} + +.book-summary__footer .ion-close { + position: absolute; + right: 15px; + top: 15px; + cursor: pointer; + color: #bec3c7; + line-height: 1; +} + +.book-summary__footer .qr-icon { + width: 20px; + position: relative; +} + +.book-summary__footer .qr-icon img { + cursor: pointer; + width: 100%; +} + +.book-summary__footer .qr-tips { + z-index: -1; + opacity: 0; + position: absolute; + left: 16px; + bottom: 50px; + width: 180px; + height: 235px; + box-sizing: border-box; + background-color: #fff; + padding: 20px 30px 0; + border-radius: 2px; + transition: all 0.3s ease; + visibility: hidden; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); +} + +.book-summary__footer .qr-tips.show { + z-index: 1; + visibility: visible; + opacity: 1; +} + +.book-summary__footer .qr-tips .title { + margin-top: 10px; + text-align: center; +} + +.book-summary__footer .qr-tips .title span { + display: block; + font-size: 16px; +} + +.book-summary__footer .qr-tips .qr-img { + margin-top: 5px; +} + +.book-summary__footer .qr-tips .qr-img img { + width: 100%; +} + +.book-summary__footer .qr-tips:after { + content: ""; + position: absolute; + transform: rotate(45deg); + box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.15); + left: 9px; + bottom: 0; + width: 0; + height: 0; + border-color: transparent #fff #fff transparent; + border-style: solid; + border-width: 5px; +} + +/* 底部左右切换 */ +.direction { + margin: 0 auto; +} + +.article-change { + z-index: 100; + position: fixed; + bottom: 70px; + width: 900px; + display: flex; + justify-content: center; + align-items: center; + transition: all 0.3s; +} + +.article-change-item { + cursor: pointer; + position: absolute; + bottom: 0; + z-index: 10; + width: 50px; + height: 50px; + display: flex; + justify-content: center; + align-items: center; + font-size: 14px; + border-radius: 50%; + background-color: var(--pai-brand-1-normal); + color: var(--pai-color-fff-normal); + user-select: none; + box-shadow: 0 4px 10px rgb(0 0 0 / 15%); +} + +.step-btn--prev .article-change-item { + left: -10%; +} + +.step-btn--next .article-change-item { + right: 8%; +} + +.right { + margin-left: 15px; + font-size: 15px; + line-height: 24px; +} + +.right .label { + height: 24px; + background: var(--pai-bg-normal-1); + line-height: 24px; + border-radius: 12px; + padding: 0 8px; + color: var(--pai-brand-1-normal); + font-size: 12px; + white-space: nowrap; +} + +.right .label-star { + color: #17bb98; + background: #e9ffdb +} + +.right .label-free { + color: #5ab4fe; + background: #f2f3f5; +} + + +.book-directory-comp .section-list .section.active:after { + content: ""; + position: absolute; + width: 4px; + height: 24px; + left: 0; + top: 9px; + background: var(--pai-brand-2-hover); + border-radius: 0 8px 8px 0; +} + +.book-directory-comp .section-list .section.active .center .main-line .title, +.book-directory-comp .section-list .section.active .left .index { + color: var(--pai-brand-2-hover); +} + +.needlock { + width: 100%; + text-align: center; + font-size: xx-large; + font-weight: 400; + background-image: -webkit-gradient(linear,left top, left bottom,from(rgba(255,255,255,0)),color-stop(70%, #fff)); + background-image: linear-gradient(-180deg,rgba(255,255,255,0) 0%,#fff 70%); + padding-bottom: 24px; position: relative; + padding-top: 160px; + margin-top: -220px; + z-index: 996; + bottom: -1px; +} + +.needlock h2 { + font-size: 1.5rem; +} + +.join-star { + font-size: small; +} + +.join-star p { + margin-bottom: 8px; +} +.join-star .category { + color: var(--pai-brand-1-normal); + font-weight: bold; +} + +.bd-search { + position: relative; + padding: 1rem 15px; +} + +@media (max-width: 700px) { + .book-directory-comp { + overflow-y: auto; + overflow-x: hidden; + } + + .book-directory-comp .section:hover { + background-color: transparent; + } + + .article-change { + width: 100%; + } + + .article-content-wrap{ + padding: 0; + background-color: var(--pai-bg-white-fff); + flex: none; + display: block; + } + + .for-menu { + border-bottom: 1px solid rgba(0,0,0,0.1); + } + + .book-summary { + height: auto; + width: 100%; + } + + .article-wrap { + display: block; + } + + .step-btn--prev .article-change-item { + left: -6%; + } + + .book-summary__footer { + display: none; + } + + .article-change-item { + width: 40px; + height: 40px; + font-size: 12px; + } +} + +@media (min-width: 721px) { + .beautify-scrollbar-warp { + overflow-x: hidden; + } + + .beautify-scrollbar-warp:hover { + overflow: auto; + } + + .beautify-scrollbar-warp::-webkit-scrollbar { + width: 12px; + height: 4px; + } + + .beautify-scrollbar-warp::-webkit-scrollbar-thumb { + border: 4px solid transparent; + background-clip: padding-box; + border-radius: 7px; + } + + .for-menu { + display: none; + } +} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/css/views/column-home.css b/paicoding-ui/src/main/resources/static/css/views/column-home.css new file mode 100644 index 000000000..ea8adbaab --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/views/column-home.css @@ -0,0 +1,257 @@ +.custom-home { + overflow: auto; + padding-top: 20px; + height: calc(100vh - 60px); +} + +.custom-home-wrap { + width: 1200px; + display: flex; + margin: 0 auto 20px; + min-height: calc(100% - 90px); +} + +.custom-home-body { + flex: 1; + margin-right: 20px; + border-radius: 8px; +} + +/* 详情列表 */ +.item { + display: flex; + padding: 25px; + box-sizing: border-box; + position: relative; + border-bottom: var(--pai-hr-color-1) 1px solid; +} + +.poster { + width: 110px; + height: 156px; + flex-shrink: 0; + position: relative; +} + +.poster img { + width: 100%; + height: 100%; + border-radius: 2px; +} + +.info { + position: relative; + flex-grow: 1; + overflow: hidden; + box-sizing: border-box; + font-size: 16px; + padding-left: 22px; +} + +.info .messages { + font-size: 14px; +} + +.author .name { + color: var(--pai-color-3-gray); + font-size: 14px; +} + +.title { + font-size: 20px; + line-height: 28px; +} + +.info .desc { + margin-top: 10px; + font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + color: var(--pai-color-3-gray); +} + +.user { + display: inline-flex; + align-items: center; +} + +.user img { + width: 26px; + height: 26px; + border-radius: 50%; + margin-right: 8px; + background-position: 50%; + background-size: cover; + background-repeat: no-repeat; +} + +.other { + margin-top: 6px; + display: flex; + align-items: center; + color: #8a919f; +} + +.author { + margin-top: 6px; +} + +/*限时免费*/ + +.new-tag-wrap { + padding: 0 6px; + height: 20px; + line-height: 20px; + color: var(--pai-color-fff-normal); + font-size: 12px; + border-radius: 2px; + display: inline-block; + vertical-align: middle; + cursor: default; + margin-right: 3px; + transform: translateY(-3px); + background-color: var(--pai-brand-5-bak); +} + +.info .tag { + display: inline-block; + vertical-align: middle; + cursor: default; + margin-right: 5px; + transform: translateY(-2px); +} + +/*level*/ + +.com-2-level { + display: inline-block; + vertical-align: middle; + box-sizing: border-box; + width: 30px; + height: 30px; + border: 2px solid #fff; + border-radius: 50%; + background-color: var(--pai-brand-5-bak); + font-size: 12px; + text-align: center; + line-height: 26px; + color: #fff; + font-style: oblique; + font-weight: 700; +} + +.uc-hero-level, .uc-hero-name { + margin-right: 10px; + margin-left: 10px; +} + +.com-2-level.skin-2 { + width: auto; + height: 16px; + border: none; + border-radius: 9px; + padding: 0 8px; + font-size: 12px; + line-height: 16px; + font-weight: 700; + font-style: normal; +} + +.com-2-level .text { + position: relative; + top: 1px; + display: block; + -webkit-transform: scale(.8); + transform: scale(.8); +} + +.com-2-level.skin-2 .text { + top: 0; + -webkit-transform: none; + transform: none; +} + +/*作者简介*/ +.self-description { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-left: 4px; + font-size: 14px; + color: var(--pai-color-3-gray); +} + +/*限时免费*/ + +.sale-tooltip { + position: relative; + flex: 0 0 auto; + align-items: center; + background: linear-gradient(90deg,rgba(246,66,66,.25),rgba(246,66,66,0)); + border-radius: 100px; + padding: 0 48px 0 8px; + color: var(--pai-brand-6-mq); + font-size: 12px; + font-weight: 500; +} + +.sale-tooltip .count-down-text:before { + width: 6px; + content: "·"; + margin: 0 2px; +} + +.read-count:before { + width: 6px; + content: "·"; + margin: 0 4px; +} + +@media screen and (max-width: 768px) { + .custom-home { + padding-top: 0; + } + + .custom-home-right, + .self-description, + .article-count, + .sale-tooltip, + .read-count:before { + display: none; + } + + .custom-home-wrap { + width: 100%; + margin: 0; + display: block; + } + + .custom-home-body { + margin-right: 0; + } + + .other { + /*最多显示一行,超出部分省略号*/ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-top: 0; + } + + .item { + padding: 15px; + } + + .poster { + width: 90px; + height: 140px; + } + + .title { + font-size: 16px; + line-height: 22px; + } +} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/css/views/home.css b/paicoding-ui/src/main/resources/static/css/views/home.css new file mode 100644 index 000000000..b2d8415fc --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/views/home.css @@ -0,0 +1,255 @@ +.home { + /*修复返回顶部时不起效的 bug overflow 必须默认*/ + /*overflow: auto;*/ + height: calc(100vh - 60px); +} + +.home-wrap { + min-height: calc(100% - 90px); + padding-top: 20px; +} + +.home-inter-wrap { + max-width: 1200px; + margin: 0 auto; + display: flex; +} + +.home-body { + flex: 1; + margin-right: 20px; + background-color: #fff; + padding: 0 20px; + margin-bottom: 20px; +} + +.home-body-nav { + color: #262626; +} + +/* 头部导航 */ +.category--item { + cursor: pointer; + font-size: 18px; + font-weight: 700; + line-height: 2.3rem; + white-space: nowrap; +} + +.category--item:hover { + color: var(--pai-brand-2-hover); +} + +.category--item:first-child { + padding-left: 0; +} + +.category--active { + color: var(--pai-brand-1-normal); +} + +.category-wrap { + max-width: 1200px; + margin: 0 auto; + padding: 4px 0; +} + +.align-content-start { + overflow-x: auto; +} + +/* 头部搜索 */ +.search-has-result ul { + margin-bottom: 0; + list-style-type: none; + padding: 0; + font-size: 1rem; + font-weight: 500; + line-height: 1.77778rem; + margin-top: 0; +} + +.search-has-result ul li a { + --tw-border-opacity: 1; + border: 0 solid; + border-bottom-width: 1px; + border-color: rgba(127, 125, 131, var(--tw-border-opacity)); +} + +.search-has-result ul li span.text-sm { + line-height: 2.4rem; +} + +.hover\:bg-gray-400:hover { + --tw-bg-opacity: 1; + background-color: rgba(127, 125, 131, var(--tw-bg-opacity)); +} + +.search-result-block .search-no-result mark { + font-size: 1.2rem; +} + +.category-search-btn, +.category-cancel-btn { + align-self: center; + justify-items: center; + background-color: transparent; + background-image: none; + border: none; +} + +.category-search-btn:hover, +.category-cancel-btn:hover { + color: var(--pai-brand-2-hover); +} + +.svg-inline--fa, +svg:not(:root).svg-inline--fa { + overflow: visible; +} + +.svg-inline--fa.fa-w-16 { + width: 1.2em; +} + +.svg-inline--fa.fa-w-14 { + width: 1em; +} + +/*主要文章详情页 头部图片*/ +.home-carouse-wrap { + background-color: #346; + padding: 20px; +} + +.home-carouse-inter-wrap { + max-width: 1200px; + margin: 0 auto; + display: flex; + gap: 20px; + flex-wrap: wrap; +} + +.home-carouse-item { + width: calc((100% - 60px) * 0.25); + transition: all 0.2s ease 0s; + box-shadow: 0 6px 12px 0 rgb(0 0 0 / 25%); +} + +.home-carouse-item img { + width: 100%; + height: 150px; +} + +.home-carouse-item-body { + background-color: #fff; + padding: 20px; + height: 160px; +} + +.home-carouse-item-title { + font-size: 18px; + font-weight: 700; + height: 58px; + margin-bottom: 10px; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.home-carouse-item-dot { + display: inline-block; + background-color: #a2a2a2; + border-radius: 100%; + height: 8px; + width: 8px; + margin-right: 4px; + flex-shrink: 0; +} + +.home-carouse-item-tag { + display: flex; + align-items: center; + color: #a2a2a2; + font-size: 14px; +} + +.home-carouse-item-first-text { + font-weight: 900; + padding: 0 8px; + align-items: center; + position: relative; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; +} + +.home-carouse-item-tag .home-carouse-item-first-text:not(:last-child):after { + position: absolute; + right: -1px; + display: block; + content: " "; + width: 2px; + height: 2px; + border-radius: 50%; + background: #4e5969; +} + +.home-carouse-item-tag .author { + padding-left: 8px; + color: #a2a2a2; + display: inline-block; + max-width: 100px; + overflow-x: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.home-carouse-item:hover { + transform: translateY(-4px); +} + +/* home 适配 */ +@media screen and (max-width: 768px) { + .home-right, + .home-carouse-wrap { + display: none; + } + .home-body { + margin-right: 0; + } + + .home-wrap { + padding-top: 0; + } + + .user-article-item-value-text { + -webkit-line-clamp: 3; + } + .home-inter-wrap { + width: auto; + display: block; + } + .user-article-img { + width: 110px; + position: absolute; + top: 45%; + transform: translateY(-50%); + right: 0; + } + .align-content-start { + padding: 4px 18px; + } + .cdc-article-panel__operate { + margin-left: 8px; + } + .article-show-wrap { + margin-right: 8px; + } + + .cdc-article-panel__inner:has(.user-article-img) .cdc-article-panel__media { + max-width: 65%; + } +} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/css/views/notice.css b/paicoding-ui/src/main/resources/static/css/views/notice.css new file mode 100644 index 000000000..a4616c67d --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/views/notice.css @@ -0,0 +1,109 @@ +.notice-wrap { + height: calc(100vh - 60px); + overflow-y: auto; + background-color: #f4f5f5; +} +.notice-nav { + position: sticky; + top: 0; + margin-bottom: 10px; + background-color: var(--pai-bg-white-fff); + border-top: 1px solid #f1f1f1; +} +.notice-nav-inner { + width: 1000px; + margin: 0 auto; + display: flex; + align-items: center; + padding-left: 20px; +} +.notice-nav-item { + padding: 14px 0; + margin-right: 36px; + line-height: 16px; +} +.notice-nav-item--active { + padding: 14px 0; + margin-right: 36px; + color: var(--pai-brand-2-hover); +} +.unread-count { + display: inline-block; + color: var(--pai-color-fff-normal); + transform: scale(0.8); + font-size: 12px; + padding: 2px 8px; + background: var(--pai-brand-6-mq); + border-radius: 100px; + text-align: center; +} +.notice-content { + width: 1000px; + margin: 0 auto; + min-height: calc(100% - 133px); +} +.notification { + margin-bottom: 12px; + display: flex; + padding: 20px; + background-color: var(--pai-bg-white-fff); + border-radius: 2px; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 5%); + align-items: center; +} +.notification-img { + width: 32px; + height: 32px; + border-radius: 50%; + margin-right: 24px; +} +.notification-right { + flex: 1; + display: flex; + flex-direction: column; +} +.notification-content { + display: flex; + font-size: 15px; + margin-bottom: 8px; +} +.notification-comment { + font-size: 14px; + width: 100%; + min-height: 40px; + background-color: #fafbfc; + padding: 10px; + border-radius: 4px; + border: 1px solid #f1f1f2; + margin-bottom: 8px; +} +.notification a { + color: var(--pai-brand-1-normal); +} +.notification a:hover { + color: var(--pai-brand-2-hover); +} +.notification-bottom { + display: flex; + justify-content: space-between; +} +.notification-time { + font-size: 12px; + color: #ccc; +} +.notification-action { + display: flex; +} + +.notification a.notification-action-item { + display: flex; + align-items: center; + font-size: 12px; + margin-right: 8px; + cursor: pointer; + color: var(--pai-color-999-gray); +} + +.action-text { + font-size: 12px; +} diff --git a/paicoding-ui/src/main/resources/static/css/views/rank.css b/paicoding-ui/src/main/resources/static/css/views/rank.css new file mode 100644 index 000000000..6def9b2c6 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/views/rank.css @@ -0,0 +1,216 @@ +.custom-home { + overflow: auto; + padding-top: 20px; + height: calc(100vh - 60px); +} + +.custom-home-wrap { + width: 1200px; + display: flex; + margin: 0 auto 20px; + min-height: calc(100% - 90px); +} + +.hot-list-wrap { + display: flex; + flex-direction: column; + flex: 1; + width: 80%; + margin-left: 5%; + background-color: #fff; + padding: 1.66rem 1rem; + border-radius: 4px; +} + +.author-type-select { + padding: 0.25rem; + box-sizing: border-box; + background-color: #f2f3f5; + border-radius: 4px; +} + +.author-type-select, .hot-list-header { + display: flex; + flex-direction: row; + align-items: center; +} + +.author-type-link { + font-size: 1.16rem; + line-height: 1.83rem; + display: inline-block; + padding: 2px 1rem; + color: #8a919f; + border-radius: 4px; + font-weight: 400; +} + +.author-type-link-active { + background-color: #fff; + color: #252933; +} + +.hot-list-header { + padding: 0 1rem 1.33rem; + font-size: 1.5rem; + line-height: 2.16rem; + justify-content: space-between +} + +.author-item-link { + display: inline-block; + padding: 0; + margin: 0; + width: 100%; +} + +.author-item-wrap { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + padding: 1.33rem 1rem; + border-radius: 4px; + cursor: pointer; +} + +.author-item-left { + display: flex; + flex-direction: row; + align-items: center; + min-width: 300px; +} + +.author-hot, .author-right { + display: flex; + flex-direction: row; + align-items: center +} + +.author-hot { + border-right: 1px solid #e4e6eb; + min-height: 3.33rem; + padding-right: 2.5rem; + margin-right: 2.5rem; + display: flex; + flex-direction: row; + align-items: center; +} + +.author-right { + justify-content: flex-end; + width: 21rem; + flex-shrink: 0; +} + +.author-number { + min-width: 2.66rem; + text-align: center; + font-size: 1.5rem; + font-weight: 600; + line-height: 2rem; + color: #515767; + margin-right: 2rem; + flex-shrink: 0; +} + +.author-number-1 { + background: linear-gradient(180deg, #f64242 30%, rgba(246, 66, 66, .4) 80%) +} + +.author-number-2 { + background: linear-gradient(180deg, #ff7426 30%, rgba(255, 116, 38, .4) 80%) +} + +.author-number-3 { + background: linear-gradient(180deg, #ffac0c 30%, rgba(255, 172, 12, .4) 80%) +} + +.author-number-1, .author-number-2, .author-number-3 { + -webkit-background-clip: text; + color: transparent; + font-family: Archivo; + font-size: 1.66rem +} + +.author-detail { + display: flex; + flex-direction: row; + align-items: center; + min-width: 285px; + max-width: 400px; +} + +.author-author-img { + width: 3rem; + height: 3rem; + border-radius: 24px; + margin-right: 1.66rem; +} + +.username { + font-size: 1em; + font-weight: 600; + color: #252933; + display: flex; + align-items: center; +} + +.username .name { + display: inline-block; + vertical-align: top; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.author-desc { + display: flex; + flex-direction: row; + align-items: center; + margin-top: 0.66rem; + flex-wrap: wrap; +} + +.author-text { + color: #8a919f; + font-size: 0.9rem; +} + +.author-hot { + border-right: 1px solid #e4e6eb; + min-height: 3.33rem; + padding-right: 2.5rem; + margin-right: 2.5rem; +} + +.author-hot, .author-right { + display: flex; + flex-direction: row; + align-items: center +} + +.hot-number { + color: #252933; + font-size: 1.125rem; + font-weight: 500; + margin-right: 0.5rem; +} + +.hot-text { + font-size: 1.125rem; + color: #8a919f; + line-height: 1.83rem; +} + +.author-item-wrap .author-right .author-item-button { + width: 6.3rem; + box-sizing: border-box; + height: 2.83rem; + line-height: 2.5rem; + background-color: rgba(30, 128, 255, 0.05); + border-color: rgba(30, 128, 255, 0.3); + border-radius: 4px; + color: #1e80ff; + padding: 0; +} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/css/views/user.css b/paicoding-ui/src/main/resources/static/css/views/user.css new file mode 100644 index 000000000..753e57cd0 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/css/views/user.css @@ -0,0 +1,469 @@ +/**/ +.user-left-wrap { + background: linear-gradient( + 203.77deg, + rgba(44, 128, 255, 0.2) -31.27%, + rgba(44, 128, 255, 0) 54.55% + ), + linear-gradient( + 35.19deg, + rgba(255, 203, 154, 0.7) -24.65%, + rgba(255, 255, 255, 0) 38.1% + ), + rgba(255, 255, 255, 0.7); + border-radius: 8px; + padding-top: 27px; + padding-bottom: 24px; + margin-bottom: 16px; + display: flex; + flex-direction: column; + align-items: center; +} + +.user-head-name { + font-weight: 500; + font-size: 26px; + line-height: 120%; + color: rgb(51, 51, 51); + margin: 8px 0; +} + +.user-num-wrap { + margin: 40px auto; + width: 192px; + height: 63px; + display: flex; + flex-direction: row; + justify-content: space-between; +} +.user-head-num-wrap { + display: flex; + flex-direction: column; +} +.user-head-num { + font-weight: 600; + font-size: 32px; + height: 42px; + line-height: 42px; + color: rgb(51, 51, 51); + margin-bottom: 4px; + text-align: center; +} +.achievement-wrap, +.process-wrap { + background-color: rgb(255, 255, 255); + padding: 20px; + border-radius: 4px; + overflow: hidden; +} + +.achievement-title, +.process-title { + font-size: 18px; + line-height: 24px; + color: rgb(51, 51, 51); + font-weight: 600; + margin-bottom: 24px; +} +.achievement-item { + height: 32px; + line-height: 32px; + margin-bottom: 16px; + display: flex; + justify-content: space-between; +} + +.achievement-item span { + font-weight: 500; +} + +/* tag选择样式 */ +.tag-select-active { + color: var(--pai-brand-2-hover) !important; + border-bottom: 4px solid var(--pai-brand-2-hover); +} + +.tag-select { + color: #000000; + font-weight: 500; + margin: 0 0 0 32px; + position: relative; + display: inline-flex; + align-items: center; + padding: 12px 0; + font-size: 18px; + background: transparent; + outline: none; + cursor: pointer; +} + +.tag-select:first-child { + margin-left: 0; +} + +.user-select-tag-wrap { + border-bottom: 1px solid rgb(240 240 240); + margin-bottom: 16px; +} + +/* 整体布局 */ +.user { + overflow: auto; + height: calc(100vh - 60px); + background-color: #f7f8f9; +} + +.user-wrap { + margin: 0 auto; + width: 1200px; + display: flex; + flex-direction: column; + margin-bottom: 20px; + min-height: calc(100% - 90px); +} + +.user-body { + flex: 1; + border-radius: 4px; + padding: 20px; + background-color: #fff; + z-index: 10; + margin-right: 20px; +} + +.user-content { + display: flex; +} + +.user-left { + z-index: 10; + width: 30%; + border-radius: 8px; +} + +/* 关注 */ +.follow-select-tag { + padding: 0 24px; + color: #333; + border-right: 1px solid #e6e6e6; + margin: 0; + cursor: pointer; + font-size: 18px; + line-height: 22px; + font-weight: 500; +} + +.follow-select-tag:last-child { + padding-right: 0; + border-right: 0; +} + +.follow-select-tag:hover, +.follow-select-tag-active { + color: var(--pai-brand-2-hover); +} + +.follow-item { + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + padding: 8px 16px 8px 0; + height: fit-content; + font-size: 18px; +} + +.follow-item img { + width: 66px; + height: 66px; + border-radius: 50%; + margin-right: 15px; + font-size: 18px; +} + +.follow-item-icon { + width: 68px; + height: 24px; + border-radius: 40px; + display: flex; + align-items: center; + justify-content: center; + color: var(--pai-brand-1-normal); + border: 1px solid var(--pai-brand-1-normal); + font-size: 12px; + cursor: pointer; +} + +#saveModel .modal-dialog { + max-width: 900px; +} + +/* 个人信息编辑弹窗 */ +#saveModel .input-group { + height: 60px; + display: flex; + align-items: center; +} + +#saveModel .input-group:last-child { + border: 0; +} + +#saveModel .form-label { + width: 80px; + text-align: left; + font-weight: 500; + font-size: 14px; + color: #333; +} + +#saveModel .form-control { + height: 32px; + display: flex; + align-items: center; + color: var(--pai-color-3-black); + background: var(--pai-bg-light-2); + border: 1px solid var(--pai-border-color-1); + font-size: 14px; +} + +#saveModel .form-control:focus { + border-color: var(--pai-brand-3-click); + background: var(--pai-bg-white-fff); + box-shadow: none; +} + +#saveModel .modal-body { + display: flex; + padding: 20px; +} + +.person-img-wrap { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + width: 136px; + margin-left: 60px; +} +.person-img-inter-wrap { + width: 90px; + height: 90px; + position: relative; +} +.person-img { + width: 100%; + height: 100%; + border-radius: 50%; +} +.person-upload-text { + color: #1d2129; + font-weight: 500; + font-size: 14px; + margin-top: 10px; + margin-bottom: 8px; +} +.person-upload-limit { + color: var(--pai-color-3-gray); + font-size: 12px; + line-height: 17px; + font-weight: 400; +} +.cancel-title { + color: var(--pai-brand-1-normal); + cursor: pointer; +} +.click-cover { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + color: var(--pai-color-fff-normal); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 50%; + background-color: rgba(29, 33, 41, 0.5); + z-index: 2; + visibility: hidden; + cursor: pointer; +} +.click-text { + font-size: 12px; + margin-top: 7px; + line-height: 17px; + font-weight: 400; +} +.click-input { + display: none; +} +.person-info { + width: 80%; +} + +/* 头部改造 */ +.user-bg { + background-image: url(../../img/lucky.png); + background-position: top; + background-repeat: no-repeat; + background-size: auto 288px; + position: relative; + height: 268px; + padding-top: 42px; +} + +.user-bg-mask { + background: linear-gradient( + 180deg, + hsla(0, 0%, 98%, 0), + hsla(0, 0%, 98%, 0.95) 85%, + #f9f9f9 + ); + bottom: 0; + height: 100px; + left: 0; + position: absolute; + width: 100%; +} + +.user-head { + position: relative; + border-radius: 8px; + padding-top: 36px; + clip-path: path( + "M85 0c18.703 0 35.339 8.853 45.945 22.597.41.53.84 1.132 1.293 1.804A24 24 0 0 0 152.148 35H1188c6.627 0 12 5.373 12 12v169c0 6.627-5.373 12-12 12H12c-6.627 0-12-5.373-12-12V47c0-6.627 5.373-12 12-12h5.857a24 24 0 0 0 19.916-10.608c.478-.712.933-1.345 1.364-1.901C49.747 8.807 66.345 0 85 0Z" + ); + background: linear-gradient( + 180deg, + hsla(0, 0%, 100%, 0.6), + hsla(0, 0%, 100%, 0.9) 76%, + #fff + ); + backdrop-filter: blur(10px); + width: 1200px; + margin: 0 auto; + z-index: 10; +} + +.user-head-img { + width: 90px; + height: 90px; + border-radius: 50%; + background-color: #fff; + position: absolute; + left: 40px; + top: 13px; +} + +.user-head-title-wrap { + margin-left: 160px; + display: flex; + justify-content: space-between; +} + +.user-head-title-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 380px; + font-size: 24px; + font-weight: 500; + padding-top: 12px; +} +.user-head-title-classify { + display: flex; + height: 86px; + padding-top: 20px; + align-items: center; +} +.user-head-title-classify-item { + width: 110px; + display: flex; + flex-direction: column; + align-items: center; + color: var(--pai-color-4-gray); +} +.user-head-title-classify-item span:last-child { + margin-top: 12px; +} + +.user-head-cell { + width: 1px; + height: 20px; + background-color: #adb5bd; +} +.user-head-footer { + display: flex; + justify-content: space-between; + padding: 2px 30px 6px; +} +.user-edit { + font-size: 16px; + padding-top: 14px; + width: 280px; + color: var(--pai-brand-1-normal); + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; +} +.edit-btn:hover { + color: var(--pai-brand-2-hover); +} +.user-edit span:first-child { + color: var(--pai-color-3-gray); +} + +.user-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-bottom: 12px; +} +.user-text-icon { + cursor: pointer; +} + +.iconSize___29n8N { + filter: brightness(1.2); +} + +.text-base-pure { + color: var(--pai-color-999-gray); +} +.tw-flex-1 { + flex: 1 1 0%; +} + +.user-edit .edit-btn { + padding-left: 20px; +} + +.tags { + padding-bottom: 12px; + font-size: 14px; +} + +.tag-item { + align-items: center; + background: rgba(51,51,51,.05); + border-radius: 14px; + color: var(--pai-color-3-gray); + display: flex; + height: 28px; + margin-right: 8px; + padding-left: 8px; + padding-right: 8px; +} + +.tw-w-3 { + width: 12px; +} +.tw-h-3 { + height: 12px; +} +.tw-mr-1 { + margin-right: 4px; +} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/Gulpfile.js b/paicoding-ui/src/main/resources/static/editormd/Gulpfile.js new file mode 100644 index 000000000..09ccf04ce --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/Gulpfile.js @@ -0,0 +1,342 @@ +"use strict"; + +var os = require("os"); +var gulp = require("gulp"); +var gutil = require("gulp-util"); +var sass = require("gulp-ruby-sass"); +var jshint = require("gulp-jshint"); +var uglify = require("gulp-uglifyjs"); +var rename = require("gulp-rename"); +var concat = require("gulp-concat"); +var notify = require("gulp-notify"); +var header = require("gulp-header"); +var minifycss = require("gulp-minify-css"); +//var jsdoc = require("gulp-jsdoc"); +//var jsdoc2md = require("gulp-jsdoc-to-markdown"); +var pkg = require("./package.json"); +var dateFormat = require("dateformatter").format; +var replace = require("gulp-replace"); + +pkg.name = "Editor.md"; +pkg.today = dateFormat; + +var headerComment = ["/*", + " * <%= pkg.name %>", + " *", + " * @file <%= fileName(file) %> ", + " * @version v<%= pkg.version %> ", + " * @description <%= pkg.description %>", + " * @license MIT License", + " * @author <%= pkg.author %>", + " * {@link <%= pkg.homepage %>}", + " * @updateTime <%= pkg.today('Y-m-d') %>", + " */", + "\r\n"].join("\r\n"); + +var headerMiniComment = "/*! <%= pkg.name %> v<%= pkg.version %> | <%= fileName(file) %> | <%= pkg.description %> | MIT License | By: <%= pkg.author %> | <%= pkg.homepage %> | <%=pkg.today('Y-m-d') %> */\r\n"; + +var scssTask = function(fileName, path) { + + path = path || "scss/"; + + var distPath = "css"; + + return sass(path + fileName + ".scss", { style: "expanded", sourcemap: false, noCache : true }) + .pipe(gulp.dest(distPath)) + .pipe(header(headerComment, {pkg : pkg, fileName : function(file) { + var name = file.path.split(file.base); + return name[1].replace("\\", ""); + }})) + .pipe(gulp.dest(distPath)) + .pipe(rename({ suffix: ".min" })) + .pipe(gulp.dest(distPath)) + .pipe(minifycss()) + .pipe(gulp.dest(distPath)) + .pipe(header(headerMiniComment, {pkg : pkg, fileName : function(file) { + var name = file.path.split(file.base); + return name[1].replace("\\", ""); + }})) + .pipe(gulp.dest(distPath)) + .pipe(notify({ message: fileName + ".scss task completed!" })); +}; + +gulp.task("scss", function() { + return scssTask("editormd"); +}); + +gulp.task("scss2", function() { + return scssTask("editormd.preview"); +}); + +gulp.task("scss3", function() { + return scssTask("editormd.logo"); +}); + +gulp.task("js", function() { + return gulp.src("./src/editormd.js") + .pipe(jshint("./.jshintrc")) + .pipe(jshint.reporter("default")) + .pipe(header(headerComment, {pkg : pkg, fileName : function(file) { + var name = file.path.split(file.base); + return name[1].replace(/[\\\/]?/, ""); + }})) + .pipe(gulp.dest("./")) + .pipe(rename({ suffix: ".min" })) + .pipe(uglify()) // {outSourceMap: true, sourceRoot: './'} + .pipe(gulp.dest("./")) + .pipe(header(headerMiniComment, {pkg : pkg, fileName : function(file) { + var name = file.path.split(file.base + ( (os.platform() === "win32") ? "\\" : "/") ); + return name[1].replace(/[\\\/]?/, ""); + }})) + .pipe(gulp.dest("./")) + .pipe(notify({ message: "editormd.js task complete" })); +}); + +gulp.task("amd", function() { + var replaceText1 = [ + 'var cmModePath = "codemirror/mode/";', + ' var cmAddonPath = "codemirror/addon/";', + '', + ' var codeMirrorModules = [', + ' "jquery", "marked", "prettify",', + ' "katex", "raphael", "underscore", "flowchart", "jqueryflowchart", "sequenceDiagram",', + '', + ' "codemirror/lib/codemirror",', + ' cmModePath + "css/css",', + ' cmModePath + "sass/sass",', + ' cmModePath + "shell/shell",', + ' cmModePath + "sql/sql",', + ' cmModePath + "clike/clike",', + ' cmModePath + "php/php",', + ' cmModePath + "xml/xml",', + ' cmModePath + "markdown/markdown",', + ' cmModePath + "javascript/javascript",', + ' cmModePath + "htmlmixed/htmlmixed",', + ' cmModePath + "gfm/gfm",', + ' cmModePath + "http/http",', + ' cmModePath + "go/go",', + ' cmModePath + "dart/dart",', + ' cmModePath + "coffeescript/coffeescript",', + ' cmModePath + "nginx/nginx",', + ' cmModePath + "python/python",', + ' cmModePath + "perl/perl",', + ' cmModePath + "lua/lua",', + ' cmModePath + "r/r", ', + ' cmModePath + "ruby/ruby", ', + ' cmModePath + "rst/rst",', + ' cmModePath + "smartymixed/smartymixed",', + ' cmModePath + "vb/vb",', + ' cmModePath + "vbscript/vbscript",', + ' cmModePath + "velocity/velocity",', + ' cmModePath + "xquery/xquery",', + ' cmModePath + "yaml/yaml",', + ' cmModePath + "erlang/erlang",', + ' cmModePath + "jade/jade",', + '', + ' cmAddonPath + "edit/trailingspace", ', + ' cmAddonPath + "dialog/dialog", ', + ' cmAddonPath + "search/searchcursor", ', + ' cmAddonPath + "search/search", ', + ' cmAddonPath + "scroll/annotatescrollbar", ', + ' cmAddonPath + "search/matchesonscrollbar", ', + ' cmAddonPath + "display/placeholder", ', + ' cmAddonPath + "edit/closetag", ', + ' cmAddonPath + "fold/foldcode",', + ' cmAddonPath + "fold/foldgutter",', + ' cmAddonPath + "fold/indent-fold",', + ' cmAddonPath + "fold/brace-fold",', + ' cmAddonPath + "fold/xml-fold", ', + ' cmAddonPath + "fold/markdown-fold",', + ' cmAddonPath + "fold/comment-fold", ', + ' cmAddonPath + "mode/overlay", ', + ' cmAddonPath + "selection/active-line", ', + ' cmAddonPath + "edit/closebrackets", ', + ' cmAddonPath + "display/fullscreen",', + ' cmAddonPath + "search/match-highlighter"', + ' ];', + '', + ' define(codeMirrorModules, factory);' + ].join("\r\n"); + + var replaceText2 = [ + "if (typeof define == \"function\" && define.amd) {", + " $ = arguments[0];", + " marked = arguments[1];", + " prettify = arguments[2];", + " katex = arguments[3];", + " Raphael = arguments[4];", + " _ = arguments[5];", + " flowchart = arguments[6];", + " CodeMirror = arguments[9];", + " }" + ].join("\r\n"); + + gulp.src("src/editormd.js") + .pipe(rename({ suffix: ".amd" })) + .pipe(gulp.dest('./')) + .pipe(header(headerComment, {pkg : pkg, fileName : function(file) { + var name = file.path.split(file.base); + return name[1].replace(/[\\\/]?/, ""); + }})) + .pipe(gulp.dest("./")) + .pipe(replace("/* Require.js define replace */", replaceText1)) + .pipe(gulp.dest('./')) + .pipe(replace("/* Require.js assignment replace */", replaceText2)) + .pipe(gulp.dest('./')) + .pipe(rename({ suffix: ".min" })) + .pipe(uglify()) //{outSourceMap: true, sourceRoot: './'} + .pipe(gulp.dest("./")) + .pipe(header(headerMiniComment, {pkg : pkg, fileName : function(file) { + var name = file.path.split(file.base + ( (os.platform() === "win32") ? "\\" : "/") ); + return name[1].replace(/[\\\/]?/, ""); + }})) + .pipe(gulp.dest("./")) + .pipe(notify({ message: "amd version task complete"})); +}); + + +var codeMirror = { + path : { + src : { + mode : "lib/codemirror/mode", + addon : "lib/codemirror/addon" + }, + dist : "lib/codemirror" + }, + modes : [ + "css", + "sass", + "shell", + "sql", + "clike", + "php", + "xml", + "markdown", + "javascript", + "htmlmixed", + "gfm", + "http", + "go", + "dart", + "coffeescript", + "nginx", + "python", + "perl", + "lua", + "r", + "ruby", + "rst", + "smartymixed", + "vb", + "vbscript", + "velocity", + "xquery", + "yaml", + "erlang", + "jade", + ], + + addons : [ + "edit/trailingspace", + "dialog/dialog", + "search/searchcursor", + "search/search", + "scroll/annotatescrollbar", + "search/matchesonscrollbar", + "display/placeholder", + "edit/closetag", + "fold/foldcode", + "fold/foldgutter", + "fold/indent-fold", + "fold/brace-fold", + "fold/xml-fold", + "fold/markdown-fold", + "fold/comment-fold", + "mode/overlay", + "selection/active-line", + "edit/closebrackets", + "display/fullscreen", + "search/match-highlighter" + ] +}; + +gulp.task("cm-mode", function() { + + var modes = [ + codeMirror.path.src.mode + "/meta.js" + ]; + + for(var i in codeMirror.modes) { + var mode = codeMirror.modes[i]; + modes.push(codeMirror.path.src.mode + "/" + mode + "/" + mode + ".js"); + } + + return gulp.src(modes) + .pipe(concat("modes.min.js")) + .pipe(gulp.dest(codeMirror.path.dist)) + .pipe(uglify()) // {outSourceMap: true, sourceRoot: codeMirror.path.dist} + .pipe(gulp.dest(codeMirror.path.dist)) + .pipe(header(headerMiniComment, {pkg : pkg, fileName : function(file) { + var name = file.path.split(file.base + "\\"); + return (name[1]?name[1]:name[0]).replace(/\\/g, ""); + }})) + .pipe(gulp.dest(codeMirror.path.dist)) + .pipe(notify({ message: "codemirror-mode task complete!" })); +}); + +gulp.task("cm-addon", function() { + + var addons = []; + + for(var i in codeMirror.addons) { + var addon = codeMirror.addons[i]; + addons.push(codeMirror.path.src.addon + "/" + addon + ".js"); + } + + return gulp.src(addons) + .pipe(concat("addons.min.js")) + .pipe(gulp.dest(codeMirror.path.dist)) + .pipe(uglify()) //{outSourceMap: true, sourceRoot: codeMirror.path.dist} + .pipe(gulp.dest(codeMirror.path.dist)) + .pipe(header(headerMiniComment, {pkg : pkg, fileName : function(file) { + var name = file.path.split(file.base + "\\"); + return (name[1]?name[1]:name[0]).replace(/\\/g, ""); + }})) + .pipe(gulp.dest(codeMirror.path.dist)) + .pipe(notify({ message: "codemirror-addon.js task complete" })); +}); +/* +gulp.task("jsdoc", function(){ + return gulp.src(["./src/editormd.js", "README.md"]) + .pipe(jsdoc.parser()) + .pipe(jsdoc.generator("./docs/html")); +}); + +gulp.task("jsdoc2md", function() { + return gulp.src("src/js/editormd.js") + .pipe(jsdoc2md()) + .on("error", function(err){ + gutil.log(gutil.colors.red("jsdoc2md failed"), err.message); + }) + .pipe(rename(function(path) { + path.extname = ".md"; + })) + .pipe(gulp.dest("docs/markdown")); +}); +*/ +gulp.task("watch", function() { + gulp.watch("scss/editormd.scss", ["scss"]); + gulp.watch("scss/editormd.preview.scss", ["scss", "scss2"]); + gulp.watch("scss/editormd.logo.scss", ["scss", "scss3"]); + gulp.watch("src/editormd.js", ["js", "amd"]); +}); + +gulp.task("default", function() { + gulp.run("scss"); + gulp.run("scss2"); + gulp.run("scss3"); + gulp.run("js"); + gulp.run("amd"); + gulp.run("cm-addon"); + gulp.run("cm-mode"); +}); \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/css/editormd.css b/paicoding-ui/src/main/resources/static/editormd/css/editormd.css new file mode 100644 index 000000000..31865bae1 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/css/editormd.css @@ -0,0 +1,4488 @@ +/* + * Editor.md + * + * @file editormd.css + * @version v1.5.0 + * @description Open source online markdown editor. + * @license MIT License + * @author Pandao + * {@link https://github.com/pandao/editor.md} + * @updateTime 2015-06-09 + */ + +@charset "UTF-8"; +/*! prefixes.scss v0.1.0 | Author: Pandao | https://github.com/pandao/prefixes.scss | MIT license | Copyright (c) 2015 */ +.editormd { + width: 90%; + height: 640px; + margin: 0 auto; + text-align: left; + overflow: hidden; + position: relative; + margin-bottom: 15px; + border: 1px solid #ddd; + font-family: "Meiryo UI", "Microsoft YaHei", "Malgun Gothic", "Segoe UI", "Trebuchet MS", Helvetica, "Monaco", monospace, Tahoma, STXihei, "华文细黑", STHeiti, "Helvetica Neue", "Droid Sans", "wenquanyi micro hei", FreeSans, Arimo, Arial, SimSun, "宋体", Heiti, "黑体", sans-serif; +} +.editormd *, .editormd *:before, .editormd *:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.editormd a { + text-decoration: none; +} +.editormd img { + border: none; + vertical-align: middle; +} +.editormd > textarea, +.editormd .editormd-html-textarea, +.editormd .editormd-markdown-textarea { + width: 0; + height: 0; + outline: 0; + resize: none; +} +.editormd .editormd-html-textarea, +.editormd .editormd-markdown-textarea { + display: none; +} +.editormd input[type="text"], +.editormd input[type="button"], +.editormd input[type="submit"], +.editormd select, .editormd textarea, .editormd button { + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + appearance: none; +} +.editormd ::-webkit-scrollbar { + height: 10px; + width: 7px; + background: rgba(0, 0, 0, 0.1); +} +.editormd ::-webkit-scrollbar:hover { + background: rgba(0, 0, 0, 0.2); +} +.editormd ::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.3); + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + -ms-border-radius: 6px; + -o-border-radius: 6px; + border-radius: 6px; +} +.editormd ::-webkit-scrollbar-thumb:hover { + -webkit-box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25); + /* Webkit browsers */ + -moz-box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25); + /* Firefox */ + -ms-box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25); + /* IE9 */ + -o-box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25); + /* Opera(Old) */ + box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25); + /* IE9+, News */ + background-color: rgba(0, 0, 0, 0.4); +} + +.editormd-user-unselect { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} + +.editormd-toolbar { + width: 100%; + min-height: 37px; + background: #fff; + display: none; + position: absolute; + top: 0; + left: 0; + z-index: 10; + border-bottom: 1px solid #ddd; +} + +.editormd-toolbar-container { + padding: 0 8px; + min-height: 35px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} + +.editormd-menu { + margin: 0; + padding: 0; + list-style: none; +} +.editormd-menu > li { + margin: 0; + padding: 5px 1px; + display: inline-block; + position: relative; +} +.editormd-menu > li.divider { + display: inline-block; + text-indent: -9999px; + margin: 0 5px; + height: 65%; + border-right: 1px solid #ddd; +} +.editormd-menu > li > a { + outline: 0; + color: #666; + display: inline-block; + min-width: 24px; + font-size: 16px; + text-decoration: none; + text-align: center; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + -ms-border-radius: 2px; + -o-border-radius: 2px; + border-radius: 2px; + border: 1px solid #fff; + -webkit-transition: all 300ms ease-out; + /* Safari, Chrome */ + -moz-transition: all 300ms ease-out; + /* Firefox 4.0~16.0 */ + transition: all 300ms ease-out; + /* IE >9, FF >15, Opera >12.0 */ +} +.editormd-menu > li > a:hover, .editormd-menu > li > a.active { + border: 1px solid #ddd; + background: #eee; +} +.editormd-menu > li > a > .fa { + text-align: center; + display: block; + padding: 5px; +} +.editormd-menu > li > a > .editormd-bold { + padding: 5px 2px; + display: inline-block; + font-weight: bold; +} +.editormd-menu > li:hover .editormd-dropdown-menu { + display: block; +} +.editormd-menu > li + li > a { + margin-left: 3px; +} + +.editormd-dropdown-menu { + display: none; + background: #fff; + border: 1px solid #ddd; + width: 148px; + list-style: none; + position: absolute; + top: 33px; + left: 0; + z-index: 100; + -webkit-box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.15); + /* Webkit browsers */ + -moz-box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.15); + /* Firefox */ + -ms-box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.15); + /* IE9 */ + -o-box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.15); + /* Opera(Old) */ + box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.15); + /* IE9+, News */ +} +.editormd-dropdown-menu:before, .editormd-dropdown-menu:after { + width: 0; + height: 0; + display: block; + content: ""; + position: absolute; + top: -11px; + left: 8px; + border: 5px solid transparent; +} +.editormd-dropdown-menu:before { + border-bottom-color: #ccc; +} +.editormd-dropdown-menu:after { + border-bottom-color: #ffffff; + top: -10px; +} +.editormd-dropdown-menu > li > a { + color: #666; + display: block; + text-decoration: none; + padding: 8px 10px; +} +.editormd-dropdown-menu > li > a:hover { + background: #f6f6f6; + -webkit-transition: all 300ms ease-out; + /* Safari, Chrome */ + -moz-transition: all 300ms ease-out; + /* Firefox 4.0~16.0 */ + transition: all 300ms ease-out; + /* IE >9, FF >15, Opera >12.0 */ +} +.editormd-dropdown-menu > li + li { + border-top: 1px solid #ddd; +} + +.editormd-container { + margin: 0; + width: 100%; + height: 100%; + overflow: hidden; + padding: 35px 0 0; + position: relative; + background: #fff; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.editormd-dialog { + color: #666; + position: fixed; + z-index: 99999; + display: none; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -ms-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + /* Webkit browsers */ + -moz-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + /* Firefox */ + -ms-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + /* IE9 */ + -o-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + /* Opera(Old) */ + box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + /* IE9+, News */ + background: #fff; + font-size: 14px; +} + +.editormd-dialog-container { + position: relative; + padding: 20px; + line-height: 1.4; +} +.editormd-dialog-container h1 { + font-size: 24px; + margin-bottom: 10px; +} +.editormd-dialog-container h1 .fa { + color: #2C7EEA; + padding-right: 5px; +} +.editormd-dialog-container h1 small { + padding-left: 5px; + font-weight: normal; + font-size: 12px; + color: #999; +} +.editormd-dialog-container select { + color: #999; + padding: 3px 8px; + border: 1px solid #ddd; +} + +.editormd-dialog-close { + position: absolute; + top: 12px; + right: 15px; + font-size: 18px; + color: #ccc; + -webkit-transition: color 300ms ease-out; + /* Safari, Chrome */ + -moz-transition: color 300ms ease-out; + /* Firefox 4.0~16.0 */ + transition: color 300ms ease-out; + /* IE >9, FF >15, Opera >12.0 */ +} +.editormd-dialog-close:hover { + color: #999; +} + +.editormd-dialog-header { + padding: 11px 20px; + border-bottom: 1px solid #eee; + -webkit-transition: background 300ms ease-out; + /* Safari, Chrome */ + -moz-transition: background 300ms ease-out; + /* Firefox 4.0~16.0 */ + transition: background 300ms ease-out; + /* IE >9, FF >15, Opera >12.0 */ +} +.editormd-dialog-header:hover { + background: #f6f6f6; +} + +.editormd-dialog-title { + font-size: 14px; +} + +.editormd-dialog-footer { + padding: 10px 0 0 0; + text-align: right; +} + +.editormd-dialog-info { + width: 420px; +} +.editormd-dialog-info h1 { + font-weight: normal; +} +.editormd-dialog-info .editormd-dialog-container { + padding: 20px 25px 25px; +} +.editormd-dialog-info .editormd-dialog-close { + top: 10px; + right: 10px; +} +.editormd-dialog-info p > a, .editormd-dialog-info .hover-link:hover { + color: #2196F3; +} +.editormd-dialog-info .hover-link { + color: #666; +} +.editormd-dialog-info a .fa-external-link { + display: none; +} +.editormd-dialog-info a:hover { + color: #2196F3; +} +.editormd-dialog-info a:hover .fa-external-link { + display: inline-block; +} + +.editormd-mask, +.editormd-container-mask, +.editormd-dialog-mask { + display: none; + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; +} + +.editormd-mask, +.editormd-dialog-mask-bg { + background: #fff; + opacity: 0.5; + filter: alpha(opacity=50); +} + +.editormd-mask { + position: fixed; + background: #000; + opacity: 0.2; + /* W3C */ + filter: alpha(opacity=20); + /* IE */ + z-index: 99998; +} + +.editormd-container-mask, +.editormd-dialog-mask-con { + background: url(../images/loading.gif) no-repeat center center; + -webkit-background-size: 32px 32px; + /* Chrome, iOS, Safari */ + -moz-background-size: 32px 32px; + /* Firefox 3.6~4.0 */ + -o-background-size: 32px 32px; + /* Opera 9.5 */ + background-size: 32px 32px; + /* IE9+, New */ +} + +.editormd-container-mask { + z-index: 20; + display: block; + background-color: #fff; +} + +@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2) { + .editormd-container-mask, + .editormd-dialog-mask-con { + background-image: url(../images/loading@2x.gif); + } +} +@media only screen and (-webkit-min-device-pixel-ratio: 3), only screen and (min-device-pixel-ratio: 3) { + .editormd-container-mask, + .editormd-dialog-mask-con { + background-image: url(../images/loading@3x.gif); + } +} +.editormd-code-block-dialog textarea, +.editormd-preformatted-text-dialog textarea { + width: 100%; + height: 400px; + margin-bottom: 6px; + overflow: auto; + border: 1px solid #eee; + background: #fff; + padding: 15px; + resize: none; +} + +.editormd-code-toolbar { + color: #999; + font-size: 14px; + margin: -5px 0 10px; +} + +.editormd-grid-table { + width: 99%; + display: table; + border: 1px solid #ddd; + border-collapse: collapse; +} + +.editormd-grid-table-row { + width: 100%; + display: table-row; +} +.editormd-grid-table-row a { + font-size: 1.4em; + width: 5%; + height: 36px; + color: #999; + text-align: center; + display: table-cell; + vertical-align: middle; + border: 1px solid #ddd; + text-decoration: none; + -webkit-transition: background-color 300ms ease-out, color 100ms ease-in; + /* Safari, Chrome */ + -moz-transition: background-color 300ms ease-out, color 100ms ease-in; + /* Firefox 4.0~16.0 */ + transition: background-color 300ms ease-out, color 100ms ease-in; + /* IE >9, FF >15, Opera >12.0 */ +} +.editormd-grid-table-row a.selected { + color: #666; + background-color: #eee; +} +.editormd-grid-table-row a:hover { + color: #777; + background-color: #f6f6f6; +} + +.editormd-tab-head { + list-style: none; + border-bottom: 1px solid #ddd; +} +.editormd-tab-head li { + display: inline-block; +} +.editormd-tab-head li a { + color: #999; + display: block; + padding: 6px 12px 5px; + text-align: center; + text-decoration: none; + margin-bottom: -1px; + border: 1px solid #ddd; + -webkit-border-top-left-radius: 3px; + -moz-border-top-left-radius: 3px; + -ms-border-top-left-radius: 3px; + -o-border-top-left-radius: 3px; + border-top-left-radius: 3px; + -webkit-border-top-right-radius: 3px; + -moz-border-top-right-radius: 3px; + -ms-border-top-right-radius: 3px; + -o-border-top-right-radius: 3px; + border-top-right-radius: 3px; + background: #f6f6f6; + -webkit-transition: all 300ms ease-out; + /* Safari, Chrome */ + -moz-transition: all 300ms ease-out; + /* Firefox 4.0~16.0 */ + transition: all 300ms ease-out; + /* IE >9, FF >15, Opera >12.0 */ +} +.editormd-tab-head li a:hover { + color: #666; + background: #eee; +} +.editormd-tab-head li.active a { + color: #666; + background: #fff; + border-bottom-color: #fff; +} +.editormd-tab-head li + li { + margin-left: 3px; +} + +.editormd-tab-box { + padding: 20px 0; +} + +.editormd-form { + color: #666; +} +.editormd-form label { + float: left; + display: block; + width: 75px; + text-align: left; + padding: 7px 0 15px 5px; + margin: 0 0 2px; + font-weight: normal; +} +.editormd-form br { + clear: both; +} +.editormd-form iframe { + display: none; +} +.editormd-form input:focus { + outline: 0; +} +.editormd-form input[type="text"], .editormd-form input[type="number"] { + color: #999; + padding: 8px; + border: 1px solid #ddd; +} +.editormd-form input[type="number"] { + width: 40px; + display: inline-block; + padding: 6px 8px; +} +.editormd-form input[type="text"] { + display: inline-block; + width: 264px; +} +.editormd-form .fa-btns { + display: inline-block; +} +.editormd-form .fa-btns a { + color: #999; + padding: 7px 10px 0 0; + display: inline-block; + text-decoration: none; + text-align: center; +} +.editormd-form .fa-btns .fa { + font-size: 1.3em; +} +.editormd-form .fa-btns label { + float: none; + display: inline-block; + width: auto; + text-align: left; + padding: 0 0 0 5px; + cursor: pointer; +} + +.editormd-form input[type="submit"], .editormd-form .editormd-btn, .editormd-form button, +.editormd-dialog-container input[type="submit"], +.editormd-dialog-container .editormd-btn, +.editormd-dialog-container button, +.editormd-dialog-footer input[type="submit"], +.editormd-dialog-footer .editormd-btn, +.editormd-dialog-footer button { + color: #666; + min-width: 75px; + cursor: pointer; + background: #fff; + padding: 7px 10px; + border: 1px solid #ddd; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -ms-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; + -webkit-transition: background 300ms ease-out; + /* Safari, Chrome */ + -moz-transition: background 300ms ease-out; + /* Firefox 4.0~16.0 */ + transition: background 300ms ease-out; + /* IE >9, FF >15, Opera >12.0 */ +} +.editormd-form input[type="submit"]:hover, .editormd-form .editormd-btn:hover, .editormd-form button:hover, +.editormd-dialog-container input[type="submit"]:hover, +.editormd-dialog-container .editormd-btn:hover, +.editormd-dialog-container button:hover, +.editormd-dialog-footer input[type="submit"]:hover, +.editormd-dialog-footer .editormd-btn:hover, +.editormd-dialog-footer button:hover { + background: var(--pai-brand-7-light); +} +.editormd-form .editormd-btn, +.editormd-dialog-container .editormd-btn, +.editormd-dialog-footer .editormd-btn { + padding: 5px 8px 4px\0; +} +.editormd-form .editormd-btn + .editormd-btn, +.editormd-dialog-container .editormd-btn + .editormd-btn, +.editormd-dialog-footer .editormd-btn + .editormd-btn { + margin-left: 8px; +} + +.editormd-file-input { + width: 75px; + height: 32px; + margin-left: 8px; + position: relative; + display: inline-block; +} +.editormd-file-input input[type="file"] { + width: 75px; + height: 32px; + opacity: 0; + cursor: pointer; + background: #000; + display: inline-block; + position: absolute; + top: 0; + right: 0; +} +.editormd-file-input input[type="file"]::-webkit-file-upload-button { + visibility: hidden; +} +.editormd-file-input:hover input[type="submit"] { + background: var(--pai-brand-7-light); +} + +.editormd .CodeMirror, .editormd-preview { + display: inline-block; + width: 50%; + height: 100%; + vertical-align: top; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + margin: 0; +} + +.editormd-preview { + position: absolute; + top: 35px; + right: 0; + right: -1px\0; + overflow: auto; + line-height: 1.6; + display: none; + background: #fff; +} + +.editormd .CodeMirror { + z-index: 10; + float: left; + border-right: 1px solid #ddd; + font-size: 14px; + font-family: "YaHei Consolas Hybrid", Consolas, "微软雅黑", "Meiryo UI", "Malgun Gothic", "Segoe UI", "Trebuchet MS", Helvetica, "Monaco", courier, monospace; + line-height: 1.6; + margin-top: 35px; +} +.editormd .CodeMirror pre { + font-size: 1.2em; + padding: 0 12px; +} +.editormd .CodeMirror-linenumbers { + padding: 0 5px; +} +.editormd .CodeMirror-selected { + background: #70B7FF; +} +.editormd .CodeMirror-focused .CodeMirror-selected { + background: #70B7FF; +} +.editormd .CodeMirror, .editormd .CodeMirror-scroll, .editormd .editormd-preview { + -webkit-overflow-scrolling: touch; +} +.editormd .styled-background { + background-color: #ff7; +} +.editormd .CodeMirror-focused .cm-matchhighlight { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==); + background-position: bottom; + background-repeat: repeat-x; +} +.editormd .CodeMirror-empty.CodeMirror-focused { + outline: none; +} +.editormd .CodeMirror pre.CodeMirror-placeholder { + color: #999; +} +.editormd .cm-trailingspace { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QUXCToH00Y1UgAAACFJREFUCNdjPMDBUc/AwNDAAAFMTAwMDA0OP34wQgX/AQBYgwYEx4f9lQAAAABJRU5ErkJggg==); + background-position: bottom left; + background-repeat: repeat-x; +} +.editormd .cm-tab { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAYAAAAkuj5RAAAAAXNSR0IArs4c6QAAAGFJREFUSMft1LsRQFAQheHPowAKoACx3IgEKtaEHujDjORSgWTH/ZOdnZOcM/sgk/kFFWY0qV8foQwS4MKBCS3qR6ixBJvElOobYAtivseIE120FaowJPN75GMu8j/LfMwNjh4HUpwg4LUAAAAASUVORK5CYII=); + background-position: right; + background-repeat: no-repeat; +} + +/*! prefixes.scss v0.1.0 | Author: Pandao | https://github.com/pandao/prefixes.scss | MIT license | Copyright (c) 2015 */ +/*! + * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: 'FontAwesome'; + src: url("../fonts/fontawesome-webfont.eot?v=4.3.0"); + src: url("../fonts/fontawesome-webfont.eot?#iefix&v=4.3.0") format("embedded-opentype"), url("../fonts/fontawesome-webfont.woff2?v=4.3.0") format("woff2"), url("../fonts/fontawesome-webfont.woff?v=4.3.0") format("woff"), url("../fonts/fontawesome-webfont.ttf?v=4.3.0") format("truetype"), url("../fonts/fontawesome-webfont.svg?v=4.3.0#fontawesomeregular") format("svg"); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + transform: translate(0, 0); +} + +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.33333333em; + line-height: 0.75em; + vertical-align: -15%; +} + +.fa-2x { + font-size: 2em; +} + +.fa-3x { + font-size: 3em; +} + +.fa-4x { + font-size: 4em; +} + +.fa-5x { + font-size: 5em; +} + +.fa-fw { + width: 1.28571429em; + text-align: center; +} + +.fa-ul { + padding-left: 0; + margin-left: 2.14285714em; + list-style-type: none; +} + +.fa-ul > li { + position: relative; +} + +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} + +.fa-li.fa-lg { + left: -1.85714286em; +} + +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} + +.pull-right { + float: right; +} + +.pull-left { + float: left; +} + +.fa.pull-left { + margin-right: .3em; +} + +.fa.pull-right { + margin-left: .3em; +} + +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} + +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} + +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} + +.fa-rotate-180 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} + +.fa-rotate-270 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} + +.fa-flip-horizontal { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} + +.fa-flip-vertical { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} + +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} + +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} + +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} + +.fa-stack-1x { + line-height: inherit; +} + +.fa-stack-2x { + font-size: 2em; +} + +.fa-inverse { + color: #ffffff; +} + +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} + +.fa-music:before { + content: "\f001"; +} + +.fa-search:before { + content: "\f002"; +} + +.fa-envelope-o:before { + content: "\f003"; +} + +.fa-heart:before { + content: "\f004"; +} + +.fa-star:before { + content: "\f005"; +} + +.fa-star-o:before { + content: "\f006"; +} + +.fa-user:before { + content: "\f007"; +} + +.fa-film:before { + content: "\f008"; +} + +.fa-th-large:before { + content: "\f009"; +} + +.fa-th:before { + content: "\f00a"; +} + +.fa-th-list:before { + content: "\f00b"; +} + +.fa-check:before { + content: "\f00c"; +} + +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "\f00d"; +} + +.fa-search-plus:before { + content: "\f00e"; +} + +.fa-search-minus:before { + content: "\f010"; +} + +.fa-power-off:before { + content: "\f011"; +} + +.fa-signal:before { + content: "\f012"; +} + +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} + +.fa-trash-o:before { + content: "\f014"; +} + +.fa-home:before { + content: "\f015"; +} + +.fa-file-o:before { + content: "\f016"; +} + +.fa-clock-o:before { + content: "\f017"; +} + +.fa-road:before { + content: "\f018"; +} + +.fa-download:before { + content: "\f019"; +} + +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} + +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} + +.fa-inbox:before { + content: "\f01c"; +} + +.fa-play-circle-o:before { + content: "\f01d"; +} + +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} + +.fa-refresh:before { + content: "\f021"; +} + +.fa-list-alt:before { + content: "\f022"; +} + +.fa-lock:before { + content: "\f023"; +} + +.fa-flag:before { + content: "\f024"; +} + +.fa-headphones:before { + content: "\f025"; +} + +.fa-volume-off:before { + content: "\f026"; +} + +.fa-volume-down:before { + content: "\f027"; +} + +.fa-volume-up:before { + content: "\f028"; +} + +.fa-qrcode:before { + content: "\f029"; +} + +.fa-barcode:before { + content: "\f02a"; +} + +.fa-tag:before { + content: "\f02b"; +} + +.fa-tags:before { + content: "\f02c"; +} + +.fa-book:before { + content: "\f02d"; +} + +.fa-bookmark:before { + content: "\f02e"; +} + +.fa-print:before { + content: "\f02f"; +} + +.fa-camera:before { + content: "\f030"; +} + +.fa-font:before { + content: "\f031"; +} + +.fa-bold:before { + content: "\f032"; +} + +.fa-italic:before { + content: "\f033"; +} + +.fa-text-height:before { + content: "\f034"; +} + +.fa-text-width:before { + content: "\f035"; +} + +.fa-align-left:before { + content: "\f036"; +} + +.fa-align-center:before { + content: "\f037"; +} + +.fa-align-right:before { + content: "\f038"; +} + +.fa-align-justify:before { + content: "\f039"; +} + +.fa-list:before { + content: "\f03a"; +} + +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} + +.fa-indent:before { + content: "\f03c"; +} + +.fa-video-camera:before { + content: "\f03d"; +} + +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} + +.fa-pencil:before { + content: "\f040"; +} + +.fa-map-marker:before { + content: "\f041"; +} + +.fa-adjust:before { + content: "\f042"; +} + +.fa-tint:before { + content: "\f043"; +} + +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} + +.fa-share-square-o:before { + content: "\f045"; +} + +.fa-check-square-o:before { + content: "\f046"; +} + +.fa-arrows:before { + content: "\f047"; +} + +.fa-step-backward:before { + content: "\f048"; +} + +.fa-fast-backward:before { + content: "\f049"; +} + +.fa-backward:before { + content: "\f04a"; +} + +.fa-play:before { + content: "\f04b"; +} + +.fa-pause:before { + content: "\f04c"; +} + +.fa-stop:before { + content: "\f04d"; +} + +.fa-forward:before { + content: "\f04e"; +} + +.fa-fast-forward:before { + content: "\f050"; +} + +.fa-step-forward:before { + content: "\f051"; +} + +.fa-eject:before { + content: "\f052"; +} + +.fa-chevron-left:before { + content: "\f053"; +} + +.fa-chevron-right:before { + content: "\f054"; +} + +.fa-plus-circle:before { + content: "\f055"; +} + +.fa-minus-circle:before { + content: "\f056"; +} + +.fa-times-circle:before { + content: "\f057"; +} + +.fa-check-circle:before { + content: "\f058"; +} + +.fa-question-circle:before { + content: "\f059"; +} + +.fa-info-circle:before { + content: "\f05a"; +} + +.fa-crosshairs:before { + content: "\f05b"; +} + +.fa-times-circle-o:before { + content: "\f05c"; +} + +.fa-check-circle-o:before { + content: "\f05d"; +} + +.fa-ban:before { + content: "\f05e"; +} + +.fa-arrow-left:before { + content: "\f060"; +} + +.fa-arrow-right:before { + content: "\f061"; +} + +.fa-arrow-up:before { + content: "\f062"; +} + +.fa-arrow-down:before { + content: "\f063"; +} + +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} + +.fa-expand:before { + content: "\f065"; +} + +.fa-compress:before { + content: "\f066"; +} + +.fa-plus:before { + content: "\f067"; +} + +.fa-minus:before { + content: "\f068"; +} + +.fa-asterisk:before { + content: "\f069"; +} + +.fa-exclamation-circle:before { + content: "\f06a"; +} + +.fa-gift:before { + content: "\f06b"; +} + +.fa-leaf:before { + content: "\f06c"; +} + +.fa-fire:before { + content: "\f06d"; +} + +.fa-eye:before { + content: "\f06e"; +} + +.fa-eye-slash:before { + content: "\f070"; +} + +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} + +.fa-plane:before { + content: "\f072"; +} + +.fa-calendar:before { + content: "\f073"; +} + +.fa-random:before { + content: "\f074"; +} + +.fa-comment:before { + content: "\f075"; +} + +.fa-magnet:before { + content: "\f076"; +} + +.fa-chevron-up:before { + content: "\f077"; +} + +.fa-chevron-down:before { + content: "\f078"; +} + +.fa-retweet:before { + content: "\f079"; +} + +.fa-shopping-cart:before { + content: "\f07a"; +} + +.fa-folder:before { + content: "\f07b"; +} + +.fa-folder-open:before { + content: "\f07c"; +} + +.fa-arrows-v:before { + content: "\f07d"; +} + +.fa-arrows-h:before { + content: "\f07e"; +} + +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "\f080"; +} + +.fa-twitter-square:before { + content: "\f081"; +} + +.fa-facebook-square:before { + content: "\f082"; +} + +.fa-camera-retro:before { + content: "\f083"; +} + +.fa-key:before { + content: "\f084"; +} + +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} + +.fa-comments:before { + content: "\f086"; +} + +.fa-thumbs-o-up:before { + content: "\f087"; +} + +.fa-thumbs-o-down:before { + content: "\f088"; +} + +.fa-star-half:before { + content: "\f089"; +} + +.fa-heart-o:before { + content: "\f08a"; +} + +.fa-sign-out:before { + content: "\f08b"; +} + +.fa-linkedin-square:before { + content: "\f08c"; +} + +.fa-thumb-tack:before { + content: "\f08d"; +} + +.fa-external-link:before { + content: "\f08e"; +} + +.fa-sign-in:before { + content: "\f090"; +} + +.fa-trophy:before { + content: "\f091"; +} + +.fa-github-square:before { + content: "\f092"; +} + +.fa-upload:before { + content: "\f093"; +} + +.fa-lemon-o:before { + content: "\f094"; +} + +.fa-phone:before { + content: "\f095"; +} + +.fa-square-o:before { + content: "\f096"; +} + +.fa-bookmark-o:before { + content: "\f097"; +} + +.fa-phone-square:before { + content: "\f098"; +} + +.fa-twitter:before { + content: "\f099"; +} + +.fa-facebook-f:before, +.fa-facebook:before { + content: "\f09a"; +} + +.fa-github:before { + content: "\f09b"; +} + +.fa-unlock:before { + content: "\f09c"; +} + +.fa-credit-card:before { + content: "\f09d"; +} + +.fa-rss:before { + content: "\f09e"; +} + +.fa-hdd-o:before { + content: "\f0a0"; +} + +.fa-bullhorn:before { + content: "\f0a1"; +} + +.fa-bell:before { + content: "\f0f3"; +} + +.fa-certificate:before { + content: "\f0a3"; +} + +.fa-hand-o-right:before { + content: "\f0a4"; +} + +.fa-hand-o-left:before { + content: "\f0a5"; +} + +.fa-hand-o-up:before { + content: "\f0a6"; +} + +.fa-hand-o-down:before { + content: "\f0a7"; +} + +.fa-arrow-circle-left:before { + content: "\f0a8"; +} + +.fa-arrow-circle-right:before { + content: "\f0a9"; +} + +.fa-arrow-circle-up:before { + content: "\f0aa"; +} + +.fa-arrow-circle-down:before { + content: "\f0ab"; +} + +.fa-globe:before { + content: "\f0ac"; +} + +.fa-wrench:before { + content: "\f0ad"; +} + +.fa-tasks:before { + content: "\f0ae"; +} + +.fa-filter:before { + content: "\f0b0"; +} + +.fa-briefcase:before { + content: "\f0b1"; +} + +.fa-arrows-alt:before { + content: "\f0b2"; +} + +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} + +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} + +.fa-cloud:before { + content: "\f0c2"; +} + +.fa-flask:before { + content: "\f0c3"; +} + +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} + +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} + +.fa-paperclip:before { + content: "\f0c6"; +} + +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} + +.fa-square:before { + content: "\f0c8"; +} + +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} + +.fa-list-ul:before { + content: "\f0ca"; +} + +.fa-list-ol:before { + content: "\f0cb"; +} + +.fa-strikethrough:before { + content: "\f0cc"; +} + +.fa-underline:before { + content: "\f0cd"; +} + +.fa-table:before { + content: "\f0ce"; +} + +.fa-magic:before { + content: "\f0d0"; +} + +.fa-truck:before { + content: "\f0d1"; +} + +.fa-pinterest:before { + content: "\f0d2"; +} + +.fa-pinterest-square:before { + content: "\f0d3"; +} + +.fa-google-plus-square:before { + content: "\f0d4"; +} + +.fa-google-plus:before { + content: "\f0d5"; +} + +.fa-money:before { + content: "\f0d6"; +} + +.fa-caret-down:before { + content: "\f0d7"; +} + +.fa-caret-up:before { + content: "\f0d8"; +} + +.fa-caret-left:before { + content: "\f0d9"; +} + +.fa-caret-right:before { + content: "\f0da"; +} + +.fa-columns:before { + content: "\f0db"; +} + +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} + +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} + +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} + +.fa-envelope:before { + content: "\f0e0"; +} + +.fa-linkedin:before { + content: "\f0e1"; +} + +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} + +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} + +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} + +.fa-comment-o:before { + content: "\f0e5"; +} + +.fa-comments-o:before { + content: "\f0e6"; +} + +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} + +.fa-sitemap:before { + content: "\f0e8"; +} + +.fa-umbrella:before { + content: "\f0e9"; +} + +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} + +.fa-lightbulb-o:before { + content: "\f0eb"; +} + +.fa-exchange:before { + content: "\f0ec"; +} + +.fa-cloud-download:before { + content: "\f0ed"; +} + +.fa-cloud-upload:before { + content: "\f0ee"; +} + +.fa-user-md:before { + content: "\f0f0"; +} + +.fa-stethoscope:before { + content: "\f0f1"; +} + +.fa-suitcase:before { + content: "\f0f2"; +} + +.fa-bell-o:before { + content: "\f0a2"; +} + +.fa-coffee:before { + content: "\f0f4"; +} + +.fa-cutlery:before { + content: "\f0f5"; +} + +.fa-file-text-o:before { + content: "\f0f6"; +} + +.fa-building-o:before { + content: "\f0f7"; +} + +.fa-hospital-o:before { + content: "\f0f8"; +} + +.fa-ambulance:before { + content: "\f0f9"; +} + +.fa-medkit:before { + content: "\f0fa"; +} + +.fa-fighter-jet:before { + content: "\f0fb"; +} + +.fa-beer:before { + content: "\f0fc"; +} + +.fa-h-square:before { + content: "\f0fd"; +} + +.fa-plus-square:before { + content: "\f0fe"; +} + +.fa-angle-double-left:before { + content: "\f100"; +} + +.fa-angle-double-right:before { + content: "\f101"; +} + +.fa-angle-double-up:before { + content: "\f102"; +} + +.fa-angle-double-down:before { + content: "\f103"; +} + +.fa-angle-left:before { + content: "\f104"; +} + +.fa-angle-right:before { + content: "\f105"; +} + +.fa-angle-up:before { + content: "\f106"; +} + +.fa-angle-down:before { + content: "\f107"; +} + +.fa-desktop:before { + content: "\f108"; +} + +.fa-laptop:before { + content: "\f109"; +} + +.fa-tablet:before { + content: "\f10a"; +} + +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} + +.fa-circle-o:before { + content: "\f10c"; +} + +.fa-quote-left:before { + content: "\f10d"; +} + +.fa-quote-right:before { + content: "\f10e"; +} + +.fa-spinner:before { + content: "\f110"; +} + +.fa-circle:before { + content: "\f111"; +} + +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} + +.fa-github-alt:before { + content: "\f113"; +} + +.fa-folder-o:before { + content: "\f114"; +} + +.fa-folder-open-o:before { + content: "\f115"; +} + +.fa-smile-o:before { + content: "\f118"; +} + +.fa-frown-o:before { + content: "\f119"; +} + +.fa-meh-o:before { + content: "\f11a"; +} + +.fa-gamepad:before { + content: "\f11b"; +} + +.fa-keyboard-o:before { + content: "\f11c"; +} + +.fa-flag-o:before { + content: "\f11d"; +} + +.fa-flag-checkered:before { + content: "\f11e"; +} + +.fa-terminal:before { + content: "\f120"; +} + +.fa-code:before { + content: "\f121"; +} + +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} + +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} + +.fa-location-arrow:before { + content: "\f124"; +} + +.fa-crop:before { + content: "\f125"; +} + +.fa-code-fork:before { + content: "\f126"; +} + +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} + +.fa-question:before { + content: "\f128"; +} + +.fa-info:before { + content: "\f129"; +} + +.fa-exclamation:before { + content: "\f12a"; +} + +.fa-superscript:before { + content: "\f12b"; +} + +.fa-subscript:before { + content: "\f12c"; +} + +.fa-eraser:before { + content: "\f12d"; +} + +.fa-puzzle-piece:before { + content: "\f12e"; +} + +.fa-microphone:before { + content: "\f130"; +} + +.fa-microphone-slash:before { + content: "\f131"; +} + +.fa-shield:before { + content: "\f132"; +} + +.fa-calendar-o:before { + content: "\f133"; +} + +.fa-fire-extinguisher:before { + content: "\f134"; +} + +.fa-rocket:before { + content: "\f135"; +} + +.fa-maxcdn:before { + content: "\f136"; +} + +.fa-chevron-circle-left:before { + content: "\f137"; +} + +.fa-chevron-circle-right:before { + content: "\f138"; +} + +.fa-chevron-circle-up:before { + content: "\f139"; +} + +.fa-chevron-circle-down:before { + content: "\f13a"; +} + +.fa-html5:before { + content: "\f13b"; +} + +.fa-css3:before { + content: "\f13c"; +} + +.fa-anchor:before { + content: "\f13d"; +} + +.fa-unlock-alt:before { + content: "\f13e"; +} + +.fa-bullseye:before { + content: "\f140"; +} + +.fa-ellipsis-h:before { + content: "\f141"; +} + +.fa-ellipsis-v:before { + content: "\f142"; +} + +.fa-rss-square:before { + content: "\f143"; +} + +.fa-play-circle:before { + content: "\f144"; +} + +.fa-ticket:before { + content: "\f145"; +} + +.fa-minus-square:before { + content: "\f146"; +} + +.fa-minus-square-o:before { + content: "\f147"; +} + +.fa-level-up:before { + content: "\f148"; +} + +.fa-level-down:before { + content: "\f149"; +} + +.fa-check-square:before { + content: "\f14a"; +} + +.fa-pencil-square:before { + content: "\f14b"; +} + +.fa-external-link-square:before { + content: "\f14c"; +} + +.fa-share-square:before { + content: "\f14d"; +} + +.fa-compass:before { + content: "\f14e"; +} + +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} + +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} + +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} + +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} + +.fa-gbp:before { + content: "\f154"; +} + +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} + +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} + +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} + +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} + +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} + +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} + +.fa-file:before { + content: "\f15b"; +} + +.fa-file-text:before { + content: "\f15c"; +} + +.fa-sort-alpha-asc:before { + content: "\f15d"; +} + +.fa-sort-alpha-desc:before { + content: "\f15e"; +} + +.fa-sort-amount-asc:before { + content: "\f160"; +} + +.fa-sort-amount-desc:before { + content: "\f161"; +} + +.fa-sort-numeric-asc:before { + content: "\f162"; +} + +.fa-sort-numeric-desc:before { + content: "\f163"; +} + +.fa-thumbs-up:before { + content: "\f164"; +} + +.fa-thumbs-down:before { + content: "\f165"; +} + +.fa-youtube-square:before { + content: "\f166"; +} + +.fa-youtube:before { + content: "\f167"; +} + +.fa-xing:before { + content: "\f168"; +} + +.fa-xing-square:before { + content: "\f169"; +} + +.fa-youtube-play:before { + content: "\f16a"; +} + +.fa-dropbox:before { + content: "\f16b"; +} + +.fa-stack-overflow:before { + content: "\f16c"; +} + +.fa-instagram:before { + content: "\f16d"; +} + +.fa-flickr:before { + content: "\f16e"; +} + +.fa-adn:before { + content: "\f170"; +} + +.fa-bitbucket:before { + content: "\f171"; +} + +.fa-bitbucket-square:before { + content: "\f172"; +} + +.fa-tumblr:before { + content: "\f173"; +} + +.fa-tumblr-square:before { + content: "\f174"; +} + +.fa-long-arrow-down:before { + content: "\f175"; +} + +.fa-long-arrow-up:before { + content: "\f176"; +} + +.fa-long-arrow-left:before { + content: "\f177"; +} + +.fa-long-arrow-right:before { + content: "\f178"; +} + +.fa-apple:before { + content: "\f179"; +} + +.fa-windows:before { + content: "\f17a"; +} + +.fa-android:before { + content: "\f17b"; +} + +.fa-linux:before { + content: "\f17c"; +} + +.fa-dribbble:before { + content: "\f17d"; +} + +.fa-skype:before { + content: "\f17e"; +} + +.fa-foursquare:before { + content: "\f180"; +} + +.fa-trello:before { + content: "\f181"; +} + +.fa-female:before { + content: "\f182"; +} + +.fa-male:before { + content: "\f183"; +} + +.fa-gittip:before, +.fa-gratipay:before { + content: "\f184"; +} + +.fa-sun-o:before { + content: "\f185"; +} + +.fa-moon-o:before { + content: "\f186"; +} + +.fa-archive:before { + content: "\f187"; +} + +.fa-bug:before { + content: "\f188"; +} + +.fa-vk:before { + content: "\f189"; +} + +.fa-weibo:before { + content: "\f18a"; +} + +.fa-renren:before { + content: "\f18b"; +} + +.fa-pagelines:before { + content: "\f18c"; +} + +.fa-stack-exchange:before { + content: "\f18d"; +} + +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} + +.fa-arrow-circle-o-left:before { + content: "\f190"; +} + +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} + +.fa-dot-circle-o:before { + content: "\f192"; +} + +.fa-wheelchair:before { + content: "\f193"; +} + +.fa-vimeo-square:before { + content: "\f194"; +} + +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} + +.fa-plus-square-o:before { + content: "\f196"; +} + +.fa-space-shuttle:before { + content: "\f197"; +} + +.fa-slack:before { + content: "\f198"; +} + +.fa-envelope-square:before { + content: "\f199"; +} + +.fa-wordpress:before { + content: "\f19a"; +} + +.fa-openid:before { + content: "\f19b"; +} + +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} + +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} + +.fa-yahoo:before { + content: "\f19e"; +} + +.fa-google:before { + content: "\f1a0"; +} + +.fa-reddit:before { + content: "\f1a1"; +} + +.fa-reddit-square:before { + content: "\f1a2"; +} + +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} + +.fa-stumbleupon:before { + content: "\f1a4"; +} + +.fa-delicious:before { + content: "\f1a5"; +} + +.fa-digg:before { + content: "\f1a6"; +} + +.fa-pied-piper:before { + content: "\f1a7"; +} + +.fa-pied-piper-alt:before { + content: "\f1a8"; +} + +.fa-drupal:before { + content: "\f1a9"; +} + +.fa-joomla:before { + content: "\f1aa"; +} + +.fa-language:before { + content: "\f1ab"; +} + +.fa-fax:before { + content: "\f1ac"; +} + +.fa-building:before { + content: "\f1ad"; +} + +.fa-child:before { + content: "\f1ae"; +} + +.fa-paw:before { + content: "\f1b0"; +} + +.fa-spoon:before { + content: "\f1b1"; +} + +.fa-cube:before { + content: "\f1b2"; +} + +.fa-cubes:before { + content: "\f1b3"; +} + +.fa-behance:before { + content: "\f1b4"; +} + +.fa-behance-square:before { + content: "\f1b5"; +} + +.fa-steam:before { + content: "\f1b6"; +} + +.fa-steam-square:before { + content: "\f1b7"; +} + +.fa-recycle:before { + content: "\f1b8"; +} + +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} + +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} + +.fa-tree:before { + content: "\f1bb"; +} + +.fa-spotify:before { + content: "\f1bc"; +} + +.fa-deviantart:before { + content: "\f1bd"; +} + +.fa-soundcloud:before { + content: "\f1be"; +} + +.fa-database:before { + content: "\f1c0"; +} + +.fa-file-pdf-o:before { + content: "\f1c1"; +} + +.fa-file-word-o:before { + content: "\f1c2"; +} + +.fa-file-excel-o:before { + content: "\f1c3"; +} + +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} + +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} + +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} + +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} + +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} + +.fa-file-code-o:before { + content: "\f1c9"; +} + +.fa-vine:before { + content: "\f1ca"; +} + +.fa-codepen:before { + content: "\f1cb"; +} + +.fa-jsfiddle:before { + content: "\f1cc"; +} + +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} + +.fa-circle-o-notch:before { + content: "\f1ce"; +} + +.fa-ra:before, +.fa-rebel:before { + content: "\f1d0"; +} + +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} + +.fa-git-square:before { + content: "\f1d2"; +} + +.fa-git:before { + content: "\f1d3"; +} + +.fa-hacker-news:before { + content: "\f1d4"; +} + +.fa-tencent-weibo:before { + content: "\f1d5"; +} + +.fa-qq:before { + content: "\f1d6"; +} + +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} + +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} + +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} + +.fa-history:before { + content: "\f1da"; +} + +.fa-genderless:before, +.fa-circle-thin:before { + content: "\f1db"; +} + +.fa-header:before { + content: "\f1dc"; +} + +.fa-paragraph:before { + content: "\f1dd"; +} + +.fa-sliders:before { + content: "\f1de"; +} + +.fa-share-alt:before { + content: "\f1e0"; +} + +.fa-share-alt-square:before { + content: "\f1e1"; +} + +.fa-bomb:before { + content: "\f1e2"; +} + +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "\f1e3"; +} + +.fa-tty:before { + content: "\f1e4"; +} + +.fa-binoculars:before { + content: "\f1e5"; +} + +.fa-plug:before { + content: "\f1e6"; +} + +.fa-slideshare:before { + content: "\f1e7"; +} + +.fa-twitch:before { + content: "\f1e8"; +} + +.fa-yelp:before { + content: "\f1e9"; +} + +.fa-newspaper-o:before { + content: "\f1ea"; +} + +.fa-wifi:before { + content: "\f1eb"; +} + +.fa-calculator:before { + content: "\f1ec"; +} + +.fa-paypal:before { + content: "\f1ed"; +} + +.fa-google-wallet:before { + content: "\f1ee"; +} + +.fa-cc-visa:before { + content: "\f1f0"; +} + +.fa-cc-mastercard:before { + content: "\f1f1"; +} + +.fa-cc-discover:before { + content: "\f1f2"; +} + +.fa-cc-amex:before { + content: "\f1f3"; +} + +.fa-cc-paypal:before { + content: "\f1f4"; +} + +.fa-cc-stripe:before { + content: "\f1f5"; +} + +.fa-bell-slash:before { + content: "\f1f6"; +} + +.fa-bell-slash-o:before { + content: "\f1f7"; +} + +.fa-trash:before { + content: "\f1f8"; +} + +.fa-copyright:before { + content: "\f1f9"; +} + +.fa-at:before { + content: "\f1fa"; +} + +.fa-eyedropper:before { + content: "\f1fb"; +} + +.fa-paint-brush:before { + content: "\f1fc"; +} + +.fa-birthday-cake:before { + content: "\f1fd"; +} + +.fa-area-chart:before { + content: "\f1fe"; +} + +.fa-pie-chart:before { + content: "\f200"; +} + +.fa-line-chart:before { + content: "\f201"; +} + +.fa-lastfm:before { + content: "\f202"; +} + +.fa-lastfm-square:before { + content: "\f203"; +} + +.fa-toggle-off:before { + content: "\f204"; +} + +.fa-toggle-on:before { + content: "\f205"; +} + +.fa-bicycle:before { + content: "\f206"; +} + +.fa-bus:before { + content: "\f207"; +} + +.fa-ioxhost:before { + content: "\f208"; +} + +.fa-angellist:before { + content: "\f209"; +} + +.fa-cc:before { + content: "\f20a"; +} + +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "\f20b"; +} + +.fa-meanpath:before { + content: "\f20c"; +} + +.fa-buysellads:before { + content: "\f20d"; +} + +.fa-connectdevelop:before { + content: "\f20e"; +} + +.fa-dashcube:before { + content: "\f210"; +} + +.fa-forumbee:before { + content: "\f211"; +} + +.fa-leanpub:before { + content: "\f212"; +} + +.fa-sellsy:before { + content: "\f213"; +} + +.fa-shirtsinbulk:before { + content: "\f214"; +} + +.fa-simplybuilt:before { + content: "\f215"; +} + +.fa-skyatlas:before { + content: "\f216"; +} + +.fa-cart-plus:before { + content: "\f217"; +} + +.fa-cart-arrow-down:before { + content: "\f218"; +} + +.fa-diamond:before { + content: "\f219"; +} + +.fa-ship:before { + content: "\f21a"; +} + +.fa-user-secret:before { + content: "\f21b"; +} + +.fa-motorcycle:before { + content: "\f21c"; +} + +.fa-street-view:before { + content: "\f21d"; +} + +.fa-heartbeat:before { + content: "\f21e"; +} + +.fa-venus:before { + content: "\f221"; +} + +.fa-mars:before { + content: "\f222"; +} + +.fa-mercury:before { + content: "\f223"; +} + +.fa-transgender:before { + content: "\f224"; +} + +.fa-transgender-alt:before { + content: "\f225"; +} + +.fa-venus-double:before { + content: "\f226"; +} + +.fa-mars-double:before { + content: "\f227"; +} + +.fa-venus-mars:before { + content: "\f228"; +} + +.fa-mars-stroke:before { + content: "\f229"; +} + +.fa-mars-stroke-v:before { + content: "\f22a"; +} + +.fa-mars-stroke-h:before { + content: "\f22b"; +} + +.fa-neuter:before { + content: "\f22c"; +} + +.fa-facebook-official:before { + content: "\f230"; +} + +.fa-pinterest-p:before { + content: "\f231"; +} + +.fa-whatsapp:before { + content: "\f232"; +} + +.fa-server:before { + content: "\f233"; +} + +.fa-user-plus:before { + content: "\f234"; +} + +.fa-user-times:before { + content: "\f235"; +} + +.fa-hotel:before, +.fa-bed:before { + content: "\f236"; +} + +.fa-viacoin:before { + content: "\f237"; +} + +.fa-train:before { + content: "\f238"; +} + +.fa-subway:before { + content: "\f239"; +} + +.fa-medium:before { + content: "\f23a"; +} + +/*! prefixes.scss v0.1.0 | Author: Pandao | https://github.com/pandao/prefixes.scss | MIT license | Copyright (c) 2015 */ +@font-face { + font-family: 'editormd-logo'; + src: url("../fonts/editormd-logo.eot?-5y8q6h"); + src: url("../fonts/editormd-logo.eot?#iefix-5y8q6h") format("embedded-opentype"), url("../fonts/editormd-logo.woff?-5y8q6h") format("woff"), url("../fonts/editormd-logo.ttf?-5y8q6h") format("truetype"), url("../fonts/editormd-logo.svg?-5y8q6h#icomoon") format("svg"); + font-weight: normal; + font-style: normal; +} +.editormd-logo, +.editormd-logo-1x, +.editormd-logo-2x, +.editormd-logo-3x, +.editormd-logo-4x, +.editormd-logo-5x, +.editormd-logo-6x, +.editormd-logo-7x, +.editormd-logo-8x { + font-family: 'editormd-logo'; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + font-size: inherit; + line-height: 1; + display: inline-block; + text-rendering: auto; + vertical-align: inherit; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.editormd-logo:before, +.editormd-logo-1x:before, +.editormd-logo-2x:before, +.editormd-logo-3x:before, +.editormd-logo-4x:before, +.editormd-logo-5x:before, +.editormd-logo-6x:before, +.editormd-logo-7x:before, +.editormd-logo-8x:before { + content: "\e1987"; + /* + HTML Entity 󡦇 + example: + */ +} + +.editormd-logo-1x { + font-size: 1em; +} + +.editormd-logo-lg { + font-size: 1.2em; +} + +.editormd-logo-2x { + font-size: 2em; +} + +.editormd-logo-3x { + font-size: 3em; +} + +.editormd-logo-4x { + font-size: 4em; +} + +.editormd-logo-5x { + font-size: 5em; +} + +.editormd-logo-6x { + font-size: 6em; +} + +.editormd-logo-7x { + font-size: 7em; +} + +.editormd-logo-8x { + font-size: 8em; +} + +.editormd-logo-color { + color: #2196F3; +} + +/*! github-markdown-css | The MIT License (MIT) | Copyright (c) Sindre Sorhus (sindresorhus.com) | https://github.com/sindresorhus/github-markdown-css */ +@font-face { + font-family: octicons-anchor; + src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAYcAA0AAAAACjQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABMAAAABwAAAAca8vGTk9TLzIAAAFMAAAARAAAAFZG1VHVY21hcAAAAZAAAAA+AAABQgAP9AdjdnQgAAAB0AAAAAQAAAAEACICiGdhc3AAAAHUAAAACAAAAAj//wADZ2x5ZgAAAdwAAADRAAABEKyikaNoZWFkAAACsAAAAC0AAAA2AtXoA2hoZWEAAALgAAAAHAAAACQHngNFaG10eAAAAvwAAAAQAAAAEAwAACJsb2NhAAADDAAAAAoAAAAKALIAVG1heHAAAAMYAAAAHwAAACABEAB2bmFtZQAAAzgAAALBAAAFu3I9x/Nwb3N0AAAF/AAAAB0AAAAvaoFvbwAAAAEAAAAAzBdyYwAAAADP2IQvAAAAAM/bz7t4nGNgZGFgnMDAysDB1Ml0hoGBoR9CM75mMGLkYGBgYmBlZsAKAtJcUxgcPsR8iGF2+O/AEMPsznAYKMwIkgMA5REMOXicY2BgYGaAYBkGRgYQsAHyGMF8FgYFIM0ChED+h5j//yEk/3KoSgZGNgYYk4GRCUgwMaACRoZhDwCs7QgGAAAAIgKIAAAAAf//AAJ4nHWMMQrCQBBF/0zWrCCIKUQsTDCL2EXMohYGSSmorScInsRGL2DOYJe0Ntp7BK+gJ1BxF1stZvjz/v8DRghQzEc4kIgKwiAppcA9LtzKLSkdNhKFY3HF4lK69ExKslx7Xa+vPRVS43G98vG1DnkDMIBUgFN0MDXflU8tbaZOUkXUH0+U27RoRpOIyCKjbMCVejwypzJJG4jIwb43rfl6wbwanocrJm9XFYfskuVC5K/TPyczNU7b84CXcbxks1Un6H6tLH9vf2LRnn8Ax7A5WQAAAHicY2BkYGAA4teL1+yI57f5ysDNwgAC529f0kOmWRiYVgEpDgYmEA8AUzEKsQAAAHicY2BkYGB2+O/AEMPCAAJAkpEBFbAAADgKAe0EAAAiAAAAAAQAAAAEAAAAAAAAKgAqACoAiAAAeJxjYGRgYGBhsGFgYgABEMkFhAwM/xn0QAIAD6YBhwB4nI1Ty07cMBS9QwKlQapQW3VXySvEqDCZGbGaHULiIQ1FKgjWMxknMfLEke2A+IJu+wntrt/QbVf9gG75jK577Lg8K1qQPCfnnnt8fX1NRC/pmjrk/zprC+8D7tBy9DHgBXoWfQ44Av8t4Bj4Z8CLtBL9CniJluPXASf0Lm4CXqFX8Q84dOLnMB17N4c7tBo1AS/Qi+hTwBH4rwHHwN8DXqQ30XXAS7QaLwSc0Gn8NuAVWou/gFmnjLrEaEh9GmDdDGgL3B4JsrRPDU2hTOiMSuJUIdKQQayiAth69r6akSSFqIJuA19TrzCIaY8sIoxyrNIrL//pw7A2iMygkX5vDj+G+kuoLdX4GlGK/8Lnlz6/h9MpmoO9rafrz7ILXEHHaAx95s9lsI7AHNMBWEZHULnfAXwG9/ZqdzLI08iuwRloXE8kfhXYAvE23+23DU3t626rbs8/8adv+9DWknsHp3E17oCf+Z48rvEQNZ78paYM38qfk3v/u3l3u3GXN2Dmvmvpf1Srwk3pB/VSsp512bA/GG5i2WJ7wu430yQ5K3nFGiOqgtmSB5pJVSizwaacmUZzZhXLlZTq8qGGFY2YcSkqbth6aW1tRmlaCFs2016m5qn36SbJrqosG4uMV4aP2PHBmB3tjtmgN2izkGQyLWprekbIntJFing32a5rKWCN/SdSoga45EJykyQ7asZvHQ8PTm6cslIpwyeyjbVltNikc2HTR7YKh9LBl9DADC0U/jLcBZDKrMhUBfQBvXRzLtFtjU9eNHKin0x5InTqb8lNpfKv1s1xHzTXRqgKzek/mb7nB8RZTCDhGEX3kK/8Q75AmUM/eLkfA+0Hi908Kx4eNsMgudg5GLdRD7a84npi+YxNr5i5KIbW5izXas7cHXIMAau1OueZhfj+cOcP3P8MNIWLyYOBuxL6DRylJ4cAAAB4nGNgYoAALjDJyIAOWMCiTIxMLDmZedkABtIBygAAAA==) format("woff"); +} +.markdown-body { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + color: #333; + overflow: hidden; + font-family: "Microsoft YaHei", Helvetica, "Meiryo UI", "Malgun Gothic", "Segoe UI", "Trebuchet MS", "Monaco", monospace, Tahoma, STXihei, "华文细黑", STHeiti, "Helvetica Neue", "Droid Sans", "wenquanyi micro hei", FreeSans, Arimo, Arial, SimSun, "宋体", Heiti, "黑体", sans-serif; + font-size: 16px; + line-height: 1.6; + word-wrap: break-word; +} + +.markdown-body a { + background: transparent; +} + +.markdown-body a:active, +.markdown-body a:hover { + outline: 0; +} + +.markdown-body strong { + font-weight: bold; + color: var(--pai-brand-1-normal); +} + +.markdown-body h1 { + font-size: 2em; + margin: 0.67em 0; +} + +.markdown-body img { + border: 0; +} + +.markdown-body hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +.markdown-body pre { + overflow: auto; +} + +.markdown-body code, +.markdown-body kbd, +.markdown-body pre { + font-family: "Meiryo UI", "YaHei Consolas Hybrid", Consolas, "Malgun Gothic", "Segoe UI", "Trebuchet MS", Helvetica, monospace, monospace; + font-size: 1em; +} + +.markdown-body input { + color: inherit; + font: inherit; + margin: 0; +} + +.markdown-body html input[disabled] { + cursor: default; +} + +.markdown-body input { + line-height: normal; +} + +.markdown-body input[type="checkbox"] { + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; +} + +.markdown-body table { + border-collapse: collapse; + border-spacing: 0; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body * { + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.markdown-body input { + font: 13px/1.4 Helvetica, arial, freesans, clean, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"; +} + +.markdown-body a { + color: var(--pai-brand-1-normal); + text-decoration: none; + border-bottom: 1px solid var(--pai-brand-1-normal); + font-size: 15px; +} + +.markdown-body a:hover, +.markdown-body a:active { + text-decoration: none; +} + +.markdown-body hr { + height: 0; + margin: 15px 0; + overflow: hidden; + background: transparent; + border: 0; + border-bottom: 1px solid #ddd; +} + +.markdown-body hr:before { + display: table; + content: ""; +} + +.markdown-body hr:after { + display: table; + clear: both; + content: ""; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 15px; + margin-bottom: 15px; + line-height: 1.1; +} + +.markdown-body h1 { + font-size: 30px; +} + +.markdown-body h2 { + font-size: 21px; +} + +.markdown-body h3 { + font-size: 16px; +} + +.markdown-body h4 { + font-size: 14px; +} + +.markdown-body h5 { + font-size: 12px; +} + +.markdown-body h6 { + font-size: 11px; +} + +.markdown-body blockquote { + margin: 0; +} + +.markdown-body ul, +.markdown-body ol { + padding: 0; + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ul ul ol, +.markdown-body ul ol ol, +.markdown-body ol ul ol, +.markdown-body ol ol ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body code { + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 12px; +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; + font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace; +} + +.markdown-body .octicon { + font: normal normal 16px octicons-anchor; + line-height: 1; + display: inline-block; + text-decoration: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.markdown-body .octicon-link:before { + content: '\f05c'; +} + +.markdown-body > *:first-child { + margin-top: 0 !important; +} + +.markdown-body > *:last-child { + margin-bottom: 0 !important; +} + +.markdown-body .anchor { + position: absolute; + top: 0; + left: 0; + display: block; + padding-right: 6px; + padding-left: 30px; + margin-left: -30px; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + position: relative; + margin-top: 1em; + margin-bottom: 16px; + font-weight: bold; + line-height: 1.4; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + display: none; + color: #000; + vertical-align: middle; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + padding-left: 8px; + margin-left: -30px; + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + display: inline-block; +} + +.markdown-body h1 { + padding-bottom: 0.3em; + font-size: 2.25em; + line-height: 1.2; + border-bottom: 1px solid #eee; +} + +.markdown-body h1 .anchor { + line-height: 1; +} + +.markdown-body h2 { + margin: 10px auto; + height: 40px; + background-color: rgb(251, 251, 251); + border-bottom: 1px solid rgb(246, 246, 246); + overflow: hidden; + box-sizing: border-box; +} + +.markdown-body h2 .content{ + margin-left: -10px; + display: inline-block; + width: auto; + height: 40px; + background-color: var(--pai-brand-1-normal); + border-bottom-right-radius: 100px; + color: rgb(255, 255, 255); + padding-right: 30px; + padding-left: 30px; + line-height: 40px; + font-size: 18px; +} + +.markdown-body h2 .anchor { + line-height: 1; +} + +.markdown-body h3 { + margin: 1.2em 0 1em; + font-weight: bold; + color: var(--pai-brand-1-normal); + font-size: 18px; +} + +.markdown-body h3 .anchor { + line-height: 1.2; +} + +.markdown-body h4 { + font-size: 1.25em; +} + +.markdown-body h4 .anchor { + line-height: 1.2; +} + +.markdown-body h5 { + font-size: 1em; +} + +.markdown-body h5 .anchor { + line-height: 1.1; +} + +.markdown-body h6 { + font-size: 1em; + color: #777; +} + +.markdown-body h6 .anchor { + line-height: 1.1; +} + +.markdown-body p, +.markdown-body blockquote, +.markdown-body ul, +.markdown-body ol, +.markdown-body dl, +.markdown-body table, +.markdown-body pre { + margin-top: 0; + margin-bottom: 16px; +} + +/* +.markdown-body hr { + height: 4px; + padding: 0; + margin: 16px 0; + background-color: #e7e7e7; + border: 0 none; +}*/ +.markdown-body ul, +.markdown-body ol { + padding-left: 2em; +} + +.markdown-body ul ul, +.markdown-body ul ol, +.markdown-body ol ol, +.markdown-body ol ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li > p { + margin-top: 16px; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: 16px; + font-size: 1em; + font-style: italic; + font-weight: bold; +} + +.markdown-body dl dd { + padding: 0 16px; + margin-bottom: 16px; +} + +.markdown-body blockquote { + padding: 0 15px; + color: #777; + border-left: 4px solid #ddd; +} + +.markdown-body blockquote > :first-child { + margin-top: 0; +} + +.markdown-body blockquote > :last-child { + margin-bottom: 0; +} + +.markdown-body table { + display: block; + width: 100%; + overflow: auto; + word-break: normal; + word-break: keep-all; +} + +.markdown-body table th { + font-weight: bold; +} + +.markdown-body table th, +.markdown-body table td { + padding: 6px 13px; + border: 1px solid #ddd; +} + +.markdown-body table tr { + background-color: #fff; + border-top: 1px solid #ccc; +} + +.markdown-body table tr:nth-child(2n) { + background-color: #f8f8f8; +} + +.markdown-body img { + max-width: 100%; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.markdown-body code { + padding: 0; + padding-top: 0.2em; + padding-bottom: 0.2em; + margin: 0; + font-size: 85%; + background-color: rgba(0, 0, 0, 0.04); + border-radius: 3px; +} + +.markdown-body code:before, +.markdown-body code:after { + letter-spacing: -0.2em; + content: "\00a0"; +} + +.markdown-body pre > code { + padding: 0; + margin: 0; + font-size: 100%; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; +} + +.markdown-body .highlight { + margin-bottom: 16px; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: #f7f7f7; + border-radius: 3px; +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body pre { + word-wrap: normal; +} + +.markdown-body pre code { + display: inline; + max-width: initial; + padding: 0; + margin: 0; + overflow: initial; + line-height: inherit; + word-wrap: normal; + background-color: transparent; + border: 0; +} + +.markdown-body pre code:before, +.markdown-body pre code:after { + content: normal; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font-size: 11px; + line-height: 10px; + color: #555; + vertical-align: middle; + background-color: #fcfcfc; + border: solid 1px #ccc; + border-bottom-color: #bbb; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #bbb; +} + +.markdown-body .pl-c { + color: #969896; +} + +.markdown-body .pl-c1, +.markdown-body .pl-mdh, +.markdown-body .pl-mm, +.markdown-body .pl-mp, +.markdown-body .pl-mr, +.markdown-body .pl-s1 .pl-v, +.markdown-body .pl-s3, +.markdown-body .pl-sc, +.markdown-body .pl-sv { + color: #0086b3; +} + +.markdown-body .pl-e, +.markdown-body .pl-en { + color: #795da3; +} + +.markdown-body .pl-s1 .pl-s2, +.markdown-body .pl-smi, +.markdown-body .pl-smp, +.markdown-body .pl-stj, +.markdown-body .pl-vo, +.markdown-body .pl-vpf { + color: #333; +} + +.markdown-body .pl-ent { + color: #63a35c; +} + +.markdown-body .pl-k, +.markdown-body .pl-s, +.markdown-body .pl-st { + color: #a71d5d; +} + +.markdown-body .pl-pds, +.markdown-body .pl-s1, +.markdown-body .pl-s1 .pl-pse .pl-s2, +.markdown-body .pl-sr, +.markdown-body .pl-sr .pl-cce, +.markdown-body .pl-sr .pl-sra, +.markdown-body .pl-sr .pl-sre, +.markdown-body .pl-src { + color: #df5000; +} + +.markdown-body .pl-mo, +.markdown-body .pl-v { + color: #1d3e81; +} + +.markdown-body .pl-id { + color: #b52a1d; +} + +.markdown-body .pl-ii { + background-color: #b52a1d; + color: #f8f8f8; +} + +.markdown-body .pl-sr .pl-cce { + color: #63a35c; + font-weight: bold; +} + +.markdown-body .pl-ml { + color: #693a17; +} + +.markdown-body .pl-mh, +.markdown-body .pl-mh .pl-en, +.markdown-body .pl-ms { + color: #1d3e81; + font-weight: bold; +} + +.markdown-body .pl-mq { + color: #008080; +} + +.markdown-body .pl-mi { + color: #333; + font-style: italic; +} + +.markdown-body .pl-mb { + color: #333; + font-weight: bold; +} + +.markdown-body .pl-md, +.markdown-body .pl-mdhf { + background-color: #ffecec; + color: #bd2c00; +} + +.markdown-body .pl-mdht, +.markdown-body .pl-mi1 { + background-color: #eaffea; + color: #55a532; +} + +.markdown-body .pl-mdr { + color: #795da3; + font-weight: bold; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; + line-height: 10px; + color: #555; + vertical-align: middle; + background-color: #fcfcfc; + border: solid 1px #ccc; + border-bottom-color: #bbb; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #bbb; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item + .task-list-item { + margin-top: 3px; +} + +.markdown-body .task-list-item input { + float: left; + margin: 0.3em 0 0.25em -1.6em; + vertical-align: middle; +} + +.markdown-body :checked + .radio-label { + z-index: 1; + position: relative; + border-color: #4183c4; +} + +.editormd-preview-container, .editormd-html-preview { + text-align: left; + font-size: 1em; + line-height: 1.6; + padding: 20px; + overflow: auto; + width: 100%; + background-color: #fff; +} +.editormd-preview-container blockquote, .editormd-html-preview blockquote { + margin: 20px 5px; + color: var(--pai-color-3-black); + border-left: 4px solid var(--pai-brand-1-normal); + padding-left: 20px; + margin-left: 0; + font-size: 14px; + background: #FBF9FD; + line-height: 26px; +} + +.editormd-preview-container blockquote p, .editormd-html-preview blockquote p { + padding-top: 8px; + padding-bottom: 8px; +} + +.editormd-preview-container p code, .editormd-html-preview p code { + margin-left: 5px; + margin-right: 4px; +} +.editormd-preview-container abbr, .editormd-html-preview abbr { + background: #ffffdd; +} + +.editormd-preview-container ul li{ + list-style-type: disc; +} + +.editormd-preview-container ol li{ + list-style-type: decimal; +} + +.editormd-preview-container hr, .editormd-html-preview hr { + height: 1px; + border: none; + border-top: 1px solid var(--pai-brand-1-normal); + background: none; +} +.editormd-preview-container code, .editormd-html-preview code { + border: 1px solid #ddd; + background: #f6f6f6; + padding: 3px; + border-radius: 3px; + font-size: 14px; +} +.editormd-preview-container pre, .editormd-html-preview pre { + border: 1px solid #ddd; + background: #f6f6f6; + padding: 10px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -ms-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; +} +.editormd-preview-container pre code, .editormd-html-preview pre code { + padding: 0; +} +.editormd-preview-container pre, .editormd-preview-container code, .editormd-preview-container kbd, .editormd-html-preview pre, .editormd-html-preview code, .editormd-html-preview kbd { + font-family: "YaHei Consolas Hybrid", Consolas, "Meiryo UI", "Malgun Gothic", "Segoe UI", "Trebuchet MS", Helvetica, monospace, monospace; +} +.editormd-preview-container table thead tr, .editormd-html-preview table thead tr { + background-color: #F8F8F8; +} +.editormd-preview-container p.editormd-tex, .editormd-html-preview p.editormd-tex { + text-align: center; +} +.editormd-preview-container span.editormd-tex, .editormd-html-preview span.editormd-tex { + margin: 0 5px; +} +.editormd-preview-container .emoji, .editormd-html-preview .emoji { + width: 24px; + height: 24px; +} +.editormd-preview-container .katex, .editormd-html-preview .katex { + font-size: 1.2em; +} +.editormd-preview-container .sequence-diagram, .editormd-preview-container .flowchart, .editormd-html-preview .sequence-diagram, .editormd-html-preview .flowchart { + margin: 0 auto; + text-align: center; +} +.editormd-preview-container .sequence-diagram svg, .editormd-preview-container .flowchart svg, .editormd-html-preview .sequence-diagram svg, .editormd-html-preview .flowchart svg { + margin: 0 auto; +} +.editormd-preview-container .sequence-diagram text, .editormd-preview-container .flowchart text, .editormd-html-preview .sequence-diagram text, .editormd-html-preview .flowchart text { + font-size: 15px !important; + font-family: "YaHei Consolas Hybrid", Consolas, "Microsoft YaHei", "Malgun Gothic", "Segoe UI", Helvetica, Arial !important; +} + +/*! Pretty printing styles. Used with prettify.js. */ +/* SPAN elements with the classes below are added by prettyprint. */ +.pln { + color: #000; +} + +/* plain text */ +@media screen { + .str { + color: #080; + } + + /* string content */ + .kwd { + color: #008; + } + + /* a keyword */ + .com { + color: #800; + } + + /* a comment */ + .typ { + color: #606; + } + + /* a type name */ + .lit { + color: #066; + } + + /* a literal value */ + /* punctuation, lisp open bracket, lisp close bracket */ + .pun, .opn, .clo { + color: #660; + } + + .tag { + color: #008; + } + + /* a markup tag name */ + .atn { + color: #606; + } + + /* a markup attribute name */ + .atv { + color: #080; + } + + /* a markup attribute value */ + .dec, .var { + color: #606; + } + + /* a declaration; a variable name */ + .fun { + color: red; + } + + /* a function name */ +} +/* Use higher contrast and text-weight for printable form. */ +@media print, projection { + .str { + color: #060; + } + + .kwd { + color: #006; + font-weight: bold; + } + + .com { + color: #600; + font-style: italic; + } + + .typ { + color: #404; + font-weight: bold; + } + + .lit { + color: #044; + } + + .pun, .opn, .clo { + color: #440; + } + + .tag { + color: #006; + font-weight: bold; + } + + .atn { + color: #404; + } + + .atv { + color: #060; + } +} +/* Put a border around prettyprinted code snippets. */ +pre.prettyprint { + padding: 2px; + border: 1px solid #888; +} + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; +} + +/* IE indents via margin-left */ +li.L0, +li.L1, +li.L2, +li.L3, +li.L5, +li.L6, +li.L7, +li.L8 { + list-style-type: none; +} + +/* Alternate shading for lines */ +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 { + background: #eee; +} + +.editormd-preview-container pre.prettyprint, .editormd-html-preview pre.prettyprint { + padding: 10px; + border: 1px solid #ddd; + white-space: pre-wrap; + word-wrap: break-word; +} +.editormd-preview-container ol.linenums, .editormd-html-preview ol.linenums { + color: #999; + padding-left: 2.5em; +} +.editormd-preview-container ol.linenums li, .editormd-html-preview ol.linenums li { + list-style-type: decimal; +} +.editormd-preview-container ol.linenums li code, .editormd-html-preview ol.linenums li code { + border: none; + background: none; + padding: 0; +} + +.editormd-preview-container .editormd-toc-menu, .editormd-html-preview .editormd-toc-menu { + margin: 8px 0 12px 0; + display: inline-block; +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc, .editormd-html-preview .editormd-toc-menu > .markdown-toc { + position: relative; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -ms-border-radius: 4px; + -o-border-radius: 4px; + border-radius: 4px; + border: 1px solid #ddd; + display: inline-block; + font-size: 1em; +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc > ul, .editormd-html-preview .editormd-toc-menu > .markdown-toc > ul { + width: 160%; + min-width: 180px; + position: absolute; + left: -1px; + top: -2px; + z-index: 100; + padding: 0 10px 10px; + display: none; + background: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -ms-border-radius: 4px; + -o-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* Webkit browsers */ + -moz-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* Firefox */ + -ms-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* IE9 */ + -o-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* Opera(Old) */ + box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* IE9+, News */ +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc > ul > li ul, .editormd-html-preview .editormd-toc-menu > .markdown-toc > ul > li ul { + width: 100%; + min-width: 180px; + border: 1px solid #ddd; + display: none; + background: #fff; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -ms-border-radius: 4px; + -o-border-radius: 4px; + border-radius: 4px; +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc > ul > li a, .editormd-html-preview .editormd-toc-menu > .markdown-toc > ul > li a { + color: #666; + padding: 6px 10px; + display: block; + -webkit-transition: background-color 500ms ease-out; + /* Safari, Chrome */ + -moz-transition: background-color 500ms ease-out; + /* Firefox 4.0~16.0 */ + transition: background-color 500ms ease-out; + /* IE >9, FF >15, Opera >12.0 */ +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc > ul > li a:hover, .editormd-html-preview .editormd-toc-menu > .markdown-toc > ul > li a:hover { + background-color: #f6f6f6; +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc li, .editormd-html-preview .editormd-toc-menu > .markdown-toc li { + position: relative; +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc li > ul, .editormd-html-preview .editormd-toc-menu > .markdown-toc li > ul { + position: absolute; + top: 32px; + left: 10%; + display: none; + -webkit-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* Webkit browsers */ + -moz-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* Firefox */ + -ms-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* IE9 */ + -o-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* Opera(Old) */ + box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* IE9+, News */ +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc li > ul:before, .editormd-preview-container .editormd-toc-menu > .markdown-toc li > ul:after, .editormd-html-preview .editormd-toc-menu > .markdown-toc li > ul:before, .editormd-html-preview .editormd-toc-menu > .markdown-toc li > ul:after { + pointer-events: pointer-events; + position: absolute; + left: 15px; + top: -6px; + display: block; + content: ""; + width: 0; + height: 0; + border: 6px solid transparent; + border-width: 0 6px 6px; + z-index: 10; +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc li > ul:before, .editormd-html-preview .editormd-toc-menu > .markdown-toc li > ul:before { + border-bottom-color: #ccc; +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc li > ul:after, .editormd-html-preview .editormd-toc-menu > .markdown-toc li > ul:after { + border-bottom-color: #ffffff; + top: -5px; +} +.editormd-preview-container .editormd-toc-menu ul, .editormd-html-preview .editormd-toc-menu ul { + list-style: none; +} +.editormd-preview-container .editormd-toc-menu a, .editormd-html-preview .editormd-toc-menu a { + text-decoration: none; +} +.editormd-preview-container .editormd-toc-menu h1, .editormd-html-preview .editormd-toc-menu h1 { + font-size: 16px; + padding: 5px 0 10px 10px; + line-height: 1; + border-bottom: 1px solid #eee; +} +.editormd-preview-container .editormd-toc-menu h1 .fa, .editormd-html-preview .editormd-toc-menu h1 .fa { + padding-left: 10px; +} +.editormd-preview-container .editormd-toc-menu .toc-menu-btn, .editormd-html-preview .editormd-toc-menu .toc-menu-btn { + color: #666; + min-width: 180px; + padding: 5px 10px; + border-radius: 4px; + display: inline-block; + -webkit-transition: background-color 500ms ease-out; + /* Safari, Chrome */ + -moz-transition: background-color 500ms ease-out; + /* Firefox 4.0~16.0 */ + transition: background-color 500ms ease-out; + /* IE >9, FF >15, Opera >12.0 */ +} +.editormd-preview-container .editormd-toc-menu .toc-menu-btn:hover, .editormd-html-preview .editormd-toc-menu .toc-menu-btn:hover { + background-color: #f6f6f6; +} +.editormd-preview-container .editormd-toc-menu .toc-menu-btn .fa, .editormd-html-preview .editormd-toc-menu .toc-menu-btn .fa { + float: right; + padding: 3px 0 0 10px; + font-size: 1.3em; +} + +.markdown-body .editormd-toc-menu ul { + padding-left: 0; +} +.markdown-body .highlight pre, .markdown-body pre { + line-height: 1.6; +} + +hr.editormd-page-break { + border: 1px dotted #ccc; + font-size: 0; + height: 2px; +} + +@media only print { + hr.editormd-page-break { + background: none; + border: none; + height: 0; + } +} +.editormd-html-preview textarea { + display: none; +} +.editormd-html-preview hr.editormd-page-break { + background: none; + border: none; + height: 0; +} + +.editormd-preview-close-btn { + color: #fff; + padding: 4px 6px; + font-size: 18px; + -webkit-border-radius: 500px; + -moz-border-radius: 500px; + -ms-border-radius: 500px; + -o-border-radius: 500px; + border-radius: 500px; + display: none; + background-color: #ccc; + position: absolute; + top: 25px; + right: 35px; + z-index: 19; + -webkit-transition: background-color 300ms ease-out; + /* Safari, Chrome */ + -moz-transition: background-color 300ms ease-out; + /* Firefox 4.0~16.0 */ + transition: background-color 300ms ease-out; + /* IE >9, FF >15, Opera >12.0 */ +} +.editormd-preview-close-btn:hover { + background-color: #999; +} + +.editormd-preview-active { + width: 100%; + padding: 40px; +} + +/* Preview dark theme */ +.editormd-preview-theme-dark { + color: #777; + background: #2C2827; +} +.editormd-preview-theme-dark .editormd-preview-container { + color: #888; + background-color: #2C2827; +} +.editormd-preview-theme-dark .editormd-preview-container pre.prettyprint { + border: none; +} +.editormd-preview-theme-dark .editormd-preview-container blockquote { + color: #555; + padding: 0.5em; + background: #222; + border-color: #333; +} +.editormd-preview-theme-dark .editormd-preview-container abbr { + color: #fff; + padding: 1px 3px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -ms-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; + background: #ff9900; +} +.editormd-preview-theme-dark .editormd-preview-container code { + color: #fff; + border: none; + padding: 1px 3px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -ms-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; + background: #5A9600; +} +.editormd-preview-theme-dark .editormd-preview-container table { + border: none; +} +.editormd-preview-theme-dark .editormd-preview-container .fa-emoji { + color: #B4BF42; +} +.editormd-preview-theme-dark .editormd-preview-container .katex { + color: #FEC93F; +} +.editormd-preview-theme-dark .editormd-toc-menu > .markdown-toc { + background: #fff; + border: none; +} +.editormd-preview-theme-dark .editormd-toc-menu > .markdown-toc h1 { + border-color: #ddd; +} +.editormd-preview-theme-dark .markdown-body h1, .editormd-preview-theme-dark .markdown-body h2, .editormd-preview-theme-dark .markdown-body hr { + border-color: #222; +} +.editormd-preview-theme-dark pre { + color: #999; + background-color: #111; + background-color: rgba(0, 0, 0, 0.4); + /* plain text */ +} +.editormd-preview-theme-dark pre .pln { + color: #999; +} +.editormd-preview-theme-dark li.L1, .editormd-preview-theme-dark li.L3, .editormd-preview-theme-dark li.L5, .editormd-preview-theme-dark li.L7, .editormd-preview-theme-dark li.L9 { + background: none; +} +.editormd-preview-theme-dark [class*=editormd-logo] { + color: #2196F3; +} +.editormd-preview-theme-dark .sequence-diagram text { + fill: #fff; +} +.editormd-preview-theme-dark .sequence-diagram rect, .editormd-preview-theme-dark .sequence-diagram path { + color: #fff; + fill: #64D1CB; + stroke: #64D1CB; +} +.editormd-preview-theme-dark .flowchart rect, .editormd-preview-theme-dark .flowchart path { + stroke: #A6C6FF; +} +.editormd-preview-theme-dark .flowchart rect { + fill: #A6C6FF; +} +.editormd-preview-theme-dark .flowchart text { + fill: #5879B4; +} + +@media screen { + .editormd-preview-theme-dark { + /* string content */ + /* a keyword */ + /* a comment */ + /* a type name */ + /* a literal value */ + /* punctuation, lisp open bracket, lisp close bracket */ + /* a markup tag name */ + /* a markup attribute name */ + /* a markup attribute value */ + /* a declaration; a variable name */ + /* a function name */ + } + .editormd-preview-theme-dark .str { + color: #080; + } + .editormd-preview-theme-dark .kwd { + color: #ff9900; + } + .editormd-preview-theme-dark .com { + color: #444444; + } + .editormd-preview-theme-dark .typ { + color: #606; + } + .editormd-preview-theme-dark .lit { + color: #066; + } + .editormd-preview-theme-dark .pun, .editormd-preview-theme-dark .opn, .editormd-preview-theme-dark .clo { + color: #660; + } + .editormd-preview-theme-dark .tag { + color: #ff9900; + } + .editormd-preview-theme-dark .atn { + color: #6C95F5; + } + .editormd-preview-theme-dark .atv { + color: #080; + } + .editormd-preview-theme-dark .dec, .editormd-preview-theme-dark .var { + color: #008BA7; + } + .editormd-preview-theme-dark .fun { + color: red; + } +} +.editormd-onlyread .editormd-toolbar { + display: none; +} +.editormd-onlyread .CodeMirror { + margin-top: 0; +} +.editormd-onlyread .editormd-preview { + top: 0; +} + +.editormd-fullscreen { + position: fixed; + top: 0; + left: 0; + border: none; + margin: 0 auto; +} + +/* Editor.md Dark theme */ +.editormd-theme-dark { + border-color: #1a1a17; +} +.editormd-theme-dark .editormd-toolbar { + background: #1A1A17; + border-color: #1a1a17; +} +.editormd-theme-dark .editormd-menu > li > a { + color: #777; + border-color: #1a1a17; +} +.editormd-theme-dark .editormd-menu > li > a:hover, .editormd-theme-dark .editormd-menu > li > a.active { + border-color: #333; + background: #333; +} +.editormd-theme-dark .editormd-menu > li.divider { + border-right: 1px solid #111; +} +.editormd-theme-dark .CodeMirror { + border-right: 1px solid rgba(0, 0, 0, 0.1); +} diff --git a/paicoding-ui/src/main/resources/static/editormd/css/editormd.logo.css b/paicoding-ui/src/main/resources/static/editormd/css/editormd.logo.css new file mode 100644 index 000000000..5f901bfa5 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/css/editormd.logo.css @@ -0,0 +1,98 @@ +/* + * Editor.md + * + * @file editormd.logo.css + * @version v1.5.0 + * @description Open source online markdown editor. + * @license MIT License + * @author Pandao + * {@link https://github.com/pandao/editor.md} + * @updateTime 2015-06-09 + */ + +/*! prefixes.scss v0.1.0 | Author: Pandao | https://github.com/pandao/prefixes.scss | MIT license | Copyright (c) 2015 */ +@font-face { + font-family: 'editormd-logo'; + src: url("../fonts/editormd-logo.eot?-5y8q6h"); + src: url(".../fonts/editormd-logo.eot?#iefix-5y8q6h") format("embedded-opentype"), url("../fonts/editormd-logo.woff?-5y8q6h") format("woff"), url("../fonts/editormd-logo.ttf?-5y8q6h") format("truetype"), url("../fonts/editormd-logo.svg?-5y8q6h#icomoon") format("svg"); + font-weight: normal; + font-style: normal; +} +.editormd-logo, +.editormd-logo-1x, +.editormd-logo-2x, +.editormd-logo-3x, +.editormd-logo-4x, +.editormd-logo-5x, +.editormd-logo-6x, +.editormd-logo-7x, +.editormd-logo-8x { + font-family: 'editormd-logo'; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + font-size: inherit; + line-height: 1; + display: inline-block; + text-rendering: auto; + vertical-align: inherit; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.editormd-logo:before, +.editormd-logo-1x:before, +.editormd-logo-2x:before, +.editormd-logo-3x:before, +.editormd-logo-4x:before, +.editormd-logo-5x:before, +.editormd-logo-6x:before, +.editormd-logo-7x:before, +.editormd-logo-8x:before { + content: "\e1987"; + /* + HTML Entity 󡦇 + example: + */ +} + +.editormd-logo-1x { + font-size: 1em; +} + +.editormd-logo-lg { + font-size: 1.2em; +} + +.editormd-logo-2x { + font-size: 2em; +} + +.editormd-logo-3x { + font-size: 3em; +} + +.editormd-logo-4x { + font-size: 4em; +} + +.editormd-logo-5x { + font-size: 5em; +} + +.editormd-logo-6x { + font-size: 6em; +} + +.editormd-logo-7x { + font-size: 7em; +} + +.editormd-logo-8x { + font-size: 8em; +} + +.editormd-logo-color { + color: #2196F3; +} diff --git a/paicoding-ui/src/main/resources/static/editormd/css/editormd.logo.min.css b/paicoding-ui/src/main/resources/static/editormd/css/editormd.logo.min.css new file mode 100644 index 000000000..d1699782e --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/css/editormd.logo.min.css @@ -0,0 +1,2 @@ +/*! Editor.md v1.5.0 | editormd.logo.min.css | Open source online markdown editor. | MIT License | By: Pandao | https://github.com/pandao/editor.md | 2015-06-09 */ +/*! prefixes.scss v0.1.0 | Author: Pandao | https://github.com/pandao/prefixes.scss | MIT license | Copyright (c) 2015 */@font-face{font-family:editormd-logo;src:url(../fonts/editormd-logo.eot?-5y8q6h);src:url(.../fonts/editormd-logo.eot?#iefix-5y8q6h)format("embedded-opentype"),url(../fonts/editormd-logo.woff?-5y8q6h)format("woff"),url(../fonts/editormd-logo.ttf?-5y8q6h)format("truetype"),url(../fonts/editormd-logo.svg?-5y8q6h#icomoon)format("svg");font-weight:400;font-style:normal}.editormd-logo,.editormd-logo-1x,.editormd-logo-2x,.editormd-logo-3x,.editormd-logo-4x,.editormd-logo-5x,.editormd-logo-6x,.editormd-logo-7x,.editormd-logo-8x{font-family:editormd-logo;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;font-size:inherit;line-height:1;display:inline-block;text-rendering:auto;vertical-align:inherit;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.editormd-logo-1x:before,.editormd-logo-2x:before,.editormd-logo-3x:before,.editormd-logo-4x:before,.editormd-logo-5x:before,.editormd-logo-6x:before,.editormd-logo-7x:before,.editormd-logo-8x:before,.editormd-logo:before{content:"\e1987"}.editormd-logo-1x{font-size:1em}.editormd-logo-lg{font-size:1.2em}.editormd-logo-2x{font-size:2em}.editormd-logo-3x{font-size:3em}.editormd-logo-4x{font-size:4em}.editormd-logo-5x{font-size:5em}.editormd-logo-6x{font-size:6em}.editormd-logo-7x{font-size:7em}.editormd-logo-8x{font-size:8em}.editormd-logo-color{color:#2196F3} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/css/editormd.min.css b/paicoding-ui/src/main/resources/static/editormd/css/editormd.min.css new file mode 100644 index 000000000..05abb501c --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/css/editormd.min.css @@ -0,0 +1,5 @@ +/*! Editor.md v1.5.0 | editormd.min.css | Open source online markdown editor. | MIT License | By: Pandao | https://github.com/pandao/editor.md | 2015-06-09 */ +@charset "UTF-8";/*! prefixes.scss v0.1.0 | Author: Pandao | https://github.com/pandao/prefixes.scss | MIT license | Copyright (c) 2015 */.fa-ul,.markdown-body .task-list-item,li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}.editormd-form br,.markdown-body hr:after{clear:both}.editormd{width:90%;height:640px;margin:0 auto 15px;text-align:left;overflow:hidden;position:relative;border:1px solid #ddd;font-family:"Meiryo UI","Microsoft YaHei","Malgun Gothic","Segoe UI","Trebuchet MS",Helvetica,Monaco,monospace,Tahoma,STXihei,"华文细黑",STHeiti,"Helvetica Neue","Droid Sans","wenquanyi micro hei",FreeSans,Arimo,Arial,SimSun,"宋体",Heiti,"黑体",sans-serif}.editormd *,.editormd :after,.editormd :before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.editormd a{text-decoration:none}.editormd img{border:none;vertical-align:middle}.editormd .editormd-html-textarea,.editormd .editormd-markdown-textarea,.editormd>textarea{width:0;height:0;outline:0;resize:none}.editormd .editormd-html-textarea,.editormd .editormd-markdown-textarea{display:none}.editormd button,.editormd input[type=text],.editormd input[type=button],.editormd input[type=submit],.editormd select,.editormd textarea{-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;appearance:none}.editormd ::-webkit-scrollbar{height:10px;width:7px;background:rgba(0,0,0,.1)}.editormd ::-webkit-scrollbar:hover{background:rgba(0,0,0,.2)}.editormd ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.3);-webkit-border-radius:6px;-moz-border-radius:6px;-ms-border-radius:6px;-o-border-radius:6px;border-radius:6px}.editormd ::-webkit-scrollbar-thumb:hover{-webkit-box-shadow:inset 1px 1px 1px rgba(0,0,0,.25);-moz-box-shadow:inset 1px 1px 1px rgba(0,0,0,.25);-ms-box-shadow:inset 1px 1px 1px rgba(0,0,0,.25);-o-box-shadow:inset 1px 1px 1px rgba(0,0,0,.25);box-shadow:inset 1px 1px 1px rgba(0,0,0,.25);background-color:rgba(0,0,0,.4)}.editormd-user-unselect{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.editormd-toolbar{width:100%;min-height:37px;background:#fff;display:none;position:absolute;top:0;left:0;z-index:10;border-bottom:1px solid #ddd}.editormd-toolbar-container{padding:0 8px;min-height:35px;-o-user-select:none;user-select:none}.editormd-toolbar-container,.markdown-body .octicon{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.editormd-menu,.markdown-body ol,.markdown-body td,.markdown-body th,.markdown-body ul{padding:0}.editormd-menu{margin:0;list-style:none}.editormd-menu>li{margin:0;padding:5px 1px;display:inline-block;position:relative}.editormd-menu>li.divider{display:inline-block;text-indent:-9999px;margin:0 5px;height:65%;border-right:1px solid #ddd}.editormd-menu>li>a{outline:0;color:#666;display:inline-block;min-width:24px;font-size:16px;text-decoration:none;text-align:center;-webkit-border-radius:2px;-moz-border-radius:2px;-ms-border-radius:2px;-o-border-radius:2px;border-radius:2px;border:1px solid #fff;transition:all 300ms ease-out}.editormd-dropdown-menu>li>a:hover,.editormd-menu>li>a{-webkit-transition:all 300ms ease-out;-moz-transition:all 300ms ease-out}.editormd-menu>li>a.active,.editormd-menu>li>a:hover{border:1px solid #ddd;background:#eee}.editormd-menu>li>a>.fa{text-align:center;display:block;padding:5px}.editormd-menu>li>a>.editormd-bold{padding:5px 2px;display:inline-block;font-weight:700}.editormd-menu>li:hover .editormd-dropdown-menu{display:block}.editormd-menu>li+li>a{margin-left:3px}.editormd-dropdown-menu{display:none;background:#fff;border:1px solid #ddd;width:148px;list-style:none;position:absolute;top:33px;left:0;z-index:100;-webkit-box-shadow:1px 2px 6px rgba(0,0,0,.15);-moz-box-shadow:1px 2px 6px rgba(0,0,0,.15);-ms-box-shadow:1px 2px 6px rgba(0,0,0,.15);-o-box-shadow:1px 2px 6px rgba(0,0,0,.15);box-shadow:1px 2px 6px rgba(0,0,0,.15)}.editormd-dropdown-menu:after,.editormd-dropdown-menu:before{width:0;height:0;display:block;content:"";position:absolute;top:-11px;left:8px;border:5px solid transparent}.editormd-dropdown-menu:before{border-bottom-color:#ccc}.editormd-dropdown-menu:after{border-bottom-color:#fff;top:-10px}.editormd-dropdown-menu>li>a{color:#666;display:block;text-decoration:none;padding:8px 10px}.editormd-dropdown-menu>li>a:hover{background:#f6f6f6;transition:all 300ms ease-out}.editormd-dropdown-menu>li+li{border-top:1px solid #ddd}.editormd-container{margin:0;width:100%;height:100%;overflow:hidden;padding:35px 0 0;position:relative;background:#fff;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.editormd-dialog{color:#666;position:fixed;z-index:99999;display:none;-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 0 10px rgba(0,0,0,.3);-moz-box-shadow:0 0 10px rgba(0,0,0,.3);-ms-box-shadow:0 0 10px rgba(0,0,0,.3);-o-box-shadow:0 0 10px rgba(0,0,0,.3);box-shadow:0 0 10px rgba(0,0,0,.3);background:#fff;font-size:14px}.editormd-dialog-container{position:relative;padding:20px;line-height:1.4}.editormd-dialog-container h1{font-size:24px;margin-bottom:10px}.editormd-dialog-container h1 .fa{color:#2C7EEA;padding-right:5px}.editormd-dialog-container h1 small{padding-left:5px;font-weight:400;font-size:12px;color:#999}.editormd-dialog-container select{color:#999;padding:3px 8px;border:1px solid #ddd}.editormd-dialog-close{position:absolute;top:12px;right:15px;font-size:18px;color:#ccc;-webkit-transition:color 300ms ease-out;-moz-transition:color 300ms ease-out;transition:color 300ms ease-out}.editormd-dialog-close:hover{color:#999}.editormd-dialog-header{padding:11px 20px;border-bottom:1px solid #eee;-webkit-transition:background 300ms ease-out;-moz-transition:background 300ms ease-out;transition:background 300ms ease-out}.editormd-dialog-header:hover{background:#f6f6f6}.editormd-dialog-title{font-size:14px}.editormd-dialog-footer{padding:10px 0 0;text-align:right}.editormd-dialog-info{width:420px}.editormd-dialog-info h1{font-weight:400}.editormd-dialog-info .editormd-dialog-container{padding:20px 25px 25px}.editormd-dialog-info .editormd-dialog-close{top:10px;right:10px}.editormd-dialog-info .hover-link:hover,.editormd-dialog-info p>a{color:#2196F3}.editormd-dialog-info .hover-link{color:#666}.editormd-dialog-info a .fa-external-link{display:none}.editormd-dialog-info a:hover{color:#2196F3}.editormd-dialog-info a:hover .fa-external-link{display:inline-block}.editormd-container-mask,.editormd-dialog-mask,.editormd-mask{display:none;width:100%;height:100%;position:absolute;top:0;left:0}.editormd-dialog-mask-bg,.editormd-mask{background:#fff;opacity:.5;filter:alpha(opacity=50)}.editormd-mask{position:fixed;background:#000;opacity:.2;filter:alpha(opacity=20);z-index:99998}.editormd-container-mask,.editormd-dialog-mask-con{background:url(../images/loading.gif)center center no-repeat;-webkit-background-size:32px 32px;-moz-background-size:32px 32px;-o-background-size:32px 32px;background-size:32px 32px}.editormd-container-mask{z-index:20;display:block;background-color:#fff}@media only screen and (-webkit-min-device-pixel-ratio:2),only screen and (min-device-pixel-ratio:2){.editormd-container-mask,.editormd-dialog-mask-con{background-image:url(../images/loading@2x.gif)}}@media only screen and (-webkit-min-device-pixel-ratio:3),only screen and (min-device-pixel-ratio:3){.editormd-container-mask,.editormd-dialog-mask-con{background-image:url(../images/loading@3x.gif)}}.editormd-code-block-dialog textarea,.editormd-preformatted-text-dialog textarea{width:100%;height:400px;margin-bottom:6px;overflow:auto;border:1px solid #eee;background:#fff;padding:15px;resize:none}.editormd-code-toolbar{color:#999;font-size:14px;margin:-5px 0 10px}.editormd-grid-table{width:99%;display:table;border:1px solid #ddd;border-collapse:collapse}.editormd-grid-table-row{width:100%;display:table-row}.editormd-grid-table-row a{font-size:1.4em;width:5%;height:36px;color:#999;text-align:center;display:table-cell;vertical-align:middle;border:1px solid #ddd;text-decoration:none;-webkit-transition:background-color 300ms ease-out,color 100ms ease-in;-moz-transition:background-color 300ms ease-out,color 100ms ease-in;transition:background-color 300ms ease-out,color 100ms ease-in}.editormd-grid-table-row a.selected{color:#666;background-color:#eee}.editormd-grid-table-row a:hover{color:#777;background-color:#f6f6f6}.editormd-tab-head{list-style:none;border-bottom:1px solid #ddd}.editormd-tab-head li{display:inline-block}.editormd-tab-head li a{color:#999;display:block;padding:6px 12px 5px;text-align:center;text-decoration:none;margin-bottom:-1px;border:1px solid #ddd;-webkit-border-top-left-radius:3px;-moz-border-top-left-radius:3px;-ms-border-top-left-radius:3px;-o-border-top-left-radius:3px;border-top-left-radius:3px;-webkit-border-top-right-radius:3px;-moz-border-top-right-radius:3px;-ms-border-top-right-radius:3px;-o-border-top-right-radius:3px;border-top-right-radius:3px;background:#f6f6f6;-webkit-transition:all 300ms ease-out;-moz-transition:all 300ms ease-out;transition:all 300ms ease-out}.editormd-tab-head li a:hover{color:#666;background:#eee}.editormd-tab-head li.active a{color:#666;background:#fff;border-bottom-color:#fff}.editormd-tab-head li+li{margin-left:3px}.editormd-tab-box{padding:20px 0}.editormd-form{color:#666}.editormd-form label{float:left;display:block;width:75px;text-align:left;padding:7px 0 15px 5px;margin:0 0 2px;font-weight:400}.editormd-form iframe{display:none}.editormd-form input:focus{outline:0}.editormd-form input[type=text],.editormd-form input[type=number]{color:#999;padding:8px;border:1px solid #ddd}.editormd-form input[type=number]{width:40px;display:inline-block;padding:6px 8px}.editormd-form input[type=text]{display:inline-block;width:264px}.editormd-form .fa-btns{display:inline-block}.editormd-form .fa-btns a{color:#999;padding:7px 10px 0 0;display:inline-block;text-decoration:none;text-align:center}.editormd-form .fa-btns .fa{font-size:1.3em}.editormd-form .fa-btns label{float:none;display:inline-block;width:auto;text-align:left;padding:0 0 0 5px;cursor:pointer}.fa-fw,.fa-li{text-align:center}.editormd-dialog-container .editormd-btn,.editormd-dialog-container button,.editormd-dialog-container input[type=submit],.editormd-dialog-footer .editormd-btn,.editormd-dialog-footer button,.editormd-dialog-footer input[type=submit],.editormd-form .editormd-btn,.editormd-form button,.editormd-form input[type=submit]{color:#666;min-width:75px;cursor:pointer;background:#fff;padding:7px 10px;border:1px solid #ddd;-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px;-webkit-transition:background 300ms ease-out;-moz-transition:background 300ms ease-out;transition:background 300ms ease-out}.editormd-dialog-container .editormd-btn:hover,.editormd-dialog-container button:hover,.editormd-dialog-container input[type=submit]:hover,.editormd-dialog-footer .editormd-btn:hover,.editormd-dialog-footer button:hover,.editormd-dialog-footer input[type=submit]:hover,.editormd-form .editormd-btn:hover,.editormd-form button:hover,.editormd-form input[type=submit]:hover{background:#eee}.editormd-dialog-container .editormd-btn+.editormd-btn,.editormd-dialog-footer .editormd-btn+.editormd-btn,.editormd-form .editormd-btn+.editormd-btn{margin-left:8px}.editormd-file-input{width:75px;height:32px;margin-left:8px;position:relative;display:inline-block}.editormd-file-input input[type=file]{width:75px;height:32px;opacity:0;cursor:pointer;background:#000;display:inline-block;position:absolute;top:0;right:0}.editormd-file-input input[type=file]::-webkit-file-upload-button{visibility:hidden}.editormd-file-input:hover input[type=submit]{background:#eee}.editormd .CodeMirror,.editormd-preview{display:inline-block;width:50%;height:100%;vertical-align:top;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;margin:0}.editormd-preview{position:absolute;top:35px;right:0;overflow:auto;line-height:1.6;display:none;background:#fff}.fa,.fa-stack{display:inline-block}.editormd .CodeMirror{z-index:10;float:left;border-right:1px solid #ddd;font-size:14px;font-family:"YaHei Consolas Hybrid",Consolas,"微软雅黑","Meiryo UI","Malgun Gothic","Segoe UI","Trebuchet MS",Helvetica,Monaco,courier,monospace;line-height:1.6;margin-top:35px}.editormd .CodeMirror pre{font-size:14px;padding:0 12px}.editormd .CodeMirror-linenumbers{padding:0 5px}.editormd .CodeMirror-focused .CodeMirror-selected,.editormd .CodeMirror-selected{background:#70B7FF}.editormd .CodeMirror,.editormd .CodeMirror-scroll,.editormd .editormd-preview{-webkit-overflow-scrolling:touch}.editormd .styled-background{background-color:#ff7}.editormd .CodeMirror-focused .cm-matchhighlight{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);background-position:bottom;background-repeat:repeat-x}.editormd .CodeMirror-empty.CodeMirror-focused{outline:0}.editormd .CodeMirror pre.CodeMirror-placeholder{color:#999}.editormd .cm-trailingspace{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QUXCToH00Y1UgAAACFJREFUCNdjPMDBUc/AwNDAAAFMTAwMDA0OP34wQgX/AQBYgwYEx4f9lQAAAABJRU5ErkJggg==);background-position:bottom left;background-repeat:repeat-x}.editormd .cm-tab{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAYAAAAkuj5RAAAAAXNSR0IArs4c6QAAAGFJREFUSMft1LsRQFAQheHPowAKoACx3IgEKtaEHujDjORSgWTH/ZOdnZOcM/sgk/kFFWY0qV8foQwS4MKBCS3qR6ixBJvElOobYAtivseIE120FaowJPN75GMu8j/LfMwNjh4HUpwg4LUAAAAASUVORK5CYII=)right no-repeat}/*! prefixes.scss v0.1.0 | Author: Pandao | https://github.com/pandao/prefixes.scss | MIT license | Copyright (c) 2015 *//*! + * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(../fonts/fontawesome-webfont.eot?v=4.3.0);src:url(../fonts/fontawesome-webfont.eot?#iefix&v=4.3.0)format("embedded-opentype"),url(../fonts/fontawesome-webfont.woff2?v=4.3.0)format("woff2"),url(../fonts/fontawesome-webfont.woff?v=4.3.0)format("woff"),url(../fonts/fontawesome-webfont.ttf?v=4.3.0)format("truetype"),url(../fonts/fontawesome-webfont.svg?v=4.3.0#fontawesomeregular)format("svg");font-weight:400;font-style:normal}.fa{font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transform:translate(0,0)}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em}.fa-ul{padding-left:0;margin-left:2.14285714em}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1,-1);-ms-transform:scale(1,-1);transform:scale(1,-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{filter:none}.fa-stack{position:relative;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-close:before,.fa-remove:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-cog:before,.fa-gear:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-repeat:before,.fa-rotate-right:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-exclamation-triangle:before,.fa-warning:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-cogs:before,.fa-gears:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-floppy-o:before,.fa-save:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-sort:before,.fa-unsorted:before{content:"\f0dc"}.fa-sort-desc:before,.fa-sort-down:before{content:"\f0dd"}.fa-sort-asc:before,.fa-sort-up:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-gavel:before,.fa-legal:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-bolt:before,.fa-flash:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-clipboard:before,.fa-paste:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-chain-broken:before,.fa-unlink:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:"\f150"}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:"\f151"}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:"\f152"}.fa-eur:before,.fa-euro:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-inr:before,.fa-rupee:before{content:"\f156"}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:"\f157"}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:"\f158"}.fa-krw:before,.fa-won:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-try:before,.fa-turkish-lira:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-bank:before,.fa-institution:before,.fa-university:before{content:"\f19c"}.fa-graduation-cap:before,.fa-mortar-board:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:"\f1c5"}.fa-file-archive-o:before,.fa-file-zip-o:before{content:"\f1c6"}.fa-file-audio-o:before,.fa-file-sound-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-empire:before,.fa-ge:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-paper-plane:before,.fa-send:before{content:"\f1d8"}.fa-paper-plane-o:before,.fa-send-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before,.fa-genderless:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-bed:before,.fa-hotel:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}/*! prefixes.scss v0.1.0 | Author: Pandao | https://github.com/pandao/prefixes.scss | MIT license | Copyright (c) 2015 */@font-face{font-family:editormd-logo;src:url(../fonts/editormd-logo.eot?-5y8q6h);src:url(.../fonts/editormd-logo.eot?#iefix-5y8q6h)format("embedded-opentype"),url(../fonts/editormd-logo.woff?-5y8q6h)format("woff"),url(../fonts/editormd-logo.ttf?-5y8q6h)format("truetype"),url(../fonts/editormd-logo.svg?-5y8q6h#icomoon)format("svg");font-weight:400;font-style:normal}.editormd-logo,.editormd-logo-1x,.editormd-logo-2x,.editormd-logo-3x,.editormd-logo-4x,.editormd-logo-5x,.editormd-logo-6x,.editormd-logo-7x,.editormd-logo-8x{font-family:editormd-logo;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;font-size:inherit;line-height:1;display:inline-block;text-rendering:auto;vertical-align:inherit;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.markdown-body hr:after,.markdown-body hr:before{content:"";display:table}.editormd-logo-1x:before,.editormd-logo-2x:before,.editormd-logo-3x:before,.editormd-logo-4x:before,.editormd-logo-5x:before,.editormd-logo-6x:before,.editormd-logo-7x:before,.editormd-logo-8x:before,.editormd-logo:before{content:"\e1987"}.editormd-logo-1x{font-size:1em}.editormd-logo-lg{font-size:1.2em}.editormd-logo-2x{font-size:2em}.editormd-logo-3x{font-size:3em}.editormd-logo-4x{font-size:4em}.editormd-logo-5x{font-size:5em}.editormd-logo-6x{font-size:6em}.editormd-logo-7x{font-size:7em}.editormd-logo-8x{font-size:8em}.editormd-logo-color{color:#2196F3}/*! github-markdown-css | The MIT License (MIT) | Copyright (c) Sindre Sorhus (sindresorhus.com) | https://github.com/sindresorhus/github-markdown-css */@font-face{font-family:octicons-anchor;src:url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAYcAA0AAAAACjQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABMAAAABwAAAAca8vGTk9TLzIAAAFMAAAARAAAAFZG1VHVY21hcAAAAZAAAAA+AAABQgAP9AdjdnQgAAAB0AAAAAQAAAAEACICiGdhc3AAAAHUAAAACAAAAAj//wADZ2x5ZgAAAdwAAADRAAABEKyikaNoZWFkAAACsAAAAC0AAAA2AtXoA2hoZWEAAALgAAAAHAAAACQHngNFaG10eAAAAvwAAAAQAAAAEAwAACJsb2NhAAADDAAAAAoAAAAKALIAVG1heHAAAAMYAAAAHwAAACABEAB2bmFtZQAAAzgAAALBAAAFu3I9x/Nwb3N0AAAF/AAAAB0AAAAvaoFvbwAAAAEAAAAAzBdyYwAAAADP2IQvAAAAAM/bz7t4nGNgZGFgnMDAysDB1Ml0hoGBoR9CM75mMGLkYGBgYmBlZsAKAtJcUxgcPsR8iGF2+O/AEMPsznAYKMwIkgMA5REMOXicY2BgYGaAYBkGRgYQsAHyGMF8FgYFIM0ChED+h5j//yEk/3KoSgZGNgYYk4GRCUgwMaACRoZhDwCs7QgGAAAAIgKIAAAAAf//AAJ4nHWMMQrCQBBF/0zWrCCIKUQsTDCL2EXMohYGSSmorScInsRGL2DOYJe0Ntp7BK+gJ1BxF1stZvjz/v8DRghQzEc4kIgKwiAppcA9LtzKLSkdNhKFY3HF4lK69ExKslx7Xa+vPRVS43G98vG1DnkDMIBUgFN0MDXflU8tbaZOUkXUH0+U27RoRpOIyCKjbMCVejwypzJJG4jIwb43rfl6wbwanocrJm9XFYfskuVC5K/TPyczNU7b84CXcbxks1Un6H6tLH9vf2LRnn8Ax7A5WQAAAHicY2BkYGAA4teL1+yI57f5ysDNwgAC529f0kOmWRiYVgEpDgYmEA8AUzEKsQAAAHicY2BkYGB2+O/AEMPCAAJAkpEBFbAAADgKAe0EAAAiAAAAAAQAAAAEAAAAAAAAKgAqACoAiAAAeJxjYGRgYGBhsGFgYgABEMkFhAwM/xn0QAIAD6YBhwB4nI1Ty07cMBS9QwKlQapQW3VXySvEqDCZGbGaHULiIQ1FKgjWMxknMfLEke2A+IJu+wntrt/QbVf9gG75jK577Lg8K1qQPCfnnnt8fX1NRC/pmjrk/zprC+8D7tBy9DHgBXoWfQ44Av8t4Bj4Z8CLtBL9CniJluPXASf0Lm4CXqFX8Q84dOLnMB17N4c7tBo1AS/Qi+hTwBH4rwHHwN8DXqQ30XXAS7QaLwSc0Gn8NuAVWou/gFmnjLrEaEh9GmDdDGgL3B4JsrRPDU2hTOiMSuJUIdKQQayiAth69r6akSSFqIJuA19TrzCIaY8sIoxyrNIrL//pw7A2iMygkX5vDj+G+kuoLdX4GlGK/8Lnlz6/h9MpmoO9rafrz7ILXEHHaAx95s9lsI7AHNMBWEZHULnfAXwG9/ZqdzLI08iuwRloXE8kfhXYAvE23+23DU3t626rbs8/8adv+9DWknsHp3E17oCf+Z48rvEQNZ78paYM38qfk3v/u3l3u3GXN2Dmvmvpf1Srwk3pB/VSsp512bA/GG5i2WJ7wu430yQ5K3nFGiOqgtmSB5pJVSizwaacmUZzZhXLlZTq8qGGFY2YcSkqbth6aW1tRmlaCFs2016m5qn36SbJrqosG4uMV4aP2PHBmB3tjtmgN2izkGQyLWprekbIntJFing32a5rKWCN/SdSoga45EJykyQ7asZvHQ8PTm6cslIpwyeyjbVltNikc2HTR7YKh9LBl9DADC0U/jLcBZDKrMhUBfQBvXRzLtFtjU9eNHKin0x5InTqb8lNpfKv1s1xHzTXRqgKzek/mb7nB8RZTCDhGEX3kK/8Q75AmUM/eLkfA+0Hi908Kx4eNsMgudg5GLdRD7a84npi+YxNr5i5KIbW5izXas7cHXIMAau1OueZhfj+cOcP3P8MNIWLyYOBuxL6DRylJ4cAAAB4nGNgYoAALjDJyIAOWMCiTIxMLDmZedkABtIBygAAAA==)format("woff")}.markdown-body{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;color:#333;overflow:hidden;font-family:"Microsoft YaHei",Helvetica,"Meiryo UI","Malgun Gothic","Segoe UI","Trebuchet MS",Monaco,monospace,Tahoma,STXihei,"华文细黑",STHeiti,"Helvetica Neue","Droid Sans","wenquanyi micro hei",FreeSans,Arimo,Arial,SimSun,"宋体",Heiti,"黑体",sans-serif;font-size:16px;line-height:1.6;word-wrap:break-word}.markdown-body strong{font-weight:700}.markdown-body h1{margin:.67em 0}.markdown-body img{border:0}.markdown-body hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}.markdown-body input{color:inherit;margin:0;line-height:normal;font:13px/1.4 Helvetica,arial,freesans,clean,sans-serif,"Segoe UI Emoji","Segoe UI Symbol"}.markdown-body html input[disabled]{cursor:default}.markdown-body input[type=checkbox]{-moz-box-sizing:border-box;box-sizing:border-box;padding:0}.markdown-body *{-moz-box-sizing:border-box;box-sizing:border-box}.markdown-body a{background:0 0;color:#4183c4;text-decoration:none}.markdown-body a:active,.markdown-body a:hover{outline:0;text-decoration:underline}.markdown-body hr{margin:15px 0;overflow:hidden;background:0 0;border:0;border-bottom:1px solid #ddd}.markdown-body h1,.markdown-body h2{padding-bottom:.3em;border-bottom:1px solid #eee}.markdown-body blockquote{margin:0}.markdown-body ol ol,.markdown-body ul ol{list-style-type:lower-roman}.markdown-body ol ol ol,.markdown-body ol ul ol,.markdown-body ul ol ol,.markdown-body ul ul ol{list-style-type:lower-alpha}.markdown-body dd{margin-left:0}.markdown-body code{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace}.markdown-body pre{font:12px Consolas,"Liberation Mono",Menlo,Courier,monospace;word-wrap:normal}.markdown-body .octicon{font:normal normal 16px octicons-anchor;line-height:1;display:inline-block;text-decoration:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;user-select:none}.markdown-body .octicon-link:before{content:'\f05c'}.markdown-body>:first-child{margin-top:0!important}.markdown-body>:last-child{margin-bottom:0!important}.markdown-body .anchor{position:absolute;top:0;left:0;display:block;padding-right:6px;padding-left:30px;margin-left:-30px}.markdown-body .anchor:focus{outline:0}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{position:relative;margin-top:1em;margin-bottom:16px;font-weight:700;line-height:1.4}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{display:none;color:#000;vertical-align:middle}.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{padding-left:8px;margin-left:-30px;text-decoration:none}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{display:inline-block}.markdown-body h1{font-size:2.25em;line-height:1.2}.markdown-body h1 .anchor{line-height:1}.markdown-body h2{font-size:1.75em;line-height:1.225}.markdown-body h2 .anchor{line-height:1}.markdown-body h3{font-size:1.5em;line-height:1.43}.markdown-body h3 .anchor,.markdown-body h4 .anchor{line-height:1.2}.markdown-body h4{font-size:1.25em}.markdown-body h5 .anchor,.markdown-body h6 .anchor{line-height:1.1}.markdown-body h5{font-size:1em}.markdown-body h6{font-size:1em;color:#777}.markdown-body blockquote,.markdown-body dl,.markdown-body ol,.markdown-body p,.markdown-body pre,.markdown-body table,.markdown-body ul{margin-top:0;margin-bottom:16px}.markdown-body ol,.markdown-body ul{padding-left:2em}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:0;margin-bottom:0}.markdown-body li>p{margin-top:16px}.markdown-body dl{padding:0}.markdown-body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:700}.markdown-body dl dd{padding:0 16px;margin-bottom:16px}.markdown-body blockquote{padding:0 15px;color:#777;border-left:4px solid #ddd}.markdown-body blockquote>:first-child{margin-top:0}.markdown-body blockquote>:last-child{margin-bottom:0}.markdown-body table{border-collapse:collapse;border-spacing:0;display:block;width:100%;overflow:auto;word-break:normal;word-break:keep-all}.markdown-body table th{font-weight:700}.markdown-body table td,.markdown-body table th{padding:6px 13px;border:1px solid #ddd}.markdown-body table tr{background-color:#fff;border-top:1px solid #ccc}.markdown-body table tr:nth-child(2n){background-color:#f8f8f8}.markdown-body img{max-width:100%;-moz-box-sizing:border-box;box-sizing:border-box}.markdown-body code{padding:.2em 0;margin:0;font-size:85%;background-color:rgba(0,0,0,.04);border-radius:3px}.markdown-body code:after,.markdown-body code:before{letter-spacing:-.2em;content:"\00a0"}.markdown-body pre>code{padding:0;margin:0;font-size:100%;word-break:normal;white-space:pre;background:0 0;border:0}.markdown-body .highlight{margin-bottom:16px}.markdown-body .highlight pre,.markdown-body pre{padding:16px;overflow:auto;font-size:85%;background-color:#f7f7f7;border-radius:3px}.markdown-body .highlight pre{margin-bottom:0;word-break:normal}.markdown-body pre code{display:inline;max-width:initial;padding:0;margin:0;overflow:initial;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}.markdown-body pre code:after,.markdown-body pre code:before{content:normal}.markdown-body .pl-c{color:#969896}.markdown-body .pl-c1,.markdown-body .pl-mdh,.markdown-body .pl-mm,.markdown-body .pl-mp,.markdown-body .pl-mr,.markdown-body .pl-s1 .pl-v,.markdown-body .pl-s3,.markdown-body .pl-sc,.markdown-body .pl-sv{color:#0086b3}.markdown-body .pl-e,.markdown-body .pl-en{color:#795da3}.markdown-body .pl-s1 .pl-s2,.markdown-body .pl-smi,.markdown-body .pl-smp,.markdown-body .pl-stj,.markdown-body .pl-vo,.markdown-body .pl-vpf{color:#333}.markdown-body .pl-ent{color:#63a35c}.markdown-body .pl-k,.markdown-body .pl-s,.markdown-body .pl-st{color:#a71d5d}.markdown-body .pl-pds,.markdown-body .pl-s1,.markdown-body .pl-s1 .pl-pse .pl-s2,.markdown-body .pl-sr,.markdown-body .pl-sr .pl-cce,.markdown-body .pl-sr .pl-sra,.markdown-body .pl-sr .pl-sre,.markdown-body .pl-src{color:#df5000}.markdown-body .pl-mo,.markdown-body .pl-v{color:#1d3e81}.markdown-body .pl-id{color:#b52a1d}.markdown-body .pl-ii{background-color:#b52a1d;color:#f8f8f8}.markdown-body .pl-sr .pl-cce{color:#63a35c;font-weight:700}.markdown-body .pl-ml{color:#693a17}.markdown-body .pl-mh,.markdown-body .pl-mh .pl-en,.markdown-body .pl-ms{color:#1d3e81;font-weight:700}.markdown-body .pl-mq{color:teal}.markdown-body .pl-mi{color:#333;font-style:italic}.markdown-body .pl-mb{color:#333;font-weight:700}.markdown-body .pl-md,.markdown-body .pl-mdhf{background-color:#ffecec;color:#bd2c00}.markdown-body .pl-mdht,.markdown-body .pl-mi1{background-color:#eaffea;color:#55a532}.markdown-body .pl-mdr{color:#795da3;font-weight:700}.markdown-body kbd{display:inline-block;padding:3px 5px;font:11px Consolas,"Liberation Mono",Menlo,Courier,monospace;line-height:10px;color:#555;vertical-align:middle;background-color:#fcfcfc;border:1px solid #ccc;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}.markdown-body .task-list-item+.task-list-item{margin-top:3px}.markdown-body .task-list-item input{float:left;margin:.3em 0 .25em -1.6em;vertical-align:middle}.markdown-body :checked+.radio-label{z-index:1;position:relative;border-color:#4183c4}.editormd-html-preview,.editormd-preview-container{text-align:left;font-size:14px;line-height:1.6;padding:20px;overflow:auto;width:100%;background-color:#fff}.editormd-html-preview blockquote,.editormd-preview-container blockquote{color:#666;border-left:4px solid #ddd;padding-left:20px;margin-left:0;font-size:14px;font-style:italic}.editormd-html-preview p code,.editormd-preview-container p code{margin-left:5px;margin-right:4px}.editormd-html-preview abbr,.editormd-preview-container abbr{background:#ffd}.editormd-html-preview hr,.editormd-preview-container hr{height:1px;border:none;border-top:1px solid #ddd;background:0 0}.editormd-html-preview code,.editormd-preview-container code{border:1px solid #ddd;background:#f6f6f6;padding:3px;border-radius:3px;font-size:14px}.editormd-html-preview pre,.editormd-preview-container pre{border:1px solid #ddd;background:#f6f6f6;padding:10px;-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px}.editormd-html-preview pre code,.editormd-preview-container pre code{padding:0}.editormd-html-preview code,.editormd-html-preview kbd,.editormd-html-preview pre,.editormd-preview-container code,.editormd-preview-container kbd,.editormd-preview-container pre{font-family:"YaHei Consolas Hybrid",Consolas,"Meiryo UI","Malgun Gothic","Segoe UI","Trebuchet MS",Helvetica,monospace,monospace}.editormd-html-preview table thead tr,.editormd-preview-container table thead tr{background-color:#F8F8F8}.editormd-html-preview p.editormd-tex,.editormd-preview-container p.editormd-tex{text-align:center}.editormd-html-preview span.editormd-tex,.editormd-preview-container span.editormd-tex{margin:0 5px}.editormd-html-preview .emoji,.editormd-preview-container .emoji{width:24px;height:24px}.editormd-html-preview .katex,.editormd-preview-container .katex{font-size:1.4em}.editormd-html-preview .flowchart,.editormd-html-preview .sequence-diagram,.editormd-preview-container .flowchart,.editormd-preview-container .sequence-diagram{margin:0 auto;text-align:center}.editormd-html-preview .flowchart svg,.editormd-html-preview .sequence-diagram svg,.editormd-preview-container .flowchart svg,.editormd-preview-container .sequence-diagram svg{margin:0 auto}.editormd-html-preview .flowchart text,.editormd-html-preview .sequence-diagram text,.editormd-preview-container .flowchart text,.editormd-preview-container .sequence-diagram text{font-size:15px!important;font-family:"YaHei Consolas Hybrid",Consolas,"Microsoft YaHei","Malgun Gothic","Segoe UI",Helvetica,Arial!important}/*! Pretty printing styles. Used with prettify.js. */.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.clo,.opn,.pun{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.kwd,.tag,.typ{font-weight:700}.str{color:#060}.kwd{color:#006}.com{color:#600;font-style:italic}.typ{color:#404}.lit{color:#044}.clo,.opn,.pun{color:#440}.tag{color:#006}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}.editormd-html-preview pre.prettyprint,.editormd-preview-container pre.prettyprint{padding:10px;border:1px solid #ddd;white-space:pre-wrap;word-wrap:break-word}.editormd-html-preview ol.linenums,.editormd-preview-container ol.linenums{color:#999;padding-left:2.5em}.editormd-html-preview ol.linenums li,.editormd-preview-container ol.linenums li{list-style-type:decimal}.editormd-html-preview ol.linenums li code,.editormd-preview-container ol.linenums li code{border:none;background:0 0;padding:0}.editormd-html-preview .editormd-toc-menu,.editormd-preview-container .editormd-toc-menu{margin:8px 0 12px;display:inline-block}.editormd-html-preview .editormd-toc-menu>.markdown-toc,.editormd-preview-container .editormd-toc-menu>.markdown-toc{position:relative;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;border:1px solid #ddd;display:inline-block;font-size:1em}.editormd-html-preview .editormd-toc-menu>.markdown-toc>ul,.editormd-preview-container .editormd-toc-menu>.markdown-toc>ul{width:160%;min-width:180px;position:absolute;left:-1px;top:-2px;z-index:100;padding:0 10px 10px;display:none;background:#fff;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 3px 5px rgba(0,0,0,.2);-moz-box-shadow:0 3px 5px rgba(0,0,0,.2);-ms-box-shadow:0 3px 5px rgba(0,0,0,.2);-o-box-shadow:0 3px 5px rgba(0,0,0,.2);box-shadow:0 3px 5px rgba(0,0,0,.2)}.editormd-html-preview .editormd-toc-menu>.markdown-toc>ul>li ul,.editormd-preview-container .editormd-toc-menu>.markdown-toc>ul>li ul{width:100%;min-width:180px;border:1px solid #ddd;display:none;background:#fff;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px}.editormd-html-preview .editormd-toc-menu .toc-menu-btn:hover,.editormd-html-preview .editormd-toc-menu>.markdown-toc>ul>li a:hover,.editormd-preview-container .editormd-toc-menu .toc-menu-btn:hover,.editormd-preview-container .editormd-toc-menu>.markdown-toc>ul>li a:hover{background-color:#f6f6f6}.editormd-html-preview .editormd-toc-menu>.markdown-toc>ul>li a,.editormd-preview-container .editormd-toc-menu>.markdown-toc>ul>li a{color:#666;padding:6px 10px;display:block;-webkit-transition:background-color 500ms ease-out;-moz-transition:background-color 500ms ease-out;transition:background-color 500ms ease-out}.editormd-html-preview .editormd-toc-menu>.markdown-toc li,.editormd-preview-container .editormd-toc-menu>.markdown-toc li{position:relative}.editormd-html-preview .editormd-toc-menu>.markdown-toc li>ul,.editormd-preview-container .editormd-toc-menu>.markdown-toc li>ul{position:absolute;top:32px;left:10%;display:none;-webkit-box-shadow:0 3px 5px rgba(0,0,0,.2);-moz-box-shadow:0 3px 5px rgba(0,0,0,.2);-ms-box-shadow:0 3px 5px rgba(0,0,0,.2);-o-box-shadow:0 3px 5px rgba(0,0,0,.2);box-shadow:0 3px 5px rgba(0,0,0,.2)}.editormd-html-preview .editormd-toc-menu>.markdown-toc li>ul:after,.editormd-html-preview .editormd-toc-menu>.markdown-toc li>ul:before,.editormd-preview-container .editormd-toc-menu>.markdown-toc li>ul:after,.editormd-preview-container .editormd-toc-menu>.markdown-toc li>ul:before{pointer-events:pointer-events;position:absolute;left:15px;top:-6px;display:block;content:"";width:0;height:0;border:6px solid transparent;border-width:0 6px 6px;z-index:10}.editormd-html-preview .editormd-toc-menu>.markdown-toc li>ul:before,.editormd-preview-container .editormd-toc-menu>.markdown-toc li>ul:before{border-bottom-color:#ccc}.editormd-html-preview .editormd-toc-menu>.markdown-toc li>ul:after,.editormd-preview-container .editormd-toc-menu>.markdown-toc li>ul:after{border-bottom-color:#fff;top:-5px}.editormd-html-preview .editormd-toc-menu ul,.editormd-preview-container .editormd-toc-menu ul{list-style:none}.editormd-html-preview .editormd-toc-menu a,.editormd-preview-container .editormd-toc-menu a{text-decoration:none}.editormd-html-preview .editormd-toc-menu h1,.editormd-preview-container .editormd-toc-menu h1{font-size:16px;padding:5px 0 10px 10px;line-height:1;border-bottom:1px solid #eee}.editormd-html-preview .editormd-toc-menu h1 .fa,.editormd-preview-container .editormd-toc-menu h1 .fa{padding-left:10px}.editormd-html-preview .editormd-toc-menu .toc-menu-btn,.editormd-preview-container .editormd-toc-menu .toc-menu-btn{color:#666;min-width:180px;padding:5px 10px;border-radius:4px;display:inline-block;-webkit-transition:background-color 500ms ease-out;-moz-transition:background-color 500ms ease-out;transition:background-color 500ms ease-out}.editormd-html-preview textarea,.editormd-onlyread .editormd-toolbar{display:none}.editormd-html-preview .editormd-toc-menu .toc-menu-btn .fa,.editormd-preview-container .editormd-toc-menu .toc-menu-btn .fa{float:right;padding:3px 0 0 10px;font-size:1.3em}.markdown-body .editormd-toc-menu ul{padding-left:0}.markdown-body .highlight pre,.markdown-body pre{line-height:1.6}hr.editormd-page-break{border:1px dotted #ccc;font-size:0;height:2px}@media only print{hr.editormd-page-break{background:0 0;border:none;height:0}}.editormd-html-preview hr.editormd-page-break{background:0 0;border:none;height:0}.editormd-preview-close-btn{color:#fff;padding:4px 6px;font-size:18px;-webkit-border-radius:500px;-moz-border-radius:500px;-ms-border-radius:500px;-o-border-radius:500px;border-radius:500px;display:none;background-color:#ccc;position:absolute;top:25px;right:35px;z-index:19;-webkit-transition:background-color 300ms ease-out;-moz-transition:background-color 300ms ease-out;transition:background-color 300ms ease-out}.editormd-preview-close-btn:hover{background-color:#999}.editormd-preview-active{width:100%;padding:40px}.editormd-preview-theme-dark{color:#777;background:#2C2827}.editormd-preview-theme-dark .editormd-preview-container{color:#888;background-color:#2C2827}.editormd-preview-theme-dark .editormd-preview-container pre.prettyprint{border:none}.editormd-preview-theme-dark .editormd-preview-container blockquote{color:#555;padding:.5em;background:#222;border-color:#333}.editormd-preview-theme-dark .editormd-preview-container abbr{color:#fff;padding:1px 3px;-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px;background:#f90}.editormd-preview-theme-dark .editormd-preview-container code{color:#fff;border:none;padding:1px 3px;-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px;background:#5A9600}.editormd-preview-theme-dark .editormd-preview-container table{border:none}.editormd-preview-theme-dark .editormd-preview-container .fa-emoji{color:#B4BF42}.editormd-preview-theme-dark .editormd-preview-container .katex{color:#FEC93F}.editormd-preview-theme-dark .editormd-toc-menu>.markdown-toc{background:#fff;border:none}.editormd-preview-theme-dark .editormd-toc-menu>.markdown-toc h1{border-color:#ddd}.editormd-preview-theme-dark .markdown-body h1,.editormd-preview-theme-dark .markdown-body h2,.editormd-preview-theme-dark .markdown-body hr{border-color:#222}.editormd-preview-theme-dark pre{color:#999;background-color:#111;background-color:rgba(0,0,0,.4)}.editormd-preview-theme-dark pre .pln{color:#999}.editormd-preview-theme-dark li.L1,.editormd-preview-theme-dark li.L3,.editormd-preview-theme-dark li.L5,.editormd-preview-theme-dark li.L7,.editormd-preview-theme-dark li.L9{background:0 0}.editormd-preview-theme-dark [class*=editormd-logo]{color:#2196F3}.editormd-preview-theme-dark .sequence-diagram text{fill:#fff}.editormd-preview-theme-dark .sequence-diagram path,.editormd-preview-theme-dark .sequence-diagram rect{color:#fff;fill:#64D1CB;stroke:#64D1CB}.editormd-preview-theme-dark .flowchart path,.editormd-preview-theme-dark .flowchart rect{stroke:#A6C6FF}.editormd-preview-theme-dark .flowchart rect{fill:#A6C6FF}.editormd-preview-theme-dark .flowchart text{fill:#5879B4}@media screen{.editormd-preview-theme-dark .str{color:#080}.editormd-preview-theme-dark .kwd{color:#f90}.editormd-preview-theme-dark .com{color:#444}.editormd-preview-theme-dark .typ{color:#606}.editormd-preview-theme-dark .lit{color:#066}.editormd-preview-theme-dark .clo,.editormd-preview-theme-dark .opn,.editormd-preview-theme-dark .pun{color:#660}.editormd-preview-theme-dark .tag{color:#f90}.editormd-preview-theme-dark .atn{color:#6C95F5}.editormd-preview-theme-dark .atv{color:#080}.editormd-preview-theme-dark .dec,.editormd-preview-theme-dark .var{color:#008BA7}.editormd-preview-theme-dark .fun{color:red}}.editormd-onlyread .CodeMirror{margin-top:0}.editormd-onlyread .editormd-preview{top:0}.editormd-fullscreen{position:fixed;top:0;left:0;border:none;margin:0 auto}.editormd-theme-dark{border-color:#1a1a17}.editormd-theme-dark .editormd-toolbar{background:#1A1A17;border-color:#1a1a17}.editormd-theme-dark .editormd-menu>li>a{color:#777;border-color:#1a1a17}.editormd-theme-dark .editormd-menu>li>a.active,.editormd-theme-dark .editormd-menu>li>a:hover{border-color:#333;background:#333}.editormd-theme-dark .editormd-menu>li.divider{border-right:1px solid #111}.editormd-theme-dark .CodeMirror{border-right:1px solid rgba(0,0,0,.1)} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/css/editormd.preview.css b/paicoding-ui/src/main/resources/static/editormd/css/editormd.preview.css new file mode 100644 index 000000000..60303304a --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/css/editormd.preview.css @@ -0,0 +1,3554 @@ +/* + * Editor.md + * + * @file editormd.preview.css + * @version v1.5.0 + * @description Open source online markdown editor. + * @license MIT License + * @author Pandao + * {@link https://github.com/pandao/editor.md} + * @updateTime 2015-06-09 + */ + +@charset "UTF-8"; +/*! prefixes.scss v0.1.0 | Author: Pandao | https://github.com/pandao/prefixes.scss | MIT license | Copyright (c) 2015 */ +/*! + * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: 'FontAwesome'; + src: url("../fonts/fontawesome-webfont.eot?v=4.3.0"); + src: url("../fonts/fontawesome-webfont.eot?#iefix&v=4.3.0") format("embedded-opentype"), url("../fonts/fontawesome-webfont.woff2?v=4.3.0") format("woff2"), url("../fonts/fontawesome-webfont.woff?v=4.3.0") format("woff"), url("../fonts/fontawesome-webfont.ttf?v=4.3.0") format("truetype"), url("../fonts/fontawesome-webfont.svg?v=4.3.0#fontawesomeregular") format("svg"); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + transform: translate(0, 0); +} + +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.33333333em; + line-height: 0.75em; + vertical-align: -15%; +} + +.fa-2x { + font-size: 2em; +} + +.fa-3x { + font-size: 3em; +} + +.fa-4x { + font-size: 4em; +} + +.fa-5x { + font-size: 5em; +} + +.fa-fw { + width: 1.28571429em; + text-align: center; +} + +.fa-ul { + padding-left: 0; + margin-left: 2.14285714em; + list-style-type: none; +} + +.fa-ul > li { + position: relative; +} + +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} + +.fa-li.fa-lg { + left: -1.85714286em; +} + +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} + +.pull-right { + float: right; +} + +.pull-left { + float: left; +} + +.fa.pull-left { + margin-right: .3em; +} + +.fa.pull-right { + margin-left: .3em; +} + +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} + +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} + +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} + +.fa-rotate-180 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} + +.fa-rotate-270 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} + +.fa-flip-horizontal { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} + +.fa-flip-vertical { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} + +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} + +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} + +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} + +.fa-stack-1x { + line-height: inherit; +} + +.fa-stack-2x { + font-size: 2em; +} + +.fa-inverse { + color: #ffffff; +} + +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} + +.fa-music:before { + content: "\f001"; +} + +.fa-search:before { + content: "\f002"; +} + +.fa-envelope-o:before { + content: "\f003"; +} + +.fa-heart:before { + content: "\f004"; +} + +.fa-star:before { + content: "\f005"; +} + +.fa-star-o:before { + content: "\f006"; +} + +.fa-user:before { + content: "\f007"; +} + +.fa-film:before { + content: "\f008"; +} + +.fa-th-large:before { + content: "\f009"; +} + +.fa-th:before { + content: "\f00a"; +} + +.fa-th-list:before { + content: "\f00b"; +} + +.fa-check:before { + content: "\f00c"; +} + +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "\f00d"; +} + +.fa-search-plus:before { + content: "\f00e"; +} + +.fa-search-minus:before { + content: "\f010"; +} + +.fa-power-off:before { + content: "\f011"; +} + +.fa-signal:before { + content: "\f012"; +} + +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} + +.fa-trash-o:before { + content: "\f014"; +} + +.fa-home:before { + content: "\f015"; +} + +.fa-file-o:before { + content: "\f016"; +} + +.fa-clock-o:before { + content: "\f017"; +} + +.fa-road:before { + content: "\f018"; +} + +.fa-download:before { + content: "\f019"; +} + +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} + +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} + +.fa-inbox:before { + content: "\f01c"; +} + +.fa-play-circle-o:before { + content: "\f01d"; +} + +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} + +.fa-refresh:before { + content: "\f021"; +} + +.fa-list-alt:before { + content: "\f022"; +} + +.fa-lock:before { + content: "\f023"; +} + +.fa-flag:before { + content: "\f024"; +} + +.fa-headphones:before { + content: "\f025"; +} + +.fa-volume-off:before { + content: "\f026"; +} + +.fa-volume-down:before { + content: "\f027"; +} + +.fa-volume-up:before { + content: "\f028"; +} + +.fa-qrcode:before { + content: "\f029"; +} + +.fa-barcode:before { + content: "\f02a"; +} + +.fa-tag:before { + content: "\f02b"; +} + +.fa-tags:before { + content: "\f02c"; +} + +.fa-book:before { + content: "\f02d"; +} + +.fa-bookmark:before { + content: "\f02e"; +} + +.fa-print:before { + content: "\f02f"; +} + +.fa-camera:before { + content: "\f030"; +} + +.fa-font:before { + content: "\f031"; +} + +.fa-bold:before { + content: "\f032"; +} + +.fa-italic:before { + content: "\f033"; +} + +.fa-text-height:before { + content: "\f034"; +} + +.fa-text-width:before { + content: "\f035"; +} + +.fa-align-left:before { + content: "\f036"; +} + +.fa-align-center:before { + content: "\f037"; +} + +.fa-align-right:before { + content: "\f038"; +} + +.fa-align-justify:before { + content: "\f039"; +} + +.fa-list:before { + content: "\f03a"; +} + +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} + +.fa-indent:before { + content: "\f03c"; +} + +.fa-video-camera:before { + content: "\f03d"; +} + +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} + +.fa-pencil:before { + content: "\f040"; +} + +.fa-map-marker:before { + content: "\f041"; +} + +.fa-adjust:before { + content: "\f042"; +} + +.fa-tint:before { + content: "\f043"; +} + +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} + +.fa-share-square-o:before { + content: "\f045"; +} + +.fa-check-square-o:before { + content: "\f046"; +} + +.fa-arrows:before { + content: "\f047"; +} + +.fa-step-backward:before { + content: "\f048"; +} + +.fa-fast-backward:before { + content: "\f049"; +} + +.fa-backward:before { + content: "\f04a"; +} + +.fa-play:before { + content: "\f04b"; +} + +.fa-pause:before { + content: "\f04c"; +} + +.fa-stop:before { + content: "\f04d"; +} + +.fa-forward:before { + content: "\f04e"; +} + +.fa-fast-forward:before { + content: "\f050"; +} + +.fa-step-forward:before { + content: "\f051"; +} + +.fa-eject:before { + content: "\f052"; +} + +.fa-chevron-left:before { + content: "\f053"; +} + +.fa-chevron-right:before { + content: "\f054"; +} + +.fa-plus-circle:before { + content: "\f055"; +} + +.fa-minus-circle:before { + content: "\f056"; +} + +.fa-times-circle:before { + content: "\f057"; +} + +.fa-check-circle:before { + content: "\f058"; +} + +.fa-question-circle:before { + content: "\f059"; +} + +.fa-info-circle:before { + content: "\f05a"; +} + +.fa-crosshairs:before { + content: "\f05b"; +} + +.fa-times-circle-o:before { + content: "\f05c"; +} + +.fa-check-circle-o:before { + content: "\f05d"; +} + +.fa-ban:before { + content: "\f05e"; +} + +.fa-arrow-left:before { + content: "\f060"; +} + +.fa-arrow-right:before { + content: "\f061"; +} + +.fa-arrow-up:before { + content: "\f062"; +} + +.fa-arrow-down:before { + content: "\f063"; +} + +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} + +.fa-expand:before { + content: "\f065"; +} + +.fa-compress:before { + content: "\f066"; +} + +.fa-plus:before { + content: "\f067"; +} + +.fa-minus:before { + content: "\f068"; +} + +.fa-asterisk:before { + content: "\f069"; +} + +.fa-exclamation-circle:before { + content: "\f06a"; +} + +.fa-gift:before { + content: "\f06b"; +} + +.fa-leaf:before { + content: "\f06c"; +} + +.fa-fire:before { + content: "\f06d"; +} + +.fa-eye:before { + content: "\f06e"; +} + +.fa-eye-slash:before { + content: "\f070"; +} + +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} + +.fa-plane:before { + content: "\f072"; +} + +.fa-calendar:before { + content: "\f073"; +} + +.fa-random:before { + content: "\f074"; +} + +.fa-comment:before { + content: "\f075"; +} + +.fa-magnet:before { + content: "\f076"; +} + +.fa-chevron-up:before { + content: "\f077"; +} + +.fa-chevron-down:before { + content: "\f078"; +} + +.fa-retweet:before { + content: "\f079"; +} + +.fa-shopping-cart:before { + content: "\f07a"; +} + +.fa-folder:before { + content: "\f07b"; +} + +.fa-folder-open:before { + content: "\f07c"; +} + +.fa-arrows-v:before { + content: "\f07d"; +} + +.fa-arrows-h:before { + content: "\f07e"; +} + +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "\f080"; +} + +.fa-twitter-square:before { + content: "\f081"; +} + +.fa-facebook-square:before { + content: "\f082"; +} + +.fa-camera-retro:before { + content: "\f083"; +} + +.fa-key:before { + content: "\f084"; +} + +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} + +.fa-comments:before { + content: "\f086"; +} + +.fa-thumbs-o-up:before { + content: "\f087"; +} + +.fa-thumbs-o-down:before { + content: "\f088"; +} + +.fa-star-half:before { + content: "\f089"; +} + +.fa-heart-o:before { + content: "\f08a"; +} + +.fa-sign-out:before { + content: "\f08b"; +} + +.fa-linkedin-square:before { + content: "\f08c"; +} + +.fa-thumb-tack:before { + content: "\f08d"; +} + +.fa-external-link:before { + content: "\f08e"; +} + +.fa-sign-in:before { + content: "\f090"; +} + +.fa-trophy:before { + content: "\f091"; +} + +.fa-github-square:before { + content: "\f092"; +} + +.fa-upload:before { + content: "\f093"; +} + +.fa-lemon-o:before { + content: "\f094"; +} + +.fa-phone:before { + content: "\f095"; +} + +.fa-square-o:before { + content: "\f096"; +} + +.fa-bookmark-o:before { + content: "\f097"; +} + +.fa-phone-square:before { + content: "\f098"; +} + +.fa-twitter:before { + content: "\f099"; +} + +.fa-facebook-f:before, +.fa-facebook:before { + content: "\f09a"; +} + +.fa-github:before { + content: "\f09b"; +} + +.fa-unlock:before { + content: "\f09c"; +} + +.fa-credit-card:before { + content: "\f09d"; +} + +.fa-rss:before { + content: "\f09e"; +} + +.fa-hdd-o:before { + content: "\f0a0"; +} + +.fa-bullhorn:before { + content: "\f0a1"; +} + +.fa-bell:before { + content: "\f0f3"; +} + +.fa-certificate:before { + content: "\f0a3"; +} + +.fa-hand-o-right:before { + content: "\f0a4"; +} + +.fa-hand-o-left:before { + content: "\f0a5"; +} + +.fa-hand-o-up:before { + content: "\f0a6"; +} + +.fa-hand-o-down:before { + content: "\f0a7"; +} + +.fa-arrow-circle-left:before { + content: "\f0a8"; +} + +.fa-arrow-circle-right:before { + content: "\f0a9"; +} + +.fa-arrow-circle-up:before { + content: "\f0aa"; +} + +.fa-arrow-circle-down:before { + content: "\f0ab"; +} + +.fa-globe:before { + content: "\f0ac"; +} + +.fa-wrench:before { + content: "\f0ad"; +} + +.fa-tasks:before { + content: "\f0ae"; +} + +.fa-filter:before { + content: "\f0b0"; +} + +.fa-briefcase:before { + content: "\f0b1"; +} + +.fa-arrows-alt:before { + content: "\f0b2"; +} + +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} + +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} + +.fa-cloud:before { + content: "\f0c2"; +} + +.fa-flask:before { + content: "\f0c3"; +} + +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} + +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} + +.fa-paperclip:before { + content: "\f0c6"; +} + +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} + +.fa-square:before { + content: "\f0c8"; +} + +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} + +.fa-list-ul:before { + content: "\f0ca"; +} + +.fa-list-ol:before { + content: "\f0cb"; +} + +.fa-strikethrough:before { + content: "\f0cc"; +} + +.fa-underline:before { + content: "\f0cd"; +} + +.fa-table:before { + content: "\f0ce"; +} + +.fa-magic:before { + content: "\f0d0"; +} + +.fa-truck:before { + content: "\f0d1"; +} + +.fa-pinterest:before { + content: "\f0d2"; +} + +.fa-pinterest-square:before { + content: "\f0d3"; +} + +.fa-google-plus-square:before { + content: "\f0d4"; +} + +.fa-google-plus:before { + content: "\f0d5"; +} + +.fa-money:before { + content: "\f0d6"; +} + +.fa-caret-down:before { + content: "\f0d7"; +} + +.fa-caret-up:before { + content: "\f0d8"; +} + +.fa-caret-left:before { + content: "\f0d9"; +} + +.fa-caret-right:before { + content: "\f0da"; +} + +.fa-columns:before { + content: "\f0db"; +} + +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} + +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} + +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} + +.fa-envelope:before { + content: "\f0e0"; +} + +.fa-linkedin:before { + content: "\f0e1"; +} + +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} + +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} + +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} + +.fa-comment-o:before { + content: "\f0e5"; +} + +.fa-comments-o:before { + content: "\f0e6"; +} + +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} + +.fa-sitemap:before { + content: "\f0e8"; +} + +.fa-umbrella:before { + content: "\f0e9"; +} + +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} + +.fa-lightbulb-o:before { + content: "\f0eb"; +} + +.fa-exchange:before { + content: "\f0ec"; +} + +.fa-cloud-download:before { + content: "\f0ed"; +} + +.fa-cloud-upload:before { + content: "\f0ee"; +} + +.fa-user-md:before { + content: "\f0f0"; +} + +.fa-stethoscope:before { + content: "\f0f1"; +} + +.fa-suitcase:before { + content: "\f0f2"; +} + +.fa-bell-o:before { + content: "\f0a2"; +} + +.fa-coffee:before { + content: "\f0f4"; +} + +.fa-cutlery:before { + content: "\f0f5"; +} + +.fa-file-text-o:before { + content: "\f0f6"; +} + +.fa-building-o:before { + content: "\f0f7"; +} + +.fa-hospital-o:before { + content: "\f0f8"; +} + +.fa-ambulance:before { + content: "\f0f9"; +} + +.fa-medkit:before { + content: "\f0fa"; +} + +.fa-fighter-jet:before { + content: "\f0fb"; +} + +.fa-beer:before { + content: "\f0fc"; +} + +.fa-h-square:before { + content: "\f0fd"; +} + +.fa-plus-square:before { + content: "\f0fe"; +} + +.fa-angle-double-left:before { + content: "\f100"; +} + +.fa-angle-double-right:before { + content: "\f101"; +} + +.fa-angle-double-up:before { + content: "\f102"; +} + +.fa-angle-double-down:before { + content: "\f103"; +} + +.fa-angle-left:before { + content: "\f104"; +} + +.fa-angle-right:before { + content: "\f105"; +} + +.fa-angle-up:before { + content: "\f106"; +} + +.fa-angle-down:before { + content: "\f107"; +} + +.fa-desktop:before { + content: "\f108"; +} + +.fa-laptop:before { + content: "\f109"; +} + +.fa-tablet:before { + content: "\f10a"; +} + +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} + +.fa-circle-o:before { + content: "\f10c"; +} + +.fa-quote-left:before { + content: "\f10d"; +} + +.fa-quote-right:before { + content: "\f10e"; +} + +.fa-spinner:before { + content: "\f110"; +} + +.fa-circle:before { + content: "\f111"; +} + +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} + +.fa-github-alt:before { + content: "\f113"; +} + +.fa-folder-o:before { + content: "\f114"; +} + +.fa-folder-open-o:before { + content: "\f115"; +} + +.fa-smile-o:before { + content: "\f118"; +} + +.fa-frown-o:before { + content: "\f119"; +} + +.fa-meh-o:before { + content: "\f11a"; +} + +.fa-gamepad:before { + content: "\f11b"; +} + +.fa-keyboard-o:before { + content: "\f11c"; +} + +.fa-flag-o:before { + content: "\f11d"; +} + +.fa-flag-checkered:before { + content: "\f11e"; +} + +.fa-terminal:before { + content: "\f120"; +} + +.fa-code:before { + content: "\f121"; +} + +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} + +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} + +.fa-location-arrow:before { + content: "\f124"; +} + +.fa-crop:before { + content: "\f125"; +} + +.fa-code-fork:before { + content: "\f126"; +} + +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} + +.fa-question:before { + content: "\f128"; +} + +.fa-info:before { + content: "\f129"; +} + +.fa-exclamation:before { + content: "\f12a"; +} + +.fa-superscript:before { + content: "\f12b"; +} + +.fa-subscript:before { + content: "\f12c"; +} + +.fa-eraser:before { + content: "\f12d"; +} + +.fa-puzzle-piece:before { + content: "\f12e"; +} + +.fa-microphone:before { + content: "\f130"; +} + +.fa-microphone-slash:before { + content: "\f131"; +} + +.fa-shield:before { + content: "\f132"; +} + +.fa-calendar-o:before { + content: "\f133"; +} + +.fa-fire-extinguisher:before { + content: "\f134"; +} + +.fa-rocket:before { + content: "\f135"; +} + +.fa-maxcdn:before { + content: "\f136"; +} + +.fa-chevron-circle-left:before { + content: "\f137"; +} + +.fa-chevron-circle-right:before { + content: "\f138"; +} + +.fa-chevron-circle-up:before { + content: "\f139"; +} + +.fa-chevron-circle-down:before { + content: "\f13a"; +} + +.fa-html5:before { + content: "\f13b"; +} + +.fa-css3:before { + content: "\f13c"; +} + +.fa-anchor:before { + content: "\f13d"; +} + +.fa-unlock-alt:before { + content: "\f13e"; +} + +.fa-bullseye:before { + content: "\f140"; +} + +.fa-ellipsis-h:before { + content: "\f141"; +} + +.fa-ellipsis-v:before { + content: "\f142"; +} + +.fa-rss-square:before { + content: "\f143"; +} + +.fa-play-circle:before { + content: "\f144"; +} + +.fa-ticket:before { + content: "\f145"; +} + +.fa-minus-square:before { + content: "\f146"; +} + +.fa-minus-square-o:before { + content: "\f147"; +} + +.fa-level-up:before { + content: "\f148"; +} + +.fa-level-down:before { + content: "\f149"; +} + +.fa-check-square:before { + content: "\f14a"; +} + +.fa-pencil-square:before { + content: "\f14b"; +} + +.fa-external-link-square:before { + content: "\f14c"; +} + +.fa-share-square:before { + content: "\f14d"; +} + +.fa-compass:before { + content: "\f14e"; +} + +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} + +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} + +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} + +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} + +.fa-gbp:before { + content: "\f154"; +} + +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} + +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} + +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} + +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} + +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} + +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} + +.fa-file:before { + content: "\f15b"; +} + +.fa-file-text:before { + content: "\f15c"; +} + +.fa-sort-alpha-asc:before { + content: "\f15d"; +} + +.fa-sort-alpha-desc:before { + content: "\f15e"; +} + +.fa-sort-amount-asc:before { + content: "\f160"; +} + +.fa-sort-amount-desc:before { + content: "\f161"; +} + +.fa-sort-numeric-asc:before { + content: "\f162"; +} + +.fa-sort-numeric-desc:before { + content: "\f163"; +} + +.fa-thumbs-up:before { + content: "\f164"; +} + +.fa-thumbs-down:before { + content: "\f165"; +} + +.fa-youtube-square:before { + content: "\f166"; +} + +.fa-youtube:before { + content: "\f167"; +} + +.fa-xing:before { + content: "\f168"; +} + +.fa-xing-square:before { + content: "\f169"; +} + +.fa-youtube-play:before { + content: "\f16a"; +} + +.fa-dropbox:before { + content: "\f16b"; +} + +.fa-stack-overflow:before { + content: "\f16c"; +} + +.fa-instagram:before { + content: "\f16d"; +} + +.fa-flickr:before { + content: "\f16e"; +} + +.fa-adn:before { + content: "\f170"; +} + +.fa-bitbucket:before { + content: "\f171"; +} + +.fa-bitbucket-square:before { + content: "\f172"; +} + +.fa-tumblr:before { + content: "\f173"; +} + +.fa-tumblr-square:before { + content: "\f174"; +} + +.fa-long-arrow-down:before { + content: "\f175"; +} + +.fa-long-arrow-up:before { + content: "\f176"; +} + +.fa-long-arrow-left:before { + content: "\f177"; +} + +.fa-long-arrow-right:before { + content: "\f178"; +} + +.fa-apple:before { + content: "\f179"; +} + +.fa-windows:before { + content: "\f17a"; +} + +.fa-android:before { + content: "\f17b"; +} + +.fa-linux:before { + content: "\f17c"; +} + +.fa-dribbble:before { + content: "\f17d"; +} + +.fa-skype:before { + content: "\f17e"; +} + +.fa-foursquare:before { + content: "\f180"; +} + +.fa-trello:before { + content: "\f181"; +} + +.fa-female:before { + content: "\f182"; +} + +.fa-male:before { + content: "\f183"; +} + +.fa-gittip:before, +.fa-gratipay:before { + content: "\f184"; +} + +.fa-sun-o:before { + content: "\f185"; +} + +.fa-moon-o:before { + content: "\f186"; +} + +.fa-archive:before { + content: "\f187"; +} + +.fa-bug:before { + content: "\f188"; +} + +.fa-vk:before { + content: "\f189"; +} + +.fa-weibo:before { + content: "\f18a"; +} + +.fa-renren:before { + content: "\f18b"; +} + +.fa-pagelines:before { + content: "\f18c"; +} + +.fa-stack-exchange:before { + content: "\f18d"; +} + +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} + +.fa-arrow-circle-o-left:before { + content: "\f190"; +} + +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} + +.fa-dot-circle-o:before { + content: "\f192"; +} + +.fa-wheelchair:before { + content: "\f193"; +} + +.fa-vimeo-square:before { + content: "\f194"; +} + +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} + +.fa-plus-square-o:before { + content: "\f196"; +} + +.fa-space-shuttle:before { + content: "\f197"; +} + +.fa-slack:before { + content: "\f198"; +} + +.fa-envelope-square:before { + content: "\f199"; +} + +.fa-wordpress:before { + content: "\f19a"; +} + +.fa-openid:before { + content: "\f19b"; +} + +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} + +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} + +.fa-yahoo:before { + content: "\f19e"; +} + +.fa-google:before { + content: "\f1a0"; +} + +.fa-reddit:before { + content: "\f1a1"; +} + +.fa-reddit-square:before { + content: "\f1a2"; +} + +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} + +.fa-stumbleupon:before { + content: "\f1a4"; +} + +.fa-delicious:before { + content: "\f1a5"; +} + +.fa-digg:before { + content: "\f1a6"; +} + +.fa-pied-piper:before { + content: "\f1a7"; +} + +.fa-pied-piper-alt:before { + content: "\f1a8"; +} + +.fa-drupal:before { + content: "\f1a9"; +} + +.fa-joomla:before { + content: "\f1aa"; +} + +.fa-language:before { + content: "\f1ab"; +} + +.fa-fax:before { + content: "\f1ac"; +} + +.fa-building:before { + content: "\f1ad"; +} + +.fa-child:before { + content: "\f1ae"; +} + +.fa-paw:before { + content: "\f1b0"; +} + +.fa-spoon:before { + content: "\f1b1"; +} + +.fa-cube:before { + content: "\f1b2"; +} + +.fa-cubes:before { + content: "\f1b3"; +} + +.fa-behance:before { + content: "\f1b4"; +} + +.fa-behance-square:before { + content: "\f1b5"; +} + +.fa-steam:before { + content: "\f1b6"; +} + +.fa-steam-square:before { + content: "\f1b7"; +} + +.fa-recycle:before { + content: "\f1b8"; +} + +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} + +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} + +.fa-tree:before { + content: "\f1bb"; +} + +.fa-spotify:before { + content: "\f1bc"; +} + +.fa-deviantart:before { + content: "\f1bd"; +} + +.fa-soundcloud:before { + content: "\f1be"; +} + +.fa-database:before { + content: "\f1c0"; +} + +.fa-file-pdf-o:before { + content: "\f1c1"; +} + +.fa-file-word-o:before { + content: "\f1c2"; +} + +.fa-file-excel-o:before { + content: "\f1c3"; +} + +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} + +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} + +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} + +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} + +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} + +.fa-file-code-o:before { + content: "\f1c9"; +} + +.fa-vine:before { + content: "\f1ca"; +} + +.fa-codepen:before { + content: "\f1cb"; +} + +.fa-jsfiddle:before { + content: "\f1cc"; +} + +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} + +.fa-circle-o-notch:before { + content: "\f1ce"; +} + +.fa-ra:before, +.fa-rebel:before { + content: "\f1d0"; +} + +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} + +.fa-git-square:before { + content: "\f1d2"; +} + +.fa-git:before { + content: "\f1d3"; +} + +.fa-hacker-news:before { + content: "\f1d4"; +} + +.fa-tencent-weibo:before { + content: "\f1d5"; +} + +.fa-qq:before { + content: "\f1d6"; +} + +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} + +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} + +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} + +.fa-history:before { + content: "\f1da"; +} + +.fa-genderless:before, +.fa-circle-thin:before { + content: "\f1db"; +} + +.fa-header:before { + content: "\f1dc"; +} + +.fa-paragraph:before { + content: "\f1dd"; +} + +.fa-sliders:before { + content: "\f1de"; +} + +.fa-share-alt:before { + content: "\f1e0"; +} + +.fa-share-alt-square:before { + content: "\f1e1"; +} + +.fa-bomb:before { + content: "\f1e2"; +} + +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "\f1e3"; +} + +.fa-tty:before { + content: "\f1e4"; +} + +.fa-binoculars:before { + content: "\f1e5"; +} + +.fa-plug:before { + content: "\f1e6"; +} + +.fa-slideshare:before { + content: "\f1e7"; +} + +.fa-twitch:before { + content: "\f1e8"; +} + +.fa-yelp:before { + content: "\f1e9"; +} + +.fa-newspaper-o:before { + content: "\f1ea"; +} + +.fa-wifi:before { + content: "\f1eb"; +} + +.fa-calculator:before { + content: "\f1ec"; +} + +.fa-paypal:before { + content: "\f1ed"; +} + +.fa-google-wallet:before { + content: "\f1ee"; +} + +.fa-cc-visa:before { + content: "\f1f0"; +} + +.fa-cc-mastercard:before { + content: "\f1f1"; +} + +.fa-cc-discover:before { + content: "\f1f2"; +} + +.fa-cc-amex:before { + content: "\f1f3"; +} + +.fa-cc-paypal:before { + content: "\f1f4"; +} + +.fa-cc-stripe:before { + content: "\f1f5"; +} + +.fa-bell-slash:before { + content: "\f1f6"; +} + +.fa-bell-slash-o:before { + content: "\f1f7"; +} + +.fa-trash:before { + content: "\f1f8"; +} + +.fa-copyright:before { + content: "\f1f9"; +} + +.fa-at:before { + content: "\f1fa"; +} + +.fa-eyedropper:before { + content: "\f1fb"; +} + +.fa-paint-brush:before { + content: "\f1fc"; +} + +.fa-birthday-cake:before { + content: "\f1fd"; +} + +.fa-area-chart:before { + content: "\f1fe"; +} + +.fa-pie-chart:before { + content: "\f200"; +} + +.fa-line-chart:before { + content: "\f201"; +} + +.fa-lastfm:before { + content: "\f202"; +} + +.fa-lastfm-square:before { + content: "\f203"; +} + +.fa-toggle-off:before { + content: "\f204"; +} + +.fa-toggle-on:before { + content: "\f205"; +} + +.fa-bicycle:before { + content: "\f206"; +} + +.fa-bus:before { + content: "\f207"; +} + +.fa-ioxhost:before { + content: "\f208"; +} + +.fa-angellist:before { + content: "\f209"; +} + +.fa-cc:before { + content: "\f20a"; +} + +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "\f20b"; +} + +.fa-meanpath:before { + content: "\f20c"; +} + +.fa-buysellads:before { + content: "\f20d"; +} + +.fa-connectdevelop:before { + content: "\f20e"; +} + +.fa-dashcube:before { + content: "\f210"; +} + +.fa-forumbee:before { + content: "\f211"; +} + +.fa-leanpub:before { + content: "\f212"; +} + +.fa-sellsy:before { + content: "\f213"; +} + +.fa-shirtsinbulk:before { + content: "\f214"; +} + +.fa-simplybuilt:before { + content: "\f215"; +} + +.fa-skyatlas:before { + content: "\f216"; +} + +.fa-cart-plus:before { + content: "\f217"; +} + +.fa-cart-arrow-down:before { + content: "\f218"; +} + +.fa-diamond:before { + content: "\f219"; +} + +.fa-ship:before { + content: "\f21a"; +} + +.fa-user-secret:before { + content: "\f21b"; +} + +.fa-motorcycle:before { + content: "\f21c"; +} + +.fa-street-view:before { + content: "\f21d"; +} + +.fa-heartbeat:before { + content: "\f21e"; +} + +.fa-venus:before { + content: "\f221"; +} + +.fa-mars:before { + content: "\f222"; +} + +.fa-mercury:before { + content: "\f223"; +} + +.fa-transgender:before { + content: "\f224"; +} + +.fa-transgender-alt:before { + content: "\f225"; +} + +.fa-venus-double:before { + content: "\f226"; +} + +.fa-mars-double:before { + content: "\f227"; +} + +.fa-venus-mars:before { + content: "\f228"; +} + +.fa-mars-stroke:before { + content: "\f229"; +} + +.fa-mars-stroke-v:before { + content: "\f22a"; +} + +.fa-mars-stroke-h:before { + content: "\f22b"; +} + +.fa-neuter:before { + content: "\f22c"; +} + +.fa-facebook-official:before { + content: "\f230"; +} + +.fa-pinterest-p:before { + content: "\f231"; +} + +.fa-whatsapp:before { + content: "\f232"; +} + +.fa-server:before { + content: "\f233"; +} + +.fa-user-plus:before { + content: "\f234"; +} + +.fa-user-times:before { + content: "\f235"; +} + +.fa-hotel:before, +.fa-bed:before { + content: "\f236"; +} + +.fa-viacoin:before { + content: "\f237"; +} + +.fa-train:before { + content: "\f238"; +} + +.fa-subway:before { + content: "\f239"; +} + +.fa-medium:before { + content: "\f23a"; +} + +/*! prefixes.scss v0.1.0 | Author: Pandao | https://github.com/pandao/prefixes.scss | MIT license | Copyright (c) 2015 */ +@font-face { + font-family: 'editormd-logo'; + src: url("../fonts/editormd-logo.eot?-5y8q6h"); + src: url(".../fonts/editormd-logo.eot?#iefix-5y8q6h") format("embedded-opentype"), url("../fonts/editormd-logo.woff?-5y8q6h") format("woff"), url("../fonts/editormd-logo.ttf?-5y8q6h") format("truetype"), url("../fonts/editormd-logo.svg?-5y8q6h#icomoon") format("svg"); + font-weight: normal; + font-style: normal; +} +.editormd-logo, +.editormd-logo-1x, +.editormd-logo-2x, +.editormd-logo-3x, +.editormd-logo-4x, +.editormd-logo-5x, +.editormd-logo-6x, +.editormd-logo-7x, +.editormd-logo-8x { + font-family: 'editormd-logo'; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + font-size: inherit; + line-height: 1; + display: inline-block; + text-rendering: auto; + vertical-align: inherit; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.editormd-logo:before, +.editormd-logo-1x:before, +.editormd-logo-2x:before, +.editormd-logo-3x:before, +.editormd-logo-4x:before, +.editormd-logo-5x:before, +.editormd-logo-6x:before, +.editormd-logo-7x:before, +.editormd-logo-8x:before { + content: "\e1987"; + /* + HTML Entity 󡦇 + example: + */ +} + +.editormd-logo-1x { + font-size: 1em; +} + +.editormd-logo-lg { + font-size: 1.2em; +} + +.editormd-logo-2x { + font-size: 2em; +} + +.editormd-logo-3x { + font-size: 3em; +} + +.editormd-logo-4x { + font-size: 4em; +} + +.editormd-logo-5x { + font-size: 5em; +} + +.editormd-logo-6x { + font-size: 6em; +} + +.editormd-logo-7x { + font-size: 7em; +} + +.editormd-logo-8x { + font-size: 8em; +} + +.editormd-logo-color { + color: #2196F3; +} + +/*! github-markdown-css | The MIT License (MIT) | Copyright (c) Sindre Sorhus (sindresorhus.com) | https://github.com/sindresorhus/github-markdown-css */ +@font-face { + font-family: octicons-anchor; + src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAYcAA0AAAAACjQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABMAAAABwAAAAca8vGTk9TLzIAAAFMAAAARAAAAFZG1VHVY21hcAAAAZAAAAA+AAABQgAP9AdjdnQgAAAB0AAAAAQAAAAEACICiGdhc3AAAAHUAAAACAAAAAj//wADZ2x5ZgAAAdwAAADRAAABEKyikaNoZWFkAAACsAAAAC0AAAA2AtXoA2hoZWEAAALgAAAAHAAAACQHngNFaG10eAAAAvwAAAAQAAAAEAwAACJsb2NhAAADDAAAAAoAAAAKALIAVG1heHAAAAMYAAAAHwAAACABEAB2bmFtZQAAAzgAAALBAAAFu3I9x/Nwb3N0AAAF/AAAAB0AAAAvaoFvbwAAAAEAAAAAzBdyYwAAAADP2IQvAAAAAM/bz7t4nGNgZGFgnMDAysDB1Ml0hoGBoR9CM75mMGLkYGBgYmBlZsAKAtJcUxgcPsR8iGF2+O/AEMPsznAYKMwIkgMA5REMOXicY2BgYGaAYBkGRgYQsAHyGMF8FgYFIM0ChED+h5j//yEk/3KoSgZGNgYYk4GRCUgwMaACRoZhDwCs7QgGAAAAIgKIAAAAAf//AAJ4nHWMMQrCQBBF/0zWrCCIKUQsTDCL2EXMohYGSSmorScInsRGL2DOYJe0Ntp7BK+gJ1BxF1stZvjz/v8DRghQzEc4kIgKwiAppcA9LtzKLSkdNhKFY3HF4lK69ExKslx7Xa+vPRVS43G98vG1DnkDMIBUgFN0MDXflU8tbaZOUkXUH0+U27RoRpOIyCKjbMCVejwypzJJG4jIwb43rfl6wbwanocrJm9XFYfskuVC5K/TPyczNU7b84CXcbxks1Un6H6tLH9vf2LRnn8Ax7A5WQAAAHicY2BkYGAA4teL1+yI57f5ysDNwgAC529f0kOmWRiYVgEpDgYmEA8AUzEKsQAAAHicY2BkYGB2+O/AEMPCAAJAkpEBFbAAADgKAe0EAAAiAAAAAAQAAAAEAAAAAAAAKgAqACoAiAAAeJxjYGRgYGBhsGFgYgABEMkFhAwM/xn0QAIAD6YBhwB4nI1Ty07cMBS9QwKlQapQW3VXySvEqDCZGbGaHULiIQ1FKgjWMxknMfLEke2A+IJu+wntrt/QbVf9gG75jK577Lg8K1qQPCfnnnt8fX1NRC/pmjrk/zprC+8D7tBy9DHgBXoWfQ44Av8t4Bj4Z8CLtBL9CniJluPXASf0Lm4CXqFX8Q84dOLnMB17N4c7tBo1AS/Qi+hTwBH4rwHHwN8DXqQ30XXAS7QaLwSc0Gn8NuAVWou/gFmnjLrEaEh9GmDdDGgL3B4JsrRPDU2hTOiMSuJUIdKQQayiAth69r6akSSFqIJuA19TrzCIaY8sIoxyrNIrL//pw7A2iMygkX5vDj+G+kuoLdX4GlGK/8Lnlz6/h9MpmoO9rafrz7ILXEHHaAx95s9lsI7AHNMBWEZHULnfAXwG9/ZqdzLI08iuwRloXE8kfhXYAvE23+23DU3t626rbs8/8adv+9DWknsHp3E17oCf+Z48rvEQNZ78paYM38qfk3v/u3l3u3GXN2Dmvmvpf1Srwk3pB/VSsp512bA/GG5i2WJ7wu430yQ5K3nFGiOqgtmSB5pJVSizwaacmUZzZhXLlZTq8qGGFY2YcSkqbth6aW1tRmlaCFs2016m5qn36SbJrqosG4uMV4aP2PHBmB3tjtmgN2izkGQyLWprekbIntJFing32a5rKWCN/SdSoga45EJykyQ7asZvHQ8PTm6cslIpwyeyjbVltNikc2HTR7YKh9LBl9DADC0U/jLcBZDKrMhUBfQBvXRzLtFtjU9eNHKin0x5InTqb8lNpfKv1s1xHzTXRqgKzek/mb7nB8RZTCDhGEX3kK/8Q75AmUM/eLkfA+0Hi908Kx4eNsMgudg5GLdRD7a84npi+YxNr5i5KIbW5izXas7cHXIMAau1OueZhfj+cOcP3P8MNIWLyYOBuxL6DRylJ4cAAAB4nGNgYoAALjDJyIAOWMCiTIxMLDmZedkABtIBygAAAA==) format("woff"); +} +.markdown-body { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + color: #333; + overflow: hidden; + font-family: "Microsoft YaHei", Helvetica, "Meiryo UI", "Malgun Gothic", "Segoe UI", "Trebuchet MS", "Monaco", monospace, Tahoma, STXihei, "华文细黑", STHeiti, "Helvetica Neue", "Droid Sans", "wenquanyi micro hei", FreeSans, Arimo, Arial, SimSun, "宋体", Heiti, "黑体", sans-serif; + font-size: 16px; + line-height: 1.6; + word-wrap: break-word; +} + +.markdown-body a { + background: transparent; +} + +.markdown-body a:active, +.markdown-body a:hover { + outline: 0; +} + +.markdown-body strong { + font-weight: bold; +} + +.markdown-body h1 { + font-size: 2em; + margin: 0.67em 0; +} + +.markdown-body img { + border: 0; +} + +.markdown-body hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +.markdown-body pre { + overflow: auto; +} + +.markdown-body code, +.markdown-body kbd, +.markdown-body pre { + font-family: "Meiryo UI", "YaHei Consolas Hybrid", Consolas, "Malgun Gothic", "Segoe UI", "Trebuchet MS", Helvetica, monospace, monospace; + font-size: 1em; +} + +.markdown-body input { + color: inherit; + font: inherit; + margin: 0; +} + +.markdown-body html input[disabled] { + cursor: default; +} + +.markdown-body input { + line-height: normal; +} + +.markdown-body input[type="checkbox"] { + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; +} + +.markdown-body table { + border-collapse: collapse; + border-spacing: 0; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body * { + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.markdown-body input { + font: 13px/1.4 Helvetica, arial, freesans, clean, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"; +} + +.markdown-body a { + color: #4183c4; + text-decoration: none; +} + +.markdown-body a:hover, +.markdown-body a:active { + text-decoration: underline; +} + +.markdown-body hr { + height: 0; + margin: 15px 0; + overflow: hidden; + background: transparent; + border: 0; + border-bottom: 1px solid #ddd; +} + +.markdown-body hr:before { + display: table; + content: ""; +} + +.markdown-body hr:after { + display: table; + clear: both; + content: ""; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 15px; + margin-bottom: 15px; + line-height: 1.1; +} + +.markdown-body h1 { + font-size: 30px; +} + +.markdown-body h2 { + font-size: 21px; +} + +.markdown-body h3 { + font-size: 16px; +} + +.markdown-body h4 { + font-size: 14px; +} + +.markdown-body h5 { + font-size: 12px; +} + +.markdown-body h6 { + font-size: 11px; +} + +.markdown-body blockquote { + margin: 0; +} + +.markdown-body ul, +.markdown-body ol { + padding: 0; + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ul ul ol, +.markdown-body ul ol ol, +.markdown-body ol ul ol, +.markdown-body ol ol ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body code { + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 12px; +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; + font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace; +} + +.markdown-body .octicon { + font: normal normal 16px octicons-anchor; + line-height: 1; + display: inline-block; + text-decoration: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.markdown-body .octicon-link:before { + content: '\f05c'; +} + +.markdown-body > *:first-child { + margin-top: 0 !important; +} + +.markdown-body > *:last-child { + margin-bottom: 0 !important; +} + +.markdown-body .anchor { + position: absolute; + top: 0; + left: 0; + display: block; + padding-right: 6px; + padding-left: 30px; + margin-left: -30px; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + position: relative; + margin-top: 1em; + margin-bottom: 16px; + font-weight: bold; + line-height: 1.4; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + display: none; + color: #000; + vertical-align: middle; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + padding-left: 8px; + margin-left: -30px; + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + display: inline-block; +} + +.markdown-body h1 { + padding-bottom: 0.3em; + font-size: 2.25em; + line-height: 1.2; + border-bottom: 1px solid #eee; +} + +.markdown-body h1 .anchor { + line-height: 1; +} + +.markdown-body h2 { + padding-bottom: 0.3em; + font-size: 1.75em; + line-height: 1.225; + border-bottom: 1px solid #eee; +} + +.markdown-body h2 .anchor { + line-height: 1; +} + +.markdown-body h3 { + font-size: 1.5em; + line-height: 1.43; +} + +.markdown-body h3 .anchor { + line-height: 1.2; +} + +.markdown-body h4 { + font-size: 1.25em; +} + +.markdown-body h4 .anchor { + line-height: 1.2; +} + +.markdown-body h5 { + font-size: 1em; +} + +.markdown-body h5 .anchor { + line-height: 1.1; +} + +.markdown-body h6 { + font-size: 1em; + color: #777; +} + +.markdown-body h6 .anchor { + line-height: 1.1; +} + +.markdown-body p, +.markdown-body blockquote, +.markdown-body ul, +.markdown-body ol, +.markdown-body dl, +.markdown-body table, +.markdown-body pre { + margin-top: 0; + margin-bottom: 16px; +} + +/* +.markdown-body hr { + height: 4px; + padding: 0; + margin: 16px 0; + background-color: #e7e7e7; + border: 0 none; +}*/ +.markdown-body ul, +.markdown-body ol { + padding-left: 2em; +} + +.markdown-body ul ul, +.markdown-body ul ol, +.markdown-body ol ol, +.markdown-body ol ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li > p { + margin-top: 16px; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: 16px; + font-size: 1em; + font-style: italic; + font-weight: bold; +} + +.markdown-body dl dd { + padding: 0 16px; + margin-bottom: 16px; +} + +.markdown-body blockquote { + padding: 0 15px; + color: #777; + border-left: 4px solid #ddd; +} + +.markdown-body blockquote > :first-child { + margin-top: 0; +} + +.markdown-body blockquote > :last-child { + margin-bottom: 0; +} + +.markdown-body table { + display: block; + width: 100%; + overflow: auto; + word-break: normal; + word-break: keep-all; +} + +.markdown-body table th { + font-weight: bold; +} + +.markdown-body table th, +.markdown-body table td { + padding: 6px 13px; + border: 1px solid #ddd; +} + +.markdown-body table tr { + background-color: #fff; + border-top: 1px solid #ccc; +} + +.markdown-body table tr:nth-child(2n) { + background-color: #f8f8f8; +} + +.markdown-body img { + max-width: 100%; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.markdown-body code { + padding: 0; + padding-top: 0.2em; + padding-bottom: 0.2em; + margin: 0; + font-size: 85%; + background-color: rgba(0, 0, 0, 0.04); + border-radius: 3px; +} + +.markdown-body code:before, +.markdown-body code:after { + letter-spacing: -0.2em; + content: "\00a0"; +} + +.markdown-body pre > code { + padding: 0; + margin: 0; + font-size: 100%; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; +} + +.markdown-body .highlight { + margin-bottom: 16px; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: #f7f7f7; + border-radius: 3px; +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body pre { + word-wrap: normal; +} + +.markdown-body pre code { + display: inline; + max-width: initial; + padding: 0; + margin: 0; + overflow: initial; + line-height: inherit; + word-wrap: normal; + background-color: transparent; + border: 0; +} + +.markdown-body pre code:before, +.markdown-body pre code:after { + content: normal; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font-size: 11px; + line-height: 10px; + color: #555; + vertical-align: middle; + background-color: #fcfcfc; + border: solid 1px #ccc; + border-bottom-color: #bbb; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #bbb; +} + +.markdown-body .pl-c { + color: #969896; +} + +.markdown-body .pl-c1, +.markdown-body .pl-mdh, +.markdown-body .pl-mm, +.markdown-body .pl-mp, +.markdown-body .pl-mr, +.markdown-body .pl-s1 .pl-v, +.markdown-body .pl-s3, +.markdown-body .pl-sc, +.markdown-body .pl-sv { + color: #0086b3; +} + +.markdown-body .pl-e, +.markdown-body .pl-en { + color: #795da3; +} + +.markdown-body .pl-s1 .pl-s2, +.markdown-body .pl-smi, +.markdown-body .pl-smp, +.markdown-body .pl-stj, +.markdown-body .pl-vo, +.markdown-body .pl-vpf { + color: #333; +} + +.markdown-body .pl-ent { + color: #63a35c; +} + +.markdown-body .pl-k, +.markdown-body .pl-s, +.markdown-body .pl-st { + color: #a71d5d; +} + +.markdown-body .pl-pds, +.markdown-body .pl-s1, +.markdown-body .pl-s1 .pl-pse .pl-s2, +.markdown-body .pl-sr, +.markdown-body .pl-sr .pl-cce, +.markdown-body .pl-sr .pl-sra, +.markdown-body .pl-sr .pl-sre, +.markdown-body .pl-src { + color: #df5000; +} + +.markdown-body .pl-mo, +.markdown-body .pl-v { + color: #1d3e81; +} + +.markdown-body .pl-id { + color: #b52a1d; +} + +.markdown-body .pl-ii { + background-color: #b52a1d; + color: #f8f8f8; +} + +.markdown-body .pl-sr .pl-cce { + color: #63a35c; + font-weight: bold; +} + +.markdown-body .pl-ml { + color: #693a17; +} + +.markdown-body .pl-mh, +.markdown-body .pl-mh .pl-en, +.markdown-body .pl-ms { + color: #1d3e81; + font-weight: bold; +} + +.markdown-body .pl-mq { + color: #008080; +} + +.markdown-body .pl-mi { + color: #333; + font-style: italic; +} + +.markdown-body .pl-mb { + color: #333; + font-weight: bold; +} + +.markdown-body .pl-md, +.markdown-body .pl-mdhf { + background-color: #ffecec; + color: #bd2c00; +} + +.markdown-body .pl-mdht, +.markdown-body .pl-mi1 { + background-color: #eaffea; + color: #55a532; +} + +.markdown-body .pl-mdr { + color: #795da3; + font-weight: bold; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; + line-height: 10px; + color: #555; + vertical-align: middle; + background-color: #fcfcfc; + border: solid 1px #ccc; + border-bottom-color: #bbb; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #bbb; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item + .task-list-item { + margin-top: 3px; +} + +.markdown-body .task-list-item input { + float: left; + margin: 0.3em 0 0.25em -1.6em; + vertical-align: middle; +} + +.markdown-body :checked + .radio-label { + z-index: 1; + position: relative; + border-color: #4183c4; +} + +.editormd-preview-container, .editormd-html-preview { + text-align: left; + font-size: 14px; + line-height: 1.6; + padding: 20px; + overflow: auto; + width: 100%; + background-color: #fff; +} +.editormd-preview-container blockquote, .editormd-html-preview blockquote { + color: #666; + border-left: 4px solid #ddd; + padding-left: 20px; + margin-left: 0; + font-size: 14px; + font-style: italic; +} +.editormd-preview-container p code, .editormd-html-preview p code { + margin-left: 5px; + margin-right: 4px; +} +.editormd-preview-container abbr, .editormd-html-preview abbr { + background: #ffffdd; +} +.editormd-preview-container hr, .editormd-html-preview hr { + height: 1px; + border: none; + border-top: 1px solid #ddd; + background: none; +} +.editormd-preview-container code, .editormd-html-preview code { + border: 1px solid #ddd; + background: #f6f6f6; + padding: 3px; + border-radius: 3px; + font-size: 14px; +} +.editormd-preview-container pre, .editormd-html-preview pre { + border: 1px solid #ddd; + background: #f6f6f6; + padding: 10px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -ms-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; +} +.editormd-preview-container pre code, .editormd-html-preview pre code { + padding: 0; +} +.editormd-preview-container pre, .editormd-preview-container code, .editormd-preview-container kbd, .editormd-html-preview pre, .editormd-html-preview code, .editormd-html-preview kbd { + font-family: "YaHei Consolas Hybrid", Consolas, "Meiryo UI", "Malgun Gothic", "Segoe UI", "Trebuchet MS", Helvetica, monospace, monospace; +} +.editormd-preview-container table thead tr, .editormd-html-preview table thead tr { + background-color: #F8F8F8; +} +.editormd-preview-container p.editormd-tex, .editormd-html-preview p.editormd-tex { + text-align: center; +} +.editormd-preview-container span.editormd-tex, .editormd-html-preview span.editormd-tex { + margin: 0 5px; +} +.editormd-preview-container .emoji, .editormd-html-preview .emoji { + width: 24px; + height: 24px; +} +.editormd-preview-container .katex, .editormd-html-preview .katex { + font-size: 1.4em; +} +.editormd-preview-container .sequence-diagram, .editormd-preview-container .flowchart, .editormd-html-preview .sequence-diagram, .editormd-html-preview .flowchart { + margin: 0 auto; + text-align: center; +} +.editormd-preview-container .sequence-diagram svg, .editormd-preview-container .flowchart svg, .editormd-html-preview .sequence-diagram svg, .editormd-html-preview .flowchart svg { + margin: 0 auto; +} +.editormd-preview-container .sequence-diagram text, .editormd-preview-container .flowchart text, .editormd-html-preview .sequence-diagram text, .editormd-html-preview .flowchart text { + font-size: 15px !important; + font-family: "YaHei Consolas Hybrid", Consolas, "Microsoft YaHei", "Malgun Gothic", "Segoe UI", Helvetica, Arial !important; +} + +/*! Pretty printing styles. Used with prettify.js. */ +/* SPAN elements with the classes below are added by prettyprint. */ +.pln { + color: #000; +} + +/* plain text */ +@media screen { + .str { + color: #080; + } + + /* string content */ + .kwd { + color: #008; + } + + /* a keyword */ + .com { + color: #800; + } + + /* a comment */ + .typ { + color: #606; + } + + /* a type name */ + .lit { + color: #066; + } + + /* a literal value */ + /* punctuation, lisp open bracket, lisp close bracket */ + .pun, .opn, .clo { + color: #660; + } + + .tag { + color: #008; + } + + /* a markup tag name */ + .atn { + color: #606; + } + + /* a markup attribute name */ + .atv { + color: #080; + } + + /* a markup attribute value */ + .dec, .var { + color: #606; + } + + /* a declaration; a variable name */ + .fun { + color: red; + } + + /* a function name */ +} +/* Use higher contrast and text-weight for printable form. */ +@media print, projection { + .str { + color: #060; + } + + .kwd { + color: #006; + font-weight: bold; + } + + .com { + color: #600; + font-style: italic; + } + + .typ { + color: #404; + font-weight: bold; + } + + .lit { + color: #044; + } + + .pun, .opn, .clo { + color: #440; + } + + .tag { + color: #006; + font-weight: bold; + } + + .atn { + color: #404; + } + + .atv { + color: #060; + } +} +/* Put a border around prettyprinted code snippets. */ +pre.prettyprint { + padding: 2px; + border: 1px solid #888; +} + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; +} + +/* IE indents via margin-left */ +li.L0, +li.L1, +li.L2, +li.L3, +li.L5, +li.L6, +li.L7, +li.L8 { + list-style-type: none; +} + +/* Alternate shading for lines */ +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 { + background: #eee; +} + +.editormd-preview-container pre.prettyprint, .editormd-html-preview pre.prettyprint { + padding: 10px; + border: 1px solid #ddd; + white-space: pre-wrap; + word-wrap: break-word; +} +.editormd-preview-container ol.linenums, .editormd-html-preview ol.linenums { + color: #999; + padding-left: 2.5em; +} +.editormd-preview-container ol.linenums li, .editormd-html-preview ol.linenums li { + list-style-type: decimal; +} +.editormd-preview-container ol.linenums li code, .editormd-html-preview ol.linenums li code { + border: none; + background: none; + padding: 0; +} + +.editormd-preview-container .editormd-toc-menu, .editormd-html-preview .editormd-toc-menu { + margin: 8px 0 12px 0; + display: inline-block; +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc, .editormd-html-preview .editormd-toc-menu > .markdown-toc { + position: relative; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -ms-border-radius: 4px; + -o-border-radius: 4px; + border-radius: 4px; + border: 1px solid #ddd; + display: inline-block; + font-size: 1em; +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc > ul, .editormd-html-preview .editormd-toc-menu > .markdown-toc > ul { + width: 160%; + min-width: 180px; + position: absolute; + left: -1px; + top: -2px; + z-index: 100; + padding: 0 10px 10px; + display: none; + background: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -ms-border-radius: 4px; + -o-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* Webkit browsers */ + -moz-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* Firefox */ + -ms-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* IE9 */ + -o-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* Opera(Old) */ + box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* IE9+, News */ +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc > ul > li ul, .editormd-html-preview .editormd-toc-menu > .markdown-toc > ul > li ul { + width: 100%; + min-width: 180px; + border: 1px solid #ddd; + display: none; + background: #fff; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -ms-border-radius: 4px; + -o-border-radius: 4px; + border-radius: 4px; +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc > ul > li a, .editormd-html-preview .editormd-toc-menu > .markdown-toc > ul > li a { + color: #666; + padding: 6px 10px; + display: block; + -webkit-transition: background-color 500ms ease-out; + /* Safari, Chrome */ + -moz-transition: background-color 500ms ease-out; + /* Firefox 4.0~16.0 */ + transition: background-color 500ms ease-out; + /* IE >9, FF >15, Opera >12.0 */ +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc > ul > li a:hover, .editormd-html-preview .editormd-toc-menu > .markdown-toc > ul > li a:hover { + background-color: #f6f6f6; +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc li, .editormd-html-preview .editormd-toc-menu > .markdown-toc li { + position: relative; +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc li > ul, .editormd-html-preview .editormd-toc-menu > .markdown-toc li > ul { + position: absolute; + top: 32px; + left: 10%; + display: none; + -webkit-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* Webkit browsers */ + -moz-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* Firefox */ + -ms-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* IE9 */ + -o-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* Opera(Old) */ + box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + /* IE9+, News */ +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc li > ul:before, .editormd-preview-container .editormd-toc-menu > .markdown-toc li > ul:after, .editormd-html-preview .editormd-toc-menu > .markdown-toc li > ul:before, .editormd-html-preview .editormd-toc-menu > .markdown-toc li > ul:after { + pointer-events: pointer-events; + position: absolute; + left: 15px; + top: -6px; + display: block; + content: ""; + width: 0; + height: 0; + border: 6px solid transparent; + border-width: 0 6px 6px; + z-index: 10; +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc li > ul:before, .editormd-html-preview .editormd-toc-menu > .markdown-toc li > ul:before { + border-bottom-color: #ccc; +} +.editormd-preview-container .editormd-toc-menu > .markdown-toc li > ul:after, .editormd-html-preview .editormd-toc-menu > .markdown-toc li > ul:after { + border-bottom-color: #ffffff; + top: -5px; +} +.editormd-preview-container .editormd-toc-menu ul, .editormd-html-preview .editormd-toc-menu ul { + list-style: none; +} +.editormd-preview-container .editormd-toc-menu a, .editormd-html-preview .editormd-toc-menu a { + text-decoration: none; +} +.editormd-preview-container .editormd-toc-menu h1, .editormd-html-preview .editormd-toc-menu h1 { + font-size: 16px; + padding: 5px 0 10px 10px; + line-height: 1; + border-bottom: 1px solid #eee; +} +.editormd-preview-container .editormd-toc-menu h1 .fa, .editormd-html-preview .editormd-toc-menu h1 .fa { + padding-left: 10px; +} +.editormd-preview-container .editormd-toc-menu .toc-menu-btn, .editormd-html-preview .editormd-toc-menu .toc-menu-btn { + color: #666; + min-width: 180px; + padding: 5px 10px; + border-radius: 4px; + display: inline-block; + -webkit-transition: background-color 500ms ease-out; + /* Safari, Chrome */ + -moz-transition: background-color 500ms ease-out; + /* Firefox 4.0~16.0 */ + transition: background-color 500ms ease-out; + /* IE >9, FF >15, Opera >12.0 */ +} +.editormd-preview-container .editormd-toc-menu .toc-menu-btn:hover, .editormd-html-preview .editormd-toc-menu .toc-menu-btn:hover { + background-color: #f6f6f6; +} +.editormd-preview-container .editormd-toc-menu .toc-menu-btn .fa, .editormd-html-preview .editormd-toc-menu .toc-menu-btn .fa { + float: right; + padding: 3px 0 0 10px; + font-size: 1.3em; +} + +.markdown-body .editormd-toc-menu ul { + padding-left: 0; +} +.markdown-body .highlight pre, .markdown-body pre { + line-height: 1.6; +} + +hr.editormd-page-break { + border: 1px dotted #ccc; + font-size: 0; + height: 2px; +} + +@media only print { + hr.editormd-page-break { + background: none; + border: none; + height: 0; + } +} +.editormd-html-preview textarea { + display: none; +} +.editormd-html-preview hr.editormd-page-break { + background: none; + border: none; + height: 0; +} + +.editormd-preview-close-btn { + color: #fff; + padding: 4px 6px; + font-size: 18px; + -webkit-border-radius: 500px; + -moz-border-radius: 500px; + -ms-border-radius: 500px; + -o-border-radius: 500px; + border-radius: 500px; + display: none; + background-color: #ccc; + position: absolute; + top: 25px; + right: 35px; + z-index: 19; + -webkit-transition: background-color 300ms ease-out; + /* Safari, Chrome */ + -moz-transition: background-color 300ms ease-out; + /* Firefox 4.0~16.0 */ + transition: background-color 300ms ease-out; + /* IE >9, FF >15, Opera >12.0 */ +} +.editormd-preview-close-btn:hover { + background-color: #999; +} + +.editormd-preview-active { + width: 100%; + padding: 40px; +} diff --git a/paicoding-ui/src/main/resources/static/editormd/css/editormd.preview.min.css b/paicoding-ui/src/main/resources/static/editormd/css/editormd.preview.min.css new file mode 100644 index 000000000..a0f22adae --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/css/editormd.preview.min.css @@ -0,0 +1,5 @@ +/*! Editor.md v1.5.0 | editormd.preview.min.css | Open source online markdown editor. | MIT License | By: Pandao | https://github.com/pandao/editor.md | 2015-06-09 */ +@charset "UTF-8";/*! prefixes.scss v0.1.0 | Author: Pandao | https://github.com/pandao/prefixes.scss | MIT license | Copyright (c) 2015 *//*! + * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */.fa-ul,.markdown-body .task-list-item,li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}.fa-fw,.fa-li{text-align:center}.fa,.fa-stack{display:inline-block}.fa,.markdown-body .octicon{-moz-osx-font-smoothing:grayscale}@font-face{font-family:FontAwesome;src:url(../fonts/fontawesome-webfont.eot?v=4.3.0);src:url(../fonts/fontawesome-webfont.eot?#iefix&v=4.3.0)format("embedded-opentype"),url(../fonts/fontawesome-webfont.woff2?v=4.3.0)format("woff2"),url(../fonts/fontawesome-webfont.woff?v=4.3.0)format("woff"),url(../fonts/fontawesome-webfont.ttf?v=4.3.0)format("truetype"),url(../fonts/fontawesome-webfont.svg?v=4.3.0#fontawesomeregular)format("svg");font-weight:400;font-style:normal}.fa{font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;transform:translate(0,0)}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em}.fa-ul{padding-left:0;margin-left:2.14285714em}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1,-1);-ms-transform:scale(1,-1);transform:scale(1,-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{filter:none}.fa-stack{position:relative;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-close:before,.fa-remove:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-cog:before,.fa-gear:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-repeat:before,.fa-rotate-right:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-exclamation-triangle:before,.fa-warning:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-cogs:before,.fa-gears:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-floppy-o:before,.fa-save:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-sort:before,.fa-unsorted:before{content:"\f0dc"}.fa-sort-desc:before,.fa-sort-down:before{content:"\f0dd"}.fa-sort-asc:before,.fa-sort-up:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-gavel:before,.fa-legal:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-bolt:before,.fa-flash:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-clipboard:before,.fa-paste:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-chain-broken:before,.fa-unlink:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:"\f150"}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:"\f151"}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:"\f152"}.fa-eur:before,.fa-euro:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-inr:before,.fa-rupee:before{content:"\f156"}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:"\f157"}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:"\f158"}.fa-krw:before,.fa-won:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-try:before,.fa-turkish-lira:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-bank:before,.fa-institution:before,.fa-university:before{content:"\f19c"}.fa-graduation-cap:before,.fa-mortar-board:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:"\f1c5"}.fa-file-archive-o:before,.fa-file-zip-o:before{content:"\f1c6"}.fa-file-audio-o:before,.fa-file-sound-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-empire:before,.fa-ge:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-paper-plane:before,.fa-send:before{content:"\f1d8"}.fa-paper-plane-o:before,.fa-send-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before,.fa-genderless:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-bed:before,.fa-hotel:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}/*! prefixes.scss v0.1.0 | Author: Pandao | https://github.com/pandao/prefixes.scss | MIT license | Copyright (c) 2015 */@font-face{font-family:editormd-logo;src:url(../fonts/editormd-logo.eot?-5y8q6h);src:url(.../fonts/editormd-logo.eot?#iefix-5y8q6h)format("embedded-opentype"),url(../fonts/editormd-logo.woff?-5y8q6h)format("woff"),url(../fonts/editormd-logo.ttf?-5y8q6h)format("truetype"),url(../fonts/editormd-logo.svg?-5y8q6h#icomoon)format("svg");font-weight:400;font-style:normal}.editormd-logo,.editormd-logo-1x,.editormd-logo-2x,.editormd-logo-3x,.editormd-logo-4x,.editormd-logo-5x,.editormd-logo-6x,.editormd-logo-7x,.editormd-logo-8x{font-family:editormd-logo;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;font-size:inherit;line-height:1;display:inline-block;text-rendering:auto;vertical-align:inherit;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.markdown-body hr:after,.markdown-body hr:before{content:"";display:table}.editormd-logo-1x:before,.editormd-logo-2x:before,.editormd-logo-3x:before,.editormd-logo-4x:before,.editormd-logo-5x:before,.editormd-logo-6x:before,.editormd-logo-7x:before,.editormd-logo-8x:before,.editormd-logo:before{content:"\e1987"}.editormd-logo-1x{font-size:1em}.editormd-logo-lg{font-size:1.2em}.editormd-logo-2x{font-size:2em}.editormd-logo-3x{font-size:3em}.editormd-logo-4x{font-size:4em}.editormd-logo-5x{font-size:5em}.editormd-logo-6x{font-size:6em}.editormd-logo-7x{font-size:7em}.editormd-logo-8x{font-size:8em}.editormd-logo-color{color:#2196F3}/*! github-markdown-css | The MIT License (MIT) | Copyright (c) Sindre Sorhus (sindresorhus.com) | https://github.com/sindresorhus/github-markdown-css */@font-face{font-family:octicons-anchor;src:url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAYcAA0AAAAACjQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABMAAAABwAAAAca8vGTk9TLzIAAAFMAAAARAAAAFZG1VHVY21hcAAAAZAAAAA+AAABQgAP9AdjdnQgAAAB0AAAAAQAAAAEACICiGdhc3AAAAHUAAAACAAAAAj//wADZ2x5ZgAAAdwAAADRAAABEKyikaNoZWFkAAACsAAAAC0AAAA2AtXoA2hoZWEAAALgAAAAHAAAACQHngNFaG10eAAAAvwAAAAQAAAAEAwAACJsb2NhAAADDAAAAAoAAAAKALIAVG1heHAAAAMYAAAAHwAAACABEAB2bmFtZQAAAzgAAALBAAAFu3I9x/Nwb3N0AAAF/AAAAB0AAAAvaoFvbwAAAAEAAAAAzBdyYwAAAADP2IQvAAAAAM/bz7t4nGNgZGFgnMDAysDB1Ml0hoGBoR9CM75mMGLkYGBgYmBlZsAKAtJcUxgcPsR8iGF2+O/AEMPsznAYKMwIkgMA5REMOXicY2BgYGaAYBkGRgYQsAHyGMF8FgYFIM0ChED+h5j//yEk/3KoSgZGNgYYk4GRCUgwMaACRoZhDwCs7QgGAAAAIgKIAAAAAf//AAJ4nHWMMQrCQBBF/0zWrCCIKUQsTDCL2EXMohYGSSmorScInsRGL2DOYJe0Ntp7BK+gJ1BxF1stZvjz/v8DRghQzEc4kIgKwiAppcA9LtzKLSkdNhKFY3HF4lK69ExKslx7Xa+vPRVS43G98vG1DnkDMIBUgFN0MDXflU8tbaZOUkXUH0+U27RoRpOIyCKjbMCVejwypzJJG4jIwb43rfl6wbwanocrJm9XFYfskuVC5K/TPyczNU7b84CXcbxks1Un6H6tLH9vf2LRnn8Ax7A5WQAAAHicY2BkYGAA4teL1+yI57f5ysDNwgAC529f0kOmWRiYVgEpDgYmEA8AUzEKsQAAAHicY2BkYGB2+O/AEMPCAAJAkpEBFbAAADgKAe0EAAAiAAAAAAQAAAAEAAAAAAAAKgAqACoAiAAAeJxjYGRgYGBhsGFgYgABEMkFhAwM/xn0QAIAD6YBhwB4nI1Ty07cMBS9QwKlQapQW3VXySvEqDCZGbGaHULiIQ1FKgjWMxknMfLEke2A+IJu+wntrt/QbVf9gG75jK577Lg8K1qQPCfnnnt8fX1NRC/pmjrk/zprC+8D7tBy9DHgBXoWfQ44Av8t4Bj4Z8CLtBL9CniJluPXASf0Lm4CXqFX8Q84dOLnMB17N4c7tBo1AS/Qi+hTwBH4rwHHwN8DXqQ30XXAS7QaLwSc0Gn8NuAVWou/gFmnjLrEaEh9GmDdDGgL3B4JsrRPDU2hTOiMSuJUIdKQQayiAth69r6akSSFqIJuA19TrzCIaY8sIoxyrNIrL//pw7A2iMygkX5vDj+G+kuoLdX4GlGK/8Lnlz6/h9MpmoO9rafrz7ILXEHHaAx95s9lsI7AHNMBWEZHULnfAXwG9/ZqdzLI08iuwRloXE8kfhXYAvE23+23DU3t626rbs8/8adv+9DWknsHp3E17oCf+Z48rvEQNZ78paYM38qfk3v/u3l3u3GXN2Dmvmvpf1Srwk3pB/VSsp512bA/GG5i2WJ7wu430yQ5K3nFGiOqgtmSB5pJVSizwaacmUZzZhXLlZTq8qGGFY2YcSkqbth6aW1tRmlaCFs2016m5qn36SbJrqosG4uMV4aP2PHBmB3tjtmgN2izkGQyLWprekbIntJFing32a5rKWCN/SdSoga45EJykyQ7asZvHQ8PTm6cslIpwyeyjbVltNikc2HTR7YKh9LBl9DADC0U/jLcBZDKrMhUBfQBvXRzLtFtjU9eNHKin0x5InTqb8lNpfKv1s1xHzTXRqgKzek/mb7nB8RZTCDhGEX3kK/8Q75AmUM/eLkfA+0Hi908Kx4eNsMgudg5GLdRD7a84npi+YxNr5i5KIbW5izXas7cHXIMAau1OueZhfj+cOcP3P8MNIWLyYOBuxL6DRylJ4cAAAB4nGNgYoAALjDJyIAOWMCiTIxMLDmZedkABtIBygAAAA==)format("woff")}.markdown-body{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;color:#333;overflow:hidden;font-family:"Microsoft YaHei",Helvetica,"Meiryo UI","Malgun Gothic","Segoe UI","Trebuchet MS",Monaco,monospace,Tahoma,STXihei,"华文细黑",STHeiti,"Helvetica Neue","Droid Sans","wenquanyi micro hei",FreeSans,Arimo,Arial,SimSun,"宋体",Heiti,"黑体",sans-serif;font-size:16px;line-height:1.6;word-wrap:break-word}.markdown-body strong{font-weight:700}.markdown-body h1{margin:.67em 0}.markdown-body img{border:0}.markdown-body hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}.markdown-body input{color:inherit;margin:0;line-height:normal;font:13px/1.4 Helvetica,arial,freesans,clean,sans-serif,"Segoe UI Emoji","Segoe UI Symbol"}.markdown-body html input[disabled]{cursor:default}.markdown-body input[type=checkbox]{-moz-box-sizing:border-box;box-sizing:border-box;padding:0}.markdown-body td,.markdown-body th{padding:0}.markdown-body *{-moz-box-sizing:border-box;box-sizing:border-box}.markdown-body a{background:0 0;color:#4183c4;text-decoration:none}.markdown-body a:active,.markdown-body a:hover{outline:0;text-decoration:underline}.markdown-body hr{margin:15px 0;overflow:hidden;background:0 0;border:0;border-bottom:1px solid #ddd}.markdown-body h1,.markdown-body h2{padding-bottom:.3em;border-bottom:1px solid #eee}.markdown-body hr:after{clear:both}.markdown-body blockquote{margin:0}.markdown-body ol,.markdown-body ul{padding:0}.markdown-body ol ol,.markdown-body ul ol{list-style-type:lower-roman}.markdown-body ol ol ol,.markdown-body ol ul ol,.markdown-body ul ol ol,.markdown-body ul ul ol{list-style-type:lower-alpha}.markdown-body dd{margin-left:0}.markdown-body code{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace}.markdown-body pre{font:12px Consolas,"Liberation Mono",Menlo,Courier,monospace;word-wrap:normal}.markdown-body .octicon{font:normal normal 16px octicons-anchor;line-height:1;display:inline-block;text-decoration:none;-webkit-font-smoothing:antialiased;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.markdown-body .octicon-link:before{content:'\f05c'}.markdown-body>:first-child{margin-top:0!important}.markdown-body>:last-child{margin-bottom:0!important}.markdown-body .anchor{position:absolute;top:0;left:0;display:block;padding-right:6px;padding-left:30px;margin-left:-30px}.markdown-body .anchor:focus{outline:0}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{position:relative;margin-top:1em;margin-bottom:16px;font-weight:700;line-height:1.4}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{display:none;color:#000;vertical-align:middle}.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{padding-left:8px;margin-left:-30px;text-decoration:none}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{display:inline-block}.markdown-body h1{font-size:2.25em;line-height:1.2}.markdown-body h1 .anchor{line-height:1}.markdown-body h2{font-size:1.75em;line-height:1.225}.markdown-body h2 .anchor{line-height:1}.markdown-body h3{font-size:1.5em;line-height:1.43}.markdown-body h3 .anchor,.markdown-body h4 .anchor{line-height:1.2}.markdown-body h4{font-size:1.25em}.markdown-body h5 .anchor,.markdown-body h6 .anchor{line-height:1.1}.markdown-body h5{font-size:1em}.markdown-body h6{font-size:1em;color:#777}.markdown-body blockquote,.markdown-body dl,.markdown-body ol,.markdown-body p,.markdown-body pre,.markdown-body table,.markdown-body ul{margin-top:0;margin-bottom:16px}.markdown-body ol,.markdown-body ul{padding-left:2em}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:0;margin-bottom:0}.markdown-body li>p{margin-top:16px}.markdown-body dl{padding:0}.markdown-body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:700}.markdown-body dl dd{padding:0 16px;margin-bottom:16px}.markdown-body blockquote{padding:0 15px;color:#777;border-left:4px solid #ddd}.markdown-body blockquote>:first-child{margin-top:0}.markdown-body blockquote>:last-child{margin-bottom:0}.markdown-body table{border-collapse:collapse;border-spacing:0;display:block;width:100%;overflow:auto;word-break:normal;word-break:keep-all}.markdown-body table th{font-weight:700}.markdown-body table td,.markdown-body table th{padding:6px 13px;border:1px solid #ddd}.markdown-body table tr{background-color:#fff;border-top:1px solid #ccc}.markdown-body table tr:nth-child(2n){background-color:#f8f8f8}.markdown-body img{max-width:100%;-moz-box-sizing:border-box;box-sizing:border-box}.markdown-body code{padding:.2em 0;margin:0;font-size:85%;background-color:rgba(0,0,0,.04);border-radius:3px}.markdown-body code:after,.markdown-body code:before{letter-spacing:-.2em;content:"\00a0"}.markdown-body pre>code{padding:0;margin:0;font-size:100%;word-break:normal;white-space:pre;background:0 0;border:0}.markdown-body .highlight{margin-bottom:16px}.markdown-body .highlight pre,.markdown-body pre{padding:16px;overflow:auto;font-size:85%;background-color:#f7f7f7;border-radius:3px}.markdown-body .highlight pre{margin-bottom:0;word-break:normal}.markdown-body pre code{display:inline;max-width:initial;padding:0;margin:0;overflow:initial;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}.markdown-body pre code:after,.markdown-body pre code:before{content:normal}.markdown-body .pl-c{color:#969896}.markdown-body .pl-c1,.markdown-body .pl-mdh,.markdown-body .pl-mm,.markdown-body .pl-mp,.markdown-body .pl-mr,.markdown-body .pl-s1 .pl-v,.markdown-body .pl-s3,.markdown-body .pl-sc,.markdown-body .pl-sv{color:#0086b3}.markdown-body .pl-e,.markdown-body .pl-en{color:#795da3}.markdown-body .pl-s1 .pl-s2,.markdown-body .pl-smi,.markdown-body .pl-smp,.markdown-body .pl-stj,.markdown-body .pl-vo,.markdown-body .pl-vpf{color:#333}.markdown-body .pl-ent{color:#63a35c}.markdown-body .pl-k,.markdown-body .pl-s,.markdown-body .pl-st{color:#a71d5d}.markdown-body .pl-pds,.markdown-body .pl-s1,.markdown-body .pl-s1 .pl-pse .pl-s2,.markdown-body .pl-sr,.markdown-body .pl-sr .pl-cce,.markdown-body .pl-sr .pl-sra,.markdown-body .pl-sr .pl-sre,.markdown-body .pl-src{color:#df5000}.markdown-body .pl-mo,.markdown-body .pl-v{color:#1d3e81}.markdown-body .pl-id{color:#b52a1d}.markdown-body .pl-ii{background-color:#b52a1d;color:#f8f8f8}.markdown-body .pl-sr .pl-cce{color:#63a35c;font-weight:700}.markdown-body .pl-ml{color:#693a17}.markdown-body .pl-mh,.markdown-body .pl-mh .pl-en,.markdown-body .pl-ms{color:#1d3e81;font-weight:700}.markdown-body .pl-mq{color:teal}.markdown-body .pl-mi{color:#333;font-style:italic}.markdown-body .pl-mb{color:#333;font-weight:700}.markdown-body .pl-md,.markdown-body .pl-mdhf{background-color:#ffecec;color:#bd2c00}.markdown-body .pl-mdht,.markdown-body .pl-mi1{background-color:#eaffea;color:#55a532}.markdown-body .pl-mdr{color:#795da3;font-weight:700}.markdown-body kbd{display:inline-block;padding:3px 5px;font:11px Consolas,"Liberation Mono",Menlo,Courier,monospace;line-height:10px;color:#555;vertical-align:middle;background-color:#fcfcfc;border:1px solid #ccc;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}.markdown-body .task-list-item+.task-list-item{margin-top:3px}.markdown-body .task-list-item input{float:left;margin:.3em 0 .25em -1.6em;vertical-align:middle}.markdown-body :checked+.radio-label{z-index:1;position:relative;border-color:#4183c4}.editormd-html-preview,.editormd-preview-container{text-align:left;font-size:14px;line-height:1.6;padding:20px;overflow:auto;width:100%;background-color:#fff}.editormd-html-preview blockquote,.editormd-preview-container blockquote{color:#666;border-left:4px solid #ddd;padding-left:20px;margin-left:0;font-size:14px;font-style:italic}.editormd-html-preview p code,.editormd-preview-container p code{margin-left:5px;margin-right:4px}.editormd-html-preview abbr,.editormd-preview-container abbr{background:#ffd}.editormd-html-preview hr,.editormd-preview-container hr{height:1px;border:none;border-top:1px solid #ddd;background:0 0}.editormd-html-preview code,.editormd-preview-container code{border:1px solid #ddd;background:#f6f6f6;padding:3px;border-radius:3px;font-size:14px}.editormd-html-preview pre,.editormd-preview-container pre{border:1px solid #ddd;background:#f6f6f6;padding:10px;-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px}.editormd-html-preview pre code,.editormd-preview-container pre code{padding:0}.editormd-html-preview code,.editormd-html-preview kbd,.editormd-html-preview pre,.editormd-preview-container code,.editormd-preview-container kbd,.editormd-preview-container pre{font-family:"YaHei Consolas Hybrid",Consolas,"Meiryo UI","Malgun Gothic","Segoe UI","Trebuchet MS",Helvetica,monospace,monospace}.editormd-html-preview table thead tr,.editormd-preview-container table thead tr{background-color:#F8F8F8}.editormd-html-preview p.editormd-tex,.editormd-preview-container p.editormd-tex{text-align:center}.editormd-html-preview span.editormd-tex,.editormd-preview-container span.editormd-tex{margin:0 5px}.editormd-html-preview .emoji,.editormd-preview-container .emoji{width:24px;height:24px}.editormd-html-preview .katex,.editormd-preview-container .katex{font-size:1.4em}.editormd-html-preview .flowchart,.editormd-html-preview .sequence-diagram,.editormd-preview-container .flowchart,.editormd-preview-container .sequence-diagram{margin:0 auto;text-align:center}.editormd-html-preview .flowchart svg,.editormd-html-preview .sequence-diagram svg,.editormd-preview-container .flowchart svg,.editormd-preview-container .sequence-diagram svg{margin:0 auto}.editormd-html-preview .flowchart text,.editormd-html-preview .sequence-diagram text,.editormd-preview-container .flowchart text,.editormd-preview-container .sequence-diagram text{font-size:15px!important;font-family:"YaHei Consolas Hybrid",Consolas,"Microsoft YaHei","Malgun Gothic","Segoe UI",Helvetica,Arial!important}/*! Pretty printing styles. Used with prettify.js. */.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.clo,.opn,.pun{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.kwd,.tag,.typ{font-weight:700}.str{color:#060}.kwd{color:#006}.com{color:#600;font-style:italic}.typ{color:#404}.lit{color:#044}.clo,.opn,.pun{color:#440}.tag{color:#006}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}.editormd-html-preview pre.prettyprint,.editormd-preview-container pre.prettyprint{padding:10px;border:1px solid #ddd;white-space:pre-wrap;word-wrap:break-word}.editormd-html-preview ol.linenums,.editormd-preview-container ol.linenums{color:#999;padding-left:2.5em}.editormd-html-preview ol.linenums li,.editormd-preview-container ol.linenums li{list-style-type:decimal}.editormd-html-preview ol.linenums li code,.editormd-preview-container ol.linenums li code{border:none;background:0 0;padding:0}.editormd-html-preview .editormd-toc-menu,.editormd-preview-container .editormd-toc-menu{margin:8px 0 12px;display:inline-block}.editormd-html-preview .editormd-toc-menu>.markdown-toc,.editormd-preview-container .editormd-toc-menu>.markdown-toc{position:relative;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;border:1px solid #ddd;display:inline-block;font-size:1em}.editormd-html-preview .editormd-toc-menu>.markdown-toc>ul,.editormd-preview-container .editormd-toc-menu>.markdown-toc>ul{width:160%;min-width:180px;position:absolute;left:-1px;top:-2px;z-index:100;padding:0 10px 10px;display:none;background:#fff;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 3px 5px rgba(0,0,0,.2);-moz-box-shadow:0 3px 5px rgba(0,0,0,.2);-ms-box-shadow:0 3px 5px rgba(0,0,0,.2);-o-box-shadow:0 3px 5px rgba(0,0,0,.2);box-shadow:0 3px 5px rgba(0,0,0,.2)}.editormd-html-preview .editormd-toc-menu>.markdown-toc>ul>li ul,.editormd-preview-container .editormd-toc-menu>.markdown-toc>ul>li ul{width:100%;min-width:180px;border:1px solid #ddd;display:none;background:#fff;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px}.editormd-html-preview .editormd-toc-menu .toc-menu-btn:hover,.editormd-html-preview .editormd-toc-menu>.markdown-toc>ul>li a:hover,.editormd-preview-container .editormd-toc-menu .toc-menu-btn:hover,.editormd-preview-container .editormd-toc-menu>.markdown-toc>ul>li a:hover{background-color:#f6f6f6}.editormd-html-preview .editormd-toc-menu>.markdown-toc>ul>li a,.editormd-preview-container .editormd-toc-menu>.markdown-toc>ul>li a{color:#666;padding:6px 10px;display:block;-webkit-transition:background-color 500ms ease-out;-moz-transition:background-color 500ms ease-out;transition:background-color 500ms ease-out}.editormd-html-preview .editormd-toc-menu>.markdown-toc li,.editormd-preview-container .editormd-toc-menu>.markdown-toc li{position:relative}.editormd-html-preview .editormd-toc-menu>.markdown-toc li>ul,.editormd-preview-container .editormd-toc-menu>.markdown-toc li>ul{position:absolute;top:32px;left:10%;display:none;-webkit-box-shadow:0 3px 5px rgba(0,0,0,.2);-moz-box-shadow:0 3px 5px rgba(0,0,0,.2);-ms-box-shadow:0 3px 5px rgba(0,0,0,.2);-o-box-shadow:0 3px 5px rgba(0,0,0,.2);box-shadow:0 3px 5px rgba(0,0,0,.2)}.editormd-html-preview .editormd-toc-menu>.markdown-toc li>ul:after,.editormd-html-preview .editormd-toc-menu>.markdown-toc li>ul:before,.editormd-preview-container .editormd-toc-menu>.markdown-toc li>ul:after,.editormd-preview-container .editormd-toc-menu>.markdown-toc li>ul:before{pointer-events:pointer-events;position:absolute;left:15px;top:-6px;display:block;content:"";width:0;height:0;border:6px solid transparent;border-width:0 6px 6px;z-index:10}.editormd-html-preview .editormd-toc-menu>.markdown-toc li>ul:before,.editormd-preview-container .editormd-toc-menu>.markdown-toc li>ul:before{border-bottom-color:#ccc}.editormd-html-preview .editormd-toc-menu>.markdown-toc li>ul:after,.editormd-preview-container .editormd-toc-menu>.markdown-toc li>ul:after{border-bottom-color:#fff;top:-5px}.editormd-html-preview .editormd-toc-menu ul,.editormd-preview-container .editormd-toc-menu ul{list-style:none}.editormd-html-preview .editormd-toc-menu a,.editormd-preview-container .editormd-toc-menu a{text-decoration:none}.editormd-html-preview .editormd-toc-menu h1,.editormd-preview-container .editormd-toc-menu h1{font-size:16px;padding:5px 0 10px 10px;line-height:1;border-bottom:1px solid #eee}.editormd-html-preview .editormd-toc-menu h1 .fa,.editormd-preview-container .editormd-toc-menu h1 .fa{padding-left:10px}.editormd-html-preview .editormd-toc-menu .toc-menu-btn,.editormd-preview-container .editormd-toc-menu .toc-menu-btn{color:#666;min-width:180px;padding:5px 10px;border-radius:4px;display:inline-block;-webkit-transition:background-color 500ms ease-out;-moz-transition:background-color 500ms ease-out;transition:background-color 500ms ease-out}.editormd-html-preview .editormd-toc-menu .toc-menu-btn .fa,.editormd-preview-container .editormd-toc-menu .toc-menu-btn .fa{float:right;padding:3px 0 0 10px;font-size:1.3em}.markdown-body .editormd-toc-menu ul{padding-left:0}.markdown-body .highlight pre,.markdown-body pre{line-height:1.6}hr.editormd-page-break{border:1px dotted #ccc;font-size:0;height:2px}@media only print{hr.editormd-page-break{background:0 0;border:none;height:0}}.editormd-html-preview textarea{display:none}.editormd-html-preview hr.editormd-page-break{background:0 0;border:none;height:0}.editormd-preview-close-btn{color:#fff;padding:4px 6px;font-size:18px;-webkit-border-radius:500px;-moz-border-radius:500px;-ms-border-radius:500px;-o-border-radius:500px;border-radius:500px;display:none;background-color:#ccc;position:absolute;top:25px;right:35px;z-index:19;-webkit-transition:background-color 300ms ease-out;-moz-transition:background-color 300ms ease-out;transition:background-color 300ms ease-out}.editormd-preview-close-btn:hover{background-color:#999}.editormd-preview-active{width:100%;padding:40px} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/editormd.js.html b/paicoding-ui/src/main/resources/static/editormd/docs/editormd.js.html new file mode 100644 index 000000000..c191f8a90 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/docs/editormd.js.html @@ -0,0 +1,4407 @@ + + + + + JSDoc: Source: editormd.js + + + + + + + + + + +

+ +

Source: editormd.js

+ + + + + + +
+
+
/*
+ * Editor.md
+ *
+ * @file        editormd.js 
+ * @version     v1.4.5 
+ * @description Open source online markdown editor.
+ * @license     MIT License
+ * @author      Pandao
+ * {@link       https://github.com/pandao/editor.md}
+ * @updateTime  2015-06-02
+ */
+
+;(function(factory) {
+    "use strict";
+    
+	// CommonJS/Node.js
+	if (typeof require === "function" && typeof exports === "object" && typeof module === "object")
+    { 
+        module.exports = factory;
+    }
+	else if (typeof define === "function")  // AMD/CMD/Sea.js
+	{
+        if (define.amd) // for Require.js
+        {
+            /* Require.js define replace */
+        } 
+        else 
+        {
+		    define(["jquery"], factory);  // for Sea.js
+        }
+	} 
+	else
+	{ 
+        window.editormd = factory();
+	}
+    
+}(function() {    
+
+    /* Require.js assignment replace */
+    
+    "use strict";
+    
+    var $ = (typeof (jQuery) !== "undefined") ? jQuery : Zepto;
+
+	if (typeof ($) === "undefined") {
+		return ;
+	}
+    
+    /**
+     * editormd
+     * 
+     * @param   {String} id           编辑器的ID
+     * @param   {Object} options      配置选项 Key/Value
+     * @returns {Object} editormd     返回editormd对象
+     */
+    
+    var editormd         = function (id, options) {
+        return new editormd.fn.init(id, options);
+    };
+    
+    editormd.title        = editormd.$name = "Editor.md";
+    editormd.version      = "1.4.5";
+    editormd.homePage     = "https://pandao.github.io/editor.md/";
+    editormd.classPrefix  = "editormd-";
+    
+    editormd.toolbarModes = {
+        full : [
+            "undo", "redo", "|", 
+            "bold", "del", "italic", "quote", "ucwords", "uppercase", "lowercase", "|", 
+            "h1", "h2", "h3", "h4", "h5", "h6", "|", 
+            "list-ul", "list-ol", "hr", "|",
+            "link", "reference-link", "image", "code", "preformatted-text", "code-block", "table", "datetime", "emoji", "html-entities", "pagebreak", "|",
+            "goto-line", "watch", "preview", "fullscreen", "clear", "search", "|",
+            "help", "info"
+        ],
+        simple : [
+            "undo", "redo", "|", 
+            "bold", "del", "italic", "quote", "uppercase", "lowercase", "|", 
+            "h1", "h2", "h3", "h4", "h5", "h6", "|", 
+            "list-ul", "list-ol", "hr", "|",
+            "watch", "preview", "fullscreen", "|",
+            "help", "info"
+        ],
+        mini : [
+            "undo", "redo", "|",
+            "watch", "preview", "|",
+            "help", "info"
+        ]
+    };
+    
+    editormd.defaults     = {
+        mode                 : "gfm",          //gfm or markdown
+        theme                : "default",
+        name                 : "",
+        value                : "",             // value for CodeMirror, if mode not gfm/markdown
+        markdown             : "",
+        appendMarkdown       : "",             // if in init textarea value not empty, append markdown to textarea
+        width                : "100%",
+        height               : "100%",
+        path                 : "./lib/",       // Dependents module file directory
+        pluginPath           : "",             // If this empty, default use settings.path + "../plugins/"
+        delay                : 300,            // Delay parse markdown to html, Uint : ms
+        autoLoadModules      : true,           // Automatic load dependent module files
+        watch                : true,
+        placeholder          : "Enjoy Markdown! coding now...",
+        gotoLine             : true,
+        codeFold             : false,
+        autoHeight           : false,
+		autoFocus            : true,
+        autoCloseTags        : true,
+        searchReplace        : true,
+        syncScrolling        : true,
+        readOnly             : false,
+        tabSize              : 4,
+		indentUnit           : 4,
+        lineNumbers          : true,
+		lineWrapping         : true,
+		autoCloseBrackets    : true,
+		showTrailingSpace    : true,
+		matchBrackets        : true,
+		indentWithTabs       : true,
+		styleSelectedText    : true,
+        matchWordHighlight   : true,           // options: true, false, "onselected"
+        styleActiveLine      : true,           // Highlight the current line
+        dialogLockScreen     : true,
+        dialogShowMask       : true,
+        dialogDraggable      : true,
+        dialogMaskBgColor    : "#fff",
+        dialogMaskOpacity    : 0.1,
+        fontSize             : "13px",
+        saveHTMLToTextarea   : false,
+        disabledKeyMaps      : [],
+        
+        onload               : function() {},
+        onresize             : function() {},
+        onchange             : function() {},
+        onwatch              : null,
+        onunwatch            : null,
+        onpreviewing         : function() {},
+        onpreviewed          : function() {},
+        onfullscreen         : function() {},
+        onfullscreenExit     : function() {},
+        onscroll             : function() {},
+        onpreviewscroll      : function() {},
+        
+        imageUpload          : false,
+        imageFormats         : ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
+        imageUploadURL       : "",
+        crossDomainUpload    : false,
+        uploadCallbackURL    : "",
+        
+        toc                  : true,           // Table of contents
+        tocm                 : false,           // Using [TOCM], auto create ToC dropdown menu
+        tocTitle             : "",             // for ToC dropdown menu btn
+        tocDropdown          : false,
+        tocContainer         : "",
+        tocStartLevel        : 1,              // Said from H1 to create ToC
+        htmlDecode           : false,          // Open the HTML tag identification 
+        pageBreak            : true,           // Enable parse page break [========]
+        atLink               : true,           // for @link
+        emailLink            : true,           // for email address auto link
+        taskList             : false,          // Enable Github Flavored Markdown task lists
+        emoji                : false,          // :emoji: , Support Github emoji, Twitter Emoji (Twemoji);
+                                               // Support FontAwesome icon emoji :fa-xxx: > Using fontAwesome icon web fonts;
+                                               // Support Editor.md logo icon emoji :editormd-logo: :editormd-logo-1x: > 1~8x;
+        tex                  : false,          // TeX(LaTeX), based on KaTeX
+        flowChart            : false,          // flowChart.js only support IE9+
+        sequenceDiagram      : false,          // sequenceDiagram.js only support IE9+
+        previewCodeHighlight : true,
+                
+        toolbar              : true,           // show/hide toolbar
+        toolbarAutoFixed     : true,           // on window scroll auto fixed position
+        toolbarIcons         : "full",
+        toolbarTitles        : {},
+        toolbarHandlers      : {
+            ucwords : function() {
+                return editormd.toolbarHandlers.ucwords;
+            },
+            lowercase : function() {
+                return editormd.toolbarHandlers.lowercase;
+            }
+        },
+        toolbarCustomIcons   : {               // using html tag create toolbar icon, unused default <a> tag.
+            lowercase        : "<a href=\"javascript:;\" title=\"Lowercase\" unselectable=\"on\"><i class=\"fa\" name=\"lowercase\" style=\"font-size:24px;margin-top: -10px;\">a</i></a>",
+            "ucwords"        : "<a href=\"javascript:;\" title=\"ucwords\" unselectable=\"on\"><i class=\"fa\" name=\"ucwords\" style=\"font-size:20px;margin-top: -3px;\">Aa</i></a>"
+        }, 
+        toolbarIconsClass    : {
+            undo             : "fa-undo",
+            redo             : "fa-repeat",
+            bold             : "fa-bold",
+            del              : "fa-strikethrough",
+            italic           : "fa-italic",
+            quote            : "fa-quote-left",
+            uppercase        : "fa-font",
+            h1               : editormd.classPrefix + "bold",
+            h2               : editormd.classPrefix + "bold",
+            h3               : editormd.classPrefix + "bold",
+            h4               : editormd.classPrefix + "bold",
+            h5               : editormd.classPrefix + "bold",
+            h6               : editormd.classPrefix + "bold",
+            "list-ul"        : "fa-list-ul",
+            "list-ol"        : "fa-list-ol",
+            hr               : "fa-minus",
+            link             : "fa-link",
+            "reference-link" : "fa-anchor",
+            image            : "fa-picture-o",
+            code             : "fa-code",
+            "preformatted-text" : "fa-file-code-o",
+            "code-block"     : "fa-file-code-o",
+            table            : "fa-table",
+            datetime         : "fa-clock-o",
+            emoji            : "fa-smile-o",
+            "html-entities"  : "fa-copyright",
+            pagebreak        : "fa-newspaper-o",
+            "goto-line"      : "fa-terminal", // fa-crosshairs
+            watch            : "fa-eye-slash",
+            unwatch          : "fa-eye",
+            preview          : "fa-desktop",
+            search           : "fa-search",
+            fullscreen       : "fa-arrows-alt",
+            clear            : "fa-eraser",
+            help             : "fa-question-circle",
+            info             : "fa-info-circle"
+        },        
+        toolbarIconTexts     : {},
+        
+        lang : {
+            name        : "zh-cn",
+            description : "开源在线Markdown编辑器<br/>Open source online Markdown editor.",
+            tocTitle    : "目录",
+            toolbar     : {
+                undo             : "撤销(Ctrl+Z)",
+                redo             : "重做(Ctrl+Y)",
+                bold             : "粗体",
+                del              : "删除线",
+                italic           : "斜体",
+                quote            : "引用",
+                ucwords          : "将每个单词首字母转成大写",
+                uppercase        : "将所选转换成大写",
+                lowercase        : "将所选转换成小写",
+                h1               : "标题1",
+                h2               : "标题2",
+                h3               : "标题3",
+                h4               : "标题4",
+                h5               : "标题5",
+                h6               : "标题6",
+                "list-ul"        : "无序列表",
+                "list-ol"        : "有序列表",
+                hr               : "横线",
+                link             : "链接",
+                "reference-link" : "引用链接",
+                image            : "添加图片",
+                code             : "行内代码",
+                "preformatted-text" : "预格式文本 / 代码块(缩进风格)",
+                "code-block"     : "代码块(多语言风格)",
+                table            : "添加表格",
+                datetime         : "日期时间",
+                emoji            : "Emoji表情",
+                "html-entities"  : "HTML实体字符",
+                pagebreak        : "插入分页符",
+                "goto-line"      : "跳转到行",
+                watch            : "关闭实时预览",
+                unwatch          : "开启实时预览",
+                preview          : "全窗口预览HTML(按 Shift + ESC还原)",
+                fullscreen       : "全屏(按ESC还原)",
+                clear            : "清空",
+                search           : "搜索",
+                help             : "使用帮助",
+                info             : "关于" + editormd.title
+            },
+            buttons : {
+                enter  : "确定",
+                cancel : "取消",
+                close  : "关闭"
+            },
+            dialog : {
+                link : {
+                    title    : "添加链接",
+                    url      : "链接地址",
+                    urlTitle : "链接标题",
+                    urlEmpty : "错误:请填写链接地址。"
+                },
+                referenceLink : {
+                    title    : "添加引用链接",
+                    name     : "引用名称",
+                    url      : "链接地址",
+                    urlId    : "链接ID",
+                    urlTitle : "链接标题",
+                    nameEmpty: "错误:引用链接的名称不能为空。",
+                    idEmpty  : "错误:请填写引用链接的ID。",
+                    urlEmpty : "错误:请填写引用链接的URL地址。"
+                },
+                image : {
+                    title    : "添加图片",
+                    url      : "图片地址",
+                    link     : "图片链接",
+                    alt      : "图片描述",
+                    uploadButton     : "本地上传",
+                    imageURLEmpty    : "错误:图片地址不能为空。",
+                    uploadFileEmpty  : "错误:上传的图片不能为空。",
+                    formatNotAllowed : "错误:只允许上传图片文件,允许上传的图片文件格式有:"
+                },
+                preformattedText : {
+                    title             : "添加预格式文本或代码块", 
+                    emptyAlert        : "错误:请填写预格式文本或代码的内容。"
+                },
+                codeBlock : {
+                    title             : "添加代码块",                    
+                    selectLabel       : "代码语言:",
+                    selectDefaultText : "请选择代码语言",
+                    otherLanguage     : "其他语言",
+                    unselectedLanguageAlert : "错误:请选择代码所属的语言类型。",
+                    codeEmptyAlert    : "错误:请填写代码内容。"
+                },
+                htmlEntities : {
+                    title : "HTML 实体字符"
+                },
+                help : {
+                    title : "使用帮助"
+                }
+            }
+        }
+    };
+    
+    editormd.classNames  = {
+        tex : editormd.classPrefix + "tex"
+    };
+
+    editormd.dialogZindex = 99999;
+    
+    editormd.$katex       = null;
+    editormd.$marked      = null;
+    editormd.$CodeMirror  = null;
+    editormd.$prettyPrint = null;
+    
+    var timer, flowchartTimer;
+
+    editormd.prototype    = editormd.fn = {
+        state : {
+            watching   : false,
+            loaded     : false,
+            preview    : false,
+            fullscreen : false
+        },
+        
+        /**
+         * 构造函数/实例初始化
+         * Constructor / instance initialization
+         * 
+         * @param   {String}   id            编辑器的ID
+         * @param   {Object}   [options={}]  配置选项 Key/Value
+         * @returns {editormd}               返回editormd的实例对象
+         */
+        
+        init : function (id, options) {
+            
+            options              = options || {};
+            
+            if (typeof id === "object")
+            {
+                options = id;
+            }
+            
+            var _this            = this;
+            var classPrefix      = this.classPrefix  = editormd.classPrefix; 
+            var settings         = this.settings     = $.extend(true, editormd.defaults, options);
+            
+            id                   = (typeof id === "object") ? settings.id : id;
+            
+            var editor           = this.editor       = $("#" + id);
+            
+            this.id              = id;
+            this.lang            = settings.lang;
+            
+            var classNames       = this.classNames   = {
+                textarea : {
+                    html     : classPrefix + "html-textarea",
+                    markdown : classPrefix + "markdown-textarea"
+                }
+            };
+            
+            settings.pluginPath = (settings.pluginPath === "") ? settings.path + "../plugins/" : settings.pluginPath; 
+            
+            this.state.watching = (settings.watch) ? true : false;
+            
+            if ( !editor.hasClass("editormd") ) {
+                editor.addClass("editormd");
+            }
+            
+            editor.css({
+                width  : (typeof settings.width  === "number") ? settings.width  + "px" : settings.width,
+                height : (typeof settings.height === "number") ? settings.height + "px" : settings.height
+            });
+            
+            if (settings.autoHeight)
+            {
+                editor.css("height", "auto");
+            }
+                        
+            var markdownTextarea = this.markdownTextarea = editor.children("textarea");
+            
+            if (markdownTextarea.length < 1)
+            {
+                editor.append("<textarea></textarea>");
+                markdownTextarea = this.markdownTextarea = editor.children("textarea");
+            }
+            
+            markdownTextarea.addClass(classNames.textarea.markdown).attr("placeholder", settings.placeholder);
+            
+            if (typeof markdownTextarea.attr("name") === "undefined" || markdownTextarea.attr("name") === "")
+            {
+                markdownTextarea.attr("name", (settings.name !== "") ? settings.name : id + "-markdown-doc");
+            }
+            
+            var appendElements = [
+                (!settings.readOnly) ? "<a href=\"javascript:;\" class=\"fa fa-close " + classPrefix + "preview-close-btn\"></a>" : "",
+                ( (settings.saveHTMLToTextarea) ? "<textarea class=\"" + classNames.textarea.html + "\" name=\"" + id + "-html-code\"></textarea>" : "" ),
+                "<div class=\"" + classPrefix + "preview\"><div class=\"markdown-body " + classPrefix + "preview-container\"></div></div>",
+                "<div class=\"" + classPrefix + "container-mask\" style=\"display:block;\"></div>",
+                "<div class=\"" + classPrefix + "mask\"></div>"
+            ].join("\n");
+            
+            editor.append(appendElements).addClass(classPrefix + "vertical");
+            
+            this.mask          = editor.children("." + classPrefix + "mask");    
+            this.containerMask = editor.children("." + classPrefix  + "container-mask");
+            
+            if (settings.markdown !== "")
+            {
+                markdownTextarea.val(settings.markdown);
+            }
+            
+            if (settings.appendMarkdown !== "")
+            {
+                markdownTextarea.val(markdownTextarea.val() + settings.appendMarkdown);
+            }
+            
+            this.htmlTextarea     = editor.children("." + classNames.textarea.html);            
+            this.preview          = editor.children("." + classPrefix + "preview");
+            this.previewContainer = this.preview.children("." + classPrefix + "preview-container");
+            
+            if (typeof define === "function" && define.amd)
+            {
+                if (typeof katex !== "undefined") 
+                {
+                    editormd.$katex = katex;
+                }
+                
+                if (settings.searchReplace && !settings.readOnly) 
+                {
+                    editormd.loadCSS(settings.path + "codemirror/addon/dialog/dialog");
+                    editormd.loadCSS(settings.path + "codemirror/addon/search/matchesonscrollbar");
+                }
+            }
+            
+            if ((typeof define === "function" && define.amd) || !settings.autoLoadModules)
+            {
+                if (typeof CodeMirror !== "undefined") {
+                    editormd.$CodeMirror = CodeMirror;
+                }
+                
+                if (typeof marked     !== "undefined") {
+                    editormd.$marked     = marked;
+                }
+                
+                this.setCodeMirror().setToolbar().loadedDisplay();
+            } 
+            else 
+            {
+                this.loadQueues();
+            }
+
+            return this;
+        },
+        
+        /**
+         * 所需组件加载队列
+         * Required components loading queue
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        loadQueues : function() {
+            var _this        = this;
+            var settings     = this.settings;
+            var loadPath     = settings.path;
+                                
+            var loadFlowChartOrSequenceDiagram = function() {
+                
+                if (editormd.isIE8) 
+                {
+                    _this.loadedDisplay();
+                    
+                    return ;
+                }
+
+                if (settings.flowChart || settings.sequenceDiagram) 
+                {
+                    editormd.loadScript(loadPath + "raphael.min", function() {
+
+                        editormd.loadScript(loadPath + "underscore.min", function() {  
+
+                            if (!settings.flowChart && settings.sequenceDiagram) 
+                            {
+                                editormd.loadScript(loadPath + "sequence-diagram.min", function() {
+                                    _this.loadedDisplay();
+                                });
+                            }
+                            else if (settings.flowChart && !settings.sequenceDiagram) 
+                            {      
+                                editormd.loadScript(loadPath + "flowchart.min", function() {  
+                                    editormd.loadScript(loadPath + "jquery.flowchart.min", function() {
+                                        _this.loadedDisplay();
+                                    });
+                                });
+                            }
+                            else if (settings.flowChart && settings.sequenceDiagram) 
+                            {  
+                                editormd.loadScript(loadPath + "flowchart.min", function() {  
+                                    editormd.loadScript(loadPath + "jquery.flowchart.min", function() {
+                                        editormd.loadScript(loadPath + "sequence-diagram.min", function() {
+                                            _this.loadedDisplay();
+                                        });
+                                    });
+                                });
+                            }
+                        });
+
+                    });
+                } 
+                else
+                {
+                    _this.loadedDisplay();
+                }
+            }; 
+
+            editormd.loadCSS(loadPath + "codemirror/codemirror.min");
+            
+            if (settings.searchReplace && !settings.readOnly)
+            {
+                editormd.loadCSS(loadPath + "codemirror/addon/dialog/dialog");
+                editormd.loadCSS(loadPath + "codemirror/addon/search/matchesonscrollbar");
+            }
+            
+            if (settings.codeFold)
+            {
+                editormd.loadCSS(loadPath + "codemirror/addon/fold/foldgutter");            
+            }
+            
+            editormd.loadScript(loadPath + "codemirror/codemirror.min", function() {
+                editormd.$CodeMirror = CodeMirror;
+                
+                editormd.loadScript(loadPath + "codemirror/modes.min", function() {
+                    
+                    editormd.loadScript(loadPath + "codemirror/addons.min", function() {
+                        
+                        _this.setCodeMirror();
+                        
+                        if (settings.mode !== "gfm" && settings.mode !== "markdown") 
+                        {
+                            _this.loadedDisplay();
+                            
+                            return false;
+                        }
+                        
+                        _this.setToolbar();
+
+                        editormd.loadScript(loadPath + "marked.min", function() {
+
+                            editormd.$marked = marked;
+                                
+                            if (settings.previewCodeHighlight) 
+                            {
+                                editormd.loadScript(loadPath + "prettify.min", function() {
+                                    loadFlowChartOrSequenceDiagram();
+                                });
+                            } 
+                            else
+                            {                  
+                                loadFlowChartOrSequenceDiagram();
+                            }
+                        });
+                        
+                    });
+                    
+                });
+                
+            });
+
+            return this;
+        },
+        
+        /**
+         * 设置CodeMirror的主题
+         * Setting CodeMirror theme
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setTheme : function(theme) {  
+            var settings   = this.settings;  
+            settings.theme = theme;  
+            
+            if (theme !== "default")
+            {
+                editormd.loadCSS(settings.path + "codemirror/theme/" + settings.theme);
+            }
+            
+            this.cm.setOption("theme", theme);
+            
+            return this;
+        },
+        
+        /**
+         * 配置和初始化CodeMirror组件
+         * CodeMirror initialization
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setCodeMirror : function() { 
+            var settings         = this.settings;
+            var editor           = this.editor;
+            
+            if (settings.theme !== "default")
+            {
+                editormd.loadCSS(settings.path + "codemirror/theme/" + settings.theme);
+            }
+            
+            var codeMirrorConfig = {
+                mode                      : settings.mode,
+                theme                     : settings.theme,
+                tabSize                   : settings.tabSize,
+                dragDrop                  : false,
+                autofocus                 : settings.autoFocus,
+                autoCloseTags             : settings.autoCloseTags,
+                readOnly                  : (settings.readOnly) ? "nocursor" : false,
+                indentUnit                : settings.indentUnit,
+                lineNumbers               : settings.lineNumbers,
+                lineWrapping              : settings.lineWrapping,
+                extraKeys                 : {
+                                                "Ctrl-Q": function(cm) { 
+                                                    cm.foldCode(cm.getCursor()); 
+                                                }
+                                            },
+                foldGutter                : settings.codeFold,
+                gutters                   : ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
+                matchBrackets             : settings.matchBrackets,
+                indentWithTabs            : settings.indentWithTabs,
+                styleActiveLine           : settings.styleActiveLine,
+                styleSelectedText         : settings.styleSelectedText,
+                autoCloseBrackets         : settings.autoCloseBrackets,
+                showTrailingSpace         : settings.showTrailingSpace,
+                highlightSelectionMatches : ( (!settings.matchWordHighlight) ? false : { showToken: (settings.matchWordHighlight === "onselected") ? false : /\w/ } )
+            };
+            
+            this.codeEditor = this.cm        = editormd.$CodeMirror.fromTextArea(this.markdownTextarea[0], codeMirrorConfig);
+            this.codeMirror = this.cmElement = editor.children(".CodeMirror");
+            
+            if (settings.value !== "")
+            {
+                this.cm.setValue(settings.value);
+            }
+
+            this.codeMirror.css({
+                fontSize : settings.fontSize,
+                width    : (!settings.watch) ? "100%" : "50%"
+            });
+            
+            if (settings.autoHeight)
+            {
+                this.codeMirror.css("height", "auto");
+                this.cm.setOption("viewportMargin", Infinity);
+            }
+
+            return this;
+        },
+        
+        /**
+         * 获取CodeMirror的配置选项
+         * Get CodeMirror setting options
+         * 
+         * @returns {Mixed}                  return CodeMirror setting option value
+         */
+        
+        getCodeMirrorOption : function(key) {            
+            return this.cm.getOption(key);
+        },
+        
+        /**
+         * 配置和重配置CodeMirror的选项
+         * CodeMirror setting options / resettings
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setCodeMirrorOption : function(key, value) {
+            
+            this.cm.setOption(key, value);
+            
+            return this;
+        },
+        
+        /**
+         * 添加 CodeMirror 键盘快捷键
+         * Add CodeMirror keyboard shortcuts key map
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        addKeyMap : function(map, bottom) {
+            this.cm.addKeyMap(map, bottom);
+            
+            return this;
+        },
+        
+        /**
+         * 移除 CodeMirror 键盘快捷键
+         * Remove CodeMirror keyboard shortcuts key map
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        removeKeyMap : function(map) {
+            this.cm.removeKeyMap(map);
+            
+            return this;
+        },
+        
+        /**
+         * 跳转到指定的行
+         * Goto CodeMirror line
+         * 
+         * @param   {String|Intiger}   line      line number or "first"|"last"
+         * @returns {editormd}                   返回editormd的实例对象
+         */
+        
+        gotoLine : function (line) {
+            
+            var settings = this.settings;
+            
+            if (!settings.gotoLine)
+            {
+                return this;
+            }
+            
+            var cm       = this.cm;
+            var editor   = this.editor;
+            var count    = cm.lineCount();
+            var preview  = this.preview;
+            
+            if (typeof line === "string")
+            {
+                if(line === "last")
+                {
+                    line = count;
+                }
+            
+                if (line === "first")
+                {
+                    line = 1;
+                }
+            }
+            
+            if (typeof line !== "number") 
+            {  
+                alert("Error: The line number must be an integer.");
+                return this;
+            }
+            
+            line  = parseInt(line) - 1;
+            
+            if (line > count)
+            {
+                alert("Error: The line number range 1-" + count);
+                
+                return this;
+            }
+            
+            cm.setCursor( {line : line, ch : 0} );
+            
+            var scrollInfo   = cm.getScrollInfo();
+            var clientHeight = scrollInfo.clientHeight; 
+            var coords       = cm.charCoords({line : line, ch : 0}, "local");
+            
+            cm.scrollTo(null, (coords.top + coords.bottom - clientHeight) / 2);
+            
+            if (settings.watch)
+            {            
+                var cmScroll  = this.codeMirror.find(".CodeMirror-scroll")[0];
+                var height    = $(cmScroll).height(); 
+                var scrollTop = cmScroll.scrollTop;         
+                var percent   = (scrollTop / cmScroll.scrollHeight);
+
+                if (scrollTop === 0)
+                {
+                    preview.scrollTop(0);
+                } 
+                else if (scrollTop + height >= cmScroll.scrollHeight - 16)
+                { 
+                    preview.scrollTop(preview[0].scrollHeight);                    
+                } 
+                else
+                {                    
+                    preview.scrollTop(preview[0].scrollHeight * percent);
+                }
+            }
+
+            cm.focus();
+            
+            return this;
+        },
+        
+        /**
+         * 扩展当前实例对象,可同时设置多个或者只设置一个
+         * Extend editormd instance object, can mutil setting.
+         * 
+         * @returns {editormd}                  this(editormd instance object.)
+         */
+        
+        extend : function() {
+            if (typeof arguments[1] !== "undefined")
+            {
+                if (typeof arguments[1] === "function")
+                {
+                    arguments[1] = $.proxy(arguments[1], this);
+                }
+
+                this[arguments[0]] = arguments[1];
+            }
+            
+            if (typeof arguments[0] === "object" && typeof arguments[0].length === "undefined")
+            {
+                $.extend(true, this, arguments[0]);
+            }
+
+            return this;
+        },
+        
+        /**
+         * 设置或扩展当前实例对象,单个设置
+         * Extend editormd instance object, one by one
+         * 
+         * @param   {String|Object}   key       option key
+         * @param   {String|Object}   value     option value
+         * @returns {editormd}                  this(editormd instance object.)
+         */
+        
+        set : function (key, value) {
+            
+            if (typeof value !== "undefined" && typeof value === "function")
+            {
+                value = $.proxy(value, this);
+            }
+            
+            this[key] = value;
+
+            return this;
+        },
+        
+        /**
+         * 重新配置
+         * Resetting editor options
+         * 
+         * @param   {String|Object}   key       option key
+         * @param   {String|Object}   value     option value
+         * @returns {editormd}                  this(editormd instance object.)
+         */
+        
+        config : function(key, value) {
+            var settings = this.settings;
+            
+            if (typeof key === "object")
+            {
+                settings = $.extend(true, settings, key);
+            }
+            
+            if (typeof key === "string")
+            {
+                settings[key] = value;
+            }
+            
+            this.settings = settings;
+            this.recreate();
+            
+            return this;
+        },
+        
+        /**
+         * 注册事件处理方法
+         * Bind editor event handle
+         * 
+         * @param   {String}     eventType      event type
+         * @param   {Function}   callback       回调函数
+         * @returns {editormd}                  this(editormd instance object.)
+         */
+        
+        on : function(eventType, callback) {
+            var settings = this.settings;
+            
+            if (typeof settings["on" + eventType] !== "undefined") 
+            {                
+                settings["on" + eventType] = $.proxy(callback, this);      
+            }
+
+            return this;
+        },
+        
+        /**
+         * 解除事件处理方法
+         * Unbind editor event handle
+         * 
+         * @param   {String}   eventType          event type
+         * @returns {editormd}                    this(editormd instance object.)
+         */
+        
+        off : function(eventType) {
+            var settings = this.settings;
+            
+            if (typeof settings["on" + eventType] !== "undefined") 
+            {
+                settings["on" + eventType] = function(){};
+            }
+            
+            return this;
+        },
+        
+        /**
+         * 显示工具栏
+         * Display toolbar
+         * 
+         * @param   {Function} [callback=function(){}] 回调函数
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        showToolbar : function(callback) {
+            var settings = this.settings;
+            
+            if(settings.readOnly) {
+                return this;
+            }
+            
+            if (settings.toolbar && (this.toolbar.length < 1 || this.toolbar.find("." + this.classPrefix + "menu").html() === "") )
+            {
+                this.setToolbar();
+            }
+            
+            settings.toolbar = true; 
+            
+            this.toolbar.show();
+            this.resize();
+            
+            $.proxy(callback || function(){}, this)();
+
+            return this;
+        },
+        
+        /**
+         * 隐藏工具栏
+         * Hide toolbar
+         * 
+         * @param   {Function} [callback=function(){}] 回调函数
+         * @returns {editormd}                         this(editormd instance object.)
+         */
+        
+        hideToolbar : function(callback) { 
+            var settings = this.settings;
+            
+            settings.toolbar = false;  
+            this.toolbar.hide();
+            this.resize();
+            
+            $.proxy(callback || function(){}, this)();
+
+            return this;
+        },
+        
+        /**
+         * 页面滚动时工具栏的固定定位
+         * Set toolbar in window scroll auto fixed position
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setToolbarAutoFixed : function(fixed) {
+            
+            var state    = this.state;
+            var editor   = this.editor;
+            var toolbar  = this.toolbar;
+            var settings = this.settings;
+            
+            if (typeof fixed !== "undefined")
+            {
+                settings.toolbarAutoFixed = fixed;
+            }
+            
+            var autoFixedHandle = function(){
+                var $window = $(window);
+                var top     = $window.scrollTop();
+                
+                if (!settings.toolbarAutoFixed)
+                {
+                    return false;
+                }
+
+                if (top - editor.offset().top > 10 && top < editor.height())
+                {
+                    toolbar.css({
+                        position : "fixed",
+                        width    : editor.width() + "px",
+                        left     : ($window.width() - editor.width()) / 2 + "px"
+                    });
+                }
+                else
+                {
+                    toolbar.css({
+                        position : "absolute",
+                        width    : "100%",
+                        left     : 0
+                    });
+                }
+            };
+            
+            if (!state.fullscreen && !state.preview && settings.toolbar && settings.toolbarAutoFixed)
+            {
+                $(window).bind("scroll", autoFixedHandle);
+            }
+
+            return this;
+        },
+        
+        /**
+         * 配置和初始化工具栏
+         * Set toolbar and Initialization
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setToolbar : function() {
+            var settings    = this.settings;  
+            
+            if(settings.readOnly) {
+                return this;
+            }
+            
+            var editor      = this.editor;
+            var preview     = this.preview;
+            var classPrefix = this.classPrefix;
+            
+            var toolbar     = this.toolbar = editor.children("." + classPrefix + "toolbar");
+            
+            if (settings.toolbar && toolbar.length < 1)
+            {            
+                var toolbarHTML = "<div class=\"" + classPrefix + "toolbar\"><div class=\"" + classPrefix + "toolbar-container\"><ul class=\"" + classPrefix + "menu\"></ul></div></div>";
+                
+                editor.append(toolbarHTML);
+                toolbar = this.toolbar = editor.children("." + classPrefix + "toolbar");
+            }
+            
+            if (!settings.toolbar) 
+            {
+                toolbar.hide();
+                
+                return this;
+            }
+            
+            toolbar.show();
+            
+            var icons       = (typeof settings.toolbarIcons === "function") ? settings.toolbarIcons() 
+                            : ((typeof settings.toolbarIcons === "string")  ? editormd.toolbarModes[settings.toolbarIcons] : settings.toolbarIcons);
+            
+            var toolbarMenu = toolbar.find("." + this.classPrefix + "menu"), menu = "";
+            var pullRight   = false;
+            
+            for (var i = 0, len = icons.length; i < len; i++)
+            {
+                var name = icons[i];
+
+                if (name === "||") 
+                { 
+                    pullRight = true;
+                } 
+                else if (name === "|")
+                {
+                    menu += "<li class=\"divider\" unselectable=\"on\">|</li>";
+                }
+                else
+                {
+                    var isHeader = (/h(\d)/.test(name));
+                    var index    = name;
+                    
+                    if (name === "watch" && !settings.watch) {
+                        index = "unwatch";
+                    }
+                    
+                    var title     = settings.lang.toolbar[index];
+                    var iconTexts = settings.toolbarIconTexts[index];
+                    var iconClass = settings.toolbarIconsClass[index];
+                    
+                    title     = (typeof title     === "undefined") ? "" : title;
+                    iconTexts = (typeof iconTexts === "undefined") ? "" : iconTexts;
+                    iconClass = (typeof iconClass === "undefined") ? "" : iconClass;
+
+                    var menuItem = pullRight ? "<li class=\"pull-right\">" : "<li>";
+                    
+                    if (typeof settings.toolbarCustomIcons[name] !== "undefined" && typeof settings.toolbarCustomIcons[name] !== "function")
+                    {
+                        menuItem += settings.toolbarCustomIcons[name];
+                    }
+                    else 
+                    {
+                        menuItem += "<a href=\"javascript:;\" title=\"" + title + "\" unselectable=\"on\">";
+                        menuItem += "<i class=\"fa " + iconClass + "\" name=\""+name+"\" unselectable=\"on\">"+((isHeader) ? name.toUpperCase() : ( (iconClass === "") ? iconTexts : "") ) + "</i>";
+                        menuItem += "</a>";
+                    }
+
+                    menuItem += "</li>";
+
+                    menu = pullRight ? menuItem + menu : menu + menuItem;
+                }
+            }
+
+            toolbarMenu.html(menu);
+            
+            toolbarMenu.find("[title=\"Lowercase\"]").attr("title", settings.lang.toolbar.lowercase);
+            toolbarMenu.find("[title=\"ucwords\"]").attr("title", settings.lang.toolbar.ucwords);
+            
+            this.setToolbarHandler();
+            this.setToolbarAutoFixed();
+
+            return this;
+        },
+        
+        /**
+         * 工具栏图标事件处理对象序列
+         * Get toolbar icons event handlers
+         * 
+         * @param   {Object}   cm    CodeMirror的实例对象
+         * @param   {String}   name  要获取的事件处理器名称
+         * @returns {Object}         返回处理对象序列
+         */
+            
+        dialogLockScreen : function() {
+            $.proxy(editormd.dialogLockScreen, this)();
+            
+            return this;
+        },
+
+        dialogShowMask : function(dialog) {
+            $.proxy(editormd.dialogShowMask, this)(dialog);
+            
+            return this;
+        },
+        
+        getToolbarHandles : function(name) {  
+            var toolbarHandlers = this.toolbarHandlers = editormd.toolbarHandlers;
+            
+            return (name && typeof toolbarIconHandlers[name] !== "undefined") ? toolbarHandlers[name] : toolbarHandlers;
+        },
+        
+        /**
+         * 工具栏图标事件处理器
+         * Bind toolbar icons event handle
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setToolbarHandler : function() {
+            var _this               = this;
+            var settings            = this.settings;
+            
+            if (!settings.toolbar || settings.readOnly) {
+                return this;
+            }
+            
+            var toolbar             = this.toolbar;
+            var cm                  = this.cm;
+            var classPrefix         = this.classPrefix;           
+            var toolbarIcons        = this.toolbarIcons = toolbar.find("." + classPrefix + "menu > li > a");  
+            var toolbarIconHandlers = this.getToolbarHandles();  
+                
+            toolbarIcons.bind(editormd.mouseOrTouch("click", "touchend"), function(event) {
+
+                var icon                = $(this).children(".fa");
+                var name                = icon.attr("name");
+                var cursor              = cm.getCursor();
+                var selection           = cm.getSelection();
+
+                if (name === "") {
+                    return ;
+                }
+                
+                _this.activeIcon = icon;
+
+                if (typeof toolbarIconHandlers[name] !== "undefined") 
+                {
+                    $.proxy(toolbarIconHandlers[name], _this)(cm);
+                }
+                else 
+                {
+                    if (typeof settings.toolbarHandlers[name] !== "undefined") 
+                    {
+                        $.proxy(settings.toolbarHandlers[name], _this)(cm, icon, cursor, selection);
+                    }
+                }
+                
+                if (name !== "link" && name !== "reference-link" && name !== "image" && name !== "code-block" && 
+                    name !== "preformatted-text" && name !== "watch" && name !== "preview" && name !== "search" && name !== "fullscreen" && name !== "info") 
+                {
+                    cm.focus();
+                }
+
+                return false;
+
+            });
+
+            return this;
+        },
+        
+        /**
+         * 动态创建对话框
+         * Creating custom dialogs
+         * 
+         * @param   {Object} options  配置项键值对 Key/Value
+         * @returns {dialog}          返回创建的dialog的jQuery实例对象
+         */
+        
+        createDialog : function(options) {            
+            return $.proxy(editormd.createDialog, this)(options);
+        },
+        
+        /**
+         * 创建关于Editor.md的对话框
+         * Create about Editor.md dialog
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        createInfoDialog : function() {
+            var _this        = this;
+			var editor       = this.editor;
+            var classPrefix  = this.classPrefix;  
+            
+            var infoDialogHTML = [
+                "<div class=\"" + classPrefix + "dialog " + classPrefix + "dialog-info\" style=\"\">",
+                "<div class=\"" + classPrefix + "dialog-container\">",
+                "<h1><i class=\"editormd-logo editormd-logo-lg editormd-logo-color\"></i> " + editormd.title + "<small>v" + editormd.version + "</small></h1>",
+                "<p>" + this.lang.description + "</p>",
+                "<p style=\"margin: 10px 0 20px 0;\"><a href=\"" + editormd.homePage + "\" target=\"_blank\">" + editormd.homePage + " <i class=\"fa fa-external-link\"></i></a></p>",
+                "<p style=\"font-size: 0.85em;\">Copyright &copy; 2015 <a href=\"https://github.com/pandao\" target=\"_blank\" class=\"hover-link\">Pandao</a>, The <a href=\"https://github.com/pandao/editor.md/blob/master/LICENSE\" target=\"_blank\" class=\"hover-link\">MIT</a> License.</p>",
+                "</div>",
+                "<a href=\"javascript:;\" class=\"fa fa-close " + classPrefix + "dialog-close\"></a>",
+                "</div>"
+            ].join("\n");
+
+            editor.append(infoDialogHTML);
+            
+            var infoDialog  = this.infoDialog = editor.children("." + classPrefix + "dialog-info");
+
+            infoDialog.find("." + classPrefix + "dialog-close").bind(editormd.mouseOrTouch("click", "touchend"), function() {
+                _this.hideInfoDialog();
+            });
+            
+            infoDialog.css("border", (editormd.isIE8) ? "1px solid #ddd" : "").css("z-index", editormd.dialogZindex).show();
+            
+            this.infoDialogPosition();
+
+            return this;
+        },
+        
+        /**
+         * 关于Editor.md对话居中定位
+         * Editor.md dialog position handle
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        infoDialogPosition : function() {
+            var infoDialog = this.infoDialog;
+            
+			var _infoDialogPosition = function() {
+				infoDialog.css({
+					top  : ($(window).height() - infoDialog.height()) / 2 + "px",
+					left : ($(window).width()  - infoDialog.width()) / 2  + "px"
+				});
+			};
+
+			_infoDialogPosition();
+
+			$(window).resize(_infoDialogPosition);
+            
+            return this;
+        },
+        
+        /**
+         * 显示关于Editor.md
+         * Display about Editor.md dialog
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        showInfoDialog : function() {
+
+            $("html,body").css("overflow-x", "hidden");
+            
+            var _this       = this;
+			var editor      = this.editor;
+            var settings    = this.settings;         
+			var infoDialog  = this.infoDialog = editor.children("." + this.classPrefix + "dialog-info");
+            
+            if (infoDialog.length < 1)
+            {
+                this.createInfoDialog();
+            }
+            
+            this.lockScreen(true);
+            
+            this.mask.css({
+						opacity         : settings.dialogMaskOpacity,
+						backgroundColor : settings.dialogMaskBgColor
+					}).show();
+
+			infoDialog.css("z-index", editormd.dialogZindex).show();
+
+			this.infoDialogPosition();
+
+            return this;
+        },
+        
+        /**
+         * 隐藏关于Editor.md
+         * Hide about Editor.md dialog
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        hideInfoDialog : function() {            
+            $("html,body").css("overflow-x", "");
+            this.infoDialog.hide();
+            this.mask.hide();
+            this.lockScreen(false);
+
+            return this;
+        },
+        
+        /**
+         * 锁屏
+         * lock screen
+         * 
+         * @param   {Boolean}    lock    Boolean 布尔值,是否锁屏
+         * @returns {editormd}           返回editormd的实例对象
+         */
+        
+        lockScreen : function(lock) {
+            editormd.lockScreen(lock);
+
+            return this;
+        },
+        
+        /**
+         * 编辑器界面重建,用于动态语言包或模块加载等
+         * Recreate editor
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        recreate : function() {
+            var _this            = this;
+            var editor           = this.editor;
+            var settings         = this.settings;
+            
+            this.codeMirror.remove();
+            
+            this.setCodeMirror();
+
+            if (!settings.readOnly) 
+            {
+                if (editor.find(".editormd-dialog").length > 0) {
+                    editor.find(".editormd-dialog").remove();
+                }
+                
+                if (settings.toolbar) 
+                {  
+                    this.getToolbarHandles();                  
+                    this.setToolbar();
+                }
+            }
+            
+            this.loadedDisplay(true);
+
+            return this;
+        },
+        
+        /**
+         * 高亮预览HTML的pre代码部分
+         * highlight of preview codes
+         * 
+         * @returns {editormd}             返回editormd的实例对象
+         */
+        
+        previewCodeHighlight : function() {    
+            var settings         = this.settings;
+            var previewContainer = this.previewContainer;
+            
+            if (settings.previewCodeHighlight) 
+            {
+                previewContainer.find("pre").addClass("prettyprint linenums");
+                
+                if (typeof prettyPrint !== "undefined")
+                {                    
+                    prettyPrint();
+                }
+            }
+
+            return this;
+        },
+        
+        /**
+         * 解析TeX(KaTeX)科学公式
+         * TeX(KaTeX) Renderer
+         * 
+         * @returns {editormd}             返回editormd的实例对象
+         */
+        
+        katexRender : function() {
+            
+            if (timer === null)
+            {
+                return this;
+            }
+            
+            this.previewContainer.find("." + editormd.classNames.tex).each(function(){
+                var tex  = $(this);
+                editormd.$katex.render(tex.text(), tex[0]);
+            });   
+
+            return this;
+        },
+        
+        /**
+         * 解析和渲染流程图及时序图
+         * FlowChart and SequenceDiagram Renderer
+         * 
+         * @returns {editormd}             返回editormd的实例对象
+         */
+        
+        flowChartAndSequenceDiagramRender : function() {
+            
+            var settings         = this.settings;
+            var previewContainer = this.previewContainer;
+            
+            if (editormd.isIE8) {
+                return this;
+            }
+
+            if (settings.flowChart) {
+                if (flowchartTimer === null) {
+                    return this;
+                }
+                
+                previewContainer.find(".flowchart").flowChart(); 
+            }
+
+            if (settings.sequenceDiagram) {
+                previewContainer.find(".sequence-diagram").sequenceDiagram({theme: "simple"});
+            }
+
+            return this;
+        },
+        
+        /**
+         * 注册键盘快捷键处理
+         * Register CodeMirror keyMaps (keyboard shortcuts).
+         * 
+         * @param   {Object}    keyMap      KeyMap key/value {"(Ctrl/Shift/Alt)-Key" : function(){}}
+         * @returns {editormd}              return this
+         */
+        
+        registerKeyMaps : function(keyMap) {
+            
+            var _this           = this;
+            var cm              = this.cm;
+            var settings        = this.settings;
+            var toolbarHandlers = editormd.toolbarHandlers;
+            var disabledKeyMaps = settings.disabledKeyMaps;
+            
+            keyMap              = keyMap || null;
+            
+            if (keyMap)
+            {
+                for (var i in keyMap)
+                {
+                    if ($.inArray(i, disabledKeyMaps) < 0)
+                    {
+                        var map = {};
+                        map[i]  = keyMap[i];
+
+                        cm.addKeyMap(keyMap);
+                    }
+                }
+            }
+            else
+            {
+                for (var k in editormd.keyMaps)
+                {
+                    var _keyMap = editormd.keyMaps[k];
+                    var handle = (typeof _keyMap === "string") ? $.proxy(toolbarHandlers[_keyMap], _this) : $.proxy(_keyMap, _this);
+                    
+                    if ($.inArray(k, ["F9", "F10", "F11"]) < 0 && $.inArray(k, disabledKeyMaps) < 0)
+                    {
+                        var _map = {};
+                        _map[k] = handle;
+
+                        cm.addKeyMap(_map);
+                    }
+                }
+                
+                $(window).keydown(function(event) {
+                    
+                    var keymaps = {
+                        "120" : "F9",
+                        "121" : "F10",
+                        "122" : "F11"
+                    };
+                    
+                    if ( $.inArray(keymaps[event.keyCode], disabledKeyMaps) < 0 )
+                    {
+                        switch (event.keyCode)
+                        {
+                            case 120:
+                                    $.proxy(toolbarHandlers["watch"], _this)();
+                                    return false;
+                                break;
+                                
+                            case 121:
+                                    $.proxy(toolbarHandlers["preview"], _this)();
+                                    return false;
+                                break;
+                                
+                            case 122:
+                                    $.proxy(toolbarHandlers["fullscreen"], _this)();                        
+                                    return false;
+                                break;
+                                
+                            default:
+                                break;
+                        }
+                    }
+                });
+            }
+
+            return this;
+        },
+        
+        bindScrollEvent : function() {
+            
+            var _this            = this;
+            var preview          = this.preview;
+            var settings         = this.settings;
+            var codeMirror       = this.codeMirror;
+            var mouseOrTouch     = editormd.mouseOrTouch;
+            
+            if (!settings.syncScrolling) {
+                return this;
+            }
+                
+            var cmBindScroll = function() {    
+                codeMirror.find(".CodeMirror-scroll").bind(mouseOrTouch("scroll", "touchmove"), function(event) {
+                    var height    = $(this).height();
+                    var scrollTop = $(this).scrollTop();                    
+                    var percent   = (scrollTop / $(this)[0].scrollHeight);
+
+                    if (scrollTop === 0) 
+                    {
+                        preview.scrollTop(0);
+                    } 
+                    else if (scrollTop + height >= $(this)[0].scrollHeight - 16)
+                    { 
+                        preview.scrollTop(preview[0].scrollHeight);                        
+                    } 
+                    else
+                    {                    
+                        preview.scrollTop(preview[0].scrollHeight * percent);
+                    }
+                    
+                    $.proxy(settings.onscroll, _this)(event);
+                });
+            };
+
+            var cmUnbindScroll = function() {
+                codeMirror.find(".CodeMirror-scroll").unbind(mouseOrTouch("scroll", "touchmove"));
+            };
+
+            var previewBindScroll = function() {
+                
+                preview.bind(mouseOrTouch("scroll", "touchmove"), function(event) {
+                    var height    = $(this).height();
+                    var scrollTop = $(this).scrollTop();         
+                    var percent   = (scrollTop / $(this)[0].scrollHeight);
+                    var codeView  = codeMirror.find(".CodeMirror-scroll");
+
+                    if(scrollTop === 0) 
+                    {
+                        codeView.scrollTop(0);
+                    }
+                    else if (scrollTop + height >= $(this)[0].scrollHeight)
+                    {
+                        codeView.scrollTop(codeView[0].scrollHeight);                        
+                    }
+                    else 
+                    {
+                        codeView.scrollTop(codeView[0].scrollHeight * percent);
+                    }
+                    
+                    $.proxy(settings.onpreviewscroll, _this)(event);
+                });
+
+            };
+
+            var previewUnbindScroll = function() {
+                preview.unbind(mouseOrTouch("scroll", "touchmove"));
+            }; 
+
+			codeMirror.bind({
+				mouseover  : cmBindScroll,
+				mouseout   : cmUnbindScroll,
+				touchstart : cmBindScroll,
+				touchend   : cmUnbindScroll
+			});
+            
+			preview.bind({
+				mouseover  : previewBindScroll,
+				mouseout   : previewUnbindScroll,
+				touchstart : previewBindScroll,
+				touchend   : previewUnbindScroll
+			});
+
+            return this;
+        },
+        
+        bindChangeEvent : function() {
+            
+            var _this            = this;
+            var cm               = this.cm;
+            var settings         = this.settings;
+            
+            if (!settings.syncScrolling) {
+                return this;
+            }
+            
+            cm.on("change", function(_cm, changeObj) {
+                
+                if (settings.watch)
+                {           
+                    _this.previewContainer.css("padding", settings.autoHeight ? "20px 20px 50px 40px" : "20px");
+                }
+                
+                timer = setTimeout(function() {
+                    clearTimeout(timer);
+                    _this.save();
+                    timer = null;
+                }, settings.delay);
+            });
+
+            return this;
+        },
+        
+        /**
+         * 加载队列完成之后的显示处理
+         * Display handle of the module queues loaded after.
+         * 
+         * @param   {Boolean}   recreate   是否为重建编辑器
+         * @returns {editormd}             返回editormd的实例对象
+         */
+        
+        loadedDisplay : function(recreate) {
+            
+            recreate             = recreate || false;
+            
+            var _this            = this;
+            var editor           = this.editor;
+            var preview          = this.preview;
+            var settings         = this.settings;
+            
+            this.containerMask.hide();
+            
+            this.save();
+            
+            if (settings.watch) {
+                preview.show();
+            }
+            
+            editor.data("oldWidth", editor.width()).data("oldHeight", editor.height()); // 为了兼容Zepto
+            
+            this.resize();
+            this.registerKeyMaps();
+            
+            $(window).resize(function(){
+                _this.resize();
+            });
+            
+            this.bindScrollEvent().bindChangeEvent();
+            
+            if (!recreate)
+            {
+                $.proxy(settings.onload, this)();
+            }
+            
+            this.state.loaded = true;
+
+            return this;
+        },
+        
+        /**
+         * 设置编辑器的宽度
+         * Set editor width
+         * 
+         * @param   {Number|String} width  编辑器宽度值
+         * @returns {editormd}             返回editormd的实例对象
+         */
+        
+        width : function(width) {
+                
+            this.editor.css("width", (typeof width === "number") ? width  + "px" : width);            
+            this.resize();
+            
+            return this;
+        },
+        
+        /**
+         * 设置编辑器的高度
+         * Set editor height
+         * 
+         * @param   {Number|String} height  编辑器高度值
+         * @returns {editormd}              返回editormd的实例对象
+         */
+        
+        height : function(height) {
+                
+            this.editor.css("height", (typeof height === "number")  ? height  + "px" : height);            
+            this.resize();
+            
+            return this;
+        },
+        
+        /**
+         * 调整编辑器的尺寸和布局
+         * Resize editor layout
+         * 
+         * @param   {Number|String} [width=null]  编辑器宽度值
+         * @param   {Number|String} [height=null] 编辑器高度值
+         * @returns {editormd}                    返回editormd的实例对象
+         */
+        
+        resize : function(width, height) {
+            
+            width  = width  || null;
+            height = height || null;
+            
+            var state      = this.state;
+            var editor     = this.editor;
+            var preview    = this.preview;
+            var toolbar    = this.toolbar;
+            var settings   = this.settings;
+            var codeMirror = this.codeMirror;
+            
+            if (width)
+            {
+                editor.css("width", (typeof width  === "number") ? width  + "px" : width);
+            }
+            
+            if (settings.autoHeight && !state.fullscreen && !state.preview)
+            {
+                editor.css("height", "auto");
+                codeMirror.css("height", "auto");
+            } 
+            else 
+            {
+                if (height) 
+                {
+                    editor.css("height", (typeof height === "number") ? height + "px" : height);
+                }
+                
+                if (state.fullscreen)
+                {
+                    editor.height($(window).height());
+                }
+
+                if (settings.toolbar && !settings.readOnly) 
+                {
+                    codeMirror.css("margin-top", toolbar.height() + 1).height(editor.height() - toolbar.height());
+                } 
+                else
+                {
+                    codeMirror.css("margin-top", 0).height(editor.height());
+                }
+            }
+            
+            if(settings.watch) 
+            {
+                codeMirror.width(editor.width() / 2);
+                preview.width((!state.preview) ? editor.width() / 2 : editor.width());
+                
+                this.previewContainer.css("padding", settings.autoHeight ? "20px 20px 50px 40px" : "20px");
+                
+                if (settings.toolbar && !settings.readOnly) 
+                {
+                    preview.css("top", toolbar.height());
+                } 
+                else 
+                {
+                    preview.css("top", 0);
+                }
+                
+                if (settings.autoHeight && !state.fullscreen && !state.preview)
+                {
+                    preview.height("");
+                }
+                else
+                {                
+                    preview.height((settings.toolbar && !settings.readOnly) ? editor.height() - toolbar.height() : editor.height());
+                }
+            } 
+            else 
+            {
+                codeMirror.width(editor.width());
+                preview.hide();
+            }
+            
+            if (state.loaded) 
+            {
+                $.proxy(settings.onresize, this)();
+            }
+
+            return this;
+        },
+        
+        /**
+         * 解析和保存Markdown代码
+         * Parse & Saving Markdown source code
+         * 
+         * @returns {editormd}     返回editormd的实例对象
+         */
+        
+        save : function() {
+            
+            if (timer === null)
+            {
+                return this;
+            }
+            
+            var _this            = this;
+            var state            = this.state;
+            var settings         = this.settings;
+            var cm               = this.cm;            
+            var cmValue          = cm.getValue();
+            var previewContainer = this.previewContainer;
+
+            if (settings.mode !== "gfm" && settings.mode !== "markdown") 
+            {
+                this.markdownTextarea.val(cmValue);
+                
+                return this;
+            }
+            
+            var marked          = editormd.$marked;
+            var markdownToC     = this.markdownToC = [];            
+            var rendererOptions = this.markedRendererOptions = {  
+                toc                  : settings.toc,
+                tocm                 : settings.tocm,
+                tocStartLevel        : settings.tocStartLevel,
+                pageBreak            : settings.pageBreak,
+                taskList             : settings.taskList,
+                emoji                : settings.emoji,
+                tex                  : settings.tex,
+                atLink               : settings.atLink,           // for @link
+                emailLink            : settings.emailLink,        // for mail address auto link
+                flowChart            : settings.flowChart,
+                sequenceDiagram      : settings.sequenceDiagram,
+                previewCodeHighlight : settings.previewCodeHighlight,
+            };
+            
+            var markedOptions = this.markedOptions = {
+                renderer    : editormd.markedRenderer(markdownToC, rendererOptions),
+                gfm         : true,
+                tables      : true,
+                breaks      : true,
+                pedantic    : false,
+                sanitize    : (settings.htmlDecode) ? false : true,  // 关闭忽略HTML标签,即开启识别HTML标签,默认为false
+                smartLists  : true,
+                smartypants : true
+            };
+            
+            marked.setOptions(markedOptions);
+        
+            cmValue            = editormd.filterHTMLTags(cmValue, settings.htmlDecode);
+            
+            var newMarkdownDoc = editormd.$marked(cmValue, markedOptions);
+            
+            //console.log("cmValue", cmValue, this.markdownTextarea, this.htmlTextarea);
+            
+            this.markdownTextarea.text(cmValue);
+            
+            cm.save();
+            
+            if (settings.saveHTMLToTextarea) 
+            {
+                this.htmlTextarea.text(newMarkdownDoc);
+            }
+            
+            if(settings.watch || (!settings.watch && state.preview))
+            {
+                previewContainer.html(newMarkdownDoc);
+
+                this.previewCodeHighlight();
+                
+                if (settings.toc) 
+                {
+                    var tocContainer = (settings.tocContainer === "") ? previewContainer : $(settings.tocContainer);
+                    var tocMenu      = tocContainer.find("." + this.classPrefix + "toc-menu");
+                    
+                    tocContainer.attr("previewContainer", (settings.tocContainer === "") ? "true" : "false");
+                    
+                    if (settings.tocContainer !== "" && tocMenu.length > 0)
+                    {
+                        tocMenu.remove();
+                    }
+                    
+                    editormd.markdownToCRenderer(markdownToC, tocContainer, settings.tocDropdown, settings.tocStartLevel);
+            
+                    if (settings.tocDropdown || tocContainer.find("." + this.classPrefix + "toc-menu").length > 0)
+                    {
+                        editormd.tocDropdownMenu(tocContainer, (settings.tocTitle !== "") ? settings.tocTitle : this.lang.tocTitle);
+                    }
+            
+                    if (settings.tocContainer !== "")
+                    {
+                        previewContainer.find(".markdown-toc").css("border", "none");
+                    }
+                }
+                
+                if (settings.tex)
+                {
+                    if (!editormd.kaTeXLoaded && settings.autoLoadModules) 
+                    {
+                        editormd.loadKaTeX(function() {
+                            editormd.$katex = katex;
+                            editormd.kaTeXLoaded = true;
+                            _this.katexRender();
+                        });
+                    } 
+                    else 
+                    {
+                        editormd.$katex = katex;
+                        this.katexRender();
+                    }
+                }                
+                
+                if (settings.flowChart || settings.sequenceDiagram)
+                {
+                    flowchartTimer = setTimeout(function(){
+                        clearTimeout(flowchartTimer);
+                        _this.flowChartAndSequenceDiagramRender();
+                        flowchartTimer = null;
+                    }, 10);
+                }
+
+                if (state.loaded) 
+                {
+                    $.proxy(settings.onchange, this)();
+                }
+            }
+
+            return this;
+        },
+        
+        /**
+         * 聚焦光标位置
+         * Focusing the cursor position
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        focus : function() {
+            this.cm.focus();
+
+            return this;
+        },
+        
+        /**
+         * 设置光标的位置
+         * Set cursor position
+         * 
+         * @param   {Object}    cursor 要设置的光标位置键值对象,例:{line:1, ch:0}
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        setCursor : function(cursor) {
+            this.cm.setCursor(cursor);
+
+            return this;
+        },
+        
+        /**
+         * 获取当前光标的位置
+         * Get the current position of the cursor
+         * 
+         * @returns {Cursor}         返回一个光标Cursor对象
+         */
+        
+        getCursor : function() {
+            return this.cm.getCursor();
+        },
+        
+        /**
+         * 设置光标选中的范围
+         * Set cursor selected ranges
+         * 
+         * @param   {Object}    from   开始位置的光标键值对象,例:{line:1, ch:0}
+         * @param   {Object}    to     结束位置的光标键值对象,例:{line:1, ch:0}
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        setSelection : function(from, to) {
+        
+            this.cm.setSelection(from, to);
+        
+            return this;
+        },
+        
+        /**
+         * 获取光标选中的文本
+         * Get the texts from cursor selected
+         * 
+         * @returns {String}         返回选中文本的字符串形式
+         */
+        
+        getSelection : function() {
+            return this.cm.getSelection();
+        },
+        
+        /**
+         * 设置光标选中的文本范围
+         * Set the cursor selection ranges
+         * 
+         * @param   {Array}    ranges  cursor selection ranges array
+         * @returns {Array}            return this
+         */
+        
+        setSelections : function(ranges) {
+            this.cm.setSelections(ranges);
+            
+            return this;
+        },
+        
+        /**
+         * 获取光标选中的文本范围
+         * Get the cursor selection ranges
+         * 
+         * @returns {Array}         return selection ranges array
+         */
+        
+        getSelections : function() {
+            return this.cm.getSelections();
+        },
+        
+        /**
+         * 替换当前光标选中的文本或在当前光标处插入新字符
+         * Replace the text at the current cursor selected or insert a new character at the current cursor position
+         * 
+         * @param   {String}    value  要插入的字符值
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        replaceSelection : function(value) {
+            this.cm.replaceSelection(value);
+
+            return this;
+        },
+        
+        /**
+         * 在当前光标处插入新字符
+         * Insert a new character at the current cursor position
+         *
+         * 同replaceSelection()方法
+         * With the replaceSelection() method
+         * 
+         * @param   {String}    value  要插入的字符值
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        insertValue : function(value) {
+            this.replaceSelection(value);
+
+            return this;
+        },
+        
+        /**
+         * 追加markdown
+         * append Markdown to editor
+         * 
+         * @param   {String}    md     要追加的markdown源文档
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        appendMarkdown : function(md) {
+            var settings = this.settings;
+            var cm       = this.cm;
+            
+            cm.setValue(cm.getValue() + md);
+            
+            return this;
+        },
+        
+        /**
+         * 设置和传入编辑器的markdown源文档
+         * Set Markdown source document
+         * 
+         * @param   {String}    md     要传入的markdown源文档
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        setMarkdown : function(md) {
+            this.cm.setValue(md || this.settings.markdown);
+            
+            return this;
+        },
+        
+        /**
+         * 获取编辑器的markdown源文档
+         * Set Editor.md markdown/CodeMirror value
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        getMarkdown : function() {
+            return this.cm.getValue();
+        },
+        
+        /**
+         * 获取编辑器的源文档
+         * Get CodeMirror value
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        getValue : function() {
+            return this.cm.getValue();
+        },
+        
+        /**
+         * 设置编辑器的源文档
+         * Set CodeMirror value
+         * 
+         * @param   {String}     value   set code/value/string/text
+         * @returns {editormd}           返回editormd的实例对象
+         */
+        
+        setValue : function(value) {
+            this.cm.setValue(value);
+            
+            return this;
+        },
+        
+        /**
+         * 清空编辑器
+         * Empty CodeMirror editor container
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        clear : function() {
+            this.cm.setValue("");
+            
+            return this;
+        },
+        
+        /**
+         * 获取解析后存放在Textarea的HTML源码
+         * Get parsed html code from Textarea
+         * 
+         * @returns {String}               返回HTML源码
+         */
+        
+        getHTML : function() {
+            if (!this.settings.saveHTMLToTextarea)
+            {
+                alert("Error: settings.saveHTMLToTextarea == false");
+
+                return false;
+            }
+            
+            return this.htmlTextarea.val();
+        },
+        
+        /**
+         * getHTML()的别名
+         * getHTML (alias)
+         * 
+         * @returns {String}           Return html code 返回HTML源码
+         */
+        
+        getTextareaSavedHTML : function() {
+            return this.getHTML();
+        },
+        
+        /**
+         * 获取预览窗口的HTML源码
+         * Get html from preview container
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        getPreviewedHTML : function() {
+            if (!this.settings.watch)
+            {
+                alert("Error: settings.watch == false");
+
+                return false;
+            }
+            
+            return this.previewContainer.html();
+        },
+        
+        /**
+         * 开启实时预览
+         * Enable real-time watching
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        watch : function(callback) {     
+            var settings        = this.settings;
+            
+            if ($.inArray(settings.mode, ["gfm", "markdown"]) < 0)
+            {
+                return this;
+            }
+            
+            this.state.watching = settings.watch = true;
+            this.preview.show();
+            
+            if (this.toolbar)
+            {
+                var watchIcon   = settings.toolbarIconsClass.watch;
+                var unWatchIcon = settings.toolbarIconsClass.unwatch;
+                
+                var icon        = this.toolbar.find(".fa[name=watch]");
+                icon.parent().attr("title", settings.lang.toolbar.watch);
+                icon.removeClass(unWatchIcon).addClass(watchIcon);
+            }
+            
+            this.codeMirror.css("border-right", "1px solid #ddd").width(this.editor.width() / 2); 
+            
+            timer = 0;
+            
+            this.save().resize();
+            
+            if (!settings.onwatch)
+            {
+                settings.onwatch = callback || function() {};
+            }
+            
+            $.proxy(settings.onwatch, this)();
+            
+            return this;
+        },
+        
+        /**
+         * 关闭实时预览
+         * Disable real-time watching
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        unwatch : function(callback) {
+            var settings        = this.settings;
+            this.state.watching = settings.watch = false;
+            this.preview.hide();
+            
+            if (this.toolbar) 
+            {
+                var watchIcon   = settings.toolbarIconsClass.watch;
+                var unWatchIcon = settings.toolbarIconsClass.unwatch;
+                
+                var icon    = this.toolbar.find(".fa[name=watch]");
+                icon.parent().attr("title", settings.lang.toolbar.unwatch);
+                icon.removeClass(watchIcon).addClass(unWatchIcon);
+            }
+            
+            this.codeMirror.css("border-right", "none").width(this.editor.width());
+            
+            this.resize();
+            
+            if (!settings.onunwatch)
+            {
+                settings.onunwatch = callback || function() {};
+            }
+            
+            $.proxy(settings.onunwatch, this)();
+            
+            return this;
+        },
+        
+        /**
+         * 显示编辑器
+         * Show editor
+         * 
+         * @param   {Function} [callback=function()] 回调函数
+         * @returns {editormd}                       返回editormd的实例对象
+         */
+        
+        show : function(callback) {
+            callback  = callback || function() {};
+            
+            var _this = this;
+            this.editor.show(0, function() {
+                $.proxy(callback, _this)();
+            });
+            
+            return this;
+        },
+        
+        /**
+         * 隐藏编辑器
+         * Hide editor
+         * 
+         * @param   {Function} [callback=function()] 回调函数
+         * @returns {editormd}                       返回editormd的实例对象
+         */
+        
+        hide : function(callback) {
+            callback  = callback || function() {};
+            
+            var _this = this;
+            this.editor.hide(0, function() {
+                $.proxy(callback, _this)();
+            });
+            
+            return this;
+        },
+        
+        /**
+         * 隐藏编辑器部分,只预览HTML
+         * Enter preview html state
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        previewing : function() {
+            
+            var _this            = this;
+            var editor           = this.editor;
+            var preview          = this.preview;
+            var toolbar          = this.toolbar;
+            var settings         = this.settings;
+            var codeMirror       = this.codeMirror;
+            
+            if ($.inArray(settings.mode, ["gfm", "markdown"]) < 0) {
+                return this;
+            }
+            
+            if (settings.toolbar && toolbar) {
+                toolbar.toggle();
+                toolbar.find(".fa[name=preview]").toggleClass("active");
+            }
+            
+            codeMirror.toggle();
+            
+            var escHandle = function(event) {
+                if (event.shiftKey && event.keyCode === 27) {
+                    _this.previewed();
+                }
+            };
+
+            if (codeMirror.css("display") === "none") // 为了兼容Zepto,而不使用codeMirror.is(":hidden")
+            {
+                this.state.preview = true;
+
+                if (this.state.fullscreen) {
+                    preview.css("background", "#fff");
+                }
+                
+                editor.find("." + this.classPrefix + "preview-close-btn").show().bind(editormd.mouseOrTouch("click", "touchend"), function(){
+                    _this.previewed();
+                });
+            
+                if (!settings.watch)
+                {
+                    this.save();
+                }
+
+                preview.show().css({
+                    position  : "static",
+                    top       : 0,
+                    width     : editor.width(),
+                    height    : (settings.autoHeight && !this.state.fullscreen) ? "auto" : editor.height()
+                });
+                
+                if (this.state.loaded)
+                {
+                    $.proxy(settings.onpreviewing, this)();
+                }
+
+                $(window).bind("keyup", escHandle);
+            } 
+            else 
+            {
+                $(window).unbind("keyup", escHandle);
+                this.previewed();
+            }
+        },
+        
+        /**
+         * 显示编辑器部分,退出只预览HTML
+         * Exit preview html state
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        previewed : function() {
+            
+            var editor           = this.editor;
+            var preview          = this.preview;
+            var toolbar          = this.toolbar;
+            var settings         = this.settings;
+            var previewCloseBtn  = editor.find("." + this.classPrefix + "preview-close-btn");
+
+            this.state.preview   = false;
+            
+            this.codeMirror.show();
+            
+            if (settings.toolbar) {
+                toolbar.show();
+            }
+            
+            preview[(settings.watch) ? "show" : "hide"]();
+            
+            previewCloseBtn.hide().unbind(editormd.mouseOrTouch("click", "touchend"));
+            
+            preview.css({ 
+                background : null,
+                position   : "absolute",
+                width      : editor.width() / 2,
+                height     : (settings.autoHeight && !this.state.fullscreen) ? "auto" : editor.height() - toolbar.height(),
+                top        : (settings.toolbar)    ? toolbar.height() : 0
+            });
+
+            if (this.state.loaded)
+            {
+                $.proxy(settings.onpreviewed, this)();
+            }
+            
+            return this;
+        },
+        
+        /**
+         * 编辑器全屏显示
+         * Fullscreen show
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        fullscreen : function() {
+            
+            var _this            = this;
+            var state            = this.state;
+            var editor           = this.editor;
+            var preview          = this.preview;
+            var toolbar          = this.toolbar;
+            var settings         = this.settings;
+            var fullscreenClass  = this.classPrefix + "fullscreen";
+            
+            if (toolbar) {
+                toolbar.find(".fa[name=fullscreen]").parent().toggleClass("active"); 
+            }
+            
+            var escHandle = function(event) {
+                if (!event.shiftKey && event.keyCode === 27) 
+                {
+                    if (state.fullscreen)
+                    {
+                        _this.fullscreenExit();
+                    }
+                }
+            };
+
+            if (!editor.hasClass(fullscreenClass)) 
+            {
+                state.fullscreen = true;
+
+                $("html,body").css("overflow", "hidden");
+                
+                editor.css({
+                    position : "fixed", 
+                    top      : 0, 
+                    left     : 0, 
+                    margin   : 0, 
+                    border   : "none",
+                    width    : $(window).width(),
+                    height   : $(window).height()
+                }).addClass(fullscreenClass);
+
+                this.resize();
+    
+                $.proxy(settings.onfullscreen, this)();
+
+                $(window).bind("keyup", escHandle);
+            }
+            else
+            {           
+                $(window).unbind("keyup", escHandle); 
+                this.fullscreenExit();
+            }
+
+            return this;
+        },
+        
+        /**
+         * 编辑器退出全屏显示
+         * Exit fullscreen state
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        fullscreenExit : function() {
+            
+            var editor            = this.editor;
+            var settings          = this.settings;
+            var toolbar           = this.toolbar;
+            var fullscreenClass   = this.classPrefix + "fullscreen";  
+            
+            this.state.fullscreen = false;
+            
+            if (toolbar) {
+                toolbar.find(".fa[name=fullscreen]").parent().removeClass("active"); 
+            }
+
+            $("html,body").css("overflow", "");
+
+            editor.css({
+                position : "", 
+                top      : "",
+                left     : "", 
+                margin   : "0 auto 15px", 
+                width    : editor.data("oldWidth"),
+                height   : editor.data("oldHeight"),
+                border   : "1px solid #ddd"
+            }).removeClass(fullscreenClass);
+
+            this.resize();
+            
+            $.proxy(settings.onfullscreenExit, this)();
+
+            return this;
+        },
+        
+        /**
+         * 加载并执行插件
+         * Load and execute the plugin
+         * 
+         * @param   {String}     name    plugin name / function name
+         * @param   {String}     path    plugin load path
+         * @returns {editormd}           返回editormd的实例对象
+         */
+        
+        executePlugin : function(name, path) {
+            
+            var _this    = this;
+            var cm       = this.cm;
+            var settings = this.settings;
+            
+            path = settings.pluginPath + path;
+            
+            if (typeof define === "function") 
+            {            
+                if (typeof this[name] === "undefined")
+                {
+                    alert("Error: " + name + " plugin is not found, you are not load this plugin.");
+                    
+                    return this;
+                }
+                
+                this[name](cm);
+                
+                return this;
+            }
+            
+            if ($.inArray(path, editormd.loadFiles.plugin) < 0)
+            {
+                editormd.loadPlugin(path, function() {
+                    editormd.loadPlugins[name] = _this[name];
+                    _this[name](cm);
+                });
+            }
+            else
+            {
+                $.proxy(editormd.loadPlugins[name], this)(cm);
+            }
+            
+            return this;
+        },
+                
+        /**
+         * 搜索替换
+         * Search & replace
+         * 
+         * @param   {String}     command    CodeMirror serach commands, "find, fintNext, fintPrev, clearSearch, replace, replaceAll"
+         * @returns {editormd}              return this
+         */
+        
+        search : function(command) {
+            var settings = this.settings;
+            
+            if (!settings.searchReplace)
+            {
+                alert("Error: settings.searchReplace == false");
+                return this;
+            }
+            
+            if (!settings.readOnly)
+            {
+                this.cm.execCommand(command || "find");
+            }
+            
+            return this;
+        },
+        
+        searchReplace : function() {            
+            this.search("replace");
+            
+            return this;
+        },
+        
+        searchReplaceAll : function() {          
+            this.search("replaceAll");
+            
+            return this;
+        }
+    };
+    
+    editormd.fn.init.prototype = editormd.fn; 
+   
+    /**
+     * 锁屏
+     * lock screen when dialog opening
+     * 
+     * @returns {void}
+     */
+
+    editormd.dialogLockScreen = function() {
+        var settings = this.settings || {dialogLockScreen : true};
+        
+        if (settings.dialogLockScreen) 
+        {
+            $("html,body").css("overflow", "hidden");
+        }
+    };
+   
+    /**
+     * 显示透明背景层
+     * Display mask layer when dialog opening
+     * 
+     * @param   {Object}     dialog    dialog jQuery object
+     * @returns {void}
+     */
+    
+    editormd.dialogShowMask = function(dialog) {
+        var editor   = this.editor;
+        var settings = this.settings || {dialogShowMask : true};
+        
+        dialog.css({
+            top  : ($(window).height() - dialog.height()) / 2 + "px",
+            left : ($(window).width()  - dialog.width())  / 2 + "px"
+        });
+
+        if (settings.dialogShowMask) {
+            editor.children("." + this.classPrefix + "mask").css("z-index", parseInt(dialog.css("z-index")) - 1).show();
+        }
+    };
+
+    editormd.toolbarHandlers = {
+        undo : function() {
+            this.cm.undo();
+        },
+        
+        redo : function() {
+            this.cm.redo();
+        },
+        
+        bold : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("**" + selection + "**");
+
+            if(selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 2);
+            }
+        },
+        
+        del : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("~~" + selection + "~~");
+
+            if(selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 2);
+            }
+        },
+
+        italic : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("*" + selection + "*");
+
+            if(selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 1);
+            }
+        },
+
+        quote : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("> " + selection);
+            cm.setCursor(cursor.line, (selection === "") ? cursor.ch + 2 : cursor.ch + selection.length + 2);
+        },
+        
+        ucfirst : function() {
+            var cm         = this.cm;
+            var selection  = cm.getSelection();
+            var selections = cm.listSelections();
+
+            cm.replaceSelection(editormd.firstUpperCase(selection));
+            cm.setSelections(selections);
+        },
+        
+        ucwords : function() {
+            var cm         = this.cm;
+            var selection  = cm.getSelection();
+            var selections = cm.listSelections();
+
+            cm.replaceSelection(editormd.wordsFirstUpperCase(selection));
+            cm.setSelections(selections);
+        },
+        
+        uppercase : function() {
+            var cm         = this.cm;
+            var selection  = cm.getSelection();
+            var selections = cm.listSelections();
+
+            cm.replaceSelection(selection.toUpperCase());
+            cm.setSelections(selections);
+        },
+        
+        lowercase : function() {
+            var cm         = this.cm;
+            var cursor     = cm.getCursor();
+            var selection  = cm.getSelection();
+            var selections = cm.listSelections();
+            
+            cm.replaceSelection(selection.toLowerCase());
+            cm.setSelections(selections);
+        },
+
+        h1 : function() {
+            var cm        = this.cm;
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("# " + selection);
+        },
+
+        h2 : function() {
+            var cm        = this.cm;
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("## " + selection);
+        },
+
+        h3 : function() {
+            var cm        = this.cm;
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("### " + selection);
+        },
+
+        h4 : function() {
+            var cm        = this.cm;
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("#### " + selection);
+        },
+
+        h5 : function() {
+            var cm        = this.cm;
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("##### " + selection);
+        },
+
+        h6 : function() {
+            var cm        = this.cm;
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("###### " + selection);
+        },
+
+        "list-ul" : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (selection === "") 
+            {
+                cm.replaceSelection("- " + selection);
+            } 
+            else 
+            {
+                var selectionText = selection.split("\n");
+
+                for (var i = 0, len = selectionText.length; i < len; i++) 
+                {
+                    selectionText[i] = (selectionText[i] === "") ? "" : "- " + selectionText[i];
+                }
+
+                cm.replaceSelection(selectionText.join("\n"));
+            }
+        },
+
+        "list-ol" : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if(selection === "") 
+            {
+                cm.replaceSelection("1. " + selection);
+            }
+            else
+            {
+                var selectionText = selection.split("\n");
+
+                for (var i = 0, len = selectionText.length; i < len; i++) 
+                {
+                    selectionText[i] = (selectionText[i] === "") ? "" : (i+1) + ". " + selectionText[i];
+                }
+
+                cm.replaceSelection(selectionText.join("\n"));
+            }
+        },
+
+        hr : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("------------");
+        },
+
+        tex : function() {
+            if (!this.settings.tex)
+            {
+                alert("settings.tex === false");
+                return this;
+            }
+            
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("$$" + selection + "$$");
+
+            if(selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 2);
+            }
+        },
+
+        link : function() {
+            this.executePlugin("linkDialog", "link-dialog/link-dialog");
+        },
+
+        "reference-link" : function() {
+            this.executePlugin("referenceLinkDialog", "reference-link-dialog/reference-link-dialog");
+        },
+
+        pagebreak : function() {
+            if (!this.settings.pageBreak)
+            {
+                alert("settings.pageBreak === false");
+                return this;
+            }
+            
+            var cm        = this.cm;
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("\r\n[========]\r\n");
+        },
+
+        image : function() {
+            this.executePlugin("imageDialog", "image-dialog/image-dialog");
+        },
+        
+        code : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("`" + selection + "`");
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 1);
+            }
+        },
+
+        "code-block" : function() {
+            this.executePlugin("codeBlockDialog", "code-block-dialog/code-block-dialog");            
+        },
+
+        "preformatted-text" : function() {
+            this.executePlugin("preformattedTextDialog", "preformatted-text-dialog/preformatted-text-dialog");
+        },
+        
+        table : function() {
+            this.executePlugin("tableDialog", "table-dialog/table-dialog");         
+        },
+        
+        datetime : function() {
+            var cm        = this.cm;
+            var selection = cm.getSelection();
+            var date      = new Date();
+            var langName  = this.settings.lang.name;
+            var datefmt   = editormd.dateFormat() + " " + editormd.dateFormat((langName === "zh-cn" || langName === "zh-tw") ? "cn-week-day" : "week-day");
+
+            cm.replaceSelection(datefmt);
+        },
+        
+        emoji : function() {
+            this.executePlugin("emojiDialog", "emoji-dialog/emoji-dialog");
+        },
+                
+        "html-entities" : function() {
+            this.executePlugin("htmlEntitiesDialog", "html-entities-dialog/html-entities-dialog");
+        },
+                
+        "goto-line" : function() {
+            this.executePlugin("gotoLineDialog", "goto-line-dialog/goto-line-dialog");
+        },
+
+        watch : function() {    
+            this[this.settings.watch ? "unwatch" : "watch"]();
+        },
+
+        preview : function() {
+            this.previewing();
+        },
+
+        fullscreen : function() {
+            this.fullscreen();
+        },
+
+        clear : function() {
+            this.clear();
+        },
+        
+        search : function() {
+            this.search();
+        },
+
+        help : function() {
+            this.executePlugin("helpDialog", "help-dialog/help-dialog");
+        },
+
+        info : function() {
+            this.showInfoDialog();
+        }
+    };
+    
+    editormd.keyMaps = {
+        "Ctrl-1"       : "h1",
+        "Ctrl-2"       : "h2",
+        "Ctrl-3"       : "h3",
+        "Ctrl-4"       : "h4",
+        "Ctrl-5"       : "h5",
+        "Ctrl-6"       : "h6",
+        "Ctrl-B"       : "bold",  // if this is string ==  editormd.toolbarHandlers.xxxx
+        "Ctrl-D"       : "datetime",
+        
+        "Ctrl-E"       : function() { // emoji
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+            
+            if (!this.settings.emoji)
+            {
+                alert("Error: settings.emoji == false");
+                return ;
+            }
+
+            cm.replaceSelection(":" + selection + ":");
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 1);
+            }
+        },
+        "Ctrl-Alt-G"   : "goto-line",
+        "Ctrl-H"       : "hr",
+        "Ctrl-I"       : "italic",
+        "Ctrl-K"       : "code",
+        
+        "Ctrl-L"        : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+            
+            var title = (selection === "") ? "" : " \""+selection+"\"";
+
+            cm.replaceSelection("[" + selection + "]("+title+")");
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 1);
+            }
+        },
+        "Ctrl-U"         : "list-ul",
+        
+        "Shift-Ctrl-A"   : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+            
+            if (!this.settings.atLink)
+            {
+                alert("Error: settings.atLink == false");
+                return ;
+            }
+
+            cm.replaceSelection("@" + selection);
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 1);
+            }
+        },
+        
+        "Shift-Ctrl-C"     : "code",
+        "Shift-Ctrl-Q"     : "quote",
+        "Shift-Ctrl-S"     : "del",
+        "Shift-Ctrl-K"     : "tex",  // KaTeX
+        
+        "Shift-Alt-C"      : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+            
+            cm.replaceSelection(["```", selection, "```"].join("\n"));
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 3);
+            } 
+        },
+        
+        "Shift-Ctrl-Alt-C" : "code-block",
+        "Shift-Ctrl-H"     : "html-entities",
+        "Shift-Alt-H"      : "help",
+        "Shift-Ctrl-E"     : "emoji",
+        "Shift-Ctrl-U"     : "uppercase",
+        "Shift-Alt-U"      : "ucwords",
+        "Shift-Ctrl-Alt-U" : "ucfirst",
+        "Shift-Alt-L"      : "lowercase",
+        
+        "Shift-Ctrl-I"     : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+            
+            var title = (selection === "") ? "" : " \""+selection+"\"";
+
+            cm.replaceSelection("![" + selection + "]("+title+")");
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 4);
+            }
+        },
+        
+        "Shift-Ctrl-Alt-I" : "image",
+        "Shift-Ctrl-L"     : "link",
+        "Shift-Ctrl-O"     : "list-ol",
+        "Shift-Ctrl-P"     : "preformatted-text",
+        "Shift-Ctrl-T"     : "table",
+        "Shift-Alt-P"      : "pagebreak",
+        "F9"               : "watch",
+        "F10"              : "preview",
+        "F11"              : "fullscreen",
+    };
+    
+    /**
+     * 清除字符串两边的空格
+     * Clear the space of strings both sides.
+     * 
+     * @param   {String}    str            string
+     * @returns {String}                   trimed string    
+     */
+    
+    var trim = function(str) {
+        return (!String.prototype.trim) ? str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "") : str.trim();
+    };
+    
+    editormd.trim = trim;
+    
+    /**
+     * 所有单词首字母大写
+     * Words first to uppercase
+     * 
+     * @param   {String}    str            string
+     * @returns {String}                   string
+     */
+    
+    var ucwords = function (str) {
+        return str.toLowerCase().replace(/\b(\w)|\s(\w)/g, function($1) {  
+            return $1.toUpperCase();
+        });
+    };
+    
+    editormd.ucwords = editormd.wordsFirstUpperCase = ucwords;
+    
+    /**
+     * 字符串首字母大写
+     * Only string first char to uppercase
+     * 
+     * @param   {String}    str            string
+     * @returns {String}                   string
+     */
+    
+    var firstUpperCase = function(str) {        
+        return str.toLowerCase().replace(/\b(\w)/, function($1){
+            return $1.toUpperCase();
+        });
+    };
+    
+    var ucfirst = firstUpperCase;
+    
+    editormd.firstUpperCase = editormd.ucfirst = firstUpperCase;
+    
+    editormd.urls = {
+        atLinkBase : "https://github.com/"
+    };
+    
+    editormd.regexs = {
+        atLink        : /@(\w+)/g,
+        email         : /(\w+)@(\w+)\.(\w+)\.?(\w+)?/g,
+        emailLink     : /(mailto:)?([\w\.\_]+)@(\w+)\.(\w+)\.?(\w+)?/g,
+        emoji         : /:([\w\+-]+):/g,
+        emojiDatetime : /(\d{2}:\d{2}:\d{2})/g,
+        twemoji       : /:(tw-([\w]+)-?(\w+)?):/g,
+        fontAwesome   : /:(fa-([\w]+)(-(\w+)){0,}):/g,
+        editormdLogo  : /:(editormd-logo-?(\w+)?):/g,
+        pageBreak     : /^\[[=]{8,}\]$/
+    };
+
+    // Emoji graphics files url path
+    editormd.emoji     = {
+        path  : "http://www.emoji-cheat-sheet.com/graphics/emojis/",
+        ext   : ".png"
+    };
+
+    // Twitter Emoji (Twemoji)  graphics files url path    
+    editormd.twemoji = {
+        path : "http://twemoji.maxcdn.com/36x36/",
+        ext  : ".png"
+    };
+
+    /**
+     * 自定义marked的解析器
+     * Custom Marked renderer rules
+     * 
+     * @param   {Array}    markdownToC     传入用于接收TOC的数组
+     * @returns {Renderer} markedRenderer  返回marked的Renderer自定义对象
+     */
+
+    editormd.markedRenderer = function(markdownToC, options) {
+        var defaults = {
+            toc                  : true,           // Table of contents
+            tocm                 : false,
+            tocStartLevel        : 1,              // Said from H1 to create ToC  
+            pageBreak            : true,
+            atLink               : true,           // for @link
+            emailLink            : true,           // for mail address auto link
+            taskList             : false,          // Enable Github Flavored Markdown task lists
+            emoji                : false,          // :emoji: , Support Twemoji, fontAwesome, Editor.md logo emojis.
+            tex                  : false,          // TeX(LaTeX), based on KaTeX
+            flowChart            : false,          // flowChart.js only support IE9+
+            sequenceDiagram      : false,          // sequenceDiagram.js only support IE9+
+        };
+        
+        var settings        = $.extend(defaults, options || {});    
+        var marked          = editormd.$marked;
+        var markedRenderer  = new marked.Renderer();
+        markdownToC         = markdownToC || [];        
+            
+        var regexs          = editormd.regexs;
+        var atLinkReg       = regexs.atLink;
+        var emojiReg        = regexs.emoji;
+        var emailReg        = regexs.email;
+        var emailLinkReg    = regexs.emailLink;
+        var twemojiReg      = regexs.twemoji;
+        var faIconReg       = regexs.fontAwesome;
+        var editormdLogoReg = regexs.editormdLogo;
+        var pageBreakReg    = regexs.pageBreak;
+
+        markedRenderer.emoji = function(text) {
+            
+            text = text.replace(editormd.regexs.emojiDatetime, function($1) {           
+                return $1.replace(/:/g, "&#58;");
+            });
+            
+            var matchs = text.match(emojiReg);
+
+            if (!matchs || !settings.emoji) {
+                return text;
+            }
+
+            for (var i = 0, len = matchs.length; i < len; i++)
+            {            
+                if (matchs[i] === ":+1:") {
+                    matchs[i] = ":\\+1:";
+                }
+
+                text = text.replace(new RegExp(matchs[i]), function($1, $2){
+                    var faMatchs = $1.match(faIconReg);
+                    var name     = $1.replace(/:/g, "");
+
+                    if (faMatchs)
+                    {                        
+                        for (var fa = 0, len1 = faMatchs.length; fa < len1; fa++)
+                        {
+                            var faName = faMatchs[fa].replace(/:/g, "");
+                            
+                            return "<i class=\"fa " + faName + " fa-emoji\" title=\"" + faName.replace("fa-", "") + "\"></i>";
+                        }
+                    }
+                    else
+                    {
+                        var emdlogoMathcs = $1.match(editormdLogoReg);
+                        var twemojiMatchs = $1.match(twemojiReg);
+
+                        if (emdlogoMathcs)                                        
+                        {                            
+                            for (var x = 0, len2 = emdlogoMathcs.length; x < len2; x++)
+                            {
+                                var logoName = emdlogoMathcs[x].replace(/:/g, "");
+                                return "<i class=\"" + logoName + "\" title=\"Editor.md logo (" + logoName + ")\"></i>";
+                            }
+                        }
+                        else if (twemojiMatchs) 
+                        {
+                            for (var t = 0, len3 = twemojiMatchs.length; t < len3; t++)
+                            {
+                                var twe = twemojiMatchs[t].replace(/:/g, "").replace("tw-", "");
+                                return "<img src=\"" + editormd.twemoji.path + twe + editormd.twemoji.ext + "\" title=\"twemoji-" + twe + "\" alt=\"twemoji-" + twe + "\" class=\"emoji twemoji\" />";
+                            }
+                        }
+                        else
+                        {
+                            var src = (name === "+1") ? "plus1" : name;
+                            src     = (src === "black_large_square") ? "black_square" : src;
+
+                            return "<img src=\"" + editormd.emoji.path + src + editormd.emoji.ext + "\" class=\"emoji\" title=\"&#58;" + name + "&#58;\" alt=\"&#58;" + name + "&#58;\" />";
+                        }
+                    }
+                });
+            }
+
+            return text;
+        };
+
+        markedRenderer.atLink = function(text) {
+
+            if (atLinkReg.test(text))
+            { 
+                if (settings.atLink) 
+                {
+                    text = text.replace(emailReg, function($1, $2, $3, $4) {
+                        return $1.replace(/@/g, "_#_&#64;_#_");
+                    });
+
+                    text = text.replace(atLinkReg, function($1, $2) {
+                        return "<a href=\"" + editormd.urls.atLinkBase + "" + $2 + "\" title=\"&#64;" + $2 + "\" class=\"at-link\">" + $1 + "</a>";
+                    }).replace(/_#_&#64;_#_/g, "@");
+                }
+                
+                if (settings.emailLink)
+                {
+                    text = text.replace(emailLinkReg, function($1, $2, $3, $4, $5) {
+                        return (!$2 && $.inArray($5, "jpg|jpeg|png|gif|webp|ico|icon|pdf".split("|")) < 0) ? "<a href=\"mailto:" + $1 + "\">"+$1+"</a>" : $1;
+                    });
+                }
+
+                return text;
+            }
+
+            return text;
+        };
+                
+        markedRenderer.link = function (href, title, text) {
+
+            if (this.options.sanitize) {
+                try {
+                    var prot = decodeURIComponent(unescape(href)).replace(/[^\w:]/g,"").toLowerCase();
+                } catch(e) {
+                    return "";
+                }
+
+                if (prot.indexOf("javascript:") === 0) {
+                    return "";
+                }
+            }
+
+            var out = "<a href=\"" + href + "\"";
+            
+            if (atLinkReg.test(title) || atLinkReg.test(text))
+            {
+                if (title)
+                {
+                    out += " title=\"" + title.replace(/@/g, "&#64;");
+                }
+                
+                return out + "\">" + text.replace(/@/g, "&#64;") + "</a>";
+            }
+
+            if (title) {
+                out += " title=\"" + title + "\"";
+            }
+
+            out += ">" + text + "</a>";
+
+            return out;
+        };
+        
+        markedRenderer.heading = function(text, level, raw) {
+                    
+            var linkText       = text;
+            var hasLinkReg     = /\s*\<a\s*href\=\"(.*)\"\s*([^\>]*)\>(.*)\<\/a\>\s*/;
+            var getLinkTextReg = /\s*\<a\s*([^\>]+)\>([^\>]*)\<\/a\>\s*/g;
+
+            if (hasLinkReg.test(text)) 
+            {
+                var tempText = [];
+                text         = text.split(/\<a\s*([^\>]+)\>([^\>]*)\<\/a\>/);
+
+                for (var i = 0, len = text.length; i < len; i++)
+                {
+                    tempText.push(text[i].replace(/\s*href\=\"(.*)\"\s*/g, ""));
+                }
+
+                text = tempText.join(" ");
+            }
+            
+            text = trim(text);
+            
+            var escapedText    = text.toLowerCase().replace(/[^\w]+/g, "-");
+            var toc = {
+                text  : text,
+                level : level,
+                slug  : escapedText
+            };
+            
+            var isChinese = /^[\u4e00-\u9fa5]+$/.test(text);
+            var id        = (isChinese) ? escape(text).replace(/\%/g, "") : text.toLowerCase().replace(/[^\w]+/g, "-");
+
+            markdownToC.push(toc);
+            
+            var headingHTML = "<h" + level + " id=\"h"+ level + "-" + this.options.headerPrefix + id +"\">";
+            
+            headingHTML    += "<a name=\"" + text + "\" class=\"reference-link\"></a>";
+            headingHTML    += "<span class=\"header-link octicon1 octicon-link\"></span>";
+            headingHTML    += (hasLinkReg) ? this.atLink(this.emoji(linkText)) : this.atLink(this.emoji(text));
+            headingHTML    += "</h" + level + ">";
+
+            return headingHTML;
+        };
+        
+        markedRenderer.pageBreak = function(text) {
+            if (pageBreakReg.test(text) && settings.pageBreak)
+            {
+                text = "<hr style=\"page-break-after:always;\" class=\"page-break editormd-page-break\" />";
+            }
+            
+            return text;
+        };
+
+        markedRenderer.paragraph = function(text) {
+            var isTeXInline     = /\$\$(.*)\$\$/g.test(text);
+            var isTeXLine       = /^\$\$(.*)\$\$$/.test(text);
+            var isTeXAddClass   = (isTeXLine)     ? " class=\"" + editormd.classNames.tex + "\"" : "";
+            var isToC           = (settings.tocm) ? /^(\[TOC\]|\[TOCM\])$/.test(text) : /^\[TOC\]$/.test(text);
+            var isToCMenu       = /^\[TOCM\]$/.test(text);
+            
+            if (!isTeXLine && isTeXInline) 
+            {
+                text = text.replace(/(\$\$([^\$]*)\$\$)+/g, function($1, $2) {
+                    return "<span class=\"" + editormd.classNames.tex + "\">" + $2.replace(/\$/g, "") + "</span>";
+                });
+            } 
+            else 
+            {
+                text = (isTeXLine) ? text.replace(/\$/g, "") : text;
+            }
+            
+            var tocHTML = "<div class=\"markdown-toc editormd-markdown-toc\">" + text + "</div>";
+            
+            return (isToC) ? ( (isToCMenu) ? "<div class=\"editormd-toc-menu\">" + tocHTML + "</div><br/>" : tocHTML )
+                           : ( (pageBreakReg.test(text)) ? this.pageBreak(text) : "<p" + isTeXAddClass + ">" + this.atLink(this.emoji(text)) + "</p>\n" );
+        };
+
+        markedRenderer.code = function (code, lang, escaped) { 
+
+            if (lang === "seq" || lang === "sequence")
+            {
+                return "<div class=\"sequence-diagram\">" + code + "</div>";
+            } 
+            else if ( lang === "flow")
+            {
+                return "<div class=\"flowchart\">" + code + "</div>";
+            } 
+            else 
+            {
+
+                return marked.Renderer.prototype.code.apply(this, arguments);
+            }
+        };
+
+        markedRenderer.tablecell = function(content, flags) {
+            var type = (flags.header) ? "th" : "td";
+            var tag  = (flags.align)  ? "<" + type +" style=\"text-align:" + flags.align + "\">" : "<" + type + ">";
+            
+            return tag + this.atLink(this.emoji(content)) + "</" + type + ">\n";
+        };
+
+        markedRenderer.listitem = function(text) {
+            if (settings.taskList && /^\s*\[[x\s]\]\s*/.test(text)) 
+            {
+                text = text.replace(/^\s*\[\s\]\s*/, "<input type=\"checkbox\" class=\"task-list-item-checkbox\" /> ")
+                           .replace(/^\s*\[x\]\s*/,  "<input type=\"checkbox\" class=\"task-list-item-checkbox\" checked disabled /> ");
+
+                return "<li style=\"list-style: none;\">" + this.atLink(this.emoji(text)) + "</li>";
+            }
+            else 
+            {
+                return "<li>" + this.atLink(this.emoji(text)) + "</li>";
+            }
+        };
+        
+        return markedRenderer;
+    };
+    
+    /**
+     *
+     * 生成TOC(Table of Contents)
+     * Creating ToC (Table of Contents)
+     * 
+     * @param   {Array}    toc             从marked获取的TOC数组列表
+     * @param   {Element}  container       插入TOC的容器元素
+     * @param   {Integer}  startLevel      Hx 起始层级
+     * @returns {Object}   tocContainer    返回ToC列表容器层的jQuery对象元素
+     */
+    
+    editormd.markdownToCRenderer = function(toc, container, tocDropdown, startLevel) {
+        
+        var html        = "";    
+        var lastLevel   = 0;
+        var classPrefix = this.classPrefix;
+        
+        startLevel      = startLevel  || 1;
+        
+        for (var i = 0, len = toc.length; i < len; i++) 
+        {
+            var text  = toc[i].text;
+            var level = toc[i].level;
+            
+            if (level < startLevel) {
+                continue;
+            }
+            
+            if (level > lastLevel) 
+            {
+                html += "";
+            }
+            else if (level < lastLevel) 
+            {
+                html += (new Array(lastLevel - level + 2)).join("</ul></li>");
+            } 
+            else 
+            {
+                html += "</ul></li>";
+            }
+
+            html += "<li><a class=\"toc-level-" + level + "\" href=\"#" + text + "\" level=\"" + level + "\">" + text + "</a><ul>";
+            lastLevel = level;
+        }
+        
+        var tocContainer = container.find(".markdown-toc");
+        
+        if (tocContainer.length < 1 && container.attr("previewContainer") === "false")
+        {
+            var tocHTML = "<div class=\"markdown-toc " + classPrefix + "markdown-toc\"></div>";
+            
+            tocHTML = (tocDropdown) ? "<div class=\"" + classPrefix + "toc-menu\">" + tocHTML + "</div>" : tocHTML;
+            
+            container.html(tocHTML);
+            
+            tocContainer = container.find(".markdown-toc");
+        }
+        
+        if (tocDropdown)
+        {
+            tocContainer.wrap("<div class=\"" + classPrefix + "toc-menu\"></div><br/>");
+        }
+        
+        tocContainer.html("<ul class=\"markdown-toc-list\"></ul>").children(".markdown-toc-list").html(html.replace(/\r?\n?\<ul\>\<\/ul\>/g, ""));
+        
+        return tocContainer;
+    };
+    
+    /**
+     *
+     * 生成TOC下拉菜单
+     * Creating ToC dropdown menu
+     * 
+     * @param   {Object}   container       插入TOC的容器jQuery对象元素
+     * @param   {String}   tocTitle        ToC title
+     * @returns {Object}                   return toc-menu object
+     */
+    
+    editormd.tocDropdownMenu = function(container, tocTitle) {
+        
+        tocTitle      = tocTitle || "Table of Contents";
+        
+        var zindex    = 400;
+        var tocMenus  = container.find("." + this.classPrefix + "toc-menu");
+
+        tocMenus.each(function() {
+            var $this  = $(this);
+            var toc    = $this.children(".markdown-toc");
+            var icon   = "<i class=\"fa fa-angle-down\"></i>";
+            var btn    = "<a href=\"javascript:;\" class=\"toc-menu-btn\">" + icon + tocTitle + "</a>";
+            var menu   = toc.children("ul");            
+            var list   = menu.find("li");
+            
+            toc.append(btn);
+            
+            list.first().before("<li><h1>" + tocTitle + " " + icon + "</h1></li>");
+            
+            $this.mouseover(function(){
+                menu.show();
+
+                list.each(function(){
+                    var li = $(this);
+                    var ul = li.children("ul");
+
+                    if (ul.html() === "")
+                    {
+                        ul.remove();
+                    }
+
+                    if (ul.length > 0 && ul.html() !== "")
+                    {
+                        var firstA = li.children("a").first();
+
+                        if (firstA.children(".fa").length < 1)
+                        {
+                            firstA.append( $(icon).css({ float:"right", paddingTop:"4px" }) );
+                        }
+                    }
+
+                    li.mouseover(function(){
+                        ul.css("z-index", zindex).show();
+                        zindex += 1;
+                    }).mouseleave(function(){
+                        ul.hide();
+                    });
+                });
+            }).mouseleave(function(){
+                menu.hide();
+            }); 
+        });       
+        
+        return tocMenus;
+    };
+    
+    /**
+     * 简单地过滤指定的HTML标签
+     * Filter custom html tags
+     * 
+     * @param   {String}   html          要过滤HTML
+     * @param   {String}   filters       要过滤的标签
+     * @returns {String}   html          返回过滤的HTML
+     */
+    
+    editormd.filterHTMLTags = function(html, filters) {
+        
+        if (typeof html !== "string") {
+            html = new String(html);
+        }
+            
+        if (typeof filters !== "string") {
+            return html;
+        }
+
+        var expression = filters.split("|");
+        var filterTags = expression[0].split(",");
+        var attrs      = expression[1];
+
+        for (var i = 0, len = filterTags.length; i < len; i++)
+        {
+            var tag = filterTags[i];
+
+            html = html.replace(new RegExp("\<\s*" + tag + "\s*([^\>]*)\>([^\>]*)\<\s*\/" + tag + "\s*\>", "igm"), "");
+        }
+
+        if (typeof attrs !== "undefined")
+        {
+            var htmlTagRegex = /\<(\w+)\s*([^\>]*)\>([^\>]*)\<\/(\w+)\>/ig;
+
+            if (attrs === "*")
+            {
+                html = html.replace(htmlTagRegex, function($1, $2, $3, $4, $5) {
+                    return "<" + $2 + ">" + $4 + "</" + $5 + ">";
+                });         
+            }
+            else if (attrs === "on*")
+            {
+                html = html.replace(htmlTagRegex, function($1, $2, $3, $4, $5) {
+                    var el = $("<" + $2 + ">" + $4 + "</" + $5 + ">");
+                    var _attrs = $($1)[0].attributes;
+                    var $attrs = {};
+                    
+                    $.each(_attrs, function(i, e) {
+                        $attrs[e.nodeName] = e.nodeValue;
+                    });
+                    
+                    $.each($attrs, function(i) {                
+                        if (i.indexOf("on") === 0) {
+                            delete $attrs[i];
+                        }
+                    });
+                    
+                    el.attr($attrs);
+
+                    return el[0].outerHTML;
+                });
+            }
+            else
+            {
+                html = html.replace(htmlTagRegex, function($1, $2, $3, $4) {
+                    var filterAttrs = attrs.split(",");
+                    var el = $($1);
+                    el.html($4);
+
+                    $.each(filterAttrs, function(i) {
+                        el.attr(filterAttrs[i], null);
+                    });
+
+                    return el[0].outerHTML;
+                });
+            }
+        }
+        
+        return html;
+    };
+    
+    /**
+     * 将Markdown文档解析为HTML用于前台显示
+     * Parse Markdown to HTML for Font-end preview.
+     * 
+     * @param   {String}   id            用于显示HTML的对象ID
+     * @param   {Object}   [options={}]  配置选项,可选
+     * @returns {Object}   div           返回jQuery对象元素
+     */
+    
+    editormd.markdownToHTML = function(id, options) {
+        var defaults = {
+            gfm                  : true,
+            toc                  : true,
+            tocm                 : false,
+            tocStartLevel        : 1,
+            tocTitle             : "目录",
+            tocDropdown          : false,
+            markdown             : "",
+            htmlDecode           : false,
+            autoLoadKaTeX        : true,
+            pageBreak            : true,
+            atLink               : true,    // for @link
+            emailLink            : true,    // for mail address auto link
+            tex                  : false,
+            taskList             : false,   // Github Flavored Markdown task lists
+            emoji                : false,
+            flowChart            : false,
+            sequenceDiagram      : false,
+            previewCodeHighlight : true
+        };
+        
+        editormd.$marked  = marked;
+
+        var div           = $("#" + id);
+        var settings      = div.settings = $.extend(true, defaults, options || {});
+        var saveTo        = div.find("textarea");
+        
+        if (saveTo.length < 1)
+        {
+            div.append("<textarea></textarea>");
+            saveTo        = div.find("textarea");
+        }        
+        
+        var markdownDoc   = (settings.markdown === "") ? saveTo.val() : settings.markdown; 
+        var markdownToC   = [];
+
+        var rendererOptions = {  
+            toc                  : settings.toc,
+            tocm                 : settings.tocm,
+            tocStartLevel        : settings.tocStartLevel,
+            taskList             : settings.taskList,
+            emoji                : settings.emoji,
+            tex                  : settings.tex,
+            pageBreak            : settings.pageBreak,
+            atLink               : settings.atLink,           // for @link
+            emailLink            : settings.emailLink,        // for mail address auto link
+            flowChart            : settings.flowChart,
+            sequenceDiagram      : settings.sequenceDiagram,
+            previewCodeHighlight : settings.previewCodeHighlight,
+        };
+
+        var markedOptions = {
+            renderer    : editormd.markedRenderer(markdownToC, rendererOptions),
+            gfm         : settings.gfm,
+            tables      : true,
+            breaks      : true,
+            pedantic    : false,
+            sanitize    : (settings.htmlDecode) ? false : true, // 是否忽略HTML标签,即是否开启HTML标签解析,为了安全性,默认不开启
+            smartLists  : true,
+            smartypants : true
+        };
+        
+		markdownDoc = new String(markdownDoc);
+        markdownDoc = editormd.filterHTMLTags(markdownDoc, settings.htmlDecode);
+        
+        var markdownParsed = marked(markdownDoc, markedOptions);
+        
+        saveTo.val(markdownDoc);
+        
+        div.addClass("markdown-body " + this.classPrefix + "html-preview").append(markdownParsed);
+         
+        if (settings.toc) 
+        {
+            div.tocContainer = this.markdownToCRenderer(markdownToC, div, settings.tocDropdown, settings.tocStartLevel);
+            
+            if (settings.tocDropdown || div.find("." + this.classPrefix + "toc-menu").length > 0)
+            {
+                this.tocDropdownMenu(div, settings.tocTitle);
+            }
+        }
+            
+        if (settings.previewCodeHighlight) 
+        {
+            div.find("pre").addClass("prettyprint linenums");
+            prettyPrint();
+        }
+        
+        if (!editormd.isIE8) 
+        {
+            if (settings.flowChart) {
+                div.find(".flowchart").flowChart(); 
+            }
+
+            if (settings.sequenceDiagram) {
+                div.find(".sequence-diagram").sequenceDiagram({theme: "simple"});
+            }
+        }
+
+        if (settings.tex)
+        {
+            var katexHandle = function() {
+                div.find("." + editormd.classNames.tex).each(function(){
+                    var tex  = $(this);
+                    katex.render(tex.html().replace(/&lt;/g, "<").replace(/&gt;/g, ">"), tex[0]);
+                });
+            };
+            
+            if (settings.autoLoadKaTeX && !editormd.$katex && !editormd.kaTeXLoaded)
+            {
+                this.loadKaTeX(function() {
+                    editormd.$katex      = katex;
+                    editormd.kaTeXLoaded = true;
+                    katexHandle();
+                });
+            }
+            else
+            {
+                katexHandle();
+            }
+        }
+        
+        div.getMarkdown = function() {            
+            return saveTo.val();
+        };
+        
+        return div;
+    };
+    
+    editormd.themes = [
+        "default", "3024-day", "3024-night",
+        "ambiance", "ambiance-mobile",
+        "base16-dark", "base16-light", "blackboard",
+        "cobalt",
+        "eclipse", "elegant", "erlang-dark",
+        "lesser-dark",
+        "mbo", "mdn-like", "midnight", "monokai",
+        "neat", "neo", "night",
+        "paraiso-dark", "paraiso-light", "pastel-on-dark",
+        "rubyblue",
+        "solarized",
+        "the-matrix", "tomorrow-night-eighties", "twilight",
+        "vibrant-ink",
+        "xq-dark", "xq-light"
+    ];
+
+    editormd.loadPlugins = {};
+    
+    editormd.loadFiles = {
+        js     : [],
+        css    : [],
+        plugin : []
+    };
+    
+    /**
+     * 动态加载Editor.md插件,但不立即执行
+     * Load editor.md plugins
+     * 
+     * @param {String}   fileName              插件文件路径
+     * @param {Function} [callback=function()] 加载成功后执行的回调函数
+     * @param {String}   [into="head"]         嵌入页面的位置
+     */
+    
+    editormd.loadPlugin = function(fileName, callback, into) {
+        callback   = callback || function() {};
+        
+        this.loadScript(fileName, function() {
+            editormd.loadFiles.plugin.push(fileName);
+            callback();
+        }, into);
+    };
+    
+    /**
+     * 动态加载CSS文件的方法
+     * Load css file method
+     * 
+     * @param {String}   fileName              CSS文件名
+     * @param {Function} [callback=function()] 加载成功后执行的回调函数
+     * @param {String}   [into="head"]         嵌入页面的位置
+     */
+    
+    editormd.loadCSS   = function(fileName, callback, into) {
+        into       = into     || "head";        
+        callback   = callback || function() {};
+        
+        var css    = document.createElement("link");
+        css.type   = "text/css";
+        css.rel    = "stylesheet";
+        css.onload = css.onreadystatechange = function() {
+            editormd.loadFiles.css.push(fileName);
+            callback();
+        };
+
+        css.href   = fileName + ".css";
+
+        if(into === "head") {
+            document.getElementsByTagName("head")[0].appendChild(css);
+        } else {
+            document.body.appendChild(css);
+        }
+    };
+    
+    editormd.isIE    = (navigator.appName == "Microsoft Internet Explorer");
+    editormd.isIE8   = (editormd.isIE && navigator.appVersion.match(/8./i) == "8.");
+
+    /**
+     * 动态加载JS文件的方法
+     * Load javascript file method
+     * 
+     * @param {String}   fileName              JS文件名
+     * @param {Function} [callback=function()] 加载成功后执行的回调函数
+     * @param {String}   [into="head"]         嵌入页面的位置
+     */
+
+    editormd.loadScript = function(fileName, callback, into) {
+        
+        into          = into     || "head";
+        callback      = callback || function() {};
+        
+        var script    = null; 
+        script        = document.createElement("script");
+        script.id     = fileName.replace(/[\./]+/g, "-");
+        script.type   = "text/javascript";        
+        script.src    = fileName + ".js";
+        
+        if (editormd.isIE8) 
+        {            
+            script.onreadystatechange = function() {
+                if(script.readyState) 
+                {
+                    if (script.readyState === "loaded" || script.readyState === "complete") 
+                    {
+                        script.onreadystatechange = null; 
+                        editormd.loadFiles.js.push(fileName);
+                        callback();
+                    }
+                } 
+            };
+        }
+        else
+        {
+            script.onload = function() {
+                editormd.loadFiles.js.push(fileName);
+                callback();
+            };
+        }
+
+        if (into === "head") {
+            document.getElementsByTagName("head")[0].appendChild(script);
+        } else {
+            document.body.appendChild(script);
+        }
+    };
+    
+    // 使用国外的CDN,加载速度有时会很慢,或者自定义URL
+    // You can custom KaTeX load url.
+    editormd.katexURL  = {
+        css : "//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min",
+        js  : "//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min"
+    };
+    
+    editormd.kaTeXLoaded = false;
+    
+    /**
+     * 加载KaTeX文件
+     * load KaTeX files
+     * 
+     * @param {Function} [callback=function()]  加载成功后执行的回调函数
+     */
+    
+    editormd.loadKaTeX = function (callback) {
+        editormd.loadCSS(editormd.katexURL.css, function(){
+            editormd.loadScript(editormd.katexURL.js, callback || function(){});
+        });
+    };
+        
+    /**
+     * 锁屏
+     * lock screen
+     * 
+     * @param   {Boolean}   lock   Boolean 布尔值,是否锁屏
+     * @returns {void}
+     */
+    
+    editormd.lockScreen = function(lock) {
+        $("html,body").css("overflow", (lock) ? "hidden" : "");
+    };
+        
+    /**
+     * 动态创建对话框
+     * Creating custom dialogs
+     * 
+     * @param   {Object} options 配置项键值对 Key/Value
+     * @returns {dialog} 返回创建的dialog的jQuery实例对象
+     */
+
+    editormd.createDialog = function(options) {
+        var defaults = {
+            name : "",
+            width : 420,
+            height: 240,
+            title : "",
+            drag  : true,
+            closed : true,
+            content : "",
+            mask : true,
+            maskStyle : {
+                backgroundColor : "#fff",
+                opacity : 0.1
+            },
+            lockScreen : true,
+            footer : true,
+            buttons : false
+        };
+
+        options          = $.extend(true, defaults, options);
+
+        var editor       = this.editor;
+        var classPrefix  = editormd.classPrefix;
+        var guid         = (new Date()).getTime();
+        var dialogName   = ( (options.name === "") ? classPrefix + "dialog-" + guid : options.name);
+        var mouseOrTouch = editormd.mouseOrTouch;
+
+        var html         = "<div class=\"" + classPrefix + "dialog " + dialogName + "\">";
+
+        if (options.title !== "")
+        {
+            html += "<div class=\"" + classPrefix + "dialog-header\"" + ( (options.drag) ? " style=\"cursor: move;\"" : "" ) + ">";
+            html += "<strong class=\"" + classPrefix + "dialog-title\">" + options.title + "</strong>";
+            html += "</div>";
+        }
+
+        if (options.closed)
+        {
+            html += "<a href=\"javascript:;\" class=\"fa fa-close " + classPrefix + "dialog-close\"></a>";
+        }
+
+        html += "<div class=\"" + classPrefix + "dialog-container\">" + options.content;                    
+
+        if (options.footer || typeof options.footer === "string") 
+        {
+            html += "<div class=\"" + classPrefix + "dialog-footer\">" + ( (typeof options.footer === "boolean") ? "" : options.footer) + "</div>";
+        }
+
+        html += "</div>";
+
+        html += "<div class=\"" + classPrefix + "dialog-mask " + classPrefix + "dialog-mask-bg\"></div>";
+        html += "<div class=\"" + classPrefix + "dialog-mask " + classPrefix + "dialog-mask-con\"></div>";
+        html += "</div>";
+
+        editor.append(html);
+
+        var dialog = editor.find("." + dialogName);
+
+        dialog.lockScreen = function(lock) {
+            if (options.lockScreen)
+            {                
+                $("html,body").css("overflow", (lock) ? "hidden" : "");
+            }
+
+            return dialog;
+        };
+
+        dialog.showMask = function() {
+            if (options.mask)
+            {
+                editor.find("." + classPrefix + "mask").css(options.maskStyle).css("z-index", editormd.dialogZindex - 1).show();
+            }
+            return dialog;
+        };
+
+        dialog.hideMask = function() {
+            if (options.mask)
+            {
+                editor.find("." + classPrefix + "mask").hide();
+            }
+
+            return dialog;
+        };
+
+        dialog.loading = function(show) {                        
+            var loading = dialog.find("." + classPrefix + "dialog-mask");
+            loading[(show) ? "show" : "hide"]();
+
+            return dialog;
+        };
+
+        dialog.lockScreen(true).showMask();
+
+        dialog.show().css({
+            zIndex : editormd.dialogZindex,
+            border : (editormd.isIE8) ? "1px solid #ddd" : "",
+            width  : (typeof options.width  === "number") ? options.width + "px"  : options.width,
+            height : (typeof options.height === "number") ? options.height + "px" : options.height
+        });
+
+        var dialogPosition = function(){
+            dialog.css({
+                top    : ($(window).height() - dialog.height()) / 2 + "px",
+                left   : ($(window).width() - dialog.width()) / 2 + "px"
+            });
+        };
+
+        dialogPosition();
+
+        $(window).resize(dialogPosition);
+
+        dialog.children("." + classPrefix + "dialog-close").bind(mouseOrTouch("click", "touchend"), function() {
+            dialog.hide().lockScreen(false).hideMask();
+        });
+
+        if (typeof options.buttons === "object")
+        {
+            var footer = dialog.footer = dialog.find("." + classPrefix + "dialog-footer");
+
+            for (var key in options.buttons)
+            {
+                var btn = options.buttons[key];
+                var btnClassName = classPrefix + key + "-btn";
+
+                footer.append("<button class=\"" + classPrefix + "btn " + btnClassName + "\">" + btn[0] + "</button>");
+                btn[1] = $.proxy(btn[1], dialog);
+                footer.children("." + btnClassName).bind(mouseOrTouch("click", "touchend"), btn[1]);
+            }
+        }
+
+        if (options.title !== "" && options.drag)
+        {                        
+            var posX, posY;
+            var dialogHeader = dialog.children("." + classPrefix + "dialog-header");
+
+            if (!options.mask) {
+                dialogHeader.bind(mouseOrTouch("click", "touchend"), function(){
+                    editormd.dialogZindex += 2;
+                    dialog.css("z-index", editormd.dialogZindex);
+                });
+            }
+
+            dialogHeader.mousedown(function(e) {
+                e = e || window.event;  //IE
+                posX = e.clientX - parseInt(dialog[0].style.left);
+                posY = e.clientY - parseInt(dialog[0].style.top);
+
+                document.onmousemove = moveAction;                   
+            });
+
+            var userCanSelect = function (obj) {
+                obj.removeClass(classPrefix + "user-unselect").off("selectstart");
+            };
+
+            var userUnselect = function (obj) {
+                obj.addClass(classPrefix + "user-unselect").on("selectstart", function(event) { // selectstart for IE                        
+                    return false;
+                });
+            };
+
+            var moveAction = function (e) {
+                e = e || window.event;  //IE
+
+                var left, top, nowLeft = parseInt(dialog[0].style.left), nowTop = parseInt(dialog[0].style.top);
+
+                if( nowLeft >= 0 ) {
+                    if( nowLeft + dialog.width() <= $(window).width()) {
+                        left = e.clientX - posX;
+                    } else {	
+                        left = $(window).width() - dialog.width();
+                        document.onmousemove = null;
+                    }
+                } else {
+                    left = 0;
+                    document.onmousemove = null;
+                }
+
+                if( nowTop >= 0 ) {
+                    top = e.clientY - posY;
+                } else {
+                    top = 0;
+                    document.onmousemove = null;
+                }
+
+
+                document.onselectstart = function() {
+                    return false;
+                };
+
+                userUnselect($("body"));
+                userUnselect(dialog);
+                dialog[0].style.left = left + "px";
+                dialog[0].style.top  = top + "px";
+            };
+
+            document.onmouseup = function() {                            
+                userCanSelect($("body"));
+                userCanSelect(dialog);
+
+                document.onselectstart = null;         
+                document.onmousemove = null;
+            };
+
+            dialogHeader.touchDraggable = function() {
+                var offset = null;
+                var start  = function(e) {
+                    var orig = e.originalEvent; 
+                    var pos  = $(this).parent().position();
+
+                    offset = {
+                        x : orig.changedTouches[0].pageX - pos.left,
+                        y : orig.changedTouches[0].pageY - pos.top
+                    };
+                };
+
+                var move = function(e) {
+                    e.preventDefault();
+                    var orig = e.originalEvent;
+
+                    $(this).parent().css({
+                        top  : orig.changedTouches[0].pageY - offset.y,
+                        left : orig.changedTouches[0].pageX - offset.x
+                    });
+                };
+
+                this.bind("touchstart", start).bind("touchmove", move);
+            };
+
+            dialogHeader.touchDraggable();
+        }
+
+        editormd.dialogZindex += 2;
+
+        return dialog;
+    };
+    
+    /**
+     * 鼠标和触摸事件的判断/选择方法
+     * MouseEvent or TouchEvent type switch
+     * 
+     * @param   {String} [mouseEventType="click"]    供选择的鼠标事件
+     * @param   {String} [touchEventType="touchend"] 供选择的触摸事件
+     * @returns {String} EventType                   返回事件类型名称
+     */
+    
+    editormd.mouseOrTouch = function(mouseEventType, touchEventType) {
+        mouseEventType = mouseEventType || "click";
+        touchEventType = touchEventType || "touchend";
+        
+        var eventType  = mouseEventType;
+
+        try {
+            document.createEvent("TouchEvent");
+            eventType = touchEventType;
+        } catch(e) {}
+
+        return eventType;
+    };
+    
+    /**
+     * 日期时间的格式化方法
+     * Datetime format method
+     * 
+     * @param   {String}   [format=""]  日期时间的格式,类似PHP的格式
+     * @returns {String}   datefmt      返回格式化后的日期时间字符串
+     */
+    
+    editormd.dateFormat = function(format) {                
+        format      = format || "";
+
+        var addZero = function(d) {
+            return (d < 10) ? "0" + d : d;
+        };
+
+        var date    = new Date(); 
+        var year    = date.getFullYear();
+        var year2   = year.toString().slice(2, 4);
+        var month   = addZero(date.getMonth() + 1);
+        var day     = addZero(date.getDate());
+        var weekDay = date.getDay();
+        var hour    = addZero(date.getHours());
+        var min     = addZero(date.getMinutes());
+        var second  = addZero(date.getSeconds());
+        var ms      = addZero(date.getMilliseconds()); 
+        var datefmt = "";
+
+        var ymd     = year2 + "-" + month + "-" + day;
+        var fymd    = year  + "-" + month + "-" + day;
+        var hms     = hour  + ":" + min   + ":" + second;
+
+        switch (format) 
+        {
+            case "UNIX Time" :
+                    datefmt = date.getTime();
+                break;
+
+            case "UTC" :
+                    datefmt = date.toUTCString();
+                break;	
+
+            case "yy" :
+                    datefmt = year2;
+                break;	
+
+            case "year" :
+            case "yyyy" :
+                    datefmt = year;
+                break;
+
+            case "month" :
+            case "mm" :
+                    datefmt = month;
+                break;                        
+
+            case "cn-week-day" :
+            case "cn-wd" :
+                    var cnWeekDays = ["日", "一", "二", "三", "四", "五", "六"];
+                    datefmt = "星期" + cnWeekDays[weekDay];
+                break;
+
+            case "week-day" :
+            case "wd" :
+                    var weekDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
+                    datefmt = weekDays[weekDay];
+                break;
+
+            case "day" :
+            case "dd" :
+                    datefmt = day;
+                break;
+
+            case "hour" :
+            case "hh" :
+                    datefmt = hour;
+                break;
+
+            case "min" :
+            case "ii" :
+                    datefmt = min;
+                break;
+
+            case "second" :
+            case "ss" :
+                    datefmt = second;
+                break;
+
+            case "ms" :
+                    datefmt = ms;
+                break;
+
+            case "yy-mm-dd" :
+                    datefmt = ymd;
+                break;
+
+            case "yyyy-mm-dd" :
+                    datefmt = fymd;
+                break;
+
+            case "yyyy-mm-dd h:i:s ms" :
+            case "full + ms" : 
+                    datefmt = fymd + " " + hms + " " + ms;
+                break;
+
+            case "full" :
+            case "yyyy-mm-dd h:i:s" :
+                default:
+                    datefmt = fymd + " " + hms;
+                break;
+        }
+
+        return datefmt;
+    };
+
+    return editormd;
+
+}));
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.3.0 on Mon Jun 08 2015 01:07:40 GMT+0800 (中国标准时间) +
+ + + + + diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Bold-webfont.eot b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Bold-webfont.eot new file mode 100644 index 000000000..5d20d9163 Binary files /dev/null and b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Bold-webfont.eot differ diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Bold-webfont.svg b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Bold-webfont.svg new file mode 100644 index 000000000..3ed7be4bc --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Bold-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Bold-webfont.woff b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Bold-webfont.woff new file mode 100644 index 000000000..1205787b0 Binary files /dev/null and b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Bold-webfont.woff differ diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-BoldItalic-webfont.eot b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-BoldItalic-webfont.eot new file mode 100644 index 000000000..1f639a15f Binary files /dev/null and b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-BoldItalic-webfont.eot differ diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-BoldItalic-webfont.svg b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-BoldItalic-webfont.svg new file mode 100644 index 000000000..6a2607b9d --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-BoldItalic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-BoldItalic-webfont.woff b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-BoldItalic-webfont.woff new file mode 100644 index 000000000..ed760c062 Binary files /dev/null and b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-BoldItalic-webfont.woff differ diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Italic-webfont.eot b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Italic-webfont.eot new file mode 100644 index 000000000..0c8a0ae06 Binary files /dev/null and b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Italic-webfont.eot differ diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Italic-webfont.svg b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Italic-webfont.svg new file mode 100644 index 000000000..e1075dcc2 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Italic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Italic-webfont.woff b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Italic-webfont.woff new file mode 100644 index 000000000..ff652e643 Binary files /dev/null and b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Italic-webfont.woff differ diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Light-webfont.eot b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Light-webfont.eot new file mode 100644 index 000000000..14868406a Binary files /dev/null and b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Light-webfont.eot differ diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Light-webfont.svg b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Light-webfont.svg new file mode 100644 index 000000000..11a472ca8 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Light-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Light-webfont.woff b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Light-webfont.woff new file mode 100644 index 000000000..e78607481 Binary files /dev/null and b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Light-webfont.woff differ diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-LightItalic-webfont.eot b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-LightItalic-webfont.eot new file mode 100644 index 000000000..8f445929f Binary files /dev/null and b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-LightItalic-webfont.eot differ diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-LightItalic-webfont.svg b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-LightItalic-webfont.svg new file mode 100644 index 000000000..431d7e354 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-LightItalic-webfont.svg @@ -0,0 +1,1835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-LightItalic-webfont.woff b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-LightItalic-webfont.woff new file mode 100644 index 000000000..43e8b9e6c Binary files /dev/null and b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-LightItalic-webfont.woff differ diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Regular-webfont.eot b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Regular-webfont.eot new file mode 100644 index 000000000..6bbc3cf58 Binary files /dev/null and b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Regular-webfont.eot differ diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Regular-webfont.svg b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Regular-webfont.svg new file mode 100644 index 000000000..25a395234 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Regular-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Regular-webfont.woff b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Regular-webfont.woff new file mode 100644 index 000000000..e231183dc Binary files /dev/null and b/paicoding-ui/src/main/resources/static/editormd/docs/fonts/OpenSans-Regular-webfont.woff differ diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/index.html b/paicoding-ui/src/main/resources/static/editormd/docs/index.html new file mode 100644 index 000000000..6c67f6d77 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/docs/index.html @@ -0,0 +1,65 @@ + + + + + JSDoc: Home + + + + + + + + + + +
+ +

Home

+ + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.3.0 on Mon Jun 08 2015 01:07:40 GMT+0800 (中国标准时间) +
+ + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/scripts/linenumber.js b/paicoding-ui/src/main/resources/static/editormd/docs/scripts/linenumber.js new file mode 100644 index 000000000..8d52f7eaf --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/docs/scripts/linenumber.js @@ -0,0 +1,25 @@ +/*global document */ +(function() { + var source = document.getElementsByClassName('prettyprint source linenums'); + var i = 0; + var lineNumber = 0; + var lineId; + var lines; + var totalLines; + var anchorHash; + + if (source && source[0]) { + anchorHash = document.location.hash.substring(1); + lines = source[0].getElementsByTagName('li'); + totalLines = lines.length; + + for (; i < totalLines; i++) { + lineNumber++; + lineId = 'line' + lineNumber; + lines[i].id = lineId; + if (lineId === anchorHash) { + lines[i].className += ' selected'; + } + } + } +})(); diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/scripts/prettify/Apache-License-2.0.txt b/paicoding-ui/src/main/resources/static/editormd/docs/scripts/prettify/Apache-License-2.0.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/docs/scripts/prettify/Apache-License-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/scripts/prettify/lang-css.js b/paicoding-ui/src/main/resources/static/editormd/docs/scripts/prettify/lang-css.js new file mode 100644 index 000000000..041e1f590 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/docs/scripts/prettify/lang-css.js @@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", +/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/scripts/prettify/prettify.js b/paicoding-ui/src/main/resources/static/editormd/docs/scripts/prettify/prettify.js new file mode 100644 index 000000000..eef5ad7e6 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/docs/scripts/prettify/prettify.js @@ -0,0 +1,28 @@ +var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; +(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= +[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), +l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, +q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, +q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, +"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), +a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} +for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], +"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], +H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], +J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ +I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), +["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", +/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), +["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", +hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= +!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p p:first-child, +.props td.description > p:first-child +{ + margin-top: 0; + padding-top: 0; +} + +.params td.description > p:last-child, +.props td.description > p:last-child +{ + margin-bottom: 0; + padding-bottom: 0; +} + +.disabled { + color: #454545; +} diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/styles/prettify-jsdoc.css b/paicoding-ui/src/main/resources/static/editormd/docs/styles/prettify-jsdoc.css new file mode 100644 index 000000000..5a2526e37 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/docs/styles/prettify-jsdoc.css @@ -0,0 +1,111 @@ +/* JSDoc prettify.js theme */ + +/* plain text */ +.pln { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* string content */ +.str { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a keyword */ +.kwd { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a comment */ +.com { + font-weight: normal; + font-style: italic; +} + +/* a type name */ +.typ { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a literal value */ +.lit { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* punctuation */ +.pun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp open bracket */ +.opn { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp close bracket */ +.clo { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a markup tag name */ +.tag { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute name */ +.atn { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute value */ +.atv { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a declaration */ +.dec { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a variable name */ +.var { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a function name */ +.fun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; +} diff --git a/paicoding-ui/src/main/resources/static/editormd/docs/styles/prettify-tomorrow.css b/paicoding-ui/src/main/resources/static/editormd/docs/styles/prettify-tomorrow.css new file mode 100644 index 000000000..b6f92a78d --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/docs/styles/prettify-tomorrow.css @@ -0,0 +1,132 @@ +/* Tomorrow Theme */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* Pretty printing styles. Used with prettify.js. */ +/* SPAN elements with the classes below are added by prettyprint. */ +/* plain text */ +.pln { + color: #4d4d4c; } + +@media screen { + /* string content */ + .str { + color: #718c00; } + + /* a keyword */ + .kwd { + color: #8959a8; } + + /* a comment */ + .com { + color: #8e908c; } + + /* a type name */ + .typ { + color: #4271ae; } + + /* a literal value */ + .lit { + color: #f5871f; } + + /* punctuation */ + .pun { + color: #4d4d4c; } + + /* lisp open bracket */ + .opn { + color: #4d4d4c; } + + /* lisp close bracket */ + .clo { + color: #4d4d4c; } + + /* a markup tag name */ + .tag { + color: #c82829; } + + /* a markup attribute name */ + .atn { + color: #f5871f; } + + /* a markup attribute value */ + .atv { + color: #3e999f; } + + /* a declaration */ + .dec { + color: #f5871f; } + + /* a variable name */ + .var { + color: #c82829; } + + /* a function name */ + .fun { + color: #4271ae; } } +/* Use higher contrast and text-weight for printable form. */ +@media print, projection { + .str { + color: #060; } + + .kwd { + color: #006; + font-weight: bold; } + + .com { + color: #600; + font-style: italic; } + + .typ { + color: #404; + font-weight: bold; } + + .lit { + color: #044; } + + .pun, .opn, .clo { + color: #440; } + + .tag { + color: #006; + font-weight: bold; } + + .atn { + color: #404; } + + .atv { + color: #060; } } +/* Style */ +/* +pre.prettyprint { + background: white; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 12px; + line-height: 1.5; + border: 1px solid #ccc; + padding: 10px; } +*/ + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; } + +/* IE indents via margin-left */ +li.L0, +li.L1, +li.L2, +li.L3, +li.L4, +li.L5, +li.L6, +li.L7, +li.L8, +li.L9 { + /* */ } + +/* Alternate shading for lines */ +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 { + /* */ } diff --git a/paicoding-ui/src/main/resources/static/editormd/editormd.amd.js b/paicoding-ui/src/main/resources/static/editormd/editormd.amd.js new file mode 100644 index 000000000..14fc94e4d --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/editormd.amd.js @@ -0,0 +1,4667 @@ +/* + * Editor.md + * + * @file editormd.amd.js + * @version v1.5.0 + * @description Open source online markdown editor. + * @license MIT License + * @author Pandao + * {@link https://github.com/pandao/editor.md} + * @updateTime 2015-06-09 + */ + +;(function(factory) { + "use strict"; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) // for Require.js + { + var cmModePath = "./lib/codemirror/mode/"; + var cmAddonPath = "./lib/codemirror/addon/"; + + var codeMirrorModules = [ + "jquery", "marked", "prettify", + "katex", "raphael", "underscore", "flowchart", "jqueryflowchart", "sequenceDiagram", + + "./lib/codemirror/lib/codemirror", + cmModePath + "css/css", + cmModePath + "sass/sass", + cmModePath + "shell/shell", + cmModePath + "sql/sql", + cmModePath + "clike/clike", + cmModePath + "php/php", + cmModePath + "xml/xml", + cmModePath + "markdown/markdown", + cmModePath + "javascript/javascript", + cmModePath + "htmlmixed/htmlmixed", + cmModePath + "gfm/gfm", + cmModePath + "http/http", + cmModePath + "go/go", + cmModePath + "dart/dart", + cmModePath + "coffeescript/coffeescript", + cmModePath + "nginx/nginx", + cmModePath + "python/python", + cmModePath + "perl/perl", + cmModePath + "lua/lua", + cmModePath + "r/r", + cmModePath + "ruby/ruby", + cmModePath + "rst/rst", + cmModePath + "smartymixed/smartymixed", + cmModePath + "vb/vb", + cmModePath + "vbscript/vbscript", + cmModePath + "velocity/velocity", + cmModePath + "xquery/xquery", + cmModePath + "yaml/yaml", + cmModePath + "erlang/erlang", + cmModePath + "jade/jade", + + cmAddonPath + "edit/trailingspace", + cmAddonPath + "dialog/dialog", + cmAddonPath + "search/searchcursor", + cmAddonPath + "search/search", + cmAddonPath + "scroll/annotatescrollbar", + cmAddonPath + "search/matchesonscrollbar", + cmAddonPath + "display/placeholder", + cmAddonPath + "edit/closetag", + cmAddonPath + "fold/foldcode", + cmAddonPath + "fold/foldgutter", + cmAddonPath + "fold/indent-fold", + cmAddonPath + "fold/brace-fold", + cmAddonPath + "fold/xml-fold", + cmAddonPath + "fold/markdown-fold", + cmAddonPath + "fold/comment-fold", + cmAddonPath + "mode/overlay", + cmAddonPath + "selection/active-line", + cmAddonPath + "edit/closebrackets", + cmAddonPath + "display/fullscreen", + cmAddonPath + "search/match-highlighter" + ]; + + define(codeMirrorModules, factory); + } + else + { + define(["jquery"], factory); // for Sea.js + } + } + else + { + window.editormd = factory(); + } + +}(function() { + + if (typeof define == "function" && define.amd) { + $ = arguments[0]; + marked = arguments[1]; + prettify = arguments[2]; + katex = arguments[3]; + Raphael = arguments[4]; + _ = arguments[5]; + flowchart = arguments[6]; + CodeMirror = arguments[9]; + } + + "use strict"; + + var $ = (typeof (jQuery) !== "undefined") ? jQuery : Zepto; + + if (typeof ($) === "undefined") { + return ; + } + + /** + * editormd + * + * @param {String} id 编辑器的ID + * @param {Object} options 配置选项 Key/Value + * @returns {Object} editormd 返回editormd对象 + */ + + var editormd = function (id, options) { + return new editormd.fn.init(id, options); + }; + + editormd.title = editormd.$name = "Editor.md"; + editormd.version = "1.5.0"; + editormd.homePage = "https://pandao.github.io/editor.md/"; + editormd.classPrefix = "editormd-"; + + editormd.toolbarModes = { + full : [ + "undo", "redo", "|", + "bold", "del", "italic", "quote", "ucwords", "uppercase", "lowercase", "|", + "h1", "h2", "h3", "h4", "h5", "h6", "|", + "list-ul", "list-ol", "hr", "|", + "link", "reference-link", "image", "code", "preformatted-text", "code-block", "table", "datetime", "emoji", "html-entities", "pagebreak", "|", + "goto-line", "watch", "preview", "fullscreen", "clear", "search", "|", + "help", "info" + ], + simple : [ + "undo", "redo", "|", + "bold", "del", "italic", "quote", "uppercase", "lowercase", "|", + "h1", "h2", "h3", "h4", "h5", "h6", "|", + "list-ul", "list-ol", "hr", "|", + "watch", "preview", "fullscreen", "|", + "help", "info" + ], + mini : [ + "undo", "redo", "|", + "watch", "preview", "|", + "help", "info" + ] + }; + + editormd.defaults = { + mode : "gfm", //gfm or markdown + name : "", // Form element name + value : "", // value for CodeMirror, if mode not gfm/markdown + theme : "", // Editor.md self themes, before v1.5.0 is CodeMirror theme, default empty + editorTheme : "default", // Editor area, this is CodeMirror theme at v1.5.0 + previewTheme : "", // Preview area theme, default empty + markdown : "", // Markdown source code + appendMarkdown : "", // if in init textarea value not empty, append markdown to textarea + width : "100%", + height : "100%", + path : "./lib/", // Dependents module file directory + pluginPath : "", // If this empty, default use settings.path + "../plugins/" + delay : 300, // Delay parse markdown to html, Uint : ms + autoLoadModules : true, // Automatic load dependent module files + watch : true, + placeholder : "Enjoy Markdown! coding now...", + gotoLine : true, + codeFold : false, + autoHeight : false, + autoFocus : true, + autoCloseTags : true, + searchReplace : true, + syncScrolling : true, // true | false | "single", default true + readOnly : false, + tabSize : 4, + indentUnit : 4, + lineNumbers : true, + lineWrapping : true, + autoCloseBrackets : true, + showTrailingSpace : true, + matchBrackets : true, + indentWithTabs : true, + styleSelectedText : true, + matchWordHighlight : true, // options: true, false, "onselected" + styleActiveLine : true, // Highlight the current line + dialogLockScreen : true, + dialogShowMask : true, + dialogDraggable : true, + dialogMaskBgColor : "#fff", + dialogMaskOpacity : 0.1, + fontSize : "13px", + saveHTMLToTextarea : false, + disabledKeyMaps : [], + + onload : function() {}, + onresize : function() {}, + onchange : function() {}, + onwatch : null, + onunwatch : null, + onpreviewing : function() {}, + onpreviewed : function() {}, + onfullscreen : function() {}, + onfullscreenExit : function() {}, + onscroll : function() {}, + onpreviewscroll : function() {}, + + imageUpload : false, + imageFormats : ["jpg", "jpeg", "gif", "png", "bmp", "webp"], + imageUploadURL : "", + crossDomainUpload : false, + uploadCallbackURL : "", + + toc : true, // Table of contents + tocm : false, // Using [TOCM], auto create ToC dropdown menu + tocTitle : "", // for ToC dropdown menu btn + tocDropdown : false, + tocContainer : "", + tocStartLevel : 1, // Said from H1 to create ToC + htmlDecode : false, // Open the HTML tag identification + pageBreak : true, // Enable parse page break [========] + atLink : true, // for @link + emailLink : true, // for email address auto link + taskList : false, // Enable Github Flavored Markdown task lists + emoji : false, // :emoji: , Support Github emoji, Twitter Emoji (Twemoji); + // Support FontAwesome icon emoji :fa-xxx: > Using fontAwesome icon web fonts; + // Support Editor.md logo icon emoji :editormd-logo: :editormd-logo-1x: > 1~8x; + tex : false, // TeX(LaTeX), based on KaTeX + flowChart : false, // flowChart.js only support IE9+ + sequenceDiagram : false, // sequenceDiagram.js only support IE9+ + previewCodeHighlight : true, + + toolbar : true, // show/hide toolbar + toolbarAutoFixed : true, // on window scroll auto fixed position + toolbarIcons : "full", + toolbarTitles : {}, + toolbarHandlers : { + ucwords : function() { + return editormd.toolbarHandlers.ucwords; + }, + lowercase : function() { + return editormd.toolbarHandlers.lowercase; + } + }, + toolbarCustomIcons : { // using html tag create toolbar icon, unused default tag. + lowercase : "a", + "ucwords" : "Aa" + }, + toolbarIconsClass : { + undo : "fa-undo", + redo : "fa-repeat", + bold : "fa-bold", + del : "fa-strikethrough", + italic : "fa-italic", + quote : "fa-quote-left", + uppercase : "fa-font", + h1 : editormd.classPrefix + "bold", + h2 : editormd.classPrefix + "bold", + h3 : editormd.classPrefix + "bold", + h4 : editormd.classPrefix + "bold", + h5 : editormd.classPrefix + "bold", + h6 : editormd.classPrefix + "bold", + "list-ul" : "fa-list-ul", + "list-ol" : "fa-list-ol", + hr : "fa-minus", + link : "fa-link", + "reference-link" : "fa-anchor", + image : "fa-picture-o", + code : "fa-code", + "preformatted-text" : "fa-file-code-o", + "code-block" : "fa-file-code-o", + table : "fa-table", + datetime : "fa-clock-o", + emoji : "fa-smile-o", + "html-entities" : "fa-copyright", + pagebreak : "fa-newspaper-o", + "goto-line" : "fa-terminal", // fa-crosshairs + watch : "fa-eye-slash", + unwatch : "fa-eye", + preview : "fa-desktop", + search : "fa-search", + fullscreen : "fa-arrows-alt", + clear : "fa-eraser", + help : "fa-question-circle", + info : "fa-info-circle" + }, + toolbarIconTexts : {}, + + lang : { + name : "zh-cn", + description : "开源在线Markdown编辑器
Open source online Markdown editor.", + tocTitle : "目录", + toolbar : { + undo : "撤销(Ctrl+Z)", + redo : "重做(Ctrl+Y)", + bold : "粗体", + del : "删除线", + italic : "斜体", + quote : "引用", + ucwords : "将每个单词首字母转成大写", + uppercase : "将所选转换成大写", + lowercase : "将所选转换成小写", + h1 : "标题1", + h2 : "标题2", + h3 : "标题3", + h4 : "标题4", + h5 : "标题5", + h6 : "标题6", + "list-ul" : "无序列表", + "list-ol" : "有序列表", + hr : "横线", + link : "链接", + "reference-link" : "引用链接", + image : "添加图片", + code : "行内代码", + "preformatted-text" : "预格式文本 / 代码块(缩进风格)", + "code-block" : "代码块(多语言风格)", + table : "添加表格", + datetime : "日期时间", + emoji : "Emoji表情", + "html-entities" : "HTML实体字符", + pagebreak : "插入分页符", + "goto-line" : "跳转到行", + watch : "关闭实时预览", + unwatch : "开启实时预览", + preview : "全窗口预览HTML(按 Shift + ESC还原)", + fullscreen : "全屏(按ESC还原)", + clear : "清空", + search : "搜索", + help : "使用帮助", + info : "关于" + editormd.title + }, + buttons : { + enter : "确定", + cancel : "取消", + close : "关闭" + }, + dialog : { + link : { + title : "添加链接", + url : "链接地址", + urlTitle : "链接标题", + urlEmpty : "错误:请填写链接地址。" + }, + referenceLink : { + title : "添加引用链接", + name : "引用名称", + url : "链接地址", + urlId : "链接ID", + urlTitle : "链接标题", + nameEmpty: "错误:引用链接的名称不能为空。", + idEmpty : "错误:请填写引用链接的ID。", + urlEmpty : "错误:请填写引用链接的URL地址。" + }, + image : { + title : "添加图片", + url : "图片地址", + link : "图片链接", + alt : "图片描述", + uploadButton : "本地上传", + imageURLEmpty : "错误:图片地址不能为空。", + uploadFileEmpty : "错误:上传的图片不能为空。", + formatNotAllowed : "错误:只允许上传图片文件,允许上传的图片文件格式有:" + }, + preformattedText : { + title : "添加预格式文本或代码块", + emptyAlert : "错误:请填写预格式文本或代码的内容。" + }, + codeBlock : { + title : "添加代码块", + selectLabel : "代码语言:", + selectDefaultText : "请选择代码语言", + otherLanguage : "其他语言", + unselectedLanguageAlert : "错误:请选择代码所属的语言类型。", + codeEmptyAlert : "错误:请填写代码内容。" + }, + htmlEntities : { + title : "HTML 实体字符" + }, + help : { + title : "使用帮助" + } + } + } + }; + + editormd.classNames = { + tex : editormd.classPrefix + "tex" + }; + + editormd.dialogZindex = 99999; + + editormd.$katex = null; + editormd.$marked = null; + editormd.$CodeMirror = null; + editormd.$prettyPrint = null; + + var timer, flowchartTimer; + + editormd.prototype = editormd.fn = { + state : { + watching : false, + loaded : false, + preview : false, + fullscreen : false + }, + + /** + * 构造函数/实例初始化 + * Constructor / instance initialization + * + * @param {String} id 编辑器的ID + * @param {Object} [options={}] 配置选项 Key/Value + * @returns {editormd} 返回editormd的实例对象 + */ + + init : function (id, options) { + + options = options || {}; + + if (typeof id === "object") + { + options = id; + } + + var _this = this; + var classPrefix = this.classPrefix = editormd.classPrefix; + var settings = this.settings = $.extend(true, editormd.defaults, options); + + id = (typeof id === "object") ? settings.id : id; + + var editor = this.editor = $("#" + id); + + this.id = id; + this.lang = settings.lang; + + var classNames = this.classNames = { + textarea : { + html : classPrefix + "html-textarea", + markdown : classPrefix + "markdown-textarea" + } + }; + + settings.pluginPath = (settings.pluginPath === "") ? settings.path + "../plugins/" : settings.pluginPath; + + this.state.watching = (settings.watch) ? true : false; + + if ( !editor.hasClass("editormd") ) { + editor.addClass("editormd"); + } + + editor.css({ + width : (typeof settings.width === "number") ? settings.width + "px" : settings.width, + height : (typeof settings.height === "number") ? settings.height + "px" : settings.height + }); + + if (settings.autoHeight) + { + editor.css("height", "auto"); + } + + var markdownTextarea = this.markdownTextarea = editor.children("textarea"); + + if (markdownTextarea.length < 1) + { + editor.append(""); + markdownTextarea = this.markdownTextarea = editor.children("textarea"); + } + + markdownTextarea.addClass(classNames.textarea.markdown).attr("placeholder", settings.placeholder); + + if (typeof markdownTextarea.attr("name") === "undefined" || markdownTextarea.attr("name") === "") + { + markdownTextarea.attr("name", (settings.name !== "") ? settings.name : id + "-markdown-doc"); + } + + var appendElements = [ + (!settings.readOnly) ? "" : "", + ( (settings.saveHTMLToTextarea) ? "" : "" ), + "
", + "
", + "
" + ].join("\n"); + + editor.append(appendElements).addClass(classPrefix + "vertical"); + + if (settings.theme !== "") + { + editor.addClass(classPrefix + "theme-" + settings.theme); + } + + this.mask = editor.children("." + classPrefix + "mask"); + this.containerMask = editor.children("." + classPrefix + "container-mask"); + + if (settings.markdown !== "") + { + markdownTextarea.val(settings.markdown); + } + + if (settings.appendMarkdown !== "") + { + markdownTextarea.val(markdownTextarea.val() + settings.appendMarkdown); + } + + this.htmlTextarea = editor.children("." + classNames.textarea.html); + this.preview = editor.children("." + classPrefix + "preview"); + this.previewContainer = this.preview.children("." + classPrefix + "preview-container"); + + if (settings.previewTheme !== "") + { + this.preview.addClass(classPrefix + "preview-theme-" + settings.previewTheme); + } + + if (typeof define === "function" && define.amd) + { + if (typeof katex !== "undefined") + { + editormd.$katex = katex; + } + + if (settings.searchReplace && !settings.readOnly) + { + editormd.loadCSS(settings.path + "codemirror/addon/dialog/dialog"); + editormd.loadCSS(settings.path + "codemirror/addon/search/matchesonscrollbar"); + } + } + + if ((typeof define === "function" && define.amd) || !settings.autoLoadModules) + { + if (typeof CodeMirror !== "undefined") { + editormd.$CodeMirror = CodeMirror; + } + + if (typeof marked !== "undefined") { + editormd.$marked = marked; + } + + this.setCodeMirror().setToolbar().loadedDisplay(); + } + else + { + this.loadQueues(); + } + + return this; + }, + + /** + * 所需组件加载队列 + * Required components loading queue + * + * @returns {editormd} 返回editormd的实例对象 + */ + + loadQueues : function() { + var _this = this; + var settings = this.settings; + var loadPath = settings.path; + + var loadFlowChartOrSequenceDiagram = function() { + + if (editormd.isIE8) + { + _this.loadedDisplay(); + + return ; + } + + if (settings.flowChart || settings.sequenceDiagram) + { + editormd.loadScript(loadPath + "raphael.min", function() { + + editormd.loadScript(loadPath + "underscore.min", function() { + + if (!settings.flowChart && settings.sequenceDiagram) + { + editormd.loadScript(loadPath + "sequence-diagram.min", function() { + _this.loadedDisplay(); + }); + } + else if (settings.flowChart && !settings.sequenceDiagram) + { + editormd.loadScript(loadPath + "flowchart.min", function() { + editormd.loadScript(loadPath + "jquery.flowchart.min", function() { + _this.loadedDisplay(); + }); + }); + } + else if (settings.flowChart && settings.sequenceDiagram) + { + editormd.loadScript(loadPath + "flowchart.min", function() { + editormd.loadScript(loadPath + "jquery.flowchart.min", function() { + editormd.loadScript(loadPath + "sequence-diagram.min", function() { + _this.loadedDisplay(); + }); + }); + }); + } + }); + + }); + } + else + { + _this.loadedDisplay(); + } + }; + + editormd.loadCSS(loadPath + "codemirror/codemirror.min"); + + if (settings.searchReplace && !settings.readOnly) + { + editormd.loadCSS(loadPath + "codemirror/addon/dialog/dialog"); + editormd.loadCSS(loadPath + "codemirror/addon/search/matchesonscrollbar"); + } + + if (settings.codeFold) + { + editormd.loadCSS(loadPath + "codemirror/addon/fold/foldgutter"); + } + + editormd.loadScript(loadPath + "codemirror/codemirror.min", function() { + editormd.$CodeMirror = CodeMirror; + + editormd.loadScript(loadPath + "codemirror/modes.min", function() { + + editormd.loadScript(loadPath + "codemirror/addons.min", function() { + + _this.setCodeMirror(); + + if (settings.mode !== "gfm" && settings.mode !== "markdown") + { + _this.loadedDisplay(); + + return false; + } + + _this.setToolbar(); + + editormd.loadScript(loadPath + "marked.min", function() { + + editormd.$marked = marked; + + if (settings.previewCodeHighlight) + { + editormd.loadScript(loadPath + "prettify.min", function() { + loadFlowChartOrSequenceDiagram(); + }); + } + else + { + loadFlowChartOrSequenceDiagram(); + } + }); + + }); + + }); + + }); + + return this; + }, + + /** + * 设置 Editor.md 的整体主题,主要是工具栏 + * Setting Editor.md theme + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setTheme : function(theme) { + var editor = this.editor; + var oldTheme = this.settings.theme; + var themePrefix = this.classPrefix + "theme-"; + + editor.removeClass(themePrefix + oldTheme).addClass(themePrefix + theme); + + this.settings.theme = theme; + + return this; + }, + + /** + * 设置 CodeMirror(编辑区)的主题 + * Setting CodeMirror (Editor area) theme + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setEditorTheme : function(theme) { + var settings = this.settings; + settings.editorTheme = theme; + + if (theme !== "default") + { + editormd.loadCSS(settings.path + "codemirror/theme/" + settings.editorTheme); + } + + this.cm.setOption("theme", theme); + + return this; + }, + + /** + * setEditorTheme() 的别名 + * setEditorTheme() alias + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setCodeMirrorTheme : function (theme) { + this.setEditorTheme(theme); + + return this; + }, + + /** + * 设置 Editor.md 的主题 + * Setting Editor.md theme + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setPreviewTheme : function(theme) { + var preview = this.preview; + var oldTheme = this.settings.previewTheme; + var themePrefix = this.classPrefix + "preview-theme-"; + + preview.removeClass(themePrefix + oldTheme).addClass(themePrefix + theme); + + this.settings.previewTheme = theme; + + return this; + }, + + /** + * 配置和初始化CodeMirror组件 + * CodeMirror initialization + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setCodeMirror : function() { + var settings = this.settings; + var editor = this.editor; + + if (settings.editorTheme !== "default") + { + editormd.loadCSS(settings.path + "codemirror/theme/" + settings.editorTheme); + } + + var codeMirrorConfig = { + mode : settings.mode, + theme : settings.editorTheme, + tabSize : settings.tabSize, + dragDrop : false, + autofocus : settings.autoFocus, + autoCloseTags : settings.autoCloseTags, + readOnly : (settings.readOnly) ? "nocursor" : false, + indentUnit : settings.indentUnit, + lineNumbers : settings.lineNumbers, + lineWrapping : settings.lineWrapping, + extraKeys : { + "Ctrl-Q": function(cm) { + cm.foldCode(cm.getCursor()); + } + }, + foldGutter : settings.codeFold, + gutters : ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], + matchBrackets : settings.matchBrackets, + indentWithTabs : settings.indentWithTabs, + styleActiveLine : settings.styleActiveLine, + styleSelectedText : settings.styleSelectedText, + autoCloseBrackets : settings.autoCloseBrackets, + showTrailingSpace : settings.showTrailingSpace, + highlightSelectionMatches : ( (!settings.matchWordHighlight) ? false : { showToken: (settings.matchWordHighlight === "onselected") ? false : /\w/ } ) + }; + + this.codeEditor = this.cm = editormd.$CodeMirror.fromTextArea(this.markdownTextarea[0], codeMirrorConfig); + this.codeMirror = this.cmElement = editor.children(".CodeMirror"); + + if (settings.value !== "") + { + this.cm.setValue(settings.value); + } + + this.codeMirror.css({ + fontSize : settings.fontSize, + width : (!settings.watch) ? "100%" : "50%" + }); + + if (settings.autoHeight) + { + this.codeMirror.css("height", "auto"); + this.cm.setOption("viewportMargin", Infinity); + } + + if (!settings.lineNumbers) + { + this.codeMirror.find(".CodeMirror-gutters").css("border-right", "none"); + } + + return this; + }, + + /** + * 获取CodeMirror的配置选项 + * Get CodeMirror setting options + * + * @returns {Mixed} return CodeMirror setting option value + */ + + getCodeMirrorOption : function(key) { + return this.cm.getOption(key); + }, + + /** + * 配置和重配置CodeMirror的选项 + * CodeMirror setting options / resettings + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setCodeMirrorOption : function(key, value) { + + this.cm.setOption(key, value); + + return this; + }, + + /** + * 添加 CodeMirror 键盘快捷键 + * Add CodeMirror keyboard shortcuts key map + * + * @returns {editormd} 返回editormd的实例对象 + */ + + addKeyMap : function(map, bottom) { + this.cm.addKeyMap(map, bottom); + + return this; + }, + + /** + * 移除 CodeMirror 键盘快捷键 + * Remove CodeMirror keyboard shortcuts key map + * + * @returns {editormd} 返回editormd的实例对象 + */ + + removeKeyMap : function(map) { + this.cm.removeKeyMap(map); + + return this; + }, + + /** + * 跳转到指定的行 + * Goto CodeMirror line + * + * @param {String|Intiger} line line number or "first"|"last" + * @returns {editormd} 返回editormd的实例对象 + */ + + gotoLine : function (line) { + + var settings = this.settings; + + if (!settings.gotoLine) + { + return this; + } + + var cm = this.cm; + var editor = this.editor; + var count = cm.lineCount(); + var preview = this.preview; + + if (typeof line === "string") + { + if(line === "last") + { + line = count; + } + + if (line === "first") + { + line = 1; + } + } + + if (typeof line !== "number") + { + alert("Error: The line number must be an integer."); + return this; + } + + line = parseInt(line) - 1; + + if (line > count) + { + alert("Error: The line number range 1-" + count); + + return this; + } + + cm.setCursor( {line : line, ch : 0} ); + + var scrollInfo = cm.getScrollInfo(); + var clientHeight = scrollInfo.clientHeight; + var coords = cm.charCoords({line : line, ch : 0}, "local"); + + cm.scrollTo(null, (coords.top + coords.bottom - clientHeight) / 2); + + if (settings.watch) + { + var cmScroll = this.codeMirror.find(".CodeMirror-scroll")[0]; + var height = $(cmScroll).height(); + var scrollTop = cmScroll.scrollTop; + var percent = (scrollTop / cmScroll.scrollHeight); + + if (scrollTop === 0) + { + preview.scrollTop(0); + } + else if (scrollTop + height >= cmScroll.scrollHeight - 16) + { + preview.scrollTop(preview[0].scrollHeight); + } + else + { + preview.scrollTop(preview[0].scrollHeight * percent); + } + } + + cm.focus(); + + return this; + }, + + /** + * 扩展当前实例对象,可同时设置多个或者只设置一个 + * Extend editormd instance object, can mutil setting. + * + * @returns {editormd} this(editormd instance object.) + */ + + extend : function() { + if (typeof arguments[1] !== "undefined") + { + if (typeof arguments[1] === "function") + { + arguments[1] = $.proxy(arguments[1], this); + } + + this[arguments[0]] = arguments[1]; + } + + if (typeof arguments[0] === "object" && typeof arguments[0].length === "undefined") + { + $.extend(true, this, arguments[0]); + } + + return this; + }, + + /** + * 设置或扩展当前实例对象,单个设置 + * Extend editormd instance object, one by one + * + * @param {String|Object} key option key + * @param {String|Object} value option value + * @returns {editormd} this(editormd instance object.) + */ + + set : function (key, value) { + + if (typeof value !== "undefined" && typeof value === "function") + { + value = $.proxy(value, this); + } + + this[key] = value; + + return this; + }, + + /** + * 重新配置 + * Resetting editor options + * + * @param {String|Object} key option key + * @param {String|Object} value option value + * @returns {editormd} this(editormd instance object.) + */ + + config : function(key, value) { + var settings = this.settings; + + if (typeof key === "object") + { + settings = $.extend(true, settings, key); + } + + if (typeof key === "string") + { + settings[key] = value; + } + + this.settings = settings; + this.recreate(); + + return this; + }, + + /** + * 注册事件处理方法 + * Bind editor event handle + * + * @param {String} eventType event type + * @param {Function} callback 回调函数 + * @returns {editormd} this(editormd instance object.) + */ + + on : function(eventType, callback) { + var settings = this.settings; + + if (typeof settings["on" + eventType] !== "undefined") + { + settings["on" + eventType] = $.proxy(callback, this); + } + + return this; + }, + + /** + * 解除事件处理方法 + * Unbind editor event handle + * + * @param {String} eventType event type + * @returns {editormd} this(editormd instance object.) + */ + + off : function(eventType) { + var settings = this.settings; + + if (typeof settings["on" + eventType] !== "undefined") + { + settings["on" + eventType] = function(){}; + } + + return this; + }, + + /** + * 显示工具栏 + * Display toolbar + * + * @param {Function} [callback=function(){}] 回调函数 + * @returns {editormd} 返回editormd的实例对象 + */ + + showToolbar : function(callback) { + var settings = this.settings; + + if(settings.readOnly) { + return this; + } + + if (settings.toolbar && (this.toolbar.length < 1 || this.toolbar.find("." + this.classPrefix + "menu").html() === "") ) + { + this.setToolbar(); + } + + settings.toolbar = true; + + this.toolbar.show(); + this.resize(); + + $.proxy(callback || function(){}, this)(); + + return this; + }, + + /** + * 隐藏工具栏 + * Hide toolbar + * + * @param {Function} [callback=function(){}] 回调函数 + * @returns {editormd} this(editormd instance object.) + */ + + hideToolbar : function(callback) { + var settings = this.settings; + + settings.toolbar = false; + this.toolbar.hide(); + this.resize(); + + $.proxy(callback || function(){}, this)(); + + return this; + }, + + /** + * 页面滚动时工具栏的固定定位 + * Set toolbar in window scroll auto fixed position + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setToolbarAutoFixed : function(fixed) { + + var state = this.state; + var editor = this.editor; + var toolbar = this.toolbar; + var settings = this.settings; + + if (typeof fixed !== "undefined") + { + settings.toolbarAutoFixed = fixed; + } + + var autoFixedHandle = function(){ + var $window = $(window); + var top = $window.scrollTop(); + + if (!settings.toolbarAutoFixed) + { + return false; + } + + if (top - editor.offset().top > 10 && top < editor.height()) + { + toolbar.css({ + position : "fixed", + width : editor.width() + "px", + left : ($window.width() - editor.width()) / 2 + "px" + }); + } + else + { + toolbar.css({ + position : "absolute", + width : "100%", + left : 0 + }); + } + }; + + if (!state.fullscreen && !state.preview && settings.toolbar && settings.toolbarAutoFixed) + { + $(window).bind("scroll", autoFixedHandle); + } + + return this; + }, + + /** + * 配置和初始化工具栏 + * Set toolbar and Initialization + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setToolbar : function() { + var settings = this.settings; + + if(settings.readOnly) { + return this; + } + + var editor = this.editor; + var preview = this.preview; + var classPrefix = this.classPrefix; + + var toolbar = this.toolbar = editor.children("." + classPrefix + "toolbar"); + + if (settings.toolbar && toolbar.length < 1) + { + var toolbarHTML = "
    "; + + editor.append(toolbarHTML); + toolbar = this.toolbar = editor.children("." + classPrefix + "toolbar"); + } + + if (!settings.toolbar) + { + toolbar.hide(); + + return this; + } + + toolbar.show(); + + var icons = (typeof settings.toolbarIcons === "function") ? settings.toolbarIcons() + : ((typeof settings.toolbarIcons === "string") ? editormd.toolbarModes[settings.toolbarIcons] : settings.toolbarIcons); + + var toolbarMenu = toolbar.find("." + this.classPrefix + "menu"), menu = ""; + var pullRight = false; + + for (var i = 0, len = icons.length; i < len; i++) + { + var name = icons[i]; + + if (name === "||") + { + pullRight = true; + } + else if (name === "|") + { + menu += "
  • |
  • "; + } + else + { + var isHeader = (/h(\d)/.test(name)); + var index = name; + + if (name === "watch" && !settings.watch) { + index = "unwatch"; + } + + var title = settings.lang.toolbar[index]; + var iconTexts = settings.toolbarIconTexts[index]; + var iconClass = settings.toolbarIconsClass[index]; + + title = (typeof title === "undefined") ? "" : title; + iconTexts = (typeof iconTexts === "undefined") ? "" : iconTexts; + iconClass = (typeof iconClass === "undefined") ? "" : iconClass; + + var menuItem = pullRight ? "
  • " : "
  • "; + + if (typeof settings.toolbarCustomIcons[name] !== "undefined" && typeof settings.toolbarCustomIcons[name] !== "function") + { + menuItem += settings.toolbarCustomIcons[name]; + } + else + { + menuItem += ""; + menuItem += ""+((isHeader) ? name.toUpperCase() : ( (iconClass === "") ? iconTexts : "") ) + ""; + menuItem += ""; + } + + menuItem += "
  • "; + + menu = pullRight ? menuItem + menu : menu + menuItem; + } + } + + toolbarMenu.html(menu); + + toolbarMenu.find("[title=\"Lowercase\"]").attr("title", settings.lang.toolbar.lowercase); + toolbarMenu.find("[title=\"ucwords\"]").attr("title", settings.lang.toolbar.ucwords); + + this.setToolbarHandler(); + this.setToolbarAutoFixed(); + + return this; + }, + + /** + * 工具栏图标事件处理对象序列 + * Get toolbar icons event handlers + * + * @param {Object} cm CodeMirror的实例对象 + * @param {String} name 要获取的事件处理器名称 + * @returns {Object} 返回处理对象序列 + */ + + dialogLockScreen : function() { + $.proxy(editormd.dialogLockScreen, this)(); + + return this; + }, + + dialogShowMask : function(dialog) { + $.proxy(editormd.dialogShowMask, this)(dialog); + + return this; + }, + + getToolbarHandles : function(name) { + var toolbarHandlers = this.toolbarHandlers = editormd.toolbarHandlers; + + return (name && typeof toolbarIconHandlers[name] !== "undefined") ? toolbarHandlers[name] : toolbarHandlers; + }, + + /** + * 工具栏图标事件处理器 + * Bind toolbar icons event handle + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setToolbarHandler : function() { + var _this = this; + var settings = this.settings; + + if (!settings.toolbar || settings.readOnly) { + return this; + } + + var toolbar = this.toolbar; + var cm = this.cm; + var classPrefix = this.classPrefix; + var toolbarIcons = this.toolbarIcons = toolbar.find("." + classPrefix + "menu > li > a"); + var toolbarIconHandlers = this.getToolbarHandles(); + + toolbarIcons.bind(editormd.mouseOrTouch("click", "touchend"), function(event) { + + var icon = $(this).children(".fa"); + var name = icon.attr("name"); + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (name === "") { + return ; + } + + _this.activeIcon = icon; + + if (typeof toolbarIconHandlers[name] !== "undefined") + { + $.proxy(toolbarIconHandlers[name], _this)(cm); + } + else + { + if (typeof settings.toolbarHandlers[name] !== "undefined") + { + $.proxy(settings.toolbarHandlers[name], _this)(cm, icon, cursor, selection); + } + } + + if (name !== "link" && name !== "reference-link" && name !== "image" && name !== "code-block" && + name !== "preformatted-text" && name !== "watch" && name !== "preview" && name !== "search" && name !== "fullscreen" && name !== "info") + { + cm.focus(); + } + + return false; + + }); + + return this; + }, + + /** + * 动态创建对话框 + * Creating custom dialogs + * + * @param {Object} options 配置项键值对 Key/Value + * @returns {dialog} 返回创建的dialog的jQuery实例对象 + */ + + createDialog : function(options) { + return $.proxy(editormd.createDialog, this)(options); + }, + + /** + * 创建关于Editor.md的对话框 + * Create about Editor.md dialog + * + * @returns {editormd} 返回editormd的实例对象 + */ + + createInfoDialog : function() { + var _this = this; + var editor = this.editor; + var classPrefix = this.classPrefix; + + var infoDialogHTML = [ + "
    ", + "
    ", + "

    " + editormd.title + "v" + editormd.version + "

    ", + "

    " + this.lang.description + "

    ", + "

    " + editormd.homePage + "

    ", + "

    Copyright © 2015 Pandao, The MIT License.

    ", + "
    ", + "", + "
    " + ].join("\n"); + + editor.append(infoDialogHTML); + + var infoDialog = this.infoDialog = editor.children("." + classPrefix + "dialog-info"); + + infoDialog.find("." + classPrefix + "dialog-close").bind(editormd.mouseOrTouch("click", "touchend"), function() { + _this.hideInfoDialog(); + }); + + infoDialog.css("border", (editormd.isIE8) ? "1px solid #ddd" : "").css("z-index", editormd.dialogZindex).show(); + + this.infoDialogPosition(); + + return this; + }, + + /** + * 关于Editor.md对话居中定位 + * Editor.md dialog position handle + * + * @returns {editormd} 返回editormd的实例对象 + */ + + infoDialogPosition : function() { + var infoDialog = this.infoDialog; + + var _infoDialogPosition = function() { + infoDialog.css({ + top : ($(window).height() - infoDialog.height()) / 2 + "px", + left : ($(window).width() - infoDialog.width()) / 2 + "px" + }); + }; + + _infoDialogPosition(); + + $(window).resize(_infoDialogPosition); + + return this; + }, + + /** + * 显示关于Editor.md + * Display about Editor.md dialog + * + * @returns {editormd} 返回editormd的实例对象 + */ + + showInfoDialog : function() { + + $("html,body").css("overflow-x", "hidden"); + + var _this = this; + var editor = this.editor; + var settings = this.settings; + var infoDialog = this.infoDialog = editor.children("." + this.classPrefix + "dialog-info"); + + if (infoDialog.length < 1) + { + this.createInfoDialog(); + } + + this.lockScreen(true); + + this.mask.css({ + opacity : settings.dialogMaskOpacity, + backgroundColor : settings.dialogMaskBgColor + }).show(); + + infoDialog.css("z-index", editormd.dialogZindex).show(); + + this.infoDialogPosition(); + + return this; + }, + + /** + * 隐藏关于Editor.md + * Hide about Editor.md dialog + * + * @returns {editormd} 返回editormd的实例对象 + */ + + hideInfoDialog : function() { + $("html,body").css("overflow-x", ""); + this.infoDialog.hide(); + this.mask.hide(); + this.lockScreen(false); + + return this; + }, + + /** + * 锁屏 + * lock screen + * + * @param {Boolean} lock Boolean 布尔值,是否锁屏 + * @returns {editormd} 返回editormd的实例对象 + */ + + lockScreen : function(lock) { + editormd.lockScreen(lock); + this.resize(); + + return this; + }, + + /** + * 编辑器界面重建,用于动态语言包或模块加载等 + * Recreate editor + * + * @returns {editormd} 返回editormd的实例对象 + */ + + recreate : function() { + var _this = this; + var editor = this.editor; + var settings = this.settings; + + this.codeMirror.remove(); + + this.setCodeMirror(); + + if (!settings.readOnly) + { + if (editor.find(".editormd-dialog").length > 0) { + editor.find(".editormd-dialog").remove(); + } + + if (settings.toolbar) + { + this.getToolbarHandles(); + this.setToolbar(); + } + } + + this.loadedDisplay(true); + + return this; + }, + + /** + * 高亮预览HTML的pre代码部分 + * highlight of preview codes + * + * @returns {editormd} 返回editormd的实例对象 + */ + + previewCodeHighlight : function() { + var settings = this.settings; + var previewContainer = this.previewContainer; + + if (settings.previewCodeHighlight) + { + previewContainer.find("pre").addClass("prettyprint linenums"); + + if (typeof prettyPrint !== "undefined") + { + prettyPrint(); + } + } + + return this; + }, + + /** + * 解析TeX(KaTeX)科学公式 + * TeX(KaTeX) Renderer + * + * @returns {editormd} 返回editormd的实例对象 + */ + + katexRender : function() { + + if (timer === null) + { + return this; + } + + this.previewContainer.find("." + editormd.classNames.tex).each(function(){ + var tex = $(this); + editormd.$katex.render(tex.text(), tex[0]); + + tex.find(".katex").css("font-size", "1.6em"); + }); + + return this; + }, + + /** + * 解析和渲染流程图及时序图 + * FlowChart and SequenceDiagram Renderer + * + * @returns {editormd} 返回editormd的实例对象 + */ + + flowChartAndSequenceDiagramRender : function() { + var $this = this; + var settings = this.settings; + var previewContainer = this.previewContainer; + + if (editormd.isIE8) { + return this; + } + + if (settings.flowChart) { + if (flowchartTimer === null) { + return this; + } + + previewContainer.find(".flowchart").flowChart(); + } + + if (settings.sequenceDiagram) { + previewContainer.find(".sequence-diagram").sequenceDiagram({theme: "simple"}); + } + + var preview = $this.preview; + var codeMirror = $this.codeMirror; + var codeView = codeMirror.find(".CodeMirror-scroll"); + + var height = codeView.height(); + var scrollTop = codeView.scrollTop(); + var percent = (scrollTop / codeView[0].scrollHeight); + var tocHeight = 0; + + preview.find(".markdown-toc-list").each(function(){ + tocHeight += $(this).height(); + }); + + var tocMenuHeight = preview.find(".editormd-toc-menu").height(); + tocMenuHeight = (!tocMenuHeight) ? 0 : tocMenuHeight; + + if (scrollTop === 0) + { + preview.scrollTop(0); + } + else if (scrollTop + height >= codeView[0].scrollHeight - 16) + { + preview.scrollTop(preview[0].scrollHeight); + } + else + { + preview.scrollTop((preview[0].scrollHeight + tocHeight + tocMenuHeight) * percent); + } + + return this; + }, + + /** + * 注册键盘快捷键处理 + * Register CodeMirror keyMaps (keyboard shortcuts). + * + * @param {Object} keyMap KeyMap key/value {"(Ctrl/Shift/Alt)-Key" : function(){}} + * @returns {editormd} return this + */ + + registerKeyMaps : function(keyMap) { + + var _this = this; + var cm = this.cm; + var settings = this.settings; + var toolbarHandlers = editormd.toolbarHandlers; + var disabledKeyMaps = settings.disabledKeyMaps; + + keyMap = keyMap || null; + + if (keyMap) + { + for (var i in keyMap) + { + if ($.inArray(i, disabledKeyMaps) < 0) + { + var map = {}; + map[i] = keyMap[i]; + + cm.addKeyMap(keyMap); + } + } + } + else + { + for (var k in editormd.keyMaps) + { + var _keyMap = editormd.keyMaps[k]; + var handle = (typeof _keyMap === "string") ? $.proxy(toolbarHandlers[_keyMap], _this) : $.proxy(_keyMap, _this); + + if ($.inArray(k, ["F9", "F10", "F11"]) < 0 && $.inArray(k, disabledKeyMaps) < 0) + { + var _map = {}; + _map[k] = handle; + + cm.addKeyMap(_map); + } + } + + $(window).keydown(function(event) { + + var keymaps = { + "120" : "F9", + "121" : "F10", + "122" : "F11" + }; + + if ( $.inArray(keymaps[event.keyCode], disabledKeyMaps) < 0 ) + { + switch (event.keyCode) + { + case 120: + $.proxy(toolbarHandlers["watch"], _this)(); + return false; + break; + + case 121: + $.proxy(toolbarHandlers["preview"], _this)(); + return false; + break; + + case 122: + $.proxy(toolbarHandlers["fullscreen"], _this)(); + return false; + break; + + default: + break; + } + } + }); + } + + return this; + }, + + /** + * 绑定同步滚动 + * + * @returns {editormd} return this + */ + + bindScrollEvent : function() { + + var _this = this; + var preview = this.preview; + var settings = this.settings; + var codeMirror = this.codeMirror; + var mouseOrTouch = editormd.mouseOrTouch; + + if (!settings.syncScrolling) { + return this; + } + + var cmBindScroll = function() { + codeMirror.find(".CodeMirror-scroll").bind(mouseOrTouch("scroll", "touchmove"), function(event) { + var height = $(this).height(); + var scrollTop = $(this).scrollTop(); + var percent = (scrollTop / $(this)[0].scrollHeight); + + var tocHeight = 0; + + preview.find(".markdown-toc-list").each(function(){ + tocHeight += $(this).height(); + }); + + var tocMenuHeight = preview.find(".editormd-toc-menu").height(); + tocMenuHeight = (!tocMenuHeight) ? 0 : tocMenuHeight; + + if (scrollTop === 0) + { + preview.scrollTop(0); + } + else if (scrollTop + height >= $(this)[0].scrollHeight - 16) + { + preview.scrollTop(preview[0].scrollHeight); + } + else + { + preview.scrollTop((preview[0].scrollHeight + tocHeight + tocMenuHeight) * percent); + } + + $.proxy(settings.onscroll, _this)(event); + }); + }; + + var cmUnbindScroll = function() { + codeMirror.find(".CodeMirror-scroll").unbind(mouseOrTouch("scroll", "touchmove")); + }; + + var previewBindScroll = function() { + + preview.bind(mouseOrTouch("scroll", "touchmove"), function(event) { + var height = $(this).height(); + var scrollTop = $(this).scrollTop(); + var percent = (scrollTop / $(this)[0].scrollHeight); + var codeView = codeMirror.find(".CodeMirror-scroll"); + + if(scrollTop === 0) + { + codeView.scrollTop(0); + } + else if (scrollTop + height >= $(this)[0].scrollHeight) + { + codeView.scrollTop(codeView[0].scrollHeight); + } + else + { + codeView.scrollTop(codeView[0].scrollHeight * percent); + } + + $.proxy(settings.onpreviewscroll, _this)(event); + }); + + }; + + var previewUnbindScroll = function() { + preview.unbind(mouseOrTouch("scroll", "touchmove")); + }; + + codeMirror.bind({ + mouseover : cmBindScroll, + mouseout : cmUnbindScroll, + touchstart : cmBindScroll, + touchend : cmUnbindScroll + }); + + if (settings.syncScrolling === "single") { + return this; + } + + preview.bind({ + mouseover : previewBindScroll, + mouseout : previewUnbindScroll, + touchstart : previewBindScroll, + touchend : previewUnbindScroll + }); + + return this; + }, + + bindChangeEvent : function() { + + var _this = this; + var cm = this.cm; + var settings = this.settings; + + if (!settings.syncScrolling) { + return this; + } + + cm.on("change", function(_cm, changeObj) { + + if (settings.watch) + { + _this.previewContainer.css("padding", settings.autoHeight ? "20px 20px 50px 40px" : "20px"); + } + + timer = setTimeout(function() { + clearTimeout(timer); + _this.save(); + timer = null; + }, settings.delay); + }); + + return this; + }, + + /** + * 加载队列完成之后的显示处理 + * Display handle of the module queues loaded after. + * + * @param {Boolean} recreate 是否为重建编辑器 + * @returns {editormd} 返回editormd的实例对象 + */ + + loadedDisplay : function(recreate) { + + recreate = recreate || false; + + var _this = this; + var editor = this.editor; + var preview = this.preview; + var settings = this.settings; + + this.containerMask.hide(); + + this.save(); + + if (settings.watch) { + preview.show(); + } + + editor.data("oldWidth", editor.width()).data("oldHeight", editor.height()); // 为了兼容Zepto + + this.resize(); + this.registerKeyMaps(); + + $(window).resize(function(){ + _this.resize(); + }); + + this.bindScrollEvent().bindChangeEvent(); + + if (!recreate) + { + $.proxy(settings.onload, this)(); + } + + this.state.loaded = true; + + return this; + }, + + /** + * 设置编辑器的宽度 + * Set editor width + * + * @param {Number|String} width 编辑器宽度值 + * @returns {editormd} 返回editormd的实例对象 + */ + + width : function(width) { + + this.editor.css("width", (typeof width === "number") ? width + "px" : width); + this.resize(); + + return this; + }, + + /** + * 设置编辑器的高度 + * Set editor height + * + * @param {Number|String} height 编辑器高度值 + * @returns {editormd} 返回editormd的实例对象 + */ + + height : function(height) { + + this.editor.css("height", (typeof height === "number") ? height + "px" : height); + this.resize(); + + return this; + }, + + /** + * 调整编辑器的尺寸和布局 + * Resize editor layout + * + * @param {Number|String} [width=null] 编辑器宽度值 + * @param {Number|String} [height=null] 编辑器高度值 + * @returns {editormd} 返回editormd的实例对象 + */ + + resize : function(width, height) { + + width = width || null; + height = height || null; + + var state = this.state; + var editor = this.editor; + var preview = this.preview; + var toolbar = this.toolbar; + var settings = this.settings; + var codeMirror = this.codeMirror; + + if (width) + { + editor.css("width", (typeof width === "number") ? width + "px" : width); + } + + if (settings.autoHeight && !state.fullscreen && !state.preview) + { + editor.css("height", "auto"); + codeMirror.css("height", "auto"); + } + else + { + if (height) + { + editor.css("height", (typeof height === "number") ? height + "px" : height); + } + + if (state.fullscreen) + { + editor.height($(window).height()); + } + + if (settings.toolbar && !settings.readOnly) + { + codeMirror.css("margin-top", toolbar.height() + 1).height(editor.height() - toolbar.height()); + } + else + { + codeMirror.css("margin-top", 0).height(editor.height()); + } + } + + if(settings.watch) + { + codeMirror.width(editor.width() / 2); + preview.width((!state.preview) ? editor.width() / 2 : editor.width()); + + this.previewContainer.css("padding", settings.autoHeight ? "20px 20px 50px 40px" : "20px"); + + if (settings.toolbar && !settings.readOnly) + { + preview.css("top", toolbar.height() + 1); + } + else + { + preview.css("top", 0); + } + + if (settings.autoHeight && !state.fullscreen && !state.preview) + { + preview.height(""); + } + else + { + var previewHeight = (settings.toolbar && !settings.readOnly) ? editor.height() - toolbar.height() : editor.height(); + + preview.height(previewHeight); + } + } + else + { + codeMirror.width(editor.width()); + preview.hide(); + } + + if (state.loaded) + { + $.proxy(settings.onresize, this)(); + } + + return this; + }, + + /** + * 解析和保存Markdown代码 + * Parse & Saving Markdown source code + * + * @returns {editormd} 返回editormd的实例对象 + */ + + save : function() { + + if (timer === null) + { + return this; + } + + var _this = this; + var state = this.state; + var settings = this.settings; + var cm = this.cm; + var cmValue = cm.getValue(); + var previewContainer = this.previewContainer; + + if (settings.mode !== "gfm" && settings.mode !== "markdown") + { + this.markdownTextarea.val(cmValue); + + return this; + } + + var marked = editormd.$marked; + var markdownToC = this.markdownToC = []; + var rendererOptions = this.markedRendererOptions = { + toc : settings.toc, + tocm : settings.tocm, + tocStartLevel : settings.tocStartLevel, + pageBreak : settings.pageBreak, + taskList : settings.taskList, + emoji : settings.emoji, + tex : settings.tex, + atLink : settings.atLink, // for @link + emailLink : settings.emailLink, // for mail address auto link + flowChart : settings.flowChart, + sequenceDiagram : settings.sequenceDiagram, + previewCodeHighlight : settings.previewCodeHighlight, + }; + + var markedOptions = this.markedOptions = { + renderer : editormd.markedRenderer(markdownToC, rendererOptions), + gfm : true, + tables : true, + breaks : true, + pedantic : false, + sanitize : (settings.htmlDecode) ? false : true, // 关闭忽略HTML标签,即开启识别HTML标签,默认为false + smartLists : true, + smartypants : true + }; + + marked.setOptions(markedOptions); + + var newMarkdownDoc = editormd.$marked(cmValue, markedOptions); + + //console.info("cmValue", cmValue, newMarkdownDoc); + + newMarkdownDoc = editormd.filterHTMLTags(newMarkdownDoc, settings.htmlDecode); + + //console.error("cmValue", cmValue, newMarkdownDoc); + + this.markdownTextarea.text(cmValue); + + cm.save(); + + if (settings.saveHTMLToTextarea) + { + this.htmlTextarea.text(newMarkdownDoc); + } + + if(settings.watch || (!settings.watch && state.preview)) + { + previewContainer.html(newMarkdownDoc); + + this.previewCodeHighlight(); + + if (settings.toc) + { + var tocContainer = (settings.tocContainer === "") ? previewContainer : $(settings.tocContainer); + var tocMenu = tocContainer.find("." + this.classPrefix + "toc-menu"); + + tocContainer.attr("previewContainer", (settings.tocContainer === "") ? "true" : "false"); + + if (settings.tocContainer !== "" && tocMenu.length > 0) + { + tocMenu.remove(); + } + + editormd.markdownToCRenderer(markdownToC, tocContainer, settings.tocDropdown, settings.tocStartLevel); + + if (settings.tocDropdown || tocContainer.find("." + this.classPrefix + "toc-menu").length > 0) + { + editormd.tocDropdownMenu(tocContainer, (settings.tocTitle !== "") ? settings.tocTitle : this.lang.tocTitle); + } + + if (settings.tocContainer !== "") + { + previewContainer.find(".markdown-toc").css("border", "none"); + } + } + + if (settings.tex) + { + if (!editormd.kaTeXLoaded && settings.autoLoadModules) + { + editormd.loadKaTeX(function() { + editormd.$katex = katex; + editormd.kaTeXLoaded = true; + _this.katexRender(); + }); + } + else + { + editormd.$katex = katex; + this.katexRender(); + } + } + + if (settings.flowChart || settings.sequenceDiagram) + { + flowchartTimer = setTimeout(function(){ + clearTimeout(flowchartTimer); + _this.flowChartAndSequenceDiagramRender(); + flowchartTimer = null; + }, 10); + } + + if (state.loaded) + { + $.proxy(settings.onchange, this)(); + } + } + + return this; + }, + + /** + * 聚焦光标位置 + * Focusing the cursor position + * + * @returns {editormd} 返回editormd的实例对象 + */ + + focus : function() { + this.cm.focus(); + + return this; + }, + + /** + * 设置光标的位置 + * Set cursor position + * + * @param {Object} cursor 要设置的光标位置键值对象,例:{line:1, ch:0} + * @returns {editormd} 返回editormd的实例对象 + */ + + setCursor : function(cursor) { + this.cm.setCursor(cursor); + + return this; + }, + + /** + * 获取当前光标的位置 + * Get the current position of the cursor + * + * @returns {Cursor} 返回一个光标Cursor对象 + */ + + getCursor : function() { + return this.cm.getCursor(); + }, + + /** + * 设置光标选中的范围 + * Set cursor selected ranges + * + * @param {Object} from 开始位置的光标键值对象,例:{line:1, ch:0} + * @param {Object} to 结束位置的光标键值对象,例:{line:1, ch:0} + * @returns {editormd} 返回editormd的实例对象 + */ + + setSelection : function(from, to) { + + this.cm.setSelection(from, to); + + return this; + }, + + /** + * 获取光标选中的文本 + * Get the texts from cursor selected + * + * @returns {String} 返回选中文本的字符串形式 + */ + + getSelection : function() { + return this.cm.getSelection(); + }, + + /** + * 设置光标选中的文本范围 + * Set the cursor selection ranges + * + * @param {Array} ranges cursor selection ranges array + * @returns {Array} return this + */ + + setSelections : function(ranges) { + this.cm.setSelections(ranges); + + return this; + }, + + /** + * 获取光标选中的文本范围 + * Get the cursor selection ranges + * + * @returns {Array} return selection ranges array + */ + + getSelections : function() { + return this.cm.getSelections(); + }, + + /** + * 替换当前光标选中的文本或在当前光标处插入新字符 + * Replace the text at the current cursor selected or insert a new character at the current cursor position + * + * @param {String} value 要插入的字符值 + * @returns {editormd} 返回editormd的实例对象 + */ + + replaceSelection : function(value) { + this.cm.replaceSelection(value); + + return this; + }, + + /** + * 在当前光标处插入新字符 + * Insert a new character at the current cursor position + * + * 同replaceSelection()方法 + * With the replaceSelection() method + * + * @param {String} value 要插入的字符值 + * @returns {editormd} 返回editormd的实例对象 + */ + + insertValue : function(value) { + this.replaceSelection(value); + + return this; + }, + + /** + * 追加markdown + * append Markdown to editor + * + * @param {String} md 要追加的markdown源文档 + * @returns {editormd} 返回editormd的实例对象 + */ + + appendMarkdown : function(md) { + var settings = this.settings; + var cm = this.cm; + + cm.setValue(cm.getValue() + md); + + return this; + }, + + /** + * 设置和传入编辑器的markdown源文档 + * Set Markdown source document + * + * @param {String} md 要传入的markdown源文档 + * @returns {editormd} 返回editormd的实例对象 + */ + + setMarkdown : function(md) { + this.cm.setValue(md || this.settings.markdown); + + return this; + }, + + /** + * 获取编辑器的markdown源文档 + * Set Editor.md markdown/CodeMirror value + * + * @returns {editormd} 返回editormd的实例对象 + */ + + getMarkdown : function() { + return this.cm.getValue(); + }, + + /** + * 获取编辑器的源文档 + * Get CodeMirror value + * + * @returns {editormd} 返回editormd的实例对象 + */ + + getValue : function() { + return this.cm.getValue(); + }, + + /** + * 设置编辑器的源文档 + * Set CodeMirror value + * + * @param {String} value set code/value/string/text + * @returns {editormd} 返回editormd的实例对象 + */ + + setValue : function(value) { + this.cm.setValue(value); + + return this; + }, + + /** + * 清空编辑器 + * Empty CodeMirror editor container + * + * @returns {editormd} 返回editormd的实例对象 + */ + + clear : function() { + this.cm.setValue(""); + + return this; + }, + + /** + * 获取解析后存放在Textarea的HTML源码 + * Get parsed html code from Textarea + * + * @returns {String} 返回HTML源码 + */ + + getHTML : function() { + if (!this.settings.saveHTMLToTextarea) + { + alert("Error: settings.saveHTMLToTextarea == false"); + + return false; + } + + return this.htmlTextarea.val(); + }, + + /** + * getHTML()的别名 + * getHTML (alias) + * + * @returns {String} Return html code 返回HTML源码 + */ + + getTextareaSavedHTML : function() { + return this.getHTML(); + }, + + /** + * 获取预览窗口的HTML源码 + * Get html from preview container + * + * @returns {editormd} 返回editormd的实例对象 + */ + + getPreviewedHTML : function() { + if (!this.settings.watch) + { + alert("Error: settings.watch == false"); + + return false; + } + + return this.previewContainer.html(); + }, + + /** + * 开启实时预览 + * Enable real-time watching + * + * @returns {editormd} 返回editormd的实例对象 + */ + + watch : function(callback) { + var settings = this.settings; + + if ($.inArray(settings.mode, ["gfm", "markdown"]) < 0) + { + return this; + } + + this.state.watching = settings.watch = true; + this.preview.show(); + + if (this.toolbar) + { + var watchIcon = settings.toolbarIconsClass.watch; + var unWatchIcon = settings.toolbarIconsClass.unwatch; + + var icon = this.toolbar.find(".fa[name=watch]"); + icon.parent().attr("title", settings.lang.toolbar.watch); + icon.removeClass(unWatchIcon).addClass(watchIcon); + } + + this.codeMirror.css("border-right", "1px solid #ddd").width(this.editor.width() / 2); + + timer = 0; + + this.save().resize(); + + if (!settings.onwatch) + { + settings.onwatch = callback || function() {}; + } + + $.proxy(settings.onwatch, this)(); + + return this; + }, + + /** + * 关闭实时预览 + * Disable real-time watching + * + * @returns {editormd} 返回editormd的实例对象 + */ + + unwatch : function(callback) { + var settings = this.settings; + this.state.watching = settings.watch = false; + this.preview.hide(); + + if (this.toolbar) + { + var watchIcon = settings.toolbarIconsClass.watch; + var unWatchIcon = settings.toolbarIconsClass.unwatch; + + var icon = this.toolbar.find(".fa[name=watch]"); + icon.parent().attr("title", settings.lang.toolbar.unwatch); + icon.removeClass(watchIcon).addClass(unWatchIcon); + } + + this.codeMirror.css("border-right", "none").width(this.editor.width()); + + this.resize(); + + if (!settings.onunwatch) + { + settings.onunwatch = callback || function() {}; + } + + $.proxy(settings.onunwatch, this)(); + + return this; + }, + + /** + * 显示编辑器 + * Show editor + * + * @param {Function} [callback=function()] 回调函数 + * @returns {editormd} 返回editormd的实例对象 + */ + + show : function(callback) { + callback = callback || function() {}; + + var _this = this; + this.editor.show(0, function() { + $.proxy(callback, _this)(); + }); + + return this; + }, + + /** + * 隐藏编辑器 + * Hide editor + * + * @param {Function} [callback=function()] 回调函数 + * @returns {editormd} 返回editormd的实例对象 + */ + + hide : function(callback) { + callback = callback || function() {}; + + var _this = this; + this.editor.hide(0, function() { + $.proxy(callback, _this)(); + }); + + return this; + }, + + /** + * 隐藏编辑器部分,只预览HTML + * Enter preview html state + * + * @returns {editormd} 返回editormd的实例对象 + */ + + previewing : function() { + + var _this = this; + var editor = this.editor; + var preview = this.preview; + var toolbar = this.toolbar; + var settings = this.settings; + var codeMirror = this.codeMirror; + var previewContainer = this.previewContainer; + + if ($.inArray(settings.mode, ["gfm", "markdown"]) < 0) { + return this; + } + + if (settings.toolbar && toolbar) { + toolbar.toggle(); + toolbar.find(".fa[name=preview]").toggleClass("active"); + } + + codeMirror.toggle(); + + var escHandle = function(event) { + if (event.shiftKey && event.keyCode === 27) { + _this.previewed(); + } + }; + + if (codeMirror.css("display") === "none") // 为了兼容Zepto,而不使用codeMirror.is(":hidden") + { + this.state.preview = true; + + if (this.state.fullscreen) { + preview.css("background", "#fff"); + } + + editor.find("." + this.classPrefix + "preview-close-btn").show().bind(editormd.mouseOrTouch("click", "touchend"), function(){ + _this.previewed(); + }); + + if (!settings.watch) + { + this.save(); + } + else + { + previewContainer.css("padding", ""); + } + + previewContainer.addClass(this.classPrefix + "preview-active"); + + preview.show().css({ + position : "", + top : 0, + width : editor.width(), + height : (settings.autoHeight && !this.state.fullscreen) ? "auto" : editor.height() + }); + + if (this.state.loaded) + { + $.proxy(settings.onpreviewing, this)(); + } + + $(window).bind("keyup", escHandle); + } + else + { + $(window).unbind("keyup", escHandle); + this.previewed(); + } + }, + + /** + * 显示编辑器部分,退出只预览HTML + * Exit preview html state + * + * @returns {editormd} 返回editormd的实例对象 + */ + + previewed : function() { + + var editor = this.editor; + var preview = this.preview; + var toolbar = this.toolbar; + var settings = this.settings; + var previewContainer = this.previewContainer; + var previewCloseBtn = editor.find("." + this.classPrefix + "preview-close-btn"); + + this.state.preview = false; + + this.codeMirror.show(); + + if (settings.toolbar) { + toolbar.show(); + } + + preview[(settings.watch) ? "show" : "hide"](); + + previewCloseBtn.hide().unbind(editormd.mouseOrTouch("click", "touchend")); + + previewContainer.removeClass(this.classPrefix + "preview-active"); + + if (settings.watch) + { + previewContainer.css("padding", "20px"); + } + + preview.css({ + background : null, + position : "absolute", + width : editor.width() / 2, + height : (settings.autoHeight && !this.state.fullscreen) ? "auto" : editor.height() - toolbar.height(), + top : (settings.toolbar) ? toolbar.height() : 0 + }); + + if (this.state.loaded) + { + $.proxy(settings.onpreviewed, this)(); + } + + return this; + }, + + /** + * 编辑器全屏显示 + * Fullscreen show + * + * @returns {editormd} 返回editormd的实例对象 + */ + + fullscreen : function() { + + var _this = this; + var state = this.state; + var editor = this.editor; + var preview = this.preview; + var toolbar = this.toolbar; + var settings = this.settings; + var fullscreenClass = this.classPrefix + "fullscreen"; + + if (toolbar) { + toolbar.find(".fa[name=fullscreen]").parent().toggleClass("active"); + } + + var escHandle = function(event) { + if (!event.shiftKey && event.keyCode === 27) + { + if (state.fullscreen) + { + _this.fullscreenExit(); + } + } + }; + + if (!editor.hasClass(fullscreenClass)) + { + state.fullscreen = true; + + $("html,body").css("overflow", "hidden"); + + editor.css({ + width : $(window).width(), + height : $(window).height() + }).addClass(fullscreenClass); + + this.resize(); + + $.proxy(settings.onfullscreen, this)(); + + $(window).bind("keyup", escHandle); + } + else + { + $(window).unbind("keyup", escHandle); + this.fullscreenExit(); + } + + return this; + }, + + /** + * 编辑器退出全屏显示 + * Exit fullscreen state + * + * @returns {editormd} 返回editormd的实例对象 + */ + + fullscreenExit : function() { + + var editor = this.editor; + var settings = this.settings; + var toolbar = this.toolbar; + var fullscreenClass = this.classPrefix + "fullscreen"; + + this.state.fullscreen = false; + + if (toolbar) { + toolbar.find(".fa[name=fullscreen]").parent().removeClass("active"); + } + + $("html,body").css("overflow", ""); + + editor.css({ + width : editor.data("oldWidth"), + height : editor.data("oldHeight") + }).removeClass(fullscreenClass); + + this.resize(); + + $.proxy(settings.onfullscreenExit, this)(); + + return this; + }, + + /** + * 加载并执行插件 + * Load and execute the plugin + * + * @param {String} name plugin name / function name + * @param {String} path plugin load path + * @returns {editormd} 返回editormd的实例对象 + */ + + executePlugin : function(name, path) { + + var _this = this; + var cm = this.cm; + var settings = this.settings; + + path = settings.pluginPath + path; + + if (typeof define === "function") + { + if (typeof this[name] === "undefined") + { + alert("Error: " + name + " plugin is not found, you are not load this plugin."); + + return this; + } + + this[name](cm); + + return this; + } + + if ($.inArray(path, editormd.loadFiles.plugin) < 0) + { + editormd.loadPlugin(path, function() { + editormd.loadPlugins[name] = _this[name]; + _this[name](cm); + }); + } + else + { + $.proxy(editormd.loadPlugins[name], this)(cm); + } + + return this; + }, + + /** + * 搜索替换 + * Search & replace + * + * @param {String} command CodeMirror serach commands, "find, fintNext, fintPrev, clearSearch, replace, replaceAll" + * @returns {editormd} return this + */ + + search : function(command) { + var settings = this.settings; + + if (!settings.searchReplace) + { + alert("Error: settings.searchReplace == false"); + return this; + } + + if (!settings.readOnly) + { + this.cm.execCommand(command || "find"); + } + + return this; + }, + + searchReplace : function() { + this.search("replace"); + + return this; + }, + + searchReplaceAll : function() { + this.search("replaceAll"); + + return this; + } + }; + + editormd.fn.init.prototype = editormd.fn; + + /** + * 锁屏 + * lock screen when dialog opening + * + * @returns {void} + */ + + editormd.dialogLockScreen = function() { + var settings = this.settings || {dialogLockScreen : true}; + + if (settings.dialogLockScreen) + { + $("html,body").css("overflow", "hidden"); + this.resize(); + } + }; + + /** + * 显示透明背景层 + * Display mask layer when dialog opening + * + * @param {Object} dialog dialog jQuery object + * @returns {void} + */ + + editormd.dialogShowMask = function(dialog) { + var editor = this.editor; + var settings = this.settings || {dialogShowMask : true}; + + dialog.css({ + top : ($(window).height() - dialog.height()) / 2 + "px", + left : ($(window).width() - dialog.width()) / 2 + "px" + }); + + if (settings.dialogShowMask) { + editor.children("." + this.classPrefix + "mask").css("z-index", parseInt(dialog.css("z-index")) - 1).show(); + } + }; + + editormd.toolbarHandlers = { + undo : function() { + this.cm.undo(); + }, + + redo : function() { + this.cm.redo(); + }, + + bold : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection("**" + selection + "**"); + + if(selection === "") { + cm.setCursor(cursor.line, cursor.ch + 2); + } + }, + + del : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection("~~" + selection + "~~"); + + if(selection === "") { + cm.setCursor(cursor.line, cursor.ch + 2); + } + }, + + italic : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection("*" + selection + "*"); + + if(selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + }, + + quote : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("> " + selection); + cm.setCursor(cursor.line, cursor.ch + 2); + } + else + { + cm.replaceSelection("> " + selection); + } + + //cm.replaceSelection("> " + selection); + //cm.setCursor(cursor.line, (selection === "") ? cursor.ch + 2 : cursor.ch + selection.length + 2); + }, + + ucfirst : function() { + var cm = this.cm; + var selection = cm.getSelection(); + var selections = cm.listSelections(); + + cm.replaceSelection(editormd.firstUpperCase(selection)); + cm.setSelections(selections); + }, + + ucwords : function() { + var cm = this.cm; + var selection = cm.getSelection(); + var selections = cm.listSelections(); + + cm.replaceSelection(editormd.wordsFirstUpperCase(selection)); + cm.setSelections(selections); + }, + + uppercase : function() { + var cm = this.cm; + var selection = cm.getSelection(); + var selections = cm.listSelections(); + + cm.replaceSelection(selection.toUpperCase()); + cm.setSelections(selections); + }, + + lowercase : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + var selections = cm.listSelections(); + + cm.replaceSelection(selection.toLowerCase()); + cm.setSelections(selections); + }, + + h1 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("# " + selection); + cm.setCursor(cursor.line, cursor.ch + 2); + } + else + { + cm.replaceSelection("# " + selection); + } + }, + + h2 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("## " + selection); + cm.setCursor(cursor.line, cursor.ch + 3); + } + else + { + cm.replaceSelection("## " + selection); + } + }, + + h3 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("### " + selection); + cm.setCursor(cursor.line, cursor.ch + 4); + } + else + { + cm.replaceSelection("### " + selection); + } + }, + + h4 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("#### " + selection); + cm.setCursor(cursor.line, cursor.ch + 5); + } + else + { + cm.replaceSelection("#### " + selection); + } + }, + + h5 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("##### " + selection); + cm.setCursor(cursor.line, cursor.ch + 6); + } + else + { + cm.replaceSelection("##### " + selection); + } + }, + + h6 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("###### " + selection); + cm.setCursor(cursor.line, cursor.ch + 7); + } + else + { + cm.replaceSelection("###### " + selection); + } + }, + + "list-ul" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (selection === "") + { + cm.replaceSelection("- " + selection); + } + else + { + var selectionText = selection.split("\n"); + + for (var i = 0, len = selectionText.length; i < len; i++) + { + selectionText[i] = (selectionText[i] === "") ? "" : "- " + selectionText[i]; + } + + cm.replaceSelection(selectionText.join("\n")); + } + }, + + "list-ol" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if(selection === "") + { + cm.replaceSelection("1. " + selection); + } + else + { + var selectionText = selection.split("\n"); + + for (var i = 0, len = selectionText.length; i < len; i++) + { + selectionText[i] = (selectionText[i] === "") ? "" : (i+1) + ". " + selectionText[i]; + } + + cm.replaceSelection(selectionText.join("\n")); + } + }, + + hr : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection(((cursor.ch !== 0) ? "\n\n" : "\n") + "------------\n\n"); + }, + + tex : function() { + if (!this.settings.tex) + { + alert("settings.tex === false"); + return this; + } + + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection("$$" + selection + "$$"); + + if(selection === "") { + cm.setCursor(cursor.line, cursor.ch + 2); + } + }, + + link : function() { + this.executePlugin("linkDialog", "link-dialog/link-dialog"); + }, + + "reference-link" : function() { + this.executePlugin("referenceLinkDialog", "reference-link-dialog/reference-link-dialog"); + }, + + pagebreak : function() { + if (!this.settings.pageBreak) + { + alert("settings.pageBreak === false"); + return this; + } + + var cm = this.cm; + var selection = cm.getSelection(); + + cm.replaceSelection("\r\n[========]\r\n"); + }, + + image : function() { + this.executePlugin("imageDialog", "image-dialog/image-dialog"); + }, + + code : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection("`" + selection + "`"); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + }, + + "code-block" : function() { + this.executePlugin("codeBlockDialog", "code-block-dialog/code-block-dialog"); + }, + + "preformatted-text" : function() { + this.executePlugin("preformattedTextDialog", "preformatted-text-dialog/preformatted-text-dialog"); + }, + + table : function() { + this.executePlugin("tableDialog", "table-dialog/table-dialog"); + }, + + datetime : function() { + var cm = this.cm; + var selection = cm.getSelection(); + var date = new Date(); + var langName = this.settings.lang.name; + var datefmt = editormd.dateFormat() + " " + editormd.dateFormat((langName === "zh-cn" || langName === "zh-tw") ? "cn-week-day" : "week-day"); + + cm.replaceSelection(datefmt); + }, + + emoji : function() { + this.executePlugin("emojiDialog", "emoji-dialog/emoji-dialog"); + }, + + "html-entities" : function() { + this.executePlugin("htmlEntitiesDialog", "html-entities-dialog/html-entities-dialog"); + }, + + "goto-line" : function() { + this.executePlugin("gotoLineDialog", "goto-line-dialog/goto-line-dialog"); + }, + + watch : function() { + this[this.settings.watch ? "unwatch" : "watch"](); + }, + + preview : function() { + this.previewing(); + }, + + fullscreen : function() { + this.fullscreen(); + }, + + clear : function() { + this.clear(); + }, + + search : function() { + this.search(); + }, + + help : function() { + this.executePlugin("helpDialog", "help-dialog/help-dialog"); + }, + + info : function() { + this.showInfoDialog(); + } + }; + + editormd.keyMaps = { + "Ctrl-1" : "h1", + "Ctrl-2" : "h2", + "Ctrl-3" : "h3", + "Ctrl-4" : "h4", + "Ctrl-5" : "h5", + "Ctrl-6" : "h6", + "Ctrl-B" : "bold", // if this is string == editormd.toolbarHandlers.xxxx + "Ctrl-D" : "datetime", + + "Ctrl-E" : function() { // emoji + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (!this.settings.emoji) + { + alert("Error: settings.emoji == false"); + return ; + } + + cm.replaceSelection(":" + selection + ":"); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + }, + "Ctrl-Alt-G" : "goto-line", + "Ctrl-H" : "hr", + "Ctrl-I" : "italic", + "Ctrl-K" : "code", + + "Ctrl-L" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + var title = (selection === "") ? "" : " \""+selection+"\""; + + cm.replaceSelection("[" + selection + "]("+title+")"); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + }, + "Ctrl-U" : "list-ul", + + "Shift-Ctrl-A" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (!this.settings.atLink) + { + alert("Error: settings.atLink == false"); + return ; + } + + cm.replaceSelection("@" + selection); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + }, + + "Shift-Ctrl-C" : "code", + "Shift-Ctrl-Q" : "quote", + "Shift-Ctrl-S" : "del", + "Shift-Ctrl-K" : "tex", // KaTeX + + "Shift-Alt-C" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection(["```", selection, "```"].join("\n")); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 3); + } + }, + + "Shift-Ctrl-Alt-C" : "code-block", + "Shift-Ctrl-H" : "html-entities", + "Shift-Alt-H" : "help", + "Shift-Ctrl-E" : "emoji", + "Shift-Ctrl-U" : "uppercase", + "Shift-Alt-U" : "ucwords", + "Shift-Ctrl-Alt-U" : "ucfirst", + "Shift-Alt-L" : "lowercase", + + "Shift-Ctrl-I" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + var title = (selection === "") ? "" : " \""+selection+"\""; + + cm.replaceSelection("![" + selection + "]("+title+")"); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 4); + } + }, + + "Shift-Ctrl-Alt-I" : "image", + "Shift-Ctrl-L" : "link", + "Shift-Ctrl-O" : "list-ol", + "Shift-Ctrl-P" : "preformatted-text", + "Shift-Ctrl-T" : "table", + "Shift-Alt-P" : "pagebreak", + "F9" : "watch", + "F10" : "preview", + "F11" : "fullscreen", + }; + + /** + * 清除字符串两边的空格 + * Clear the space of strings both sides. + * + * @param {String} str string + * @returns {String} trimed string + */ + + var trim = function(str) { + return (!String.prototype.trim) ? str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "") : str.trim(); + }; + + editormd.trim = trim; + + /** + * 所有单词首字母大写 + * Words first to uppercase + * + * @param {String} str string + * @returns {String} string + */ + + var ucwords = function (str) { + return str.toLowerCase().replace(/\b(\w)|\s(\w)/g, function($1) { + return $1.toUpperCase(); + }); + }; + + editormd.ucwords = editormd.wordsFirstUpperCase = ucwords; + + /** + * 字符串首字母大写 + * Only string first char to uppercase + * + * @param {String} str string + * @returns {String} string + */ + + var firstUpperCase = function(str) { + return str.toLowerCase().replace(/\b(\w)/, function($1){ + return $1.toUpperCase(); + }); + }; + + var ucfirst = firstUpperCase; + + editormd.firstUpperCase = editormd.ucfirst = firstUpperCase; + + editormd.urls = { + atLinkBase : "https://github.com/" + }; + + editormd.regexs = { + atLink : /@(\w+)/g, + email : /(\w+)@(\w+)\.(\w+)\.?(\w+)?/g, + emailLink : /(mailto:)?([\w\.\_]+)@(\w+)\.(\w+)\.?(\w+)?/g, + emoji : /:([\w\+-]+):/g, + emojiDatetime : /(\d{2}:\d{2}:\d{2})/g, + twemoji : /:(tw-([\w]+)-?(\w+)?):/g, + fontAwesome : /:(fa-([\w]+)(-(\w+)){0,}):/g, + editormdLogo : /:(editormd-logo-?(\w+)?):/g, + pageBreak : /^\[[=]{8,}\]$/ + }; + + // Emoji graphics files url path + editormd.emoji = { + path : "http://www.emoji-cheat-sheet.com/graphics/emojis/", + ext : ".png" + }; + + // Twitter Emoji (Twemoji) graphics files url path + editormd.twemoji = { + path : "http://twemoji.maxcdn.com/36x36/", + ext : ".png" + }; + + /** + * 自定义marked的解析器 + * Custom Marked renderer rules + * + * @param {Array} markdownToC 传入用于接收TOC的数组 + * @returns {Renderer} markedRenderer 返回marked的Renderer自定义对象 + */ + + editormd.markedRenderer = function(markdownToC, options) { + var defaults = { + toc : true, // Table of contents + tocm : false, + tocStartLevel : 1, // Said from H1 to create ToC + pageBreak : true, + atLink : true, // for @link + emailLink : true, // for mail address auto link + taskList : false, // Enable Github Flavored Markdown task lists + emoji : false, // :emoji: , Support Twemoji, fontAwesome, Editor.md logo emojis. + tex : false, // TeX(LaTeX), based on KaTeX + flowChart : false, // flowChart.js only support IE9+ + sequenceDiagram : false, // sequenceDiagram.js only support IE9+ + }; + + var settings = $.extend(defaults, options || {}); + var marked = editormd.$marked; + var markedRenderer = new marked.Renderer(); + markdownToC = markdownToC || []; + + var regexs = editormd.regexs; + var atLinkReg = regexs.atLink; + var emojiReg = regexs.emoji; + var emailReg = regexs.email; + var emailLinkReg = regexs.emailLink; + var twemojiReg = regexs.twemoji; + var faIconReg = regexs.fontAwesome; + var editormdLogoReg = regexs.editormdLogo; + var pageBreakReg = regexs.pageBreak; + + markedRenderer.emoji = function(text) { + + text = text.replace(editormd.regexs.emojiDatetime, function($1) { + return $1.replace(/:/g, ":"); + }); + + var matchs = text.match(emojiReg); + + if (!matchs || !settings.emoji) { + return text; + } + + for (var i = 0, len = matchs.length; i < len; i++) + { + if (matchs[i] === ":+1:") { + matchs[i] = ":\\+1:"; + } + + text = text.replace(new RegExp(matchs[i]), function($1, $2){ + var faMatchs = $1.match(faIconReg); + var name = $1.replace(/:/g, ""); + + if (faMatchs) + { + for (var fa = 0, len1 = faMatchs.length; fa < len1; fa++) + { + var faName = faMatchs[fa].replace(/:/g, ""); + + return ""; + } + } + else + { + var emdlogoMathcs = $1.match(editormdLogoReg); + var twemojiMatchs = $1.match(twemojiReg); + + if (emdlogoMathcs) + { + for (var x = 0, len2 = emdlogoMathcs.length; x < len2; x++) + { + var logoName = emdlogoMathcs[x].replace(/:/g, ""); + return ""; + } + } + else if (twemojiMatchs) + { + for (var t = 0, len3 = twemojiMatchs.length; t < len3; t++) + { + var twe = twemojiMatchs[t].replace(/:/g, "").replace("tw-", ""); + return "\"twemoji-""; + } + } + else + { + var src = (name === "+1") ? "plus1" : name; + src = (src === "black_large_square") ? "black_square" : src; + src = (src === "moon") ? "waxing_gibbous_moon" : src; + + return "\":""; + } + } + }); + } + + return text; + }; + + markedRenderer.atLink = function(text) { + + if (atLinkReg.test(text)) + { + if (settings.atLink) + { + text = text.replace(emailReg, function($1, $2, $3, $4) { + return $1.replace(/@/g, "_#_@_#_"); + }); + + text = text.replace(atLinkReg, function($1, $2) { + return "" + $1 + ""; + }).replace(/_#_@_#_/g, "@"); + } + + if (settings.emailLink) + { + text = text.replace(emailLinkReg, function($1, $2, $3, $4, $5) { + return (!$2 && $.inArray($5, "jpg|jpeg|png|gif|webp|ico|icon|pdf".split("|")) < 0) ? ""+$1+"" : $1; + }); + } + + return text; + } + + return text; + }; + + markedRenderer.link = function (href, title, text) { + + if (this.options.sanitize) { + try { + var prot = decodeURIComponent(unescape(href)).replace(/[^\w:]/g,"").toLowerCase(); + } catch(e) { + return ""; + } + + if (prot.indexOf("javascript:") === 0) { + return ""; + } + } + + var out = "" + text.replace(/@/g, "@") + ""; + } + + if (title) { + out += " title=\"" + title + "\""; + } + + out += ">" + text + ""; + + return out; + }; + + markedRenderer.heading = function(text, level, raw) { + + var linkText = text; + var hasLinkReg = /\s*\]*)\>(.*)\<\/a\>\s*/; + var getLinkTextReg = /\s*\]+)\>([^\>]*)\<\/a\>\s*/g; + + if (hasLinkReg.test(text)) + { + var tempText = []; + text = text.split(/\]+)\>([^\>]*)\<\/a\>/); + + for (var i = 0, len = text.length; i < len; i++) + { + tempText.push(text[i].replace(/\s*href\=\"(.*)\"\s*/g, "")); + } + + text = tempText.join(" "); + } + + text = trim(text); + + var escapedText = text.toLowerCase().replace(/[^\w]+/g, "-"); + var toc = { + text : text, + level : level, + slug : escapedText + }; + + var isChinese = /^[\u4e00-\u9fa5]+$/.test(text); + var id = (isChinese) ? escape(text).replace(/\%/g, "") : text.toLowerCase().replace(/[^\w]+/g, "-"); + + markdownToC.push(toc); + + var headingHTML = ""; + + headingHTML += ""; + headingHTML += ""; + headingHTML += (hasLinkReg) ? this.atLink(this.emoji(linkText)) : this.atLink(this.emoji(text)); + headingHTML += ""; + + return headingHTML; + }; + + markedRenderer.pageBreak = function(text) { + if (pageBreakReg.test(text) && settings.pageBreak) + { + text = "
    "; + } + + return text; + }; + + markedRenderer.paragraph = function(text) { + var isTeXInline = /\$\$(.*)\$\$/g.test(text); + var isTeXLine = /^\$\$(.*)\$\$$/.test(text); + var isTeXAddClass = (isTeXLine) ? " class=\"" + editormd.classNames.tex + "\"" : ""; + var isToC = (settings.tocm) ? /^(\[TOC\]|\[TOCM\])$/.test(text) : /^\[TOC\]$/.test(text); + var isToCMenu = /^\[TOCM\]$/.test(text); + + if (!isTeXLine && isTeXInline) + { + text = text.replace(/(\$\$([^\$]*)\$\$)+/g, function($1, $2) { + return "" + $2.replace(/\$/g, "") + ""; + }); + } + else + { + text = (isTeXLine) ? text.replace(/\$/g, "") : text; + } + + var tocHTML = "
    " + text + "
    "; + + return (isToC) ? ( (isToCMenu) ? "
    " + tocHTML + "

    " : tocHTML ) + : ( (pageBreakReg.test(text)) ? this.pageBreak(text) : "" + this.atLink(this.emoji(text)) + "

    \n" ); + }; + + markedRenderer.code = function (code, lang, escaped) { + + if (lang === "seq" || lang === "sequence") + { + return "
    " + code + "
    "; + } + else if ( lang === "flow") + { + return "
    " + code + "
    "; + } + else if ( lang === "math" || lang === "latex" || lang === "katex") + { + return "

    " + code + "

    "; + } + else + { + + return marked.Renderer.prototype.code.apply(this, arguments); + } + }; + + markedRenderer.tablecell = function(content, flags) { + var type = (flags.header) ? "th" : "td"; + var tag = (flags.align) ? "<" + type +" style=\"text-align:" + flags.align + "\">" : "<" + type + ">"; + + return tag + this.atLink(this.emoji(content)) + "\n"; + }; + + markedRenderer.listitem = function(text) { + if (settings.taskList && /^\s*\[[x\s]\]\s*/.test(text)) + { + text = text.replace(/^\s*\[\s\]\s*/, " ") + .replace(/^\s*\[x\]\s*/, " "); + + return "
  • " + this.atLink(this.emoji(text)) + "
  • "; + } + else + { + return "
  • " + this.atLink(this.emoji(text)) + "
  • "; + } + }; + + return markedRenderer; + }; + + /** + * + * 生成TOC(Table of Contents) + * Creating ToC (Table of Contents) + * + * @param {Array} toc 从marked获取的TOC数组列表 + * @param {Element} container 插入TOC的容器元素 + * @param {Integer} startLevel Hx 起始层级 + * @returns {Object} tocContainer 返回ToC列表容器层的jQuery对象元素 + */ + + editormd.markdownToCRenderer = function(toc, container, tocDropdown, startLevel) { + + var html = ""; + var lastLevel = 0; + var classPrefix = this.classPrefix; + + startLevel = startLevel || 1; + + for (var i = 0, len = toc.length; i < len; i++) + { + var text = toc[i].text; + var level = toc[i].level; + + if (level < startLevel) { + continue; + } + + if (level > lastLevel) + { + html += ""; + } + else if (level < lastLevel) + { + html += (new Array(lastLevel - level + 2)).join(""); + } + else + { + html += ""; + } + + html += "
  • " + text + "
      "; + lastLevel = level; + } + + var tocContainer = container.find(".markdown-toc"); + + if ((tocContainer.length < 1 && container.attr("previewContainer") === "false")) + { + var tocHTML = "
      "; + + tocHTML = (tocDropdown) ? "
      " + tocHTML + "
      " : tocHTML; + + container.html(tocHTML); + + tocContainer = container.find(".markdown-toc"); + } + + if (tocDropdown) + { + tocContainer.wrap("

      "); + } + + tocContainer.html("
        ").children(".markdown-toc-list").html(html.replace(/\r?\n?\\<\/ul\>/g, "")); + + return tocContainer; + }; + + /** + * + * 生成TOC下拉菜单 + * Creating ToC dropdown menu + * + * @param {Object} container 插入TOC的容器jQuery对象元素 + * @param {String} tocTitle ToC title + * @returns {Object} return toc-menu object + */ + + editormd.tocDropdownMenu = function(container, tocTitle) { + + tocTitle = tocTitle || "Table of Contents"; + + var zindex = 400; + var tocMenus = container.find("." + this.classPrefix + "toc-menu"); + + tocMenus.each(function() { + var $this = $(this); + var toc = $this.children(".markdown-toc"); + var icon = ""; + var btn = "" + icon + tocTitle + ""; + var menu = toc.children("ul"); + var list = menu.find("li"); + + toc.append(btn); + + list.first().before("
      • " + tocTitle + " " + icon + "

      • "); + + $this.mouseover(function(){ + menu.show(); + + list.each(function(){ + var li = $(this); + var ul = li.children("ul"); + + if (ul.html() === "") + { + ul.remove(); + } + + if (ul.length > 0 && ul.html() !== "") + { + var firstA = li.children("a").first(); + + if (firstA.children(".fa").length < 1) + { + firstA.append( $(icon).css({ float:"right", paddingTop:"4px" }) ); + } + } + + li.mouseover(function(){ + ul.css("z-index", zindex).show(); + zindex += 1; + }).mouseleave(function(){ + ul.hide(); + }); + }); + }).mouseleave(function(){ + menu.hide(); + }); + }); + + return tocMenus; + }; + + /** + * 简单地过滤指定的HTML标签 + * Filter custom html tags + * + * @param {String} html 要过滤HTML + * @param {String} filters 要过滤的标签 + * @returns {String} html 返回过滤的HTML + */ + + editormd.filterHTMLTags = function(html, filters) { + + if (typeof html !== "string") { + html = new String(html); + } + + if (typeof filters !== "string") { + return html; + } + + var expression = filters.split("|"); + var filterTags = expression[0].split(","); + var attrs = expression[1]; + + for (var i = 0, len = filterTags.length; i < len; i++) + { + var tag = filterTags[i]; + + html = html.replace(new RegExp("\<\s*" + tag + "\s*([^\>]*)\>([^\>]*)\<\s*\/" + tag + "\s*\>", "igm"), ""); + } + + //return html; + + if (typeof attrs !== "undefined") + { + var htmlTagRegex = /\<(\w+)\s*([^\>]*)\>([^\>]*)\<\/(\w+)\>/ig; + + if (attrs === "*") + { + html = html.replace(htmlTagRegex, function($1, $2, $3, $4, $5) { + return "<" + $2 + ">" + $4 + ""; + }); + } + else if (attrs === "on*") + { + html = html.replace(htmlTagRegex, function($1, $2, $3, $4, $5) { + var el = $("<" + $2 + ">" + $4 + ""); + var _attrs = $($1)[0].attributes; + var $attrs = {}; + + $.each(_attrs, function(i, e) { + if (e.nodeName !== '"') $attrs[e.nodeName] = e.nodeValue; + }); + + $.each($attrs, function(i) { + if (i.indexOf("on") === 0) { + delete $attrs[i]; + } + }); + + el.attr($attrs); + + var text = (typeof el[1] !== "undefined") ? $(el[1]).text() : ""; + + return el[0].outerHTML + text; + }); + } + else + { + html = html.replace(htmlTagRegex, function($1, $2, $3, $4) { + var filterAttrs = attrs.split(","); + var el = $($1); + el.html($4); + + $.each(filterAttrs, function(i) { + el.attr(filterAttrs[i], null); + }); + + return el[0].outerHTML; + }); + } + } + + return html; + }; + + /** + * 将Markdown文档解析为HTML用于前台显示 + * Parse Markdown to HTML for Font-end preview. + * + * @param {String} id 用于显示HTML的对象ID + * @param {Object} [options={}] 配置选项,可选 + * @returns {Object} div 返回jQuery对象元素 + */ + + editormd.markdownToHTML = function(id, options) { + var defaults = { + gfm : true, + toc : true, + tocm : false, + tocStartLevel : 1, + tocTitle : "目录", + tocDropdown : false, + tocContainer : "", + markdown : "", + markdownSourceCode : false, + htmlDecode : false, + autoLoadKaTeX : true, + pageBreak : true, + atLink : true, // for @link + emailLink : true, // for mail address auto link + tex : false, + taskList : false, // Github Flavored Markdown task lists + emoji : false, + flowChart : false, + sequenceDiagram : false, + previewCodeHighlight : true + }; + + editormd.$marked = marked; + + var div = $("#" + id); + var settings = div.settings = $.extend(true, defaults, options || {}); + var saveTo = div.find("textarea"); + + if (saveTo.length < 1) + { + div.append(""); + saveTo = div.find("textarea"); + } + + var markdownDoc = (settings.markdown === "") ? saveTo.val() : settings.markdown; + var markdownToC = []; + + var rendererOptions = { + toc : settings.toc, + tocm : settings.tocm, + tocStartLevel : settings.tocStartLevel, + taskList : settings.taskList, + emoji : settings.emoji, + tex : settings.tex, + pageBreak : settings.pageBreak, + atLink : settings.atLink, // for @link + emailLink : settings.emailLink, // for mail address auto link + flowChart : settings.flowChart, + sequenceDiagram : settings.sequenceDiagram, + previewCodeHighlight : settings.previewCodeHighlight, + }; + + var markedOptions = { + renderer : editormd.markedRenderer(markdownToC, rendererOptions), + gfm : settings.gfm, + tables : true, + breaks : true, + pedantic : false, + sanitize : (settings.htmlDecode) ? false : true, // 是否忽略HTML标签,即是否开启HTML标签解析,为了安全性,默认不开启 + smartLists : true, + smartypants : true + }; + + markdownDoc = new String(markdownDoc); + + var markdownParsed = marked(markdownDoc, markedOptions); + + markdownParsed = editormd.filterHTMLTags(markdownParsed, settings.htmlDecode); + + if (settings.markdownSourceCode) { + saveTo.text(markdownDoc); + } else { + saveTo.remove(); + } + + div.addClass("markdown-body " + this.classPrefix + "html-preview").append(markdownParsed); + + var tocContainer = (settings.tocContainer !== "") ? $(settings.tocContainer) : div; + + if (settings.tocContainer !== "") + { + tocContainer.attr("previewContainer", false); + } + + if (settings.toc) + { + div.tocContainer = this.markdownToCRenderer(markdownToC, tocContainer, settings.tocDropdown, settings.tocStartLevel); + + if (settings.tocDropdown || div.find("." + this.classPrefix + "toc-menu").length > 0) + { + this.tocDropdownMenu(div, settings.tocTitle); + } + + if (settings.tocContainer !== "") + { + div.find(".editormd-toc-menu, .editormd-markdown-toc").remove(); + } + } + + if (settings.previewCodeHighlight) + { + div.find("pre").addClass("prettyprint linenums"); + prettyPrint(); + } + + if (!editormd.isIE8) + { + if (settings.flowChart) { + div.find(".flowchart").flowChart(); + } + + if (settings.sequenceDiagram) { + div.find(".sequence-diagram").sequenceDiagram({theme: "simple"}); + } + } + + if (settings.tex) + { + var katexHandle = function() { + div.find("." + editormd.classNames.tex).each(function(){ + var tex = $(this); + katex.render(tex.html().replace(/</g, "<").replace(/>/g, ">"), tex[0]); + tex.find(".katex").css("font-size", "1.6em"); + }); + }; + + if (settings.autoLoadKaTeX && !editormd.$katex && !editormd.kaTeXLoaded) + { + this.loadKaTeX(function() { + editormd.$katex = katex; + editormd.kaTeXLoaded = true; + katexHandle(); + }); + } + else + { + katexHandle(); + } + } + + div.getMarkdown = function() { + return saveTo.val(); + }; + + return div; + }; + + // Editor.md themes, change toolbar themes etc. + // added @1.5.0 + editormd.themes = ["default", "dark"]; + + // Preview area themes + // added @1.5.0 + editormd.previewThemes = ["default", "dark"]; + + // CodeMirror / editor area themes + // @1.5.0 rename -> editorThemes, old version -> themes + editormd.editorThemes = [ + "default", "3024-day", "3024-night", + "ambiance", "ambiance-mobile", + "base16-dark", "base16-light", "blackboard", + "cobalt", + "eclipse", "elegant", "erlang-dark", + "lesser-dark", + "mbo", "mdn-like", "midnight", "monokai", + "neat", "neo", "night", + "paraiso-dark", "paraiso-light", "pastel-on-dark", + "rubyblue", + "solarized", + "the-matrix", "tomorrow-night-eighties", "twilight", + "vibrant-ink", + "xq-dark", "xq-light" + ]; + + editormd.loadPlugins = {}; + + editormd.loadFiles = { + js : [], + css : [], + plugin : [] + }; + + /** + * 动态加载Editor.md插件,但不立即执行 + * Load editor.md plugins + * + * @param {String} fileName 插件文件路径 + * @param {Function} [callback=function()] 加载成功后执行的回调函数 + * @param {String} [into="head"] 嵌入页面的位置 + */ + + editormd.loadPlugin = function(fileName, callback, into) { + callback = callback || function() {}; + + this.loadScript(fileName, function() { + editormd.loadFiles.plugin.push(fileName); + callback(); + }, into); + }; + + /** + * 动态加载CSS文件的方法 + * Load css file method + * + * @param {String} fileName CSS文件名 + * @param {Function} [callback=function()] 加载成功后执行的回调函数 + * @param {String} [into="head"] 嵌入页面的位置 + */ + + editormd.loadCSS = function(fileName, callback, into) { + into = into || "head"; + callback = callback || function() {}; + + var css = document.createElement("link"); + css.type = "text/css"; + css.rel = "stylesheet"; + css.onload = css.onreadystatechange = function() { + editormd.loadFiles.css.push(fileName); + callback(); + }; + + css.href = fileName + ".css"; + + if(into === "head") { + document.getElementsByTagName("head")[0].appendChild(css); + } else { + document.body.appendChild(css); + } + }; + + editormd.isIE = (navigator.appName == "Microsoft Internet Explorer"); + editormd.isIE8 = (editormd.isIE && navigator.appVersion.match(/8./i) == "8."); + + /** + * 动态加载JS文件的方法 + * Load javascript file method + * + * @param {String} fileName JS文件名 + * @param {Function} [callback=function()] 加载成功后执行的回调函数 + * @param {String} [into="head"] 嵌入页面的位置 + */ + + editormd.loadScript = function(fileName, callback, into) { + + into = into || "head"; + callback = callback || function() {}; + + var script = null; + script = document.createElement("script"); + script.id = fileName.replace(/[\./]+/g, "-"); + script.type = "text/javascript"; + script.src = fileName + ".js"; + + if (editormd.isIE8) + { + script.onreadystatechange = function() { + if(script.readyState) + { + if (script.readyState === "loaded" || script.readyState === "complete") + { + script.onreadystatechange = null; + editormd.loadFiles.js.push(fileName); + callback(); + } + } + }; + } + else + { + script.onload = function() { + editormd.loadFiles.js.push(fileName); + callback(); + }; + } + + if (into === "head") { + document.getElementsByTagName("head")[0].appendChild(script); + } else { + document.body.appendChild(script); + } + }; + + // 使用国外的CDN,加载速度有时会很慢,或者自定义URL + // You can custom KaTeX load url. + editormd.katexURL = { + css : "//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min", + js : "//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min" + }; + + editormd.kaTeXLoaded = false; + + /** + * 加载KaTeX文件 + * load KaTeX files + * + * @param {Function} [callback=function()] 加载成功后执行的回调函数 + */ + + editormd.loadKaTeX = function (callback) { + editormd.loadCSS(editormd.katexURL.css, function(){ + editormd.loadScript(editormd.katexURL.js, callback || function(){}); + }); + }; + + /** + * 锁屏 + * lock screen + * + * @param {Boolean} lock Boolean 布尔值,是否锁屏 + * @returns {void} + */ + + editormd.lockScreen = function(lock) { + $("html,body").css("overflow", (lock) ? "hidden" : ""); + }; + + /** + * 动态创建对话框 + * Creating custom dialogs + * + * @param {Object} options 配置项键值对 Key/Value + * @returns {dialog} 返回创建的dialog的jQuery实例对象 + */ + + editormd.createDialog = function(options) { + var defaults = { + name : "", + width : 420, + height: 240, + title : "", + drag : true, + closed : true, + content : "", + mask : true, + maskStyle : { + backgroundColor : "#fff", + opacity : 0.1 + }, + lockScreen : true, + footer : true, + buttons : false + }; + + options = $.extend(true, defaults, options); + + var $this = this; + var editor = this.editor; + var classPrefix = editormd.classPrefix; + var guid = (new Date()).getTime(); + var dialogName = ( (options.name === "") ? classPrefix + "dialog-" + guid : options.name); + var mouseOrTouch = editormd.mouseOrTouch; + + var html = "
        "; + + if (options.title !== "") + { + html += "
        "; + html += "" + options.title + ""; + html += "
        "; + } + + if (options.closed) + { + html += ""; + } + + html += "
        " + options.content; + + if (options.footer || typeof options.footer === "string") + { + html += "
        " + ( (typeof options.footer === "boolean") ? "" : options.footer) + "
        "; + } + + html += "
        "; + + html += "
        "; + html += "
        "; + html += "
        "; + + editor.append(html); + + var dialog = editor.find("." + dialogName); + + dialog.lockScreen = function(lock) { + if (options.lockScreen) + { + $("html,body").css("overflow", (lock) ? "hidden" : ""); + $this.resize(); + } + + return dialog; + }; + + dialog.showMask = function() { + if (options.mask) + { + editor.find("." + classPrefix + "mask").css(options.maskStyle).css("z-index", editormd.dialogZindex - 1).show(); + } + return dialog; + }; + + dialog.hideMask = function() { + if (options.mask) + { + editor.find("." + classPrefix + "mask").hide(); + } + + return dialog; + }; + + dialog.loading = function(show) { + var loading = dialog.find("." + classPrefix + "dialog-mask"); + loading[(show) ? "show" : "hide"](); + + return dialog; + }; + + dialog.lockScreen(true).showMask(); + + dialog.show().css({ + zIndex : editormd.dialogZindex, + border : (editormd.isIE8) ? "1px solid #ddd" : "", + width : (typeof options.width === "number") ? options.width + "px" : options.width, + height : (typeof options.height === "number") ? options.height + "px" : options.height + }); + + var dialogPosition = function(){ + dialog.css({ + top : ($(window).height() - dialog.height()) / 2 + "px", + left : ($(window).width() - dialog.width()) / 2 + "px" + }); + }; + + dialogPosition(); + + $(window).resize(dialogPosition); + + dialog.children("." + classPrefix + "dialog-close").bind(mouseOrTouch("click", "touchend"), function() { + dialog.hide().lockScreen(false).hideMask(); + }); + + if (typeof options.buttons === "object") + { + var footer = dialog.footer = dialog.find("." + classPrefix + "dialog-footer"); + + for (var key in options.buttons) + { + var btn = options.buttons[key]; + var btnClassName = classPrefix + key + "-btn"; + + footer.append(""); + btn[1] = $.proxy(btn[1], dialog); + footer.children("." + btnClassName).bind(mouseOrTouch("click", "touchend"), btn[1]); + } + } + + if (options.title !== "" && options.drag) + { + var posX, posY; + var dialogHeader = dialog.children("." + classPrefix + "dialog-header"); + + if (!options.mask) { + dialogHeader.bind(mouseOrTouch("click", "touchend"), function(){ + editormd.dialogZindex += 2; + dialog.css("z-index", editormd.dialogZindex); + }); + } + + dialogHeader.mousedown(function(e) { + e = e || window.event; //IE + posX = e.clientX - parseInt(dialog[0].style.left); + posY = e.clientY - parseInt(dialog[0].style.top); + + document.onmousemove = moveAction; + }); + + var userCanSelect = function (obj) { + obj.removeClass(classPrefix + "user-unselect").off("selectstart"); + }; + + var userUnselect = function (obj) { + obj.addClass(classPrefix + "user-unselect").on("selectstart", function(event) { // selectstart for IE + return false; + }); + }; + + var moveAction = function (e) { + e = e || window.event; //IE + + var left, top, nowLeft = parseInt(dialog[0].style.left), nowTop = parseInt(dialog[0].style.top); + + if( nowLeft >= 0 ) { + if( nowLeft + dialog.width() <= $(window).width()) { + left = e.clientX - posX; + } else { + left = $(window).width() - dialog.width(); + document.onmousemove = null; + } + } else { + left = 0; + document.onmousemove = null; + } + + if( nowTop >= 0 ) { + top = e.clientY - posY; + } else { + top = 0; + document.onmousemove = null; + } + + + document.onselectstart = function() { + return false; + }; + + userUnselect($("body")); + userUnselect(dialog); + dialog[0].style.left = left + "px"; + dialog[0].style.top = top + "px"; + }; + + document.onmouseup = function() { + userCanSelect($("body")); + userCanSelect(dialog); + + document.onselectstart = null; + document.onmousemove = null; + }; + + dialogHeader.touchDraggable = function() { + var offset = null; + var start = function(e) { + var orig = e.originalEvent; + var pos = $(this).parent().position(); + + offset = { + x : orig.changedTouches[0].pageX - pos.left, + y : orig.changedTouches[0].pageY - pos.top + }; + }; + + var move = function(e) { + e.preventDefault(); + var orig = e.originalEvent; + + $(this).parent().css({ + top : orig.changedTouches[0].pageY - offset.y, + left : orig.changedTouches[0].pageX - offset.x + }); + }; + + this.bind("touchstart", start).bind("touchmove", move); + }; + + dialogHeader.touchDraggable(); + } + + editormd.dialogZindex += 2; + + return dialog; + }; + + /** + * 鼠标和触摸事件的判断/选择方法 + * MouseEvent or TouchEvent type switch + * + * @param {String} [mouseEventType="click"] 供选择的鼠标事件 + * @param {String} [touchEventType="touchend"] 供选择的触摸事件 + * @returns {String} EventType 返回事件类型名称 + */ + + editormd.mouseOrTouch = function(mouseEventType, touchEventType) { + mouseEventType = mouseEventType || "click"; + touchEventType = touchEventType || "touchend"; + + var eventType = mouseEventType; + + try { + document.createEvent("TouchEvent"); + eventType = touchEventType; + } catch(e) {} + + return eventType; + }; + + /** + * 日期时间的格式化方法 + * Datetime format method + * + * @param {String} [format=""] 日期时间的格式,类似PHP的格式 + * @returns {String} datefmt 返回格式化后的日期时间字符串 + */ + + editormd.dateFormat = function(format) { + format = format || ""; + + var addZero = function(d) { + return (d < 10) ? "0" + d : d; + }; + + var date = new Date(); + var year = date.getFullYear(); + var year2 = year.toString().slice(2, 4); + var month = addZero(date.getMonth() + 1); + var day = addZero(date.getDate()); + var weekDay = date.getDay(); + var hour = addZero(date.getHours()); + var min = addZero(date.getMinutes()); + var second = addZero(date.getSeconds()); + var ms = addZero(date.getMilliseconds()); + var datefmt = ""; + + var ymd = year2 + "-" + month + "-" + day; + var fymd = year + "-" + month + "-" + day; + var hms = hour + ":" + min + ":" + second; + + switch (format) + { + case "UNIX Time" : + datefmt = date.getTime(); + break; + + case "UTC" : + datefmt = date.toUTCString(); + break; + + case "yy" : + datefmt = year2; + break; + + case "year" : + case "yyyy" : + datefmt = year; + break; + + case "month" : + case "mm" : + datefmt = month; + break; + + case "cn-week-day" : + case "cn-wd" : + var cnWeekDays = ["日", "一", "二", "三", "四", "五", "六"]; + datefmt = "星期" + cnWeekDays[weekDay]; + break; + + case "week-day" : + case "wd" : + var weekDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + datefmt = weekDays[weekDay]; + break; + + case "day" : + case "dd" : + datefmt = day; + break; + + case "hour" : + case "hh" : + datefmt = hour; + break; + + case "min" : + case "ii" : + datefmt = min; + break; + + case "second" : + case "ss" : + datefmt = second; + break; + + case "ms" : + datefmt = ms; + break; + + case "yy-mm-dd" : + datefmt = ymd; + break; + + case "yyyy-mm-dd" : + datefmt = fymd; + break; + + case "yyyy-mm-dd h:i:s ms" : + case "full + ms" : + datefmt = fymd + " " + hms + " " + ms; + break; + + case "full" : + case "yyyy-mm-dd h:i:s" : + default: + datefmt = fymd + " " + hms; + break; + } + + return datefmt; + }; + + return editormd; + +})); diff --git a/paicoding-ui/src/main/resources/static/editormd/editormd.amd.min.js b/paicoding-ui/src/main/resources/static/editormd/editormd.amd.min.js new file mode 100644 index 000000000..a5e0e19aa --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/editormd.amd.min.js @@ -0,0 +1,4 @@ +/*! Editor.md v1.5.0 | editormd.amd.min.js | Open source online markdown editor. | MIT License | By: Pandao | https://github.com/pandao/editor.md | 2015-06-09 */ +!function(e){"use strict";if("function"==typeof require&&"object"==typeof exports&&"object"==typeof module)module.exports=e;else if("function"==typeof define)if(define.amd){var t="codemirror/mode/",i="codemirror/addon/",o=["jquery","marked","prettify","katex","raphael","underscore","flowchart","jqueryflowchart","sequenceDiagram","codemirror/lib/codemirror",t+"css/css",t+"sass/sass",t+"shell/shell",t+"sql/sql",t+"clike/clike",t+"php/php",t+"xml/xml",t+"markdown/markdown",t+"javascript/javascript",t+"htmlmixed/htmlmixed",t+"gfm/gfm",t+"http/http",t+"go/go",t+"dart/dart",t+"coffeescript/coffeescript",t+"nginx/nginx",t+"python/python",t+"perl/perl",t+"lua/lua",t+"r/r",t+"ruby/ruby",t+"rst/rst",t+"smartymixed/smartymixed",t+"vb/vb",t+"vbscript/vbscript",t+"velocity/velocity",t+"xquery/xquery",t+"yaml/yaml",t+"erlang/erlang",t+"jade/jade",i+"edit/trailingspace",i+"dialog/dialog",i+"search/searchcursor",i+"search/search",i+"scroll/annotatescrollbar",i+"search/matchesonscrollbar",i+"display/placeholder",i+"edit/closetag",i+"fold/foldcode",i+"fold/foldgutter",i+"fold/indent-fold",i+"fold/brace-fold",i+"fold/xml-fold",i+"fold/markdown-fold",i+"fold/comment-fold",i+"mode/overlay",i+"selection/active-line",i+"edit/closebrackets",i+"display/fullscreen",i+"search/match-highlighter"];define(o,e)}else define(["jquery"],e);else window.editormd=e()}(function(){"function"==typeof define&&define.amd&&(e=arguments[0],marked=arguments[1],prettify=arguments[2],katex=arguments[3],Raphael=arguments[4],_=arguments[5],flowchart=arguments[6],CodeMirror=arguments[9]);var e="undefined"!=typeof jQuery?jQuery:Zepto;if("undefined"!=typeof e){var t=function(e,i){return new t.fn.init(e,i)};t.title=t.$name="Editor.md",t.version="1.5.0",t.homePage="https://pandao.github.io/editor.md/",t.classPrefix="editormd-",t.toolbarModes={full:["undo","redo","|","bold","del","italic","quote","ucwords","uppercase","lowercase","|","h1","h2","h3","h4","h5","h6","|","list-ul","list-ol","hr","|","link","reference-link","image","code","preformatted-text","code-block","table","datetime","emoji","html-entities","pagebreak","|","goto-line","watch","preview","fullscreen","clear","search","|","help","info"],simple:["undo","redo","|","bold","del","italic","quote","uppercase","lowercase","|","h1","h2","h3","h4","h5","h6","|","list-ul","list-ol","hr","|","watch","preview","fullscreen","|","help","info"],mini:["undo","redo","|","watch","preview","|","help","info"]},t.defaults={mode:"gfm",name:"",value:"",theme:"",editorTheme:"default",previewTheme:"",markdown:"",appendMarkdown:"",width:"100%",height:"100%",path:"./lib/",pluginPath:"",delay:300,autoLoadModules:!0,watch:!0,placeholder:"Enjoy Markdown! coding now...",gotoLine:!0,codeFold:!1,autoHeight:!1,autoFocus:!0,autoCloseTags:!0,searchReplace:!0,syncScrolling:!0,readOnly:!1,tabSize:4,indentUnit:4,lineNumbers:!0,lineWrapping:!0,autoCloseBrackets:!0,showTrailingSpace:!0,matchBrackets:!0,indentWithTabs:!0,styleSelectedText:!0,matchWordHighlight:!0,styleActiveLine:!0,dialogLockScreen:!0,dialogShowMask:!0,dialogDraggable:!0,dialogMaskBgColor:"#fff",dialogMaskOpacity:.1,fontSize:"13px",saveHTMLToTextarea:!1,disabledKeyMaps:[],onload:function(){},onresize:function(){},onchange:function(){},onwatch:null,onunwatch:null,onpreviewing:function(){},onpreviewed:function(){},onfullscreen:function(){},onfullscreenExit:function(){},onscroll:function(){},onpreviewscroll:function(){},imageUpload:!1,imageFormats:["jpg","jpeg","gif","png","bmp","webp"],imageUploadURL:"",crossDomainUpload:!1,uploadCallbackURL:"",toc:!0,tocm:!1,tocTitle:"",tocDropdown:!1,tocContainer:"",tocStartLevel:1,htmlDecode:!1,pageBreak:!0,atLink:!0,emailLink:!0,taskList:!1,emoji:!1,tex:!1,flowChart:!1,sequenceDiagram:!1,previewCodeHighlight:!0,toolbar:!0,toolbarAutoFixed:!0,toolbarIcons:"full",toolbarTitles:{},toolbarHandlers:{ucwords:function(){return t.toolbarHandlers.ucwords},lowercase:function(){return t.toolbarHandlers.lowercase}},toolbarCustomIcons:{lowercase:'a',ucwords:'Aa'},toolbarIconsClass:{undo:"fa-undo",redo:"fa-repeat",bold:"fa-bold",del:"fa-strikethrough",italic:"fa-italic",quote:"fa-quote-left",uppercase:"fa-font",h1:t.classPrefix+"bold",h2:t.classPrefix+"bold",h3:t.classPrefix+"bold",h4:t.classPrefix+"bold",h5:t.classPrefix+"bold",h6:t.classPrefix+"bold","list-ul":"fa-list-ul","list-ol":"fa-list-ol",hr:"fa-minus",link:"fa-link","reference-link":"fa-anchor",image:"fa-picture-o",code:"fa-code","preformatted-text":"fa-file-code-o","code-block":"fa-file-code-o",table:"fa-table",datetime:"fa-clock-o",emoji:"fa-smile-o","html-entities":"fa-copyright",pagebreak:"fa-newspaper-o","goto-line":"fa-terminal",watch:"fa-eye-slash",unwatch:"fa-eye",preview:"fa-desktop",search:"fa-search",fullscreen:"fa-arrows-alt",clear:"fa-eraser",help:"fa-question-circle",info:"fa-info-circle"},toolbarIconTexts:{},lang:{name:"zh-cn",description:"开源在线Markdown编辑器
        Open source online Markdown editor.",tocTitle:"目录",toolbar:{undo:"撤销(Ctrl+Z)",redo:"重做(Ctrl+Y)",bold:"粗体",del:"删除线",italic:"斜体",quote:"引用",ucwords:"将每个单词首字母转成大写",uppercase:"将所选转换成大写",lowercase:"将所选转换成小写",h1:"标题1",h2:"标题2",h3:"标题3",h4:"标题4",h5:"标题5",h6:"标题6","list-ul":"无序列表","list-ol":"有序列表",hr:"横线",link:"链接","reference-link":"引用链接",image:"添加图片",code:"行内代码","preformatted-text":"预格式文本 / 代码块(缩进风格)","code-block":"代码块(多语言风格)",table:"添加表格",datetime:"日期时间",emoji:"Emoji表情","html-entities":"HTML实体字符",pagebreak:"插入分页符","goto-line":"跳转到行",watch:"关闭实时预览",unwatch:"开启实时预览",preview:"全窗口预览HTML(按 Shift + ESC还原)",fullscreen:"全屏(按ESC还原)",clear:"清空",search:"搜索",help:"使用帮助",info:"关于"+t.title},buttons:{enter:"确定",cancel:"取消",close:"关闭"},dialog:{link:{title:"添加链接",url:"链接地址",urlTitle:"链接标题",urlEmpty:"错误:请填写链接地址。"},referenceLink:{title:"添加引用链接",name:"引用名称",url:"链接地址",urlId:"链接ID",urlTitle:"链接标题",nameEmpty:"错误:引用链接的名称不能为空。",idEmpty:"错误:请填写引用链接的ID。",urlEmpty:"错误:请填写引用链接的URL地址。"},image:{title:"添加图片",url:"图片地址",link:"图片链接",alt:"图片描述",uploadButton:"本地上传",imageURLEmpty:"错误:图片地址不能为空。",uploadFileEmpty:"错误:上传的图片不能为空。",formatNotAllowed:"错误:只允许上传图片文件,允许上传的图片文件格式有:"},preformattedText:{title:"添加预格式文本或代码块",emptyAlert:"错误:请填写预格式文本或代码的内容。"},codeBlock:{title:"添加代码块",selectLabel:"代码语言:",selectDefaultText:"请选择代码语言",otherLanguage:"其他语言",unselectedLanguageAlert:"错误:请选择代码所属的语言类型。",codeEmptyAlert:"错误:请填写代码内容。"},htmlEntities:{title:"HTML 实体字符"},help:{title:"使用帮助"}}}},t.classNames={tex:t.classPrefix+"tex"},t.dialogZindex=99999,t.$katex=null,t.$marked=null,t.$CodeMirror=null,t.$prettyPrint=null;var i,o;t.prototype=t.fn={state:{watching:!1,loaded:!1,preview:!1,fullscreen:!1},init:function(i,o){o=o||{},"object"==typeof i&&(o=i);var r=this.classPrefix=t.classPrefix,n=this.settings=e.extend(!0,t.defaults,o);i="object"==typeof i?n.id:i;var a=this.editor=e("#"+i);this.id=i,this.lang=n.lang;var s=this.classNames={textarea:{html:r+"html-textarea",markdown:r+"markdown-textarea"}};n.pluginPath=""===n.pluginPath?n.path+"../plugins/":n.pluginPath,this.state.watching=n.watch?!0:!1,a.hasClass("editormd")||a.addClass("editormd"),a.css({width:"number"==typeof n.width?n.width+"px":n.width,height:"number"==typeof n.height?n.height+"px":n.height}),n.autoHeight&&a.css("height","auto");var l=this.markdownTextarea=a.children("textarea");l.length<1&&(a.append(""),l=this.markdownTextarea=a.children("textarea")),l.addClass(s.textarea.markdown).attr("placeholder",n.placeholder),("undefined"==typeof l.attr("name")||""===l.attr("name"))&&l.attr("name",""!==n.name?n.name:i+"-markdown-doc");var c=[n.readOnly?"":'',n.saveHTMLToTextarea?'':"",'
        ','
        ','
        '].join("\n");return a.append(c).addClass(r+"vertical"),""!==n.theme&&a.addClass(r+"theme-"+n.theme),this.mask=a.children("."+r+"mask"),this.containerMask=a.children("."+r+"container-mask"),""!==n.markdown&&l.val(n.markdown),""!==n.appendMarkdown&&l.val(l.val()+n.appendMarkdown),this.htmlTextarea=a.children("."+s.textarea.html),this.preview=a.children("."+r+"preview"),this.previewContainer=this.preview.children("."+r+"preview-container"),""!==n.previewTheme&&this.preview.addClass(r+"preview-theme-"+n.previewTheme),"function"==typeof define&&define.amd&&("undefined"!=typeof katex&&(t.$katex=katex),n.searchReplace&&!n.readOnly&&(t.loadCSS(n.path+"codemirror/addon/dialog/dialog"),t.loadCSS(n.path+"codemirror/addon/search/matchesonscrollbar"))),"function"==typeof define&&define.amd||!n.autoLoadModules?("undefined"!=typeof CodeMirror&&(t.$CodeMirror=CodeMirror),"undefined"!=typeof marked&&(t.$marked=marked),this.setCodeMirror().setToolbar().loadedDisplay()):this.loadQueues(),this},loadQueues:function(){var e=this,i=this.settings,o=i.path,r=function(){return t.isIE8?void e.loadedDisplay():void(i.flowChart||i.sequenceDiagram?t.loadScript(o+"raphael.min",function(){t.loadScript(o+"underscore.min",function(){!i.flowChart&&i.sequenceDiagram?t.loadScript(o+"sequence-diagram.min",function(){e.loadedDisplay()}):i.flowChart&&!i.sequenceDiagram?t.loadScript(o+"flowchart.min",function(){t.loadScript(o+"jquery.flowchart.min",function(){e.loadedDisplay()})}):i.flowChart&&i.sequenceDiagram&&t.loadScript(o+"flowchart.min",function(){t.loadScript(o+"jquery.flowchart.min",function(){t.loadScript(o+"sequence-diagram.min",function(){e.loadedDisplay()})})})})}):e.loadedDisplay())};return t.loadCSS(o+"codemirror/codemirror.min"),i.searchReplace&&!i.readOnly&&(t.loadCSS(o+"codemirror/addon/dialog/dialog"),t.loadCSS(o+"codemirror/addon/search/matchesonscrollbar")),i.codeFold&&t.loadCSS(o+"codemirror/addon/fold/foldgutter"),t.loadScript(o+"codemirror/codemirror.min",function(){t.$CodeMirror=CodeMirror,t.loadScript(o+"codemirror/modes.min",function(){t.loadScript(o+"codemirror/addons.min",function(){return e.setCodeMirror(),"gfm"!==i.mode&&"markdown"!==i.mode?(e.loadedDisplay(),!1):(e.setToolbar(),void t.loadScript(o+"marked.min",function(){t.$marked=marked,i.previewCodeHighlight?t.loadScript(o+"prettify.min",function(){r()}):r()}))})})}),this},setTheme:function(e){var t=this.editor,i=this.settings.theme,o=this.classPrefix+"theme-";return t.removeClass(o+i).addClass(o+e),this.settings.theme=e,this},setEditorTheme:function(e){var i=this.settings;return i.editorTheme=e,"default"!==e&&t.loadCSS(i.path+"codemirror/theme/"+i.editorTheme),this.cm.setOption("theme",e),this},setCodeMirrorTheme:function(e){return this.setEditorTheme(e),this},setPreviewTheme:function(e){var t=this.preview,i=this.settings.previewTheme,o=this.classPrefix+"preview-theme-";return t.removeClass(o+i).addClass(o+e),this.settings.previewTheme=e,this},setCodeMirror:function(){var e=this.settings,i=this.editor;"default"!==e.editorTheme&&t.loadCSS(e.path+"codemirror/theme/"+e.editorTheme);var o={mode:e.mode,theme:e.editorTheme,tabSize:e.tabSize,dragDrop:!1,autofocus:e.autoFocus,autoCloseTags:e.autoCloseTags,readOnly:e.readOnly?"nocursor":!1,indentUnit:e.indentUnit,lineNumbers:e.lineNumbers,lineWrapping:e.lineWrapping,extraKeys:{"Ctrl-Q":function(e){e.foldCode(e.getCursor())}},foldGutter:e.codeFold,gutters:["CodeMirror-linenumbers","CodeMirror-foldgutter"],matchBrackets:e.matchBrackets,indentWithTabs:e.indentWithTabs,styleActiveLine:e.styleActiveLine,styleSelectedText:e.styleSelectedText,autoCloseBrackets:e.autoCloseBrackets,showTrailingSpace:e.showTrailingSpace,highlightSelectionMatches:e.matchWordHighlight?{showToken:"onselected"===e.matchWordHighlight?!1:/\w/}:!1};return this.codeEditor=this.cm=t.$CodeMirror.fromTextArea(this.markdownTextarea[0],o),this.codeMirror=this.cmElement=i.children(".CodeMirror"),""!==e.value&&this.cm.setValue(e.value),this.codeMirror.css({fontSize:e.fontSize,width:e.watch?"50%":"100%"}),e.autoHeight&&(this.codeMirror.css("height","auto"),this.cm.setOption("viewportMargin",1/0)),e.lineNumbers||this.codeMirror.find(".CodeMirror-gutters").css("border-right","none"),this},getCodeMirrorOption:function(e){return this.cm.getOption(e)},setCodeMirrorOption:function(e,t){return this.cm.setOption(e,t),this},addKeyMap:function(e,t){return this.cm.addKeyMap(e,t),this},removeKeyMap:function(e){return this.cm.removeKeyMap(e),this},gotoLine:function(t){var i=this.settings;if(!i.gotoLine)return this;var o=this.cm,r=(this.editor,o.lineCount()),n=this.preview;if("string"==typeof t&&("last"===t&&(t=r),"first"===t&&(t=1)),"number"!=typeof t)return alert("Error: The line number must be an integer."),this;if(t=parseInt(t)-1,t>r)return alert("Error: The line number range 1-"+r),this;o.setCursor({line:t,ch:0});var a=o.getScrollInfo(),s=a.clientHeight,l=o.charCoords({line:t,ch:0},"local");if(o.scrollTo(null,(l.top+l.bottom-s)/2),i.watch){var c=this.codeMirror.find(".CodeMirror-scroll")[0],h=e(c).height(),d=c.scrollTop,u=d/c.scrollHeight;n.scrollTop(0===d?0:d+h>=c.scrollHeight-16?n[0].scrollHeight:n[0].scrollHeight*u)}return o.focus(),this},extend:function(){return"undefined"!=typeof arguments[1]&&("function"==typeof arguments[1]&&(arguments[1]=e.proxy(arguments[1],this)),this[arguments[0]]=arguments[1]),"object"==typeof arguments[0]&&"undefined"==typeof arguments[0].length&&e.extend(!0,this,arguments[0]),this},set:function(t,i){return"undefined"!=typeof i&&"function"==typeof i&&(i=e.proxy(i,this)),this[t]=i,this},config:function(t,i){var o=this.settings;return"object"==typeof t&&(o=e.extend(!0,o,t)),"string"==typeof t&&(o[t]=i),this.settings=o,this.recreate(),this},on:function(t,i){var o=this.settings;return"undefined"!=typeof o["on"+t]&&(o["on"+t]=e.proxy(i,this)),this},off:function(e){var t=this.settings;return"undefined"!=typeof t["on"+e]&&(t["on"+e]=function(){}),this},showToolbar:function(t){var i=this.settings;return i.readOnly?this:(i.toolbar&&(this.toolbar.length<1||""===this.toolbar.find("."+this.classPrefix+"menu").html())&&this.setToolbar(),i.toolbar=!0,this.toolbar.show(),this.resize(),e.proxy(t||function(){},this)(),this)},hideToolbar:function(t){var i=this.settings;return i.toolbar=!1,this.toolbar.hide(),this.resize(),e.proxy(t||function(){},this)(),this},setToolbarAutoFixed:function(t){var i=this.state,o=this.editor,r=this.toolbar,n=this.settings;"undefined"!=typeof t&&(n.toolbarAutoFixed=t);var a=function(){var t=e(window),i=t.scrollTop();return n.toolbarAutoFixed?void r.css(i-o.offset().top>10&&i
          ';i.append(n),r=this.toolbar=i.children("."+o+"toolbar")}if(!e.toolbar)return r.hide(),this;r.show();for(var a="function"==typeof e.toolbarIcons?e.toolbarIcons():"string"==typeof e.toolbarIcons?t.toolbarModes[e.toolbarIcons]:e.toolbarIcons,s=r.find("."+this.classPrefix+"menu"),l="",c=!1,h=0,d=a.length;d>h;h++){var u=a[h];if("||"===u)c=!0;else if("|"===u)l+='
        • |
        • ';else{var f=/h(\d)/.test(u),g=u;"watch"!==u||e.watch||(g="unwatch");var p=e.lang.toolbar[g],m=e.toolbarIconTexts[g],w=e.toolbarIconsClass[g];p="undefined"==typeof p?"":p,m="undefined"==typeof m?"":m,w="undefined"==typeof w?"":w;var v=c?'
        • ':"
        • ";"undefined"!=typeof e.toolbarCustomIcons[u]&&"function"!=typeof e.toolbarCustomIcons[u]?v+=e.toolbarCustomIcons[u]:(v+='',v+=''+(f?u.toUpperCase():""===w?m:"")+"",v+=""),v+="
        • ",l=c?v+l:l+v}}return s.html(l),s.find('[title="Lowercase"]').attr("title",e.lang.toolbar.lowercase),s.find('[title="ucwords"]').attr("title",e.lang.toolbar.ucwords),this.setToolbarHandler(),this.setToolbarAutoFixed(),this},dialogLockScreen:function(){return e.proxy(t.dialogLockScreen,this)(),this},dialogShowMask:function(i){return e.proxy(t.dialogShowMask,this)(i),this},getToolbarHandles:function(e){var i=this.toolbarHandlers=t.toolbarHandlers;return e&&"undefined"!=typeof toolbarIconHandlers[e]?i[e]:i},setToolbarHandler:function(){var i=this,o=this.settings;if(!o.toolbar||o.readOnly)return this;var r=this.toolbar,n=this.cm,a=this.classPrefix,s=this.toolbarIcons=r.find("."+a+"menu > li > a"),l=this.getToolbarHandles();return s.bind(t.mouseOrTouch("click","touchend"),function(t){var r=e(this).children(".fa"),a=r.attr("name"),s=n.getCursor(),c=n.getSelection();return""!==a?(i.activeIcon=r,"undefined"!=typeof l[a]?e.proxy(l[a],i)(n):"undefined"!=typeof o.toolbarHandlers[a]&&e.proxy(o.toolbarHandlers[a],i)(n,r,s,c),"link"!==a&&"reference-link"!==a&&"image"!==a&&"code-block"!==a&&"preformatted-text"!==a&&"watch"!==a&&"preview"!==a&&"search"!==a&&"fullscreen"!==a&&"info"!==a&&n.focus(),!1):void 0}),this},createDialog:function(i){return e.proxy(t.createDialog,this)(i)},createInfoDialog:function(){var e=this,i=this.editor,o=this.classPrefix,r=['
          ','
          ','

          '+t.title+"v"+t.version+"

          ","

          "+this.lang.description+"

          ",'

          '+t.homePage+'

          ','

          Copyright © 2015 Pandao, The MIT License.

          ',"
          ",'',"
          "].join("\n");i.append(r);var n=this.infoDialog=i.children("."+o+"dialog-info");return n.find("."+o+"dialog-close").bind(t.mouseOrTouch("click","touchend"),function(){e.hideInfoDialog()}),n.css("border",t.isIE8?"1px solid #ddd":"").css("z-index",t.dialogZindex).show(),this.infoDialogPosition(),this},infoDialogPosition:function(){var t=this.infoDialog,i=function(){t.css({top:(e(window).height()-t.height())/2+"px",left:(e(window).width()-t.width())/2+"px"})};return i(),e(window).resize(i),this},showInfoDialog:function(){e("html,body").css("overflow-x","hidden");var i=this.editor,o=this.settings,r=this.infoDialog=i.children("."+this.classPrefix+"dialog-info");return r.length<1&&this.createInfoDialog(),this.lockScreen(!0),this.mask.css({opacity:o.dialogMaskOpacity,backgroundColor:o.dialogMaskBgColor}).show(),r.css("z-index",t.dialogZindex).show(),this.infoDialogPosition(),this},hideInfoDialog:function(){return e("html,body").css("overflow-x",""),this.infoDialog.hide(),this.mask.hide(),this.lockScreen(!1),this},lockScreen:function(e){return t.lockScreen(e),this.resize(),this},recreate:function(){var e=this.editor,t=this.settings;return this.codeMirror.remove(),this.setCodeMirror(),t.readOnly||(e.find(".editormd-dialog").length>0&&e.find(".editormd-dialog").remove(),t.toolbar&&(this.getToolbarHandles(),this.setToolbar())),this.loadedDisplay(!0),this},previewCodeHighlight:function(){var e=this.settings,t=this.previewContainer;return e.previewCodeHighlight&&(t.find("pre").addClass("prettyprint linenums"),"undefined"!=typeof prettyPrint&&prettyPrint()),this},katexRender:function(){return null===i?this:(this.previewContainer.find("."+t.classNames.tex).each(function(){var i=e(this);t.$katex.render(i.text(),i[0]),i.find(".katex").css("font-size","1.6em")}),this)},flowChartAndSequenceDiagramRender:function(){var i=this,r=this.settings,n=this.previewContainer;if(t.isIE8)return this;if(r.flowChart){if(null===o)return this;n.find(".flowchart").flowChart()}r.sequenceDiagram&&n.find(".sequence-diagram").sequenceDiagram({theme:"simple"});var a=i.preview,s=i.codeMirror,l=s.find(".CodeMirror-scroll"),c=l.height(),h=l.scrollTop(),d=h/l[0].scrollHeight,u=0;a.find(".markdown-toc-list").each(function(){u+=e(this).height()});var f=a.find(".editormd-toc-menu").height();return f=f?f:0,a.scrollTop(0===h?0:h+c>=l[0].scrollHeight-16?a[0].scrollHeight:(a[0].scrollHeight+u+f)*d),this},registerKeyMaps:function(i){var o=this,r=this.cm,n=this.settings,a=t.toolbarHandlers,s=n.disabledKeyMaps;if(i=i||null){for(var l in i)if(e.inArray(l,s)<0){var c={};c[l]=i[l],r.addKeyMap(i)}}else{for(var h in t.keyMaps){var d=t.keyMaps[h],u="string"==typeof d?e.proxy(a[d],o):e.proxy(d,o);if(e.inArray(h,["F9","F10","F11"])<0&&e.inArray(h,s)<0){var f={};f[h]=u,r.addKeyMap(f)}}e(window).keydown(function(t){var i={120:"F9",121:"F10",122:"F11"};if(e.inArray(i[t.keyCode],s)<0)switch(t.keyCode){case 120:return e.proxy(a.watch,o)(),!1;case 121:return e.proxy(a.preview,o)(),!1;case 122:return e.proxy(a.fullscreen,o)(),!1}})}return this},bindScrollEvent:function(){var i=this,o=this.preview,r=this.settings,n=this.codeMirror,a=t.mouseOrTouch;if(!r.syncScrolling)return this;var s=function(){n.find(".CodeMirror-scroll").bind(a("scroll","touchmove"),function(t){var n=e(this).height(),a=e(this).scrollTop(),s=a/e(this)[0].scrollHeight,l=0;o.find(".markdown-toc-list").each(function(){l+=e(this).height()});var c=o.find(".editormd-toc-menu").height();c=c?c:0,o.scrollTop(0===a?0:a+n>=e(this)[0].scrollHeight-16?o[0].scrollHeight:(o[0].scrollHeight+l+c)*s),e.proxy(r.onscroll,i)(t)})},l=function(){n.find(".CodeMirror-scroll").unbind(a("scroll","touchmove"))},c=function(){o.bind(a("scroll","touchmove"),function(t){var o=e(this).height(),a=e(this).scrollTop(),s=a/e(this)[0].scrollHeight,l=n.find(".CodeMirror-scroll");l.scrollTop(0===a?0:a+o>=e(this)[0].scrollHeight?l[0].scrollHeight:l[0].scrollHeight*s),e.proxy(r.onpreviewscroll,i)(t)})},h=function(){o.unbind(a("scroll","touchmove"))};return n.bind({mouseover:s,mouseout:l,touchstart:s,touchend:l}),"single"===r.syncScrolling?this:(o.bind({mouseover:c,mouseout:h,touchstart:c,touchend:h}),this)},bindChangeEvent:function(){var e=this,t=this.cm,o=this.settings;return o.syncScrolling?(t.on("change",function(t,r){o.watch&&e.previewContainer.css("padding",o.autoHeight?"20px 20px 50px 40px":"20px"),i=setTimeout(function(){clearTimeout(i),e.save(),i=null},o.delay)}),this):this},loadedDisplay:function(t){t=t||!1;var i=this,o=this.editor,r=this.preview,n=this.settings;return this.containerMask.hide(),this.save(),n.watch&&r.show(),o.data("oldWidth",o.width()).data("oldHeight",o.height()),this.resize(),this.registerKeyMaps(),e(window).resize(function(){i.resize()}),this.bindScrollEvent().bindChangeEvent(),t||e.proxy(n.onload,this)(),this.state.loaded=!0,this},width:function(e){return this.editor.css("width","number"==typeof e?e+"px":e),this.resize(),this},height:function(e){return this.editor.css("height","number"==typeof e?e+"px":e),this.resize(),this},resize:function(t,i){t=t||null,i=i||null;var o=this.state,r=this.editor,n=this.preview,a=this.toolbar,s=this.settings,l=this.codeMirror;if(t&&r.css("width","number"==typeof t?t+"px":t),!s.autoHeight||o.fullscreen||o.preview?(i&&r.css("height","number"==typeof i?i+"px":i),o.fullscreen&&r.height(e(window).height()),s.toolbar&&!s.readOnly?l.css("margin-top",a.height()+1).height(r.height()-a.height()):l.css("margin-top",0).height(r.height())):(r.css("height","auto"),l.css("height","auto")),s.watch)if(l.width(r.width()/2),n.width(o.preview?r.width():r.width()/2),this.previewContainer.css("padding",s.autoHeight?"20px 20px 50px 40px":"20px"),s.toolbar&&!s.readOnly?n.css("top",a.height()+1):n.css("top",0),!s.autoHeight||o.fullscreen||o.preview){var c=s.toolbar&&!s.readOnly?r.height()-a.height():r.height();n.height(c)}else n.height("");else l.width(r.width()),n.hide();return o.loaded&&e.proxy(s.onresize,this)(),this},save:function(){if(null===i)return this;var r=this,n=this.state,a=this.settings,s=this.cm,l=s.getValue(),c=this.previewContainer;if("gfm"!==a.mode&&"markdown"!==a.mode)return this.markdownTextarea.val(l),this;var h=t.$marked,d=this.markdownToC=[],u=this.markedRendererOptions={toc:a.toc,tocm:a.tocm,tocStartLevel:a.tocStartLevel,pageBreak:a.pageBreak,taskList:a.taskList,emoji:a.emoji,tex:a.tex,atLink:a.atLink,emailLink:a.emailLink,flowChart:a.flowChart,sequenceDiagram:a.sequenceDiagram,previewCodeHighlight:a.previewCodeHighlight},f=this.markedOptions={renderer:t.markedRenderer(d,u),gfm:!0,tables:!0,breaks:!0,pedantic:!1,sanitize:a.htmlDecode?!1:!0,smartLists:!0,smartypants:!0};h.setOptions(f);var g=t.$marked(l,f);if(g=t.filterHTMLTags(g,a.htmlDecode),this.markdownTextarea.text(l),s.save(),a.saveHTMLToTextarea&&this.htmlTextarea.text(g),a.watch||!a.watch&&n.preview){if(c.html(g),this.previewCodeHighlight(),a.toc){var p=""===a.tocContainer?c:e(a.tocContainer),m=p.find("."+this.classPrefix+"toc-menu");p.attr("previewContainer",""===a.tocContainer?"true":"false"),""!==a.tocContainer&&m.length>0&&m.remove(),t.markdownToCRenderer(d,p,a.tocDropdown,a.tocStartLevel),(a.tocDropdown||p.find("."+this.classPrefix+"toc-menu").length>0)&&t.tocDropdownMenu(p,""!==a.tocTitle?a.tocTitle:this.lang.tocTitle),""!==a.tocContainer&&c.find(".markdown-toc").css("border","none")}a.tex&&(!t.kaTeXLoaded&&a.autoLoadModules?t.loadKaTeX(function(){t.$katex=katex,t.kaTeXLoaded=!0,r.katexRender()}):(t.$katex=katex,this.katexRender())),(a.flowChart||a.sequenceDiagram)&&(o=setTimeout(function(){clearTimeout(o),r.flowChartAndSequenceDiagramRender(),o=null},10)),n.loaded&&e.proxy(a.onchange,this)()}return this},focus:function(){return this.cm.focus(),this},setCursor:function(e){return this.cm.setCursor(e),this},getCursor:function(){return this.cm.getCursor()},setSelection:function(e,t){return this.cm.setSelection(e,t),this},getSelection:function(){return this.cm.getSelection()},setSelections:function(e){return this.cm.setSelections(e),this},getSelections:function(){return this.cm.getSelections()},replaceSelection:function(e){return this.cm.replaceSelection(e),this},insertValue:function(e){return this.replaceSelection(e),this},appendMarkdown:function(e){var t=(this.settings,this.cm);return t.setValue(t.getValue()+e),this},setMarkdown:function(e){return this.cm.setValue(e||this.settings.markdown),this},getMarkdown:function(){return this.cm.getValue()},getValue:function(){return this.cm.getValue()},setValue:function(e){return this.cm.setValue(e),this},clear:function(){return this.cm.setValue(""),this},getHTML:function(){return this.settings.saveHTMLToTextarea?this.htmlTextarea.val():(alert("Error: settings.saveHTMLToTextarea == false"),!1)},getTextareaSavedHTML:function(){return this.getHTML()},getPreviewedHTML:function(){return this.settings.watch?this.previewContainer.html():(alert("Error: settings.watch == false"),!1)},watch:function(t){var o=this.settings;if(e.inArray(o.mode,["gfm","markdown"])<0)return this;if(this.state.watching=o.watch=!0,this.preview.show(),this.toolbar){var r=o.toolbarIconsClass.watch,n=o.toolbarIconsClass.unwatch,a=this.toolbar.find(".fa[name=watch]");a.parent().attr("title",o.lang.toolbar.watch),a.removeClass(n).addClass(r)}return this.codeMirror.css("border-right","1px solid #ddd").width(this.editor.width()/2),i=0,this.save().resize(),o.onwatch||(o.onwatch=t||function(){}),e.proxy(o.onwatch,this)(),this},unwatch:function(t){var i=this.settings;if(this.state.watching=i.watch=!1,this.preview.hide(),this.toolbar){var o=i.toolbarIconsClass.watch,r=i.toolbarIconsClass.unwatch,n=this.toolbar.find(".fa[name=watch]");n.parent().attr("title",i.lang.toolbar.unwatch),n.removeClass(o).addClass(r)}return this.codeMirror.css("border-right","none").width(this.editor.width()),this.resize(),i.onunwatch||(i.onunwatch=t||function(){}),e.proxy(i.onunwatch,this)(),this},show:function(t){t=t||function(){};var i=this;return this.editor.show(0,function(){e.proxy(t,i)()}),this},hide:function(t){t=t||function(){};var i=this;return this.editor.hide(0,function(){e.proxy(t,i)()}),this},previewing:function(){var i=this,o=this.editor,r=this.preview,n=this.toolbar,a=this.settings,s=this.codeMirror,l=this.previewContainer;if(e.inArray(a.mode,["gfm","markdown"])<0)return this;a.toolbar&&n&&(n.toggle(),n.find(".fa[name=preview]").toggleClass("active")),s.toggle();var c=function(e){e.shiftKey&&27===e.keyCode&&i.previewed()};"none"===s.css("display")?(this.state.preview=!0,this.state.fullscreen&&r.css("background","#fff"),o.find("."+this.classPrefix+"preview-close-btn").show().bind(t.mouseOrTouch("click","touchend"),function(){i.previewed()}),a.watch?l.css("padding",""):this.save(),l.addClass(this.classPrefix+"preview-active"),r.show().css({position:"",top:0,width:o.width(),height:a.autoHeight&&!this.state.fullscreen?"auto":o.height()}),this.state.loaded&&e.proxy(a.onpreviewing,this)(),e(window).bind("keyup",c)):(e(window).unbind("keyup",c),this.previewed())},previewed:function(){var i=this.editor,o=this.preview,r=this.toolbar,n=this.settings,a=this.previewContainer,s=i.find("."+this.classPrefix+"preview-close-btn");return this.state.preview=!1,this.codeMirror.show(),n.toolbar&&r.show(),o[n.watch?"show":"hide"](),s.hide().unbind(t.mouseOrTouch("click","touchend")),a.removeClass(this.classPrefix+"preview-active"),n.watch&&a.css("padding","20px"),o.css({background:null,position:"absolute",width:i.width()/2,height:n.autoHeight&&!this.state.fullscreen?"auto":i.height()-r.height(),top:n.toolbar?r.height():0}),this.state.loaded&&e.proxy(n.onpreviewed,this)(),this},fullscreen:function(){var t=this,i=this.state,o=this.editor,r=(this.preview,this.toolbar),n=this.settings,a=this.classPrefix+"fullscreen";r&&r.find(".fa[name=fullscreen]").parent().toggleClass("active");var s=function(e){e.shiftKey||27!==e.keyCode||i.fullscreen&&t.fullscreenExit()};return o.hasClass(a)?(e(window).unbind("keyup",s),this.fullscreenExit()):(i.fullscreen=!0,e("html,body").css("overflow","hidden"),o.css({width:e(window).width(),height:e(window).height()}).addClass(a),this.resize(),e.proxy(n.onfullscreen,this)(),e(window).bind("keyup",s)),this},fullscreenExit:function(){var t=this.editor,i=this.settings,o=this.toolbar,r=this.classPrefix+"fullscreen";return this.state.fullscreen=!1,o&&o.find(".fa[name=fullscreen]").parent().removeClass("active"),e("html,body").css("overflow",""),t.css({width:t.data("oldWidth"),height:t.data("oldHeight")}).removeClass(r),this.resize(),e.proxy(i.onfullscreenExit,this)(),this},executePlugin:function(i,o){var r=this,n=this.cm,a=this.settings;return o=a.pluginPath+o,"function"==typeof define?"undefined"==typeof this[i]?(alert("Error: "+i+" plugin is not found, you are not load this plugin."),this):(this[i](n),this):(e.inArray(o,t.loadFiles.plugin)<0?t.loadPlugin(o,function(){t.loadPlugins[i]=r[i],r[i](n)}):e.proxy(t.loadPlugins[i],this)(n),this)},search:function(e){var t=this.settings;return t.searchReplace?(t.readOnly||this.cm.execCommand(e||"find"),this):(alert("Error: settings.searchReplace == false"),this)},searchReplace:function(){return this.search("replace"),this},searchReplaceAll:function(){return this.search("replaceAll"),this}},t.fn.init.prototype=t.fn,t.dialogLockScreen=function(){var t=this.settings||{dialogLockScreen:!0};t.dialogLockScreen&&(e("html,body").css("overflow","hidden"),this.resize())},t.dialogShowMask=function(t){var i=this.editor,o=this.settings||{dialogShowMask:!0};t.css({top:(e(window).height()-t.height())/2+"px",left:(e(window).width()-t.width())/2+"px"}),o.dialogShowMask&&i.children("."+this.classPrefix+"mask").css("z-index",parseInt(t.css("z-index"))-1).show()},t.toolbarHandlers={undo:function(){this.cm.undo()},redo:function(){this.cm.redo()},bold:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection(); + +e.replaceSelection("**"+i+"**"),""===i&&e.setCursor(t.line,t.ch+2)},del:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();e.replaceSelection("~~"+i+"~~"),""===i&&e.setCursor(t.line,t.ch+2)},italic:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();e.replaceSelection("*"+i+"*"),""===i&&e.setCursor(t.line,t.ch+1)},quote:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();0!==t.ch?(e.setCursor(t.line,0),e.replaceSelection("> "+i),e.setCursor(t.line,t.ch+2)):e.replaceSelection("> "+i)},ucfirst:function(){var e=this.cm,i=e.getSelection(),o=e.listSelections();e.replaceSelection(t.firstUpperCase(i)),e.setSelections(o)},ucwords:function(){var e=this.cm,i=e.getSelection(),o=e.listSelections();e.replaceSelection(t.wordsFirstUpperCase(i)),e.setSelections(o)},uppercase:function(){var e=this.cm,t=e.getSelection(),i=e.listSelections();e.replaceSelection(t.toUpperCase()),e.setSelections(i)},lowercase:function(){var e=this.cm,t=(e.getCursor(),e.getSelection()),i=e.listSelections();e.replaceSelection(t.toLowerCase()),e.setSelections(i)},h1:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();0!==t.ch?(e.setCursor(t.line,0),e.replaceSelection("# "+i),e.setCursor(t.line,t.ch+2)):e.replaceSelection("# "+i)},h2:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();0!==t.ch?(e.setCursor(t.line,0),e.replaceSelection("## "+i),e.setCursor(t.line,t.ch+3)):e.replaceSelection("## "+i)},h3:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();0!==t.ch?(e.setCursor(t.line,0),e.replaceSelection("### "+i),e.setCursor(t.line,t.ch+4)):e.replaceSelection("### "+i)},h4:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();0!==t.ch?(e.setCursor(t.line,0),e.replaceSelection("#### "+i),e.setCursor(t.line,t.ch+5)):e.replaceSelection("#### "+i)},h5:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();0!==t.ch?(e.setCursor(t.line,0),e.replaceSelection("##### "+i),e.setCursor(t.line,t.ch+6)):e.replaceSelection("##### "+i)},h6:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();0!==t.ch?(e.setCursor(t.line,0),e.replaceSelection("###### "+i),e.setCursor(t.line,t.ch+7)):e.replaceSelection("###### "+i)},"list-ul":function(){var e=this.cm,t=(e.getCursor(),e.getSelection());if(""===t)e.replaceSelection("- "+t);else{for(var i=t.split("\n"),o=0,r=i.length;r>o;o++)i[o]=""===i[o]?"":"- "+i[o];e.replaceSelection(i.join("\n"))}},"list-ol":function(){var e=this.cm,t=(e.getCursor(),e.getSelection());if(""===t)e.replaceSelection("1. "+t);else{for(var i=t.split("\n"),o=0,r=i.length;r>o;o++)i[o]=""===i[o]?"":o+1+". "+i[o];e.replaceSelection(i.join("\n"))}},hr:function(){{var e=this.cm,t=e.getCursor();e.getSelection()}e.replaceSelection((0!==t.ch?"\n\n":"\n")+"------------\n\n")},tex:function(){if(!this.settings.tex)return alert("settings.tex === false"),this;var e=this.cm,t=e.getCursor(),i=e.getSelection();e.replaceSelection("$$"+i+"$$"),""===i&&e.setCursor(t.line,t.ch+2)},link:function(){this.executePlugin("linkDialog","link-dialog/link-dialog")},"reference-link":function(){this.executePlugin("referenceLinkDialog","reference-link-dialog/reference-link-dialog")},pagebreak:function(){if(!this.settings.pageBreak)return alert("settings.pageBreak === false"),this;{var e=this.cm;e.getSelection()}e.replaceSelection("\r\n[========]\r\n")},image:function(){this.executePlugin("imageDialog","image-dialog/image-dialog")},code:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();e.replaceSelection("`"+i+"`"),""===i&&e.setCursor(t.line,t.ch+1)},"code-block":function(){this.executePlugin("codeBlockDialog","code-block-dialog/code-block-dialog")},"preformatted-text":function(){this.executePlugin("preformattedTextDialog","preformatted-text-dialog/preformatted-text-dialog")},table:function(){this.executePlugin("tableDialog","table-dialog/table-dialog")},datetime:function(){var e=this.cm,i=(e.getSelection(),new Date,this.settings.lang.name),o=t.dateFormat()+" "+t.dateFormat("zh-cn"===i||"zh-tw"===i?"cn-week-day":"week-day");e.replaceSelection(o)},emoji:function(){this.executePlugin("emojiDialog","emoji-dialog/emoji-dialog")},"html-entities":function(){this.executePlugin("htmlEntitiesDialog","html-entities-dialog/html-entities-dialog")},"goto-line":function(){this.executePlugin("gotoLineDialog","goto-line-dialog/goto-line-dialog")},watch:function(){this[this.settings.watch?"unwatch":"watch"]()},preview:function(){this.previewing()},fullscreen:function(){this.fullscreen()},clear:function(){this.clear()},search:function(){this.search()},help:function(){this.executePlugin("helpDialog","help-dialog/help-dialog")},info:function(){this.showInfoDialog()}},t.keyMaps={"Ctrl-1":"h1","Ctrl-2":"h2","Ctrl-3":"h3","Ctrl-4":"h4","Ctrl-5":"h5","Ctrl-6":"h6","Ctrl-B":"bold","Ctrl-D":"datetime","Ctrl-E":function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();return this.settings.emoji?(e.replaceSelection(":"+i+":"),void(""===i&&e.setCursor(t.line,t.ch+1))):void alert("Error: settings.emoji == false")},"Ctrl-Alt-G":"goto-line","Ctrl-H":"hr","Ctrl-I":"italic","Ctrl-K":"code","Ctrl-L":function(){var e=this.cm,t=e.getCursor(),i=e.getSelection(),o=""===i?"":' "'+i+'"';e.replaceSelection("["+i+"]("+o+")"),""===i&&e.setCursor(t.line,t.ch+1)},"Ctrl-U":"list-ul","Shift-Ctrl-A":function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();return this.settings.atLink?(e.replaceSelection("@"+i),void(""===i&&e.setCursor(t.line,t.ch+1))):void alert("Error: settings.atLink == false")},"Shift-Ctrl-C":"code","Shift-Ctrl-Q":"quote","Shift-Ctrl-S":"del","Shift-Ctrl-K":"tex","Shift-Alt-C":function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();e.replaceSelection(["```",i,"```"].join("\n")),""===i&&e.setCursor(t.line,t.ch+3)},"Shift-Ctrl-Alt-C":"code-block","Shift-Ctrl-H":"html-entities","Shift-Alt-H":"help","Shift-Ctrl-E":"emoji","Shift-Ctrl-U":"uppercase","Shift-Alt-U":"ucwords","Shift-Ctrl-Alt-U":"ucfirst","Shift-Alt-L":"lowercase","Shift-Ctrl-I":function(){var e=this.cm,t=e.getCursor(),i=e.getSelection(),o=""===i?"":' "'+i+'"';e.replaceSelection("!["+i+"]("+o+")"),""===i&&e.setCursor(t.line,t.ch+4)},"Shift-Ctrl-Alt-I":"image","Shift-Ctrl-L":"link","Shift-Ctrl-O":"list-ol","Shift-Ctrl-P":"preformatted-text","Shift-Ctrl-T":"table","Shift-Alt-P":"pagebreak",F9:"watch",F10:"preview",F11:"fullscreen"};var r=function(e){return String.prototype.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")};t.trim=r;var n=function(e){return e.toLowerCase().replace(/\b(\w)|\s(\w)/g,function(e){return e.toUpperCase()})};t.ucwords=t.wordsFirstUpperCase=n;var a=function(e){return e.toLowerCase().replace(/\b(\w)/,function(e){return e.toUpperCase()})};return t.firstUpperCase=t.ucfirst=a,t.urls={atLinkBase:"https://github.com/"},t.regexs={atLink:/@(\w+)/g,email:/(\w+)@(\w+)\.(\w+)\.?(\w+)?/g,emailLink:/(mailto:)?([\w\.\_]+)@(\w+)\.(\w+)\.?(\w+)?/g,emoji:/:([\w\+-]+):/g,emojiDatetime:/(\d{2}:\d{2}:\d{2})/g,twemoji:/:(tw-([\w]+)-?(\w+)?):/g,fontAwesome:/:(fa-([\w]+)(-(\w+)){0,}):/g,editormdLogo:/:(editormd-logo-?(\w+)?):/g,pageBreak:/^\[[=]{8,}\]$/},t.emoji={path:"http://www.emoji-cheat-sheet.com/graphics/emojis/",ext:".png"},t.twemoji={path:"http://twemoji.maxcdn.com/36x36/",ext:".png"},t.markedRenderer=function(i,o){var n={toc:!0,tocm:!1,tocStartLevel:1,pageBreak:!0,atLink:!0,emailLink:!0,taskList:!1,emoji:!1,tex:!1,flowChart:!1,sequenceDiagram:!1},a=e.extend(n,o||{}),s=t.$marked,l=new s.Renderer;i=i||[];var c=t.regexs,h=c.atLink,d=c.emoji,u=c.email,f=c.emailLink,g=c.twemoji,p=c.fontAwesome,m=c.editormdLogo,w=c.pageBreak;return l.emoji=function(e){e=e.replace(t.regexs.emojiDatetime,function(e){return e.replace(/:/g,":")});var i=e.match(d);if(!i||!a.emoji)return e;for(var o=0,r=i.length;r>o;o++)":+1:"===i[o]&&(i[o]=":\\+1:"),e=e.replace(new RegExp(i[o]),function(e,i){var o=e.match(p),r=e.replace(/:/g,"");if(o)for(var n=0,a=o.length;a>n;n++){var s=o[n].replace(/:/g,"");return''}else{var l=e.match(m),c=e.match(g);if(l)for(var h=0,d=l.length;d>h;h++){var u=l[h].replace(/:/g,"");return''}else{if(!c){var f="+1"===r?"plus1":r;return f="black_large_square"===f?"black_square":f,f="moon"===f?"waxing_gibbous_moon":f,':'+r+':'}for(var w=0,v=c.length;v>w;w++){var k=c[w].replace(/:/g,"").replace("tw-","");return'twemoji-'+k+''}}}});return e},l.atLink=function(i){return h.test(i)?(a.atLink&&(i=i.replace(u,function(e,t,i,o){return e.replace(/@/g,"_#_@_#_")}),i=i.replace(h,function(e,i){return''+e+""}).replace(/_#_@_#_/g,"@")),a.emailLink&&(i=i.replace(f,function(t,i,o,r,n){return!i&&e.inArray(n,"jpg|jpeg|png|gif|webp|ico|icon|pdf".split("|"))<0?''+t+"":t})),i):i},l.link=function(e,t,i){if(this.options.sanitize){try{var o=decodeURIComponent(unescape(e)).replace(/[^\w:]/g,"").toLowerCase()}catch(r){return""}if(0===o.indexOf("javascript:"))return""}var n=''+i.replace(/@/g,"@")+""):(t&&(n+=' title="'+t+'"'),n+=">"+i+"")},l.heading=function(e,t,o){var n=e,a=/\s*\]*)\>(.*)\<\/a\>\s*/;if(a.test(e)){var s=[];e=e.split(/\]+)\>([^\>]*)\<\/a\>/);for(var l=0,c=e.length;c>l;l++)s.push(e[l].replace(/\s*href\=\"(.*)\"\s*/g,""));e=s.join(" ")}e=r(e);var h=e.toLowerCase().replace(/[^\w]+/g,"-"),d={text:e,level:t,slug:h},u=/^[\u4e00-\u9fa5]+$/.test(e),f=u?escape(e).replace(/\%/g,""):e.toLowerCase().replace(/[^\w]+/g,"-");i.push(d);var g="';return g+='',g+='',g+=this.atLink(a?this.emoji(n):this.emoji(e)),g+=""},l.pageBreak=function(e){return w.test(e)&&a.pageBreak&&(e='
          '),e},l.paragraph=function(e){var i=/\$\$(.*)\$\$/g.test(e),o=/^\$\$(.*)\$\$$/.test(e),r=o?' class="'+t.classNames.tex+'"':"",n=a.tocm?/^(\[TOC\]|\[TOCM\])$/.test(e):/^\[TOC\]$/.test(e),s=/^\[TOCM\]$/.test(e);e=!o&&i?e.replace(/(\$\$([^\$]*)\$\$)+/g,function(e,i){return''+i.replace(/\$/g,"")+""}):o?e.replace(/\$/g,""):e;var l='
          '+e+"
          ";return n?s?'
          '+l+"

          ":l:w.test(e)?this.pageBreak(e):""+this.atLink(this.emoji(e))+"

          \n"},l.code=function(e,i,o){return"seq"===i||"sequence"===i?'
          '+e+"
          ":"flow"===i?'
          '+e+"
          ":"math"===i||"latex"===i||"katex"===i?'

          '+e+"

          ":s.Renderer.prototype.code.apply(this,arguments)},l.tablecell=function(e,t){var i=t.header?"th":"td",o=t.align?"<"+i+' style="text-align:'+t.align+'">':"<"+i+">";return o+this.atLink(this.emoji(e))+"\n"},l.listitem=function(e){return a.taskList&&/^\s*\[[x\s]\]\s*/.test(e)?(e=e.replace(/^\s*\[\s\]\s*/,' ').replace(/^\s*\[x\]\s*/,' '),'
        • '+this.atLink(this.emoji(e))+"
        • "):"
        • "+this.atLink(this.emoji(e))+"
        • "},l},t.markdownToCRenderer=function(e,t,i,o){var r="",n=0,a=this.classPrefix;o=o||1;for(var s=0,l=e.length;l>s;s++){var c=e[s].text,h=e[s].level;o>h||(r+=h>n?"":n>h?new Array(n-h+2).join("
      • "):"",r+='
      • '+c+"
          ",n=h)}var d=t.find(".markdown-toc");if(d.length<1&&"false"===t.attr("previewContainer")){var u='
          ';u=i?'
          '+u+"
          ":u,t.html(u),d=t.find(".markdown-toc")}return i&&d.wrap('

          '),d.html('
            ').children(".markdown-toc-list").html(r.replace(/\r?\n?\\<\/ul\>/g,"")),d},t.tocDropdownMenu=function(t,i){i=i||"Table of Contents";var o=400,r=t.find("."+this.classPrefix+"toc-menu");return r.each(function(){var t=e(this),r=t.children(".markdown-toc"),n='',a=''+n+i+"",s=r.children("ul"),l=s.find("li");r.append(a),l.first().before("
          • "+i+" "+n+"

          • "),t.mouseover(function(){s.show(),l.each(function(){var t=e(this),i=t.children("ul");if(""===i.html()&&i.remove(),i.length>0&&""!==i.html()){var r=t.children("a").first();r.children(".fa").length<1&&r.append(e(n).css({"float":"right",paddingTop:"4px"}))}t.mouseover(function(){i.css("z-index",o).show(),o+=1}).mouseleave(function(){i.hide()})})}).mouseleave(function(){s.hide()})}),r},t.filterHTMLTags=function(t,i){if("string"!=typeof t&&(t=new String(t)),"string"!=typeof i)return t;for(var o=i.split("|"),r=o[0].split(","),n=o[1],a=0,s=r.length;s>a;a++){var l=r[a];t=t.replace(new RegExp("]*)>([^>]*)","igm"),"")}if("undefined"!=typeof n){var c=/\<(\w+)\s*([^\>]*)\>([^\>]*)\<\/(\w+)\>/gi;t="*"===n?t.replace(c,function(e,t,i,o,r){return"<"+t+">"+o+""}):"on*"===n?t.replace(c,function(t,i,o,r,n){var a=e("<"+i+">"+r+""),s=e(t)[0].attributes,l={};e.each(s,function(e,t){'"'!==t.nodeName&&(l[t.nodeName]=t.nodeValue)}),e.each(l,function(e){0===e.indexOf("on")&&delete l[e]}),a.attr(l);var c="undefined"!=typeof a[1]?e(a[1]).text():"";return a[0].outerHTML+c}):t.replace(c,function(t,i,o,r){var a=n.split(","),s=e(t);return s.html(r),e.each(a,function(e){s.attr(a[e],null)}),s[0].outerHTML})}return t},t.markdownToHTML=function(i,o){var r={gfm:!0,toc:!0,tocm:!1,tocStartLevel:1,tocTitle:"目录",tocDropdown:!1,tocContainer:"",markdown:"",markdownSourceCode:!1,htmlDecode:!1,autoLoadKaTeX:!0,pageBreak:!0,atLink:!0,emailLink:!0,tex:!1,taskList:!1,emoji:!1,flowChart:!1,sequenceDiagram:!1,previewCodeHighlight:!0};t.$marked=marked;var n=e("#"+i),a=n.settings=e.extend(!0,r,o||{}),s=n.find("textarea");s.length<1&&(n.append(""),s=n.find("textarea"));var l=""===a.markdown?s.val():a.markdown,c=[],h={toc:a.toc,tocm:a.tocm,tocStartLevel:a.tocStartLevel,taskList:a.taskList,emoji:a.emoji,tex:a.tex,pageBreak:a.pageBreak,atLink:a.atLink,emailLink:a.emailLink,flowChart:a.flowChart,sequenceDiagram:a.sequenceDiagram,previewCodeHighlight:a.previewCodeHighlight},d={renderer:t.markedRenderer(c,h),gfm:a.gfm,tables:!0,breaks:!0,pedantic:!1,sanitize:a.htmlDecode?!1:!0,smartLists:!0,smartypants:!0};l=new String(l);var u=marked(l,d);u=t.filterHTMLTags(u,a.htmlDecode),a.markdownSourceCode?s.text(l):s.remove(),n.addClass("markdown-body "+this.classPrefix+"html-preview").append(u);var f=""!==a.tocContainer?e(a.tocContainer):n;if(""!==a.tocContainer&&f.attr("previewContainer",!1),a.toc&&(n.tocContainer=this.markdownToCRenderer(c,f,a.tocDropdown,a.tocStartLevel),(a.tocDropdown||n.find("."+this.classPrefix+"toc-menu").length>0)&&this.tocDropdownMenu(n,a.tocTitle),""!==a.tocContainer&&n.find(".editormd-toc-menu, .editormd-markdown-toc").remove()),a.previewCodeHighlight&&(n.find("pre").addClass("prettyprint linenums"),prettyPrint()),t.isIE8||(a.flowChart&&n.find(".flowchart").flowChart(),a.sequenceDiagram&&n.find(".sequence-diagram").sequenceDiagram({theme:"simple"})),a.tex){var g=function(){n.find("."+t.classNames.tex).each(function(){var t=e(this);katex.render(t.html().replace(/</g,"<").replace(/>/g,">"),t[0]),t.find(".katex").css("font-size","1.6em")})};!a.autoLoadKaTeX||t.$katex||t.kaTeXLoaded?g():this.loadKaTeX(function(){t.$katex=katex,t.kaTeXLoaded=!0,g()})}return n.getMarkdown=function(){return s.val()},n},t.themes=["default","dark"],t.previewThemes=["default","dark"],t.editorThemes=["default","3024-day","3024-night","ambiance","ambiance-mobile","base16-dark","base16-light","blackboard","cobalt","eclipse","elegant","erlang-dark","lesser-dark","mbo","mdn-like","midnight","monokai","neat","neo","night","paraiso-dark","paraiso-light","pastel-on-dark","rubyblue","solarized","the-matrix","tomorrow-night-eighties","twilight","vibrant-ink","xq-dark","xq-light"],t.loadPlugins={},t.loadFiles={js:[],css:[],plugin:[]},t.loadPlugin=function(e,i,o){i=i||function(){},this.loadScript(e,function(){t.loadFiles.plugin.push(e),i()},o)},t.loadCSS=function(e,i,o){o=o||"head",i=i||function(){};var r=document.createElement("link");r.type="text/css",r.rel="stylesheet",r.onload=r.onreadystatechange=function(){t.loadFiles.css.push(e),i()},r.href=e+".css","head"===o?document.getElementsByTagName("head")[0].appendChild(r):document.body.appendChild(r)},t.isIE="Microsoft Internet Explorer"==navigator.appName,t.isIE8=t.isIE&&"8."==navigator.appVersion.match(/8./i),t.loadScript=function(e,i,o){o=o||"head",i=i||function(){};var r=null;r=document.createElement("script"),r.id=e.replace(/[\./]+/g,"-"),r.type="text/javascript",r.src=e+".js",t.isIE8?r.onreadystatechange=function(){r.readyState&&("loaded"===r.readyState||"complete"===r.readyState)&&(r.onreadystatechange=null,t.loadFiles.js.push(e),i())}:r.onload=function(){t.loadFiles.js.push(e),i()},"head"===o?document.getElementsByTagName("head")[0].appendChild(r):document.body.appendChild(r)},t.katexURL={css:"//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min",js:"//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min"},t.kaTeXLoaded=!1,t.loadKaTeX=function(e){t.loadCSS(t.katexURL.css,function(){t.loadScript(t.katexURL.js,e||function(){})})},t.lockScreen=function(t){e("html,body").css("overflow",t?"hidden":"")},t.createDialog=function(i){var o={name:"",width:420,height:240,title:"",drag:!0,closed:!0,content:"",mask:!0,maskStyle:{backgroundColor:"#fff",opacity:.1},lockScreen:!0,footer:!0,buttons:!1};i=e.extend(!0,o,i);var r=this,n=this.editor,a=t.classPrefix,s=(new Date).getTime(),l=""===i.name?a+"dialog-"+s:i.name,c=t.mouseOrTouch,h='
            ';""!==i.title&&(h+='
            ",h+=''+i.title+"",h+="
            "),i.closed&&(h+=''),h+='
            '+i.content,(i.footer||"string"==typeof i.footer)&&(h+='"),h+="
            ",h+='
            ',h+='
            ',h+="
            ",n.append(h);var d=n.find("."+l);d.lockScreen=function(t){return i.lockScreen&&(e("html,body").css("overflow",t?"hidden":""),r.resize()),d},d.showMask=function(){return i.mask&&n.find("."+a+"mask").css(i.maskStyle).css("z-index",t.dialogZindex-1).show(),d},d.hideMask=function(){return i.mask&&n.find("."+a+"mask").hide(),d},d.loading=function(e){var t=d.find("."+a+"dialog-mask");return t[e?"show":"hide"](),d},d.lockScreen(!0).showMask(),d.show().css({zIndex:t.dialogZindex,border:t.isIE8?"1px solid #ddd":"",width:"number"==typeof i.width?i.width+"px":i.width,height:"number"==typeof i.height?i.height+"px":i.height});var u=function(){d.css({top:(e(window).height()-d.height())/2+"px",left:(e(window).width()-d.width())/2+"px"})};if(u(),e(window).resize(u),d.children("."+a+"dialog-close").bind(c("click","touchend"),function(){d.hide().lockScreen(!1).hideMask()}),"object"==typeof i.buttons){var f=d.footer=d.find("."+a+"dialog-footer");for(var g in i.buttons){var p=i.buttons[g],m=a+g+"-btn";f.append('"),p[1]=e.proxy(p[1],d),f.children("."+m).bind(c("click","touchend"),p[1])}}if(""!==i.title&&i.drag){var w,v,k=d.children("."+a+"dialog-header");i.mask||k.bind(c("click","touchend"),function(){t.dialogZindex+=2,d.css("z-index",t.dialogZindex)}),k.mousedown(function(e){e=e||window.event,w=e.clientX-parseInt(d[0].style.left),v=e.clientY-parseInt(d[0].style.top),document.onmousemove=y});var b=function(e){e.removeClass(a+"user-unselect").off("selectstart")},x=function(e){e.addClass(a+"user-unselect").on("selectstart",function(e){return!1})},y=function(t){t=t||window.event;var i,o,r=parseInt(d[0].style.left),n=parseInt(d[0].style.top);r>=0?r+d.width()<=e(window).width()?i=t.clientX-w:(i=e(window).width()-d.width(),document.onmousemove=null):(i=0,document.onmousemove=null),n>=0?o=t.clientY-v:(o=0,document.onmousemove=null),document.onselectstart=function(){return!1},x(e("body")),x(d),d[0].style.left=i+"px",d[0].style.top=o+"px"};document.onmouseup=function(){b(e("body")),b(d),document.onselectstart=null,document.onmousemove=null},k.touchDraggable=function(){var t=null,i=function(i){var o=i.originalEvent,r=e(this).parent().position();t={x:o.changedTouches[0].pageX-r.left,y:o.changedTouches[0].pageY-r.top}},o=function(i){i.preventDefault();var o=i.originalEvent;e(this).parent().css({top:o.changedTouches[0].pageY-t.y,left:o.changedTouches[0].pageX-t.x})};this.bind("touchstart",i).bind("touchmove",o)},k.touchDraggable()}return t.dialogZindex+=2,d},t.mouseOrTouch=function(e,t){e=e||"click",t=t||"touchend";var i=e;try{document.createEvent("TouchEvent"),i=t}catch(o){}return i},t.dateFormat=function(e){e=e||"";var t=function(e){return 10>e?"0"+e:e},i=new Date,o=i.getFullYear(),r=o.toString().slice(2,4),n=t(i.getMonth()+1),a=t(i.getDate()),s=i.getDay(),l=t(i.getHours()),c=t(i.getMinutes()),h=t(i.getSeconds()),d=t(i.getMilliseconds()),u="",f=r+"-"+n+"-"+a,g=o+"-"+n+"-"+a,p=l+":"+c+":"+h;switch(e){case"UNIX Time":u=i.getTime();break;case"UTC":u=i.toUTCString();break;case"yy":u=r;break;case"year":case"yyyy":u=o;break;case"month":case"mm":u=n;break;case"cn-week-day":case"cn-wd":var m=["日","一","二","三","四","五","六"];u="星期"+m[s];break;case"week-day":case"wd":var w=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];u=w[s];break;case"day":case"dd":u=a;break;case"hour":case"hh":u=l;break;case"min":case"ii":u=c;break;case"second":case"ss":u=h;break;case"ms":u=d;break;case"yy-mm-dd":u=f;break;case"yyyy-mm-dd":u=g;break;case"yyyy-mm-dd h:i:s ms":case"full + ms":u=g+" "+p+" "+d;break;case"full":case"yyyy-mm-dd h:i:s":default:u=g+" "+p}return u},t}}); \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/editormd.js b/paicoding-ui/src/main/resources/static/editormd/editormd.js new file mode 100644 index 000000000..427bc91dd --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/editormd.js @@ -0,0 +1,4594 @@ +/* + * Editor.md + * + * @file editormd.js + * @version v1.5.0 + * @description Open source online markdown editor. + * @license MIT License + * @author Pandao + * {@link https://github.com/pandao/editor.md} + * @updateTime 2015-06-09 + */ + +;(function(factory) { + "use strict"; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) // for Require.js + { + /* Require.js define replace */ + } + else + { + define(["jquery"], factory); // for Sea.js + } + } + else + { + window.editormd = factory(); + } + +}(function() { + + /* Require.js assignment replace */ + + "use strict"; + + var $ = (typeof (jQuery) !== "undefined") ? jQuery : Zepto; + + if (typeof ($) === "undefined") { + return ; + } + + /** + * editormd + * + * @param {String} id 编辑器的ID + * @param {Object} options 配置选项 Key/Value + * @returns {Object} editormd 返回editormd对象 + */ + + var editormd = function (id, options) { + return new editormd.fn.init(id, options); + }; + + editormd.title = editormd.$name = "Editor.md"; + editormd.version = "1.5.0"; + editormd.homePage = "https://pandao.github.io/editor.md/"; + editormd.classPrefix = "editormd-"; + + editormd.toolbarModes = { + full : [ + "undo", "redo", "|", + "bold", "del", "italic", "quote", "ucwords", "uppercase", "lowercase", "|", + "h1", "h2", "h3", "h4", "h5", "h6", "|", + "list-ul", "list-ol", "hr", "|", + "link", "reference-link", "image", "code", "preformatted-text", "code-block", "table", "datetime", "emoji", "html-entities", "pagebreak", "|", + "goto-line", "watch", "preview", "fullscreen", "clear", "search", "|", + "help", "info" + ], + simple : [ + "undo", "redo", "|", + "bold", "del", "italic", "quote", "uppercase", "lowercase", "|", + "h1", "h2", "h3", "h4", "h5", "h6", "|", + "list-ul", "list-ol", "hr", "|", + "watch", "preview", "fullscreen", "|", + "help", "info" + ], + mini : [ + "undo", "redo", "|", + "watch", "preview", "|", + "help", "info" + ] + }; + + editormd.defaults = { + mode : "gfm", //gfm or markdown + name : "", // Form element name + value : "", // value for CodeMirror, if mode not gfm/markdown + theme : "", // Editor.md self themes, before v1.5.0 is CodeMirror theme, default empty + editorTheme : "default", // Editor area, this is CodeMirror theme at v1.5.0 + previewTheme : "", // Preview area theme, default empty + markdown : "", // Markdown source code + appendMarkdown : "", // if in init textarea value not empty, append markdown to textarea + width : "100%", + height : "100%", + path : "./lib/", // Dependents module file directory + katexURL : {}, + pluginPath : "", // If this empty, default use settings.path + "../plugins/" + delay : 300, // Delay parse markdown to html, Uint : ms + autoLoadModules : true, // Automatic load dependent module files + watch : true, + placeholder : "Enjoy Markdown! coding now...", + gotoLine : true, + codeFold : false, + autoHeight : false, + autoFocus : true, + autoCloseTags : true, + searchReplace : true, + syncScrolling : true, // true | false | "single", default true + readOnly : false, + tabSize : 4, + indentUnit : 4, + lineNumbers : true, + lineWrapping : true, + autoCloseBrackets : true, + showTrailingSpace : true, + matchBrackets : true, + indentWithTabs : true, + styleSelectedText : true, + matchWordHighlight : true, // options: true, false, "onselected" + styleActiveLine : true, // Highlight the current line + dialogLockScreen : true, + dialogShowMask : true, + dialogDraggable : true, + dialogMaskBgColor : "#fff", + dialogMaskOpacity : 0.1, + fontSize : "13px", + saveHTMLToTextarea : false, + disabledKeyMaps : [], + + onload : function() {}, + onresize : function() {}, + onchange : function() {}, + onwatch : null, + onunwatch : null, + onpreviewing : function() {}, + onpreviewed : function() {}, + onfullscreen : function() {}, + onfullscreenExit : function() {}, + onscroll : function() {}, + onpreviewscroll : function() {}, + + imageUpload : false, + imageFormats : ["jpg", "jpeg", "gif", "png", "bmp", "webp"], + imageUploadURL : "", + crossDomainUpload : false, + uploadCallbackURL : "", + + toc : true, // Table of contents + tocm : false, // Using [TOCM], auto create ToC dropdown menu + tocTitle : "", // for ToC dropdown menu btn + tocDropdown : false, + tocContainer : "", + tocStartLevel : 1, // Said from H1 to create ToC + htmlDecode : false, // Open the HTML tag identification + pageBreak : true, // Enable parse page break [========] + atLink : true, // for @link + emailLink : true, // for email address auto link + taskList : false, // Enable Github Flavored Markdown task lists + emoji : false, // :emoji: , Support Github emoji, Twitter Emoji (Twemoji); + // Support FontAwesome icon emoji :fa-xxx: > Using fontAwesome icon web fonts; + // Support Editor.md logo icon emoji :editormd-logo: :editormd-logo-1x: > 1~8x; + tex : false, // TeX(LaTeX), based on KaTeX + flowChart : false, // flowChart.js only support IE9+ + sequenceDiagram : false, // sequenceDiagram.js only support IE9+ + previewCodeHighlight : true, + + toolbar : true, // show/hide toolbar + toolbarAutoFixed : true, // on window scroll auto fixed position + toolbarIcons : "full", + toolbarTitles : {}, + toolbarHandlers : { + ucwords : function() { + return editormd.toolbarHandlers.ucwords; + }, + lowercase : function() { + return editormd.toolbarHandlers.lowercase; + } + }, + toolbarCustomIcons : { // using html tag create toolbar icon, unused default tag. + lowercase : "a", + "ucwords" : "Aa" + }, + toolbarIconsClass : { + undo : "fa-undo", + redo : "fa-repeat", + bold : "fa-bold", + del : "fa-strikethrough", + italic : "fa-italic", + quote : "fa-quote-left", + uppercase : "fa-font", + h1 : editormd.classPrefix + "bold", + h2 : editormd.classPrefix + "bold", + h3 : editormd.classPrefix + "bold", + h4 : editormd.classPrefix + "bold", + h5 : editormd.classPrefix + "bold", + h6 : editormd.classPrefix + "bold", + "list-ul" : "fa-list-ul", + "list-ol" : "fa-list-ol", + hr : "fa-minus", + link : "fa-link", + "reference-link" : "fa-anchor", + image : "fa-picture-o", + code : "fa-code", + "preformatted-text" : "fa-file-code-o", + "code-block" : "fa-file-code-o", + table : "fa-table", + datetime : "fa-clock-o", + emoji : "fa-smile-o", + "html-entities" : "fa-copyright", + pagebreak : "fa-newspaper-o", + "goto-line" : "fa-terminal", // fa-crosshairs + watch : "fa-eye-slash", + unwatch : "fa-eye", + preview : "fa-desktop", + search : "fa-search", + fullscreen : "fa-arrows-alt", + clear : "fa-eraser", + help : "fa-question-circle", + info : "fa-info-circle" + }, + toolbarIconTexts : {}, + + lang : { + name : "zh-cn", + description : "开源在线Markdown编辑器
            Open source online Markdown editor.", + tocTitle : "目录", + toolbar : { + undo : "撤销(Ctrl+Z)", + redo : "重做(Ctrl+Y)", + bold : "粗体", + del : "删除线", + italic : "斜体", + quote : "引用", + ucwords : "将每个单词首字母转成大写", + uppercase : "将所选转换成大写", + lowercase : "将所选转换成小写", + h1 : "标题1", + h2 : "标题2", + h3 : "标题3", + h4 : "标题4", + h5 : "标题5", + h6 : "标题6", + "list-ul" : "无序列表", + "list-ol" : "有序列表", + hr : "横线", + link : "链接", + "reference-link" : "引用链接", + image : "添加图片", + code : "行内代码", + "preformatted-text" : "预格式文本 / 代码块(缩进风格)", + "code-block" : "代码块(多语言风格)", + table : "添加表格", + datetime : "日期时间", + emoji : "Emoji表情", + "html-entities" : "HTML实体字符", + pagebreak : "插入分页符", + "goto-line" : "跳转到行", + watch : "关闭实时预览", + unwatch : "开启实时预览", + preview : "全窗口预览HTML(按 Shift + ESC还原)", + fullscreen : "全屏(按ESC还原)", + clear : "清空", + search : "搜索", + help : "使用帮助", + info : "关于" + editormd.title + }, + buttons : { + enter : "确定", + cancel : "取消", + close : "关闭" + }, + dialog : { + link : { + title : "添加链接", + url : "链接地址", + urlTitle : "链接标题", + urlEmpty : "错误:请填写链接地址。" + }, + referenceLink : { + title : "添加引用链接", + name : "引用名称", + url : "链接地址", + urlId : "链接ID", + urlTitle : "链接标题", + nameEmpty: "错误:引用链接的名称不能为空。", + idEmpty : "错误:请填写引用链接的ID。", + urlEmpty : "错误:请填写引用链接的URL地址。" + }, + image : { + title : "添加图片", + url : "图片地址", + link : "图片链接", + alt : "图片描述", + uploadButton : "本地上传", + imageURLEmpty : "错误:图片地址不能为空。", + uploadFileEmpty : "错误:上传的图片不能为空。", + formatNotAllowed : "错误:只允许上传图片文件,允许上传的图片文件格式有:" + }, + preformattedText : { + title : "添加预格式文本或代码块", + emptyAlert : "错误:请填写预格式文本或代码的内容。" + }, + codeBlock : { + title : "添加代码块", + selectLabel : "代码语言:", + selectDefaultText : "请选择代码语言", + otherLanguage : "其他语言", + unselectedLanguageAlert : "错误:请选择代码所属的语言类型。", + codeEmptyAlert : "错误:请填写代码内容。" + }, + htmlEntities : { + title : "HTML 实体字符" + }, + help : { + title : "使用帮助" + } + } + } + }; + + editormd.classNames = { + tex : editormd.classPrefix + "tex" + }; + + editormd.dialogZindex = 99999; + + editormd.$katex = null; + editormd.$marked = null; + editormd.$CodeMirror = null; + editormd.$prettyPrint = null; + + var timer, flowchartTimer; + + editormd.prototype = editormd.fn = { + state : { + watching : false, + loaded : false, + preview : false, + fullscreen : false + }, + + /** + * 构造函数/实例初始化 + * Constructor / instance initialization + * + * @param {String} id 编辑器的ID + * @param {Object} [options={}] 配置选项 Key/Value + * @returns {editormd} 返回editormd的实例对象 + */ + + init : function (id, options) { + + options = options || {}; + + if (typeof id === "object") + { + options = id; + } + + var _this = this; + var classPrefix = this.classPrefix = editormd.classPrefix; + var settings = this.settings = $.extend(true, {}, editormd.defaults, options); + + id = (typeof id === "object") ? settings.id : id; + + var editor = this.editor = $("#" + id); + + this.id = id; + this.lang = settings.lang; + + var classNames = this.classNames = { + textarea : { + html : classPrefix + "html-textarea", + markdown : classPrefix + "markdown-textarea" + } + }; + + settings.pluginPath = (settings.pluginPath === "") ? settings.path + "../plugins/" : settings.pluginPath; + + this.state.watching = (settings.watch) ? true : false; + + if ( !editor.hasClass("editormd") ) { + editor.addClass("editormd"); + } + + editor.css({ + width : (typeof settings.width === "number") ? settings.width + "px" : settings.width, + height : (typeof settings.height === "number") ? settings.height + "px" : settings.height + }); + + if (settings.autoHeight) + { + editor.css("height", "auto"); + } + + var markdownTextarea = this.markdownTextarea = editor.children("textarea"); + + if (markdownTextarea.length < 1) + { + editor.append(""); + markdownTextarea = this.markdownTextarea = editor.children("textarea"); + } + + markdownTextarea.addClass(classNames.textarea.markdown).attr("placeholder", settings.placeholder); + + if (typeof markdownTextarea.attr("name") === "undefined" || markdownTextarea.attr("name") === "") + { + markdownTextarea.attr("name", (settings.name !== "") ? settings.name : id + "-markdown-doc"); + } + + var appendElements = [ + (!settings.readOnly) ? "" : "", + ( (settings.saveHTMLToTextarea) ? "" : "" ), + "
            ", + "
            ", + "
            " + ].join("\n"); + + editor.append(appendElements).addClass(classPrefix + "vertical"); + + if (settings.theme !== "") + { + editor.addClass(classPrefix + "theme-" + settings.theme); + } + + this.mask = editor.children("." + classPrefix + "mask"); + this.containerMask = editor.children("." + classPrefix + "container-mask"); + + if (settings.markdown !== "") + { + markdownTextarea.val(settings.markdown); + } + + if (settings.appendMarkdown !== "") + { + markdownTextarea.val(markdownTextarea.val() + settings.appendMarkdown); + } + + this.htmlTextarea = editor.children("." + classNames.textarea.html); + this.preview = editor.children("." + classPrefix + "preview"); + this.previewContainer = this.preview.children("." + classPrefix + "preview-container"); + + if (settings.previewTheme !== "") + { + this.preview.addClass(classPrefix + "preview-theme-" + settings.previewTheme); + } + + if (typeof define === "function" && define.amd) + { + if (typeof katex !== "undefined") + { + editormd.$katex = katex; + } + + if (settings.searchReplace && !settings.readOnly) + { + editormd.loadCSS(settings.path + "codemirror/addon/dialog/dialog"); + editormd.loadCSS(settings.path + "codemirror/addon/search/matchesonscrollbar"); + } + } + + if ((typeof define === "function" && define.amd) || !settings.autoLoadModules) + { + if (typeof CodeMirror !== "undefined") { + editormd.$CodeMirror = CodeMirror; + } + + if (typeof marked !== "undefined") { + editormd.$marked = marked; + } + + this.setCodeMirror().setToolbar().loadedDisplay(); + } + else + { + this.loadQueues(); + } + + return this; + }, + + /** + * 所需组件加载队列 + * Required components loading queue + * + * @returns {editormd} 返回editormd的实例对象 + */ + + loadQueues : function() { + var _this = this; + var settings = this.settings; + var loadPath = settings.path; + + var loadFlowChartOrSequenceDiagram = function() { + + if (editormd.isIE8) + { + _this.loadedDisplay(); + + return ; + } + + if (settings.flowChart || settings.sequenceDiagram) + { + editormd.loadScript(loadPath + "raphael.min", function() { + + editormd.loadScript(loadPath + "underscore.min", function() { + + if (!settings.flowChart && settings.sequenceDiagram) + { + editormd.loadScript(loadPath + "sequence-diagram.min", function() { + _this.loadedDisplay(); + }); + } + else if (settings.flowChart && !settings.sequenceDiagram) + { + editormd.loadScript(loadPath + "flowchart.min", function() { + editormd.loadScript(loadPath + "jquery.flowchart.min", function() { + _this.loadedDisplay(); + }); + }); + } + else if (settings.flowChart && settings.sequenceDiagram) + { + editormd.loadScript(loadPath + "flowchart.min", function() { + editormd.loadScript(loadPath + "jquery.flowchart.min", function() { + editormd.loadScript(loadPath + "sequence-diagram.min", function() { + _this.loadedDisplay(); + }); + }); + }); + } + }); + + }); + } + else + { + _this.loadedDisplay(); + } + }; + + editormd.loadCSS(loadPath + "codemirror/codemirror.min"); + + if (settings.searchReplace && !settings.readOnly) + { + editormd.loadCSS(loadPath + "codemirror/addon/dialog/dialog"); + editormd.loadCSS(loadPath + "codemirror/addon/search/matchesonscrollbar"); + } + + if (settings.codeFold) + { + editormd.loadCSS(loadPath + "codemirror/addon/fold/foldgutter"); + } + + editormd.loadScript(loadPath + "codemirror/codemirror.min", function() { + editormd.$CodeMirror = CodeMirror; + + editormd.loadScript(loadPath + "codemirror/modes.min", function() { + + editormd.loadScript(loadPath + "codemirror/addons.min", function() { + + _this.setCodeMirror(); + + if (settings.mode !== "gfm" && settings.mode !== "markdown") + { + _this.loadedDisplay(); + + return false; + } + + _this.setToolbar(); + + editormd.loadScript(loadPath + "marked.min", function() { + + editormd.$marked = marked; + + if (settings.previewCodeHighlight) + { + editormd.loadScript(loadPath + "prettify.min", function() { + loadFlowChartOrSequenceDiagram(); + }); + } + else + { + loadFlowChartOrSequenceDiagram(); + } + }); + + }); + + }); + + }); + + return this; + }, + + /** + * 设置 Editor.md 的整体主题,主要是工具栏 + * Setting Editor.md theme + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setTheme : function(theme) { + var editor = this.editor; + var oldTheme = this.settings.theme; + var themePrefix = this.classPrefix + "theme-"; + + editor.removeClass(themePrefix + oldTheme).addClass(themePrefix + theme); + + this.settings.theme = theme; + + return this; + }, + + /** + * 设置 CodeMirror(编辑区)的主题 + * Setting CodeMirror (Editor area) theme + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setEditorTheme : function(theme) { + var settings = this.settings; + settings.editorTheme = theme; + + if (theme !== "default") + { + editormd.loadCSS(settings.path + "codemirror/theme/" + settings.editorTheme); + } + + this.cm.setOption("theme", theme); + + return this; + }, + + /** + * setEditorTheme() 的别名 + * setEditorTheme() alias + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setCodeMirrorTheme : function (theme) { + this.setEditorTheme(theme); + + return this; + }, + + /** + * 设置 Editor.md 的主题 + * Setting Editor.md theme + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setPreviewTheme : function(theme) { + var preview = this.preview; + var oldTheme = this.settings.previewTheme; + var themePrefix = this.classPrefix + "preview-theme-"; + + preview.removeClass(themePrefix + oldTheme).addClass(themePrefix + theme); + + this.settings.previewTheme = theme; + + return this; + }, + + /** + * 配置和初始化CodeMirror组件 + * CodeMirror initialization + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setCodeMirror : function() { + var settings = this.settings; + var editor = this.editor; + + if (settings.editorTheme !== "default") + { + editormd.loadCSS(settings.path + "codemirror/theme/" + settings.editorTheme); + } + + var codeMirrorConfig = { + mode : settings.mode, + theme : settings.editorTheme, + tabSize : settings.tabSize, + dragDrop : false, + autofocus : settings.autoFocus, + autoCloseTags : settings.autoCloseTags, + readOnly : (settings.readOnly) ? "nocursor" : false, + indentUnit : settings.indentUnit, + lineNumbers : settings.lineNumbers, + lineWrapping : settings.lineWrapping, + extraKeys : { + "Ctrl-Q": function(cm) { + cm.foldCode(cm.getCursor()); + } + }, + foldGutter : settings.codeFold, + gutters : ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], + matchBrackets : settings.matchBrackets, + indentWithTabs : settings.indentWithTabs, + styleActiveLine : settings.styleActiveLine, + styleSelectedText : settings.styleSelectedText, + autoCloseBrackets : settings.autoCloseBrackets, + showTrailingSpace : settings.showTrailingSpace, + highlightSelectionMatches : ( (!settings.matchWordHighlight) ? false : { showToken: (settings.matchWordHighlight === "onselected") ? false : /\w/ } ) + }; + + this.codeEditor = this.cm = editormd.$CodeMirror.fromTextArea(this.markdownTextarea[0], codeMirrorConfig); + this.codeMirror = this.cmElement = editor.children(".CodeMirror"); + + if (settings.value !== "") + { + this.cm.setValue(settings.value); + } + + this.codeMirror.css({ + fontSize : settings.fontSize, + width : (!settings.watch) ? "100%" : "50%" + }); + + if (settings.autoHeight) + { + this.codeMirror.css("height", "auto"); + this.cm.setOption("viewportMargin", Infinity); + } + + if (!settings.lineNumbers) + { + this.codeMirror.find(".CodeMirror-gutters").css("border-right", "none"); + } + + return this; + }, + + /** + * 获取CodeMirror的配置选项 + * Get CodeMirror setting options + * + * @returns {Mixed} return CodeMirror setting option value + */ + + getCodeMirrorOption : function(key) { + return this.cm.getOption(key); + }, + + /** + * 配置和重配置CodeMirror的选项 + * CodeMirror setting options / resettings + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setCodeMirrorOption : function(key, value) { + + this.cm.setOption(key, value); + + return this; + }, + + /** + * 添加 CodeMirror 键盘快捷键 + * Add CodeMirror keyboard shortcuts key map + * + * @returns {editormd} 返回editormd的实例对象 + */ + + addKeyMap : function(map, bottom) { + this.cm.addKeyMap(map, bottom); + + return this; + }, + + /** + * 移除 CodeMirror 键盘快捷键 + * Remove CodeMirror keyboard shortcuts key map + * + * @returns {editormd} 返回editormd的实例对象 + */ + + removeKeyMap : function(map) { + this.cm.removeKeyMap(map); + + return this; + }, + + /** + * 跳转到指定的行 + * Goto CodeMirror line + * + * @param {String|Intiger} line line number or "first"|"last" + * @returns {editormd} 返回editormd的实例对象 + */ + + gotoLine : function (line) { + + var settings = this.settings; + + if (!settings.gotoLine) + { + return this; + } + + var cm = this.cm; + var editor = this.editor; + var count = cm.lineCount(); + var preview = this.preview; + + if (typeof line === "string") + { + if(line === "last") + { + line = count; + } + + if (line === "first") + { + line = 1; + } + } + + if (typeof line !== "number") + { + alert("Error: The line number must be an integer."); + return this; + } + + line = parseInt(line) - 1; + + if (line > count) + { + alert("Error: The line number range 1-" + count); + + return this; + } + + cm.setCursor( {line : line, ch : 0} ); + + var scrollInfo = cm.getScrollInfo(); + var clientHeight = scrollInfo.clientHeight; + var coords = cm.charCoords({line : line, ch : 0}, "local"); + + cm.scrollTo(null, (coords.top + coords.bottom - clientHeight) / 2); + + if (settings.watch) + { + var cmScroll = this.codeMirror.find(".CodeMirror-scroll")[0]; + var height = $(cmScroll).height(); + var scrollTop = cmScroll.scrollTop; + var percent = (scrollTop / cmScroll.scrollHeight); + + if (scrollTop === 0) + { + preview.scrollTop(0); + } + else if (scrollTop + height >= cmScroll.scrollHeight - 16) + { + preview.scrollTop(preview[0].scrollHeight); + } + else + { + preview.scrollTop(preview[0].scrollHeight * percent); + } + } + + cm.focus(); + + return this; + }, + + /** + * 扩展当前实例对象,可同时设置多个或者只设置一个 + * Extend editormd instance object, can mutil setting. + * + * @returns {editormd} this(editormd instance object.) + */ + + extend : function() { + if (typeof arguments[1] !== "undefined") + { + if (typeof arguments[1] === "function") + { + arguments[1] = $.proxy(arguments[1], this); + } + + this[arguments[0]] = arguments[1]; + } + + if (typeof arguments[0] === "object" && typeof arguments[0].length === "undefined") + { + $.extend(true, this, arguments[0]); + } + + return this; + }, + + /** + * 设置或扩展当前实例对象,单个设置 + * Extend editormd instance object, one by one + * + * @param {String|Object} key option key + * @param {String|Object} value option value + * @returns {editormd} this(editormd instance object.) + */ + + set : function (key, value) { + + if (typeof value !== "undefined" && typeof value === "function") + { + value = $.proxy(value, this); + } + + this[key] = value; + + return this; + }, + + /** + * 重新配置 + * Resetting editor options + * + * @param {String|Object} key option key + * @param {String|Object} value option value + * @returns {editormd} this(editormd instance object.) + */ + + config : function(key, value) { + var settings = this.settings; + + if (typeof key === "object") + { + settings = $.extend(true, settings, key); + } + + if (typeof key === "string") + { + settings[key] = value; + } + + this.settings = settings; + this.recreate(); + + return this; + }, + + /** + * 注册事件处理方法 + * Bind editor event handle + * + * @param {String} eventType event type + * @param {Function} callback 回调函数 + * @returns {editormd} this(editormd instance object.) + */ + + on : function(eventType, callback) { + var settings = this.settings; + + if (typeof settings["on" + eventType] !== "undefined") + { + settings["on" + eventType] = $.proxy(callback, this); + } + + return this; + }, + + /** + * 解除事件处理方法 + * Unbind editor event handle + * + * @param {String} eventType event type + * @returns {editormd} this(editormd instance object.) + */ + + off : function(eventType) { + var settings = this.settings; + + if (typeof settings["on" + eventType] !== "undefined") + { + settings["on" + eventType] = function(){}; + } + + return this; + }, + + /** + * 显示工具栏 + * Display toolbar + * + * @param {Function} [callback=function(){}] 回调函数 + * @returns {editormd} 返回editormd的实例对象 + */ + + showToolbar : function(callback) { + var settings = this.settings; + + if(settings.readOnly) { + return this; + } + + if (settings.toolbar && (this.toolbar.length < 1 || this.toolbar.find("." + this.classPrefix + "menu").html() === "") ) + { + this.setToolbar(); + } + + settings.toolbar = true; + + this.toolbar.show(); + this.resize(); + + $.proxy(callback || function(){}, this)(); + + return this; + }, + + /** + * 隐藏工具栏 + * Hide toolbar + * + * @param {Function} [callback=function(){}] 回调函数 + * @returns {editormd} this(editormd instance object.) + */ + + hideToolbar : function(callback) { + var settings = this.settings; + + settings.toolbar = false; + this.toolbar.hide(); + this.resize(); + + $.proxy(callback || function(){}, this)(); + + return this; + }, + + /** + * 页面滚动时工具栏的固定定位 + * Set toolbar in window scroll auto fixed position + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setToolbarAutoFixed : function(fixed) { + + var state = this.state; + var editor = this.editor; + var toolbar = this.toolbar; + var settings = this.settings; + + if (typeof fixed !== "undefined") + { + settings.toolbarAutoFixed = fixed; + } + + var autoFixedHandle = function(){ + var $window = $(window); + var top = $window.scrollTop(); + + if (!settings.toolbarAutoFixed) + { + return false; + } + + if (top - editor.offset().top > 10 && top < editor.height()) + { + toolbar.css({ + position : "fixed", + width : editor.width() + "px", + left : ($window.width() - editor.width()) / 2 + "px" + }); + } + else + { + toolbar.css({ + position : "absolute", + width : "100%", + left : 0 + }); + } + }; + + if (!state.fullscreen && !state.preview && settings.toolbar && settings.toolbarAutoFixed) + { + $(window).bind("scroll", autoFixedHandle); + } + + return this; + }, + + /** + * 配置和初始化工具栏 + * Set toolbar and Initialization + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setToolbar : function() { + var settings = this.settings; + + if(settings.readOnly) { + return this; + } + + var editor = this.editor; + var preview = this.preview; + var classPrefix = this.classPrefix; + + var toolbar = this.toolbar = editor.children("." + classPrefix + "toolbar"); + + if (settings.toolbar && toolbar.length < 1) + { + var toolbarHTML = "
              "; + + editor.append(toolbarHTML); + toolbar = this.toolbar = editor.children("." + classPrefix + "toolbar"); + } + + if (!settings.toolbar) + { + toolbar.hide(); + + return this; + } + + toolbar.show(); + + var icons = (typeof settings.toolbarIcons === "function") ? settings.toolbarIcons() + : ((typeof settings.toolbarIcons === "string") ? editormd.toolbarModes[settings.toolbarIcons] : settings.toolbarIcons); + + var toolbarMenu = toolbar.find("." + this.classPrefix + "menu"), menu = ""; + var pullRight = false; + + for (var i = 0, len = icons.length; i < len; i++) + { + var name = icons[i]; + + if (name === "||") + { + pullRight = true; + } + else if (name === "|") + { + menu += "
            • |
            • "; + } + else + { + var isHeader = (/h(\d)/.test(name)); + var index = name; + + if (name === "watch" && !settings.watch) { + index = "unwatch"; + } + + var title = settings.lang.toolbar[index]; + var iconTexts = settings.toolbarIconTexts[index]; + var iconClass = settings.toolbarIconsClass[index]; + + title = (typeof title === "undefined") ? "" : title; + iconTexts = (typeof iconTexts === "undefined") ? "" : iconTexts; + iconClass = (typeof iconClass === "undefined") ? "" : iconClass; + + var menuItem = pullRight ? "
            • " : "
            • "; + + if (typeof settings.toolbarCustomIcons[name] !== "undefined" && typeof settings.toolbarCustomIcons[name] !== "function") + { + menuItem += settings.toolbarCustomIcons[name]; + } + else + { + menuItem += ""; + menuItem += ""+((isHeader) ? name.toUpperCase() : ( (iconClass === "") ? iconTexts : "") ) + ""; + menuItem += ""; + } + + menuItem += "
            • "; + + menu = pullRight ? menuItem + menu : menu + menuItem; + } + } + + toolbarMenu.html(menu); + + toolbarMenu.find("[title=\"Lowercase\"]").attr("title", settings.lang.toolbar.lowercase); + toolbarMenu.find("[title=\"ucwords\"]").attr("title", settings.lang.toolbar.ucwords); + + this.setToolbarHandler(); + this.setToolbarAutoFixed(); + + return this; + }, + + /** + * 工具栏图标事件处理对象序列 + * Get toolbar icons event handlers + * + * @param {Object} cm CodeMirror的实例对象 + * @param {String} name 要获取的事件处理器名称 + * @returns {Object} 返回处理对象序列 + */ + + dialogLockScreen : function() { + $.proxy(editormd.dialogLockScreen, this)(); + + return this; + }, + + dialogShowMask : function(dialog) { + $.proxy(editormd.dialogShowMask, this)(dialog); + + return this; + }, + + getToolbarHandles : function(name) { + var toolbarHandlers = this.toolbarHandlers = editormd.toolbarHandlers; + + return (name && typeof toolbarIconHandlers[name] !== "undefined") ? toolbarHandlers[name] : toolbarHandlers; + }, + + /** + * 工具栏图标事件处理器 + * Bind toolbar icons event handle + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setToolbarHandler : function() { + var _this = this; + var settings = this.settings; + + if (!settings.toolbar || settings.readOnly) { + return this; + } + + var toolbar = this.toolbar; + var cm = this.cm; + var classPrefix = this.classPrefix; + var toolbarIcons = this.toolbarIcons = toolbar.find("." + classPrefix + "menu > li > a"); + var toolbarIconHandlers = this.getToolbarHandles(); + + toolbarIcons.bind(editormd.mouseOrTouch("click", "touchend"), function(event) { + + var icon = $(this).children(".fa"); + var name = icon.attr("name"); + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (name === "") { + return ; + } + + _this.activeIcon = icon; + + if (typeof toolbarIconHandlers[name] !== "undefined") + { + $.proxy(toolbarIconHandlers[name], _this)(cm); + } + else + { + if (typeof settings.toolbarHandlers[name] !== "undefined") + { + $.proxy(settings.toolbarHandlers[name], _this)(cm, icon, cursor, selection); + } + } + + if (name !== "link" && name !== "reference-link" && name !== "image" && name !== "code-block" && + name !== "preformatted-text" && name !== "watch" && name !== "preview" && name !== "search" && name !== "fullscreen" && name !== "info") + { + cm.focus(); + } + + return false; + + }); + + return this; + }, + + /** + * 动态创建对话框 + * Creating custom dialogs + * + * @param {Object} options 配置项键值对 Key/Value + * @returns {dialog} 返回创建的dialog的jQuery实例对象 + */ + + createDialog : function(options) { + return $.proxy(editormd.createDialog, this)(options); + }, + + /** + * 创建关于Editor.md的对话框 + * Create about Editor.md dialog + * + * @returns {editormd} 返回editormd的实例对象 + */ + + createInfoDialog : function() { + var _this = this; + var editor = this.editor; + var classPrefix = this.classPrefix; + + var infoDialogHTML = [ + "
              ", + "
              ", + "

              " + editormd.title + "v" + editormd.version + "

              ", + "

              " + this.lang.description + "

              ", + "

              " + editormd.homePage + "

              ", + "

              Copyright © 2015 Pandao, The MIT License.

              ", + "
              ", + "", + "
              " + ].join("\n"); + + editor.append(infoDialogHTML); + + var infoDialog = this.infoDialog = editor.children("." + classPrefix + "dialog-info"); + + infoDialog.find("." + classPrefix + "dialog-close").bind(editormd.mouseOrTouch("click", "touchend"), function() { + _this.hideInfoDialog(); + }); + + infoDialog.css("border", (editormd.isIE8) ? "1px solid #ddd" : "").css("z-index", editormd.dialogZindex).show(); + + this.infoDialogPosition(); + + return this; + }, + + /** + * 关于Editor.md对话居中定位 + * Editor.md dialog position handle + * + * @returns {editormd} 返回editormd的实例对象 + */ + + infoDialogPosition : function() { + var infoDialog = this.infoDialog; + + var _infoDialogPosition = function() { + infoDialog.css({ + top : ($(window).height() - infoDialog.height()) / 2 + "px", + left : ($(window).width() - infoDialog.width()) / 2 + "px" + }); + }; + + _infoDialogPosition(); + + $(window).resize(_infoDialogPosition); + + return this; + }, + + /** + * 显示关于Editor.md + * Display about Editor.md dialog + * + * @returns {editormd} 返回editormd的实例对象 + */ + + showInfoDialog : function() { + + $("html,body").css("overflow-x", "hidden"); + + var _this = this; + var editor = this.editor; + var settings = this.settings; + var infoDialog = this.infoDialog = editor.children("." + this.classPrefix + "dialog-info"); + + if (infoDialog.length < 1) + { + this.createInfoDialog(); + } + + this.lockScreen(true); + + this.mask.css({ + opacity : settings.dialogMaskOpacity, + backgroundColor : settings.dialogMaskBgColor + }).show(); + + infoDialog.css("z-index", editormd.dialogZindex).show(); + + this.infoDialogPosition(); + + return this; + }, + + /** + * 隐藏关于Editor.md + * Hide about Editor.md dialog + * + * @returns {editormd} 返回editormd的实例对象 + */ + + hideInfoDialog : function() { + $("html,body").css("overflow-x", ""); + this.infoDialog.hide(); + this.mask.hide(); + this.lockScreen(false); + + return this; + }, + + /** + * 锁屏 + * lock screen + * + * @param {Boolean} lock Boolean 布尔值,是否锁屏 + * @returns {editormd} 返回editormd的实例对象 + */ + + lockScreen : function(lock) { + editormd.lockScreen(lock); + this.resize(); + + return this; + }, + + /** + * 编辑器界面重建,用于动态语言包或模块加载等 + * Recreate editor + * + * @returns {editormd} 返回editormd的实例对象 + */ + + recreate : function() { + var _this = this; + var editor = this.editor; + var settings = this.settings; + + this.codeMirror.remove(); + + this.setCodeMirror(); + + if (!settings.readOnly) + { + if (editor.find(".editormd-dialog").length > 0) { + editor.find(".editormd-dialog").remove(); + } + + if (settings.toolbar) + { + this.getToolbarHandles(); + this.setToolbar(); + } + } + + this.loadedDisplay(true); + + return this; + }, + + /** + * 高亮预览HTML的pre代码部分 + * highlight of preview codes + * + * @returns {editormd} 返回editormd的实例对象 + */ + + previewCodeHighlight : function() { + var settings = this.settings; + var previewContainer = this.previewContainer; + + if (settings.previewCodeHighlight) + { + previewContainer.find("pre").addClass("prettyprint linenums"); + + if (typeof prettyPrint !== "undefined") + { + prettyPrint(); + } + } + + return this; + }, + + /** + * 解析TeX(KaTeX)科学公式 + * TeX(KaTeX) Renderer + * + * @returns {editormd} 返回editormd的实例对象 + */ + + katexRender : function() { + + this.previewContainer.find("." + editormd.classNames.tex).each(function(){ + var tex = $(this); + editormd.$katex.render(tex.text(), tex[0]); + }); + + // 块内的已经渲染了 CSS 所以可以找到,但是行内的则不行 + // 行内的需要特殊处理 + katexRender(this.previewContainer[0]); + + return this; + }, + + /** + * 解析和渲染流程图及时序图 + * FlowChart and SequenceDiagram Renderer + * + * @returns {editormd} 返回editormd的实例对象 + */ + + flowChartAndSequenceDiagramRender : function() { + var $this = this; + var settings = this.settings; + var previewContainer = this.previewContainer; + + if (editormd.isIE8) { + return this; + } + + if (settings.flowChart) { + if (flowchartTimer === null) { + return this; + } + + previewContainer.find(".flowchart").flowChart(); + } + + if (settings.sequenceDiagram) { + previewContainer.find(".sequence-diagram").sequenceDiagram({theme: "simple"}); + } + + var preview = $this.preview; + var codeMirror = $this.codeMirror; + var codeView = codeMirror.find(".CodeMirror-scroll"); + + var height = codeView.height(); + var scrollTop = codeView.scrollTop(); + var percent = (scrollTop / codeView[0].scrollHeight); + var tocHeight = 0; + + preview.find(".markdown-toc-list").each(function(){ + tocHeight += $(this).height(); + }); + + var tocMenuHeight = preview.find(".editormd-toc-menu").height(); + tocMenuHeight = (!tocMenuHeight) ? 0 : tocMenuHeight; + + if (scrollTop === 0) + { + preview.scrollTop(0); + } + else if (scrollTop + height >= codeView[0].scrollHeight - 16) + { + preview.scrollTop(preview[0].scrollHeight); + } + else + { + preview.scrollTop((preview[0].scrollHeight + tocHeight + tocMenuHeight) * percent); + } + + return this; + }, + + /** + * 注册键盘快捷键处理 + * Register CodeMirror keyMaps (keyboard shortcuts). + * + * @param {Object} keyMap KeyMap key/value {"(Ctrl/Shift/Alt)-Key" : function(){}} + * @returns {editormd} return this + */ + + registerKeyMaps : function(keyMap) { + + var _this = this; + var cm = this.cm; + var settings = this.settings; + var toolbarHandlers = editormd.toolbarHandlers; + var disabledKeyMaps = settings.disabledKeyMaps; + + keyMap = keyMap || null; + + if (keyMap) + { + for (var i in keyMap) + { + if ($.inArray(i, disabledKeyMaps) < 0) + { + var map = {}; + map[i] = keyMap[i]; + + cm.addKeyMap(keyMap); + } + } + } + else + { + for (var k in editormd.keyMaps) + { + var _keyMap = editormd.keyMaps[k]; + var handle = (typeof _keyMap === "string") ? $.proxy(toolbarHandlers[_keyMap], _this) : $.proxy(_keyMap, _this); + + if ($.inArray(k, ["F9", "F10", "F11"]) < 0 && $.inArray(k, disabledKeyMaps) < 0) + { + var _map = {}; + _map[k] = handle; + + cm.addKeyMap(_map); + } + } + + $(window).keydown(function(event) { + + var keymaps = { + "120" : "F9", + "121" : "F10", + "122" : "F11" + }; + + if ( $.inArray(keymaps[event.keyCode], disabledKeyMaps) < 0 ) + { + switch (event.keyCode) + { + case 120: + $.proxy(toolbarHandlers["watch"], _this)(); + return false; + break; + + case 121: + $.proxy(toolbarHandlers["preview"], _this)(); + return false; + break; + + case 122: + $.proxy(toolbarHandlers["fullscreen"], _this)(); + return false; + break; + + default: + break; + } + } + }); + } + + return this; + }, + + /** + * 绑定同步滚动 + * + * @returns {editormd} return this + */ + + bindScrollEvent : function() { + + var _this = this; + var preview = this.preview; + var settings = this.settings; + var codeMirror = this.codeMirror; + var mouseOrTouch = editormd.mouseOrTouch; + + if (!settings.syncScrolling) { + return this; + } + + var cmBindScroll = function() { + codeMirror.find(".CodeMirror-scroll").bind(mouseOrTouch("scroll", "touchmove"), function(event) { + var height = $(this).height(); + var scrollTop = $(this).scrollTop(); + var percent = (scrollTop / $(this)[0].scrollHeight); + + var tocHeight = 0; + + preview.find(".markdown-toc-list").each(function(){ + tocHeight += $(this).height(); + }); + + var tocMenuHeight = preview.find(".editormd-toc-menu").height(); + tocMenuHeight = (!tocMenuHeight) ? 0 : tocMenuHeight; + + if (scrollTop === 0) + { + preview.scrollTop(0); + } + else if (scrollTop + height >= $(this)[0].scrollHeight - 16) + { + preview.scrollTop(preview[0].scrollHeight); + } + else + { + preview.scrollTop((preview[0].scrollHeight + tocHeight + tocMenuHeight) * percent); + } + + $.proxy(settings.onscroll, _this)(event); + }); + }; + + var cmUnbindScroll = function() { + codeMirror.find(".CodeMirror-scroll").unbind(mouseOrTouch("scroll", "touchmove")); + }; + + var previewBindScroll = function() { + + preview.bind(mouseOrTouch("scroll", "touchmove"), function(event) { + var height = $(this).height(); + var scrollTop = $(this).scrollTop(); + var percent = (scrollTop / $(this)[0].scrollHeight); + var codeView = codeMirror.find(".CodeMirror-scroll"); + + if(scrollTop === 0) + { + codeView.scrollTop(0); + } + else if (scrollTop + height >= $(this)[0].scrollHeight) + { + codeView.scrollTop(codeView[0].scrollHeight); + } + else + { + codeView.scrollTop(codeView[0].scrollHeight * percent); + } + + $.proxy(settings.onpreviewscroll, _this)(event); + }); + + }; + + var previewUnbindScroll = function() { + preview.unbind(mouseOrTouch("scroll", "touchmove")); + }; + + codeMirror.bind({ + mouseover : cmBindScroll, + mouseout : cmUnbindScroll, + touchstart : cmBindScroll, + touchend : cmUnbindScroll + }); + + if (settings.syncScrolling === "single") { + return this; + } + + preview.bind({ + mouseover : previewBindScroll, + mouseout : previewUnbindScroll, + touchstart : previewBindScroll, + touchend : previewUnbindScroll + }); + + return this; + }, + + bindChangeEvent : function() { + + var _this = this; + var cm = this.cm; + var settings = this.settings; + + if (!settings.syncScrolling) { + return this; + } + + cm.on("change", function(_cm, changeObj) { + + if (settings.watch) + { + _this.previewContainer.css("padding", settings.autoHeight ? "20px 20px 50px 40px" : "20px"); + } + + timer = setTimeout(function() { + clearTimeout(timer); + _this.save(); + timer = null; + }, settings.delay); + }); + + return this; + }, + + /** + * 加载队列完成之后的显示处理 + * Display handle of the module queues loaded after. + * + * @param {Boolean} recreate 是否为重建编辑器 + * @returns {editormd} 返回editormd的实例对象 + */ + + loadedDisplay : function(recreate) { + + recreate = recreate || false; + + var _this = this; + var editor = this.editor; + var preview = this.preview; + var settings = this.settings; + + this.containerMask.hide(); + + this.save(); + + if (settings.watch) { + preview.show(); + } + + editor.data("oldWidth", editor.width()).data("oldHeight", editor.height()); // 为了兼容Zepto + + this.resize(); + this.registerKeyMaps(); + + $(window).resize(function(){ + _this.resize(); + }); + + this.bindScrollEvent().bindChangeEvent(); + + if (!recreate) + { + $.proxy(settings.onload, this)(); + } + + this.state.loaded = true; + + return this; + }, + + /** + * 设置编辑器的宽度 + * Set editor width + * + * @param {Number|String} width 编辑器宽度值 + * @returns {editormd} 返回editormd的实例对象 + */ + + width : function(width) { + + this.editor.css("width", (typeof width === "number") ? width + "px" : width); + this.resize(); + + return this; + }, + + /** + * 设置编辑器的高度 + * Set editor height + * + * @param {Number|String} height 编辑器高度值 + * @returns {editormd} 返回editormd的实例对象 + */ + + height : function(height) { + + this.editor.css("height", (typeof height === "number") ? height + "px" : height); + this.resize(); + + return this; + }, + + /** + * 调整编辑器的尺寸和布局 + * Resize editor layout + * + * @param {Number|String} [width=null] 编辑器宽度值 + * @param {Number|String} [height=null] 编辑器高度值 + * @returns {editormd} 返回editormd的实例对象 + */ + + resize : function(width, height) { + + width = width || null; + height = height || null; + + var state = this.state; + var editor = this.editor; + var preview = this.preview; + var toolbar = this.toolbar; + var settings = this.settings; + var codeMirror = this.codeMirror; + + if (width) + { + editor.css("width", (typeof width === "number") ? width + "px" : width); + } + + if (settings.autoHeight && !state.fullscreen && !state.preview) + { + editor.css("height", "auto"); + codeMirror.css("height", "auto"); + } + else + { + if (height) + { + editor.css("height", (typeof height === "number") ? height + "px" : height); + } + + if (state.fullscreen) + { + editor.height($(window).height()); + } + + if (settings.toolbar && !settings.readOnly) + { + codeMirror.css("margin-top", toolbar.height() + 1).height(editor.height() - toolbar.height()); + } + else + { + codeMirror.css("margin-top", 0).height(editor.height()); + } + } + + if(settings.watch) + { + codeMirror.width(editor.width() / 2); + preview.width((!state.preview) ? editor.width() / 2 : editor.width()); + + this.previewContainer.css("padding", settings.autoHeight ? "20px 20px 50px 40px" : "20px"); + + if (settings.toolbar && !settings.readOnly) + { + preview.css("top", toolbar.height() + 1); + } + else + { + preview.css("top", 0); + } + + if (settings.autoHeight && !state.fullscreen && !state.preview) + { + preview.height(""); + } + else + { + var previewHeight = (settings.toolbar && !settings.readOnly) ? editor.height() - toolbar.height() : editor.height(); + + preview.height(previewHeight); + } + } + else + { + codeMirror.width(editor.width()); + preview.hide(); + } + + if (state.loaded) + { + $.proxy(settings.onresize, this)(); + } + + return this; + }, + + /** + * 解析和保存Markdown代码 + * Parse & Saving Markdown source code + * + * @returns {editormd} 返回editormd的实例对象 + */ + + save : function() { + + var _this = this; + var state = this.state; + var settings = this.settings; + + if (timer === null && !(!settings.watch && state.preview)) + { + return this; + } + + var cm = this.cm; + var cmValue = cm.getValue(); + var previewContainer = this.previewContainer; + + if (settings.mode !== "gfm" && settings.mode !== "markdown") + { + this.markdownTextarea.val(cmValue); + + return this; + } + + var marked = editormd.$marked; + var markdownToC = this.markdownToC = []; + var rendererOptions = this.markedRendererOptions = { + toc : settings.toc, + tocm : settings.tocm, + tocStartLevel : settings.tocStartLevel, + pageBreak : settings.pageBreak, + taskList : settings.taskList, + emoji : settings.emoji, + tex : settings.tex, + atLink : settings.atLink, // for @link + emailLink : settings.emailLink, // for mail address auto link + flowChart : settings.flowChart, + sequenceDiagram : settings.sequenceDiagram, + previewCodeHighlight : settings.previewCodeHighlight, + }; + + var markedOptions = this.markedOptions = { + renderer : editormd.markedRenderer(markdownToC, rendererOptions), + gfm : true, + tables : true, + breaks : true, + pedantic : false, + sanitize : (settings.htmlDecode) ? false : true, // 关闭忽略HTML标签,即开启识别HTML标签,默认为false + smartLists : true, + smartypants : true + }; + + marked.setOptions(markedOptions); + + var newMarkdownDoc = editormd.$marked(cmValue, markedOptions); + + //console.info("cmValue", cmValue, newMarkdownDoc); + + newMarkdownDoc = editormd.filterHTMLTags(newMarkdownDoc, settings.htmlDecode); + + //console.error("cmValue", cmValue, newMarkdownDoc); + + this.markdownTextarea.text(cmValue); + + cm.save(); + + if (settings.saveHTMLToTextarea) + { + this.htmlTextarea.text(newMarkdownDoc); + } + + if(settings.watch || (!settings.watch && state.preview)) + { + previewContainer.html(newMarkdownDoc); + + this.previewCodeHighlight(); + + if (settings.toc) + { + var tocContainer = (settings.tocContainer === "") ? previewContainer : $(settings.tocContainer); + var tocMenu = tocContainer.find("." + this.classPrefix + "toc-menu"); + + tocContainer.attr("previewContainer", (settings.tocContainer === "") ? "true" : "false"); + + if (settings.tocContainer !== "" && tocMenu.length > 0) + { + tocMenu.remove(); + } + + editormd.markdownToCRenderer(markdownToC, tocContainer, settings.tocDropdown, settings.tocStartLevel); + + if (settings.tocDropdown || tocContainer.find("." + this.classPrefix + "toc-menu").length > 0) + { + editormd.tocDropdownMenu(tocContainer, (settings.tocTitle !== "") ? settings.tocTitle : this.lang.tocTitle); + } + + if (settings.tocContainer !== "") + { + previewContainer.find(".markdown-toc").css("border", "none"); + } + } + + if (settings.tex) + { + if (!editormd.kaTeXLoaded && settings.autoLoadModules) + { + editormd.loadKaTeX(function() { + editormd.$katex = katex; + editormd.kaTeXLoaded = true; + _this.katexRender(); + }); + } + else + { + editormd.$katex = katex; + this.katexRender(); + } + } + + if (settings.flowChart || settings.sequenceDiagram) + { + flowchartTimer = setTimeout(function(){ + clearTimeout(flowchartTimer); + _this.flowChartAndSequenceDiagramRender(); + flowchartTimer = null; + }, 10); + } + + if (state.loaded) + { + $.proxy(settings.onchange, this)(); + } + } + + return this; + }, + + /** + * 聚焦光标位置 + * Focusing the cursor position + * + * @returns {editormd} 返回editormd的实例对象 + */ + + focus : function() { + this.cm.focus(); + + return this; + }, + + /** + * 设置光标的位置 + * Set cursor position + * + * @param {Object} cursor 要设置的光标位置键值对象,例:{line:1, ch:0} + * @returns {editormd} 返回editormd的实例对象 + */ + + setCursor : function(cursor) { + this.cm.setCursor(cursor); + + return this; + }, + + /** + * 获取当前光标的位置 + * Get the current position of the cursor + * + * @returns {Cursor} 返回一个光标Cursor对象 + */ + + getCursor : function() { + return this.cm.getCursor(); + }, + + /** + * 设置光标选中的范围 + * Set cursor selected ranges + * + * @param {Object} from 开始位置的光标键值对象,例:{line:1, ch:0} + * @param {Object} to 结束位置的光标键值对象,例:{line:1, ch:0} + * @returns {editormd} 返回editormd的实例对象 + */ + + setSelection : function(from, to) { + + this.cm.setSelection(from, to); + + return this; + }, + + /** + * 获取光标选中的文本 + * Get the texts from cursor selected + * + * @returns {String} 返回选中文本的字符串形式 + */ + + getSelection : function() { + return this.cm.getSelection(); + }, + + /** + * 设置光标选中的文本范围 + * Set the cursor selection ranges + * + * @param {Array} ranges cursor selection ranges array + * @returns {Array} return this + */ + + setSelections : function(ranges) { + this.cm.setSelections(ranges); + + return this; + }, + + /** + * 获取光标选中的文本范围 + * Get the cursor selection ranges + * + * @returns {Array} return selection ranges array + */ + + getSelections : function() { + return this.cm.getSelections(); + }, + + /** + * 替换当前光标选中的文本或在当前光标处插入新字符 + * Replace the text at the current cursor selected or insert a new character at the current cursor position + * + * @param {String} value 要插入的字符值 + * @returns {editormd} 返回editormd的实例对象 + */ + + replaceSelection : function(value) { + this.cm.replaceSelection(value); + + return this; + }, + + /** + * 在当前光标处插入新字符 + * Insert a new character at the current cursor position + * + * 同replaceSelection()方法 + * With the replaceSelection() method + * + * @param {String} value 要插入的字符值 + * @returns {editormd} 返回editormd的实例对象 + */ + + insertValue : function(value) { + this.replaceSelection(value); + + return this; + }, + + /** + * 追加markdown + * append Markdown to editor + * + * @param {String} md 要追加的markdown源文档 + * @returns {editormd} 返回editormd的实例对象 + */ + + appendMarkdown : function(md) { + var settings = this.settings; + var cm = this.cm; + + cm.setValue(cm.getValue() + md); + + return this; + }, + + /** + * 设置和传入编辑器的markdown源文档 + * Set Markdown source document + * + * @param {String} md 要传入的markdown源文档 + * @returns {editormd} 返回editormd的实例对象 + */ + + setMarkdown : function(md) { + this.cm.setValue(md || this.settings.markdown); + + return this; + }, + + /** + * 获取编辑器的markdown源文档 + * Set Editor.md markdown/CodeMirror value + * + * @returns {editormd} 返回editormd的实例对象 + */ + + getMarkdown : function() { + return this.cm.getValue(); + }, + + /** + * 获取编辑器的源文档 + * Get CodeMirror value + * + * @returns {editormd} 返回editormd的实例对象 + */ + + getValue : function() { + return this.cm.getValue(); + }, + + /** + * 设置编辑器的源文档 + * Set CodeMirror value + * + * @param {String} value set code/value/string/text + * @returns {editormd} 返回editormd的实例对象 + */ + + setValue : function(value) { + this.cm.setValue(value); + + return this; + }, + + /** + * 清空编辑器 + * Empty CodeMirror editor container + * + * @returns {editormd} 返回editormd的实例对象 + */ + + clear : function() { + this.cm.setValue(""); + + return this; + }, + + /** + * 获取解析后存放在Textarea的HTML源码 + * Get parsed html code from Textarea + * + * @returns {String} 返回HTML源码 + */ + + getHTML : function() { + if (!this.settings.saveHTMLToTextarea) + { + alert("Error: settings.saveHTMLToTextarea == false"); + + return false; + } + + return this.htmlTextarea.val(); + }, + + /** + * getHTML()的别名 + * getHTML (alias) + * + * @returns {String} Return html code 返回HTML源码 + */ + + getTextareaSavedHTML : function() { + return this.getHTML(); + }, + + /** + * 获取预览窗口的HTML源码 + * Get html from preview container + * + * @returns {editormd} 返回editormd的实例对象 + */ + + getPreviewedHTML : function() { + if (!this.settings.watch) + { + alert("Error: settings.watch == false"); + + return false; + } + + return this.previewContainer.html(); + }, + + /** + * 开启实时预览 + * Enable real-time watching + * + * @returns {editormd} 返回editormd的实例对象 + */ + + watch : function(callback) { + var settings = this.settings; + + if ($.inArray(settings.mode, ["gfm", "markdown"]) < 0) + { + return this; + } + + this.state.watching = settings.watch = true; + this.preview.show(); + + if (this.toolbar) + { + var watchIcon = settings.toolbarIconsClass.watch; + var unWatchIcon = settings.toolbarIconsClass.unwatch; + + var icon = this.toolbar.find(".fa[name=watch]"); + icon.parent().attr("title", settings.lang.toolbar.watch); + icon.removeClass(unWatchIcon).addClass(watchIcon); + } + + this.codeMirror.css("border-right", "1px solid #ddd").width(this.editor.width() / 2); + + timer = 0; + + this.save().resize(); + + if (!settings.onwatch) + { + settings.onwatch = callback || function() {}; + } + + $.proxy(settings.onwatch, this)(); + + return this; + }, + + /** + * 关闭实时预览 + * Disable real-time watching + * + * @returns {editormd} 返回editormd的实例对象 + */ + + unwatch : function(callback) { + var settings = this.settings; + this.state.watching = settings.watch = false; + this.preview.hide(); + + if (this.toolbar) + { + var watchIcon = settings.toolbarIconsClass.watch; + var unWatchIcon = settings.toolbarIconsClass.unwatch; + + var icon = this.toolbar.find(".fa[name=watch]"); + icon.parent().attr("title", settings.lang.toolbar.unwatch); + icon.removeClass(watchIcon).addClass(unWatchIcon); + } + + this.codeMirror.css("border-right", "none").width(this.editor.width()); + + this.resize(); + + if (!settings.onunwatch) + { + settings.onunwatch = callback || function() {}; + } + + $.proxy(settings.onunwatch, this)(); + + return this; + }, + + /** + * 显示编辑器 + * Show editor + * + * @param {Function} [callback=function()] 回调函数 + * @returns {editormd} 返回editormd的实例对象 + */ + + show : function(callback) { + callback = callback || function() {}; + + var _this = this; + this.editor.show(0, function() { + $.proxy(callback, _this)(); + }); + + return this; + }, + + /** + * 隐藏编辑器 + * Hide editor + * + * @param {Function} [callback=function()] 回调函数 + * @returns {editormd} 返回editormd的实例对象 + */ + + hide : function(callback) { + callback = callback || function() {}; + + var _this = this; + this.editor.hide(0, function() { + $.proxy(callback, _this)(); + }); + + return this; + }, + + /** + * 隐藏编辑器部分,只预览HTML + * Enter preview html state + * + * @returns {editormd} 返回editormd的实例对象 + */ + + previewing : function() { + + var _this = this; + var editor = this.editor; + var preview = this.preview; + var toolbar = this.toolbar; + var settings = this.settings; + var codeMirror = this.codeMirror; + var previewContainer = this.previewContainer; + + if ($.inArray(settings.mode, ["gfm", "markdown"]) < 0) { + return this; + } + + if (settings.toolbar && toolbar) { + toolbar.toggle(); + toolbar.find(".fa[name=preview]").toggleClass("active"); + } + + codeMirror.toggle(); + + var escHandle = function(event) { + if (event.shiftKey && event.keyCode === 27) { + _this.previewed(); + } + }; + + if (codeMirror.css("display") === "none") // 为了兼容Zepto,而不使用codeMirror.is(":hidden") + { + this.state.preview = true; + + if (this.state.fullscreen) { + preview.css("background", "#fff"); + } + + editor.find("." + this.classPrefix + "preview-close-btn").show().bind(editormd.mouseOrTouch("click", "touchend"), function(){ + _this.previewed(); + }); + + if (!settings.watch) + { + this.save(); + } + else + { + previewContainer.css("padding", ""); + } + + previewContainer.addClass(this.classPrefix + "preview-active"); + + preview.show().css({ + position : "", + top : 0, + width : editor.width(), + height : (settings.autoHeight && !this.state.fullscreen) ? "auto" : editor.height() + }); + + if (this.state.loaded) + { + $.proxy(settings.onpreviewing, this)(); + } + + $(window).bind("keyup", escHandle); + } + else + { + $(window).unbind("keyup", escHandle); + this.previewed(); + } + }, + + /** + * 显示编辑器部分,退出只预览HTML + * Exit preview html state + * + * @returns {editormd} 返回editormd的实例对象 + */ + + previewed : function() { + + var editor = this.editor; + var preview = this.preview; + var toolbar = this.toolbar; + var settings = this.settings; + var previewContainer = this.previewContainer; + var previewCloseBtn = editor.find("." + this.classPrefix + "preview-close-btn"); + + this.state.preview = false; + + this.codeMirror.show(); + + if (settings.toolbar) { + toolbar.show(); + } + + preview[(settings.watch) ? "show" : "hide"](); + + previewCloseBtn.hide().unbind(editormd.mouseOrTouch("click", "touchend")); + + previewContainer.removeClass(this.classPrefix + "preview-active"); + + if (settings.watch) + { + previewContainer.css("padding", "20px"); + } + + preview.css({ + background : null, + position : "absolute", + width : editor.width() / 2, + height : (settings.autoHeight && !this.state.fullscreen) ? "auto" : editor.height() - toolbar.height(), + top : (settings.toolbar) ? toolbar.height() : 0 + }); + + if (this.state.loaded) + { + $.proxy(settings.onpreviewed, this)(); + } + + return this; + }, + + /** + * 编辑器全屏显示 + * Fullscreen show + * + * @returns {editormd} 返回editormd的实例对象 + */ + + fullscreen : function() { + + var _this = this; + var state = this.state; + var editor = this.editor; + var preview = this.preview; + var toolbar = this.toolbar; + var settings = this.settings; + var fullscreenClass = this.classPrefix + "fullscreen"; + + if (toolbar) { + toolbar.find(".fa[name=fullscreen]").parent().toggleClass("active"); + } + + var escHandle = function(event) { + if (!event.shiftKey && event.keyCode === 27) + { + if (state.fullscreen) + { + _this.fullscreenExit(); + } + } + }; + + if (!editor.hasClass(fullscreenClass)) + { + state.fullscreen = true; + + $("html,body").css("overflow", "hidden"); + + editor.css({ + width : $(window).width(), + height : $(window).height() + }).addClass(fullscreenClass); + + this.resize(); + + $.proxy(settings.onfullscreen, this)(); + + $(window).bind("keyup", escHandle); + } + else + { + $(window).unbind("keyup", escHandle); + this.fullscreenExit(); + } + + return this; + }, + + /** + * 编辑器退出全屏显示 + * Exit fullscreen state + * + * @returns {editormd} 返回editormd的实例对象 + */ + + fullscreenExit : function() { + + var editor = this.editor; + var settings = this.settings; + var toolbar = this.toolbar; + var fullscreenClass = this.classPrefix + "fullscreen"; + + this.state.fullscreen = false; + + if (toolbar) { + toolbar.find(".fa[name=fullscreen]").parent().removeClass("active"); + } + + $("html,body").css("overflow", ""); + + editor.css({ + width : editor.data("oldWidth"), + height : editor.data("oldHeight") + }).removeClass(fullscreenClass); + + this.resize(); + + $.proxy(settings.onfullscreenExit, this)(); + + return this; + }, + + /** + * 加载并执行插件 + * Load and execute the plugin + * + * @param {String} name plugin name / function name + * @param {String} path plugin load path + * @returns {editormd} 返回editormd的实例对象 + */ + + executePlugin : function(name, path) { + + var _this = this; + var cm = this.cm; + var settings = this.settings; + + path = settings.pluginPath + path; + + if (typeof define === "function") + { + if (typeof this[name] === "undefined") + { + alert("Error: " + name + " plugin is not found, you are not load this plugin."); + + return this; + } + + this[name](cm); + + return this; + } + + if ($.inArray(path, editormd.loadFiles.plugin) < 0) + { + editormd.loadPlugin(path, function() { + editormd.loadPlugins[name] = _this[name]; + _this[name](cm); + }); + } + else + { + $.proxy(editormd.loadPlugins[name], this)(cm); + } + + return this; + }, + + /** + * 搜索替换 + * Search & replace + * + * @param {String} command CodeMirror serach commands, "find, fintNext, fintPrev, clearSearch, replace, replaceAll" + * @returns {editormd} return this + */ + + search : function(command) { + var settings = this.settings; + + if (!settings.searchReplace) + { + alert("Error: settings.searchReplace == false"); + return this; + } + + if (!settings.readOnly) + { + this.cm.execCommand(command || "find"); + } + + return this; + }, + + searchReplace : function() { + this.search("replace"); + + return this; + }, + + searchReplaceAll : function() { + this.search("replaceAll"); + + return this; + } + }; + + editormd.fn.init.prototype = editormd.fn; + + /** + * 锁屏 + * lock screen when dialog opening + * + * @returns {void} + */ + + editormd.dialogLockScreen = function() { + var settings = this.settings || {dialogLockScreen : true}; + + if (settings.dialogLockScreen) + { + $("html,body").css("overflow", "hidden"); + this.resize(); + } + }; + + /** + * 显示透明背景层 + * Display mask layer when dialog opening + * + * @param {Object} dialog dialog jQuery object + * @returns {void} + */ + + editormd.dialogShowMask = function(dialog) { + var editor = this.editor; + var settings = this.settings || {dialogShowMask : true}; + + dialog.css({ + top : ($(window).height() - dialog.height()) / 2 + "px", + left : ($(window).width() - dialog.width()) / 2 + "px" + }); + + if (settings.dialogShowMask) { + editor.children("." + this.classPrefix + "mask").css("z-index", parseInt(dialog.css("z-index")) - 1).show(); + } + }; + + editormd.toolbarHandlers = { + undo : function() { + this.cm.undo(); + }, + + redo : function() { + this.cm.redo(); + }, + + bold : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection("**" + selection + "**"); + + if(selection === "") { + cm.setCursor(cursor.line, cursor.ch + 2); + } + }, + + del : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection("~~" + selection + "~~"); + + if(selection === "") { + cm.setCursor(cursor.line, cursor.ch + 2); + } + }, + + italic : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection("*" + selection + "*"); + + if(selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + }, + + quote : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("> " + selection); + cm.setCursor(cursor.line, cursor.ch + 2); + } + else + { + cm.replaceSelection("> " + selection); + } + + //cm.replaceSelection("> " + selection); + //cm.setCursor(cursor.line, (selection === "") ? cursor.ch + 2 : cursor.ch + selection.length + 2); + }, + + ucfirst : function() { + var cm = this.cm; + var selection = cm.getSelection(); + var selections = cm.listSelections(); + + cm.replaceSelection(editormd.firstUpperCase(selection)); + cm.setSelections(selections); + }, + + ucwords : function() { + var cm = this.cm; + var selection = cm.getSelection(); + var selections = cm.listSelections(); + + cm.replaceSelection(editormd.wordsFirstUpperCase(selection)); + cm.setSelections(selections); + }, + + uppercase : function() { + var cm = this.cm; + var selection = cm.getSelection(); + var selections = cm.listSelections(); + + cm.replaceSelection(selection.toUpperCase()); + cm.setSelections(selections); + }, + + lowercase : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + var selections = cm.listSelections(); + + cm.replaceSelection(selection.toLowerCase()); + cm.setSelections(selections); + }, + + h1 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("# " + selection); + cm.setCursor(cursor.line, cursor.ch + 2); + } + else + { + cm.replaceSelection("# " + selection); + } + }, + + h2 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("## " + selection); + cm.setCursor(cursor.line, cursor.ch + 3); + } + else + { + cm.replaceSelection("## " + selection); + } + }, + + h3 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("### " + selection); + cm.setCursor(cursor.line, cursor.ch + 4); + } + else + { + cm.replaceSelection("### " + selection); + } + }, + + h4 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("#### " + selection); + cm.setCursor(cursor.line, cursor.ch + 5); + } + else + { + cm.replaceSelection("#### " + selection); + } + }, + + h5 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("##### " + selection); + cm.setCursor(cursor.line, cursor.ch + 6); + } + else + { + cm.replaceSelection("##### " + selection); + } + }, + + h6 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("###### " + selection); + cm.setCursor(cursor.line, cursor.ch + 7); + } + else + { + cm.replaceSelection("###### " + selection); + } + }, + + "list-ul" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (selection === "") + { + cm.replaceSelection("- " + selection); + } + else + { + var selectionText = selection.split("\n"); + + for (var i = 0, len = selectionText.length; i < len; i++) + { + selectionText[i] = (selectionText[i] === "") ? "" : "- " + selectionText[i]; + } + + cm.replaceSelection(selectionText.join("\n")); + } + }, + + "list-ol" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if(selection === "") + { + cm.replaceSelection("1. " + selection); + } + else + { + var selectionText = selection.split("\n"); + + for (var i = 0, len = selectionText.length; i < len; i++) + { + selectionText[i] = (selectionText[i] === "") ? "" : (i+1) + ". " + selectionText[i]; + } + + cm.replaceSelection(selectionText.join("\n")); + } + }, + + hr : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection(((cursor.ch !== 0) ? "\n\n" : "\n") + "------------\n\n"); + }, + + tex : function() { + if (!this.settings.tex) + { + alert("settings.tex === false"); + return this; + } + + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection("$$" + selection + "$$"); + + if(selection === "") { + cm.setCursor(cursor.line, cursor.ch + 2); + } + }, + + link : function() { + this.executePlugin("linkDialog", "link-dialog/link-dialog"); + }, + + "reference-link" : function() { + this.executePlugin("referenceLinkDialog", "reference-link-dialog/reference-link-dialog"); + }, + + pagebreak : function() { + if (!this.settings.pageBreak) + { + alert("settings.pageBreak === false"); + return this; + } + + var cm = this.cm; + var selection = cm.getSelection(); + + cm.replaceSelection("\r\n[========]\r\n"); + }, + + image : function() { + this.executePlugin("imageDialog", "image-dialog/image-dialog"); + }, + + code : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection("`" + selection + "`"); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + }, + + "code-block" : function() { + this.executePlugin("codeBlockDialog", "code-block-dialog/code-block-dialog"); + }, + + "preformatted-text" : function() { + this.executePlugin("preformattedTextDialog", "preformatted-text-dialog/preformatted-text-dialog"); + }, + + table : function() { + this.executePlugin("tableDialog", "table-dialog/table-dialog"); + }, + + datetime : function() { + var cm = this.cm; + var selection = cm.getSelection(); + var date = new Date(); + var langName = this.settings.lang.name; + var datefmt = editormd.dateFormat() + " " + editormd.dateFormat((langName === "zh-cn" || langName === "zh-tw") ? "cn-week-day" : "week-day"); + + cm.replaceSelection(datefmt); + }, + + emoji : function() { + this.executePlugin("emojiDialog", "emoji-dialog/emoji-dialog"); + }, + + "html-entities" : function() { + this.executePlugin("htmlEntitiesDialog", "html-entities-dialog/html-entities-dialog"); + }, + + "goto-line" : function() { + this.executePlugin("gotoLineDialog", "goto-line-dialog/goto-line-dialog"); + }, + + watch : function() { + this[this.settings.watch ? "unwatch" : "watch"](); + }, + + preview : function() { + this.previewing(); + }, + + fullscreen : function() { + this.fullscreen(); + }, + + clear : function() { + this.clear(); + }, + + search : function() { + this.search(); + }, + + help : function() { + this.executePlugin("helpDialog", "help-dialog/help-dialog"); + }, + + info : function() { + this.showInfoDialog(); + } + }; + + editormd.keyMaps = { + "Ctrl-1" : "h1", + "Ctrl-2" : "h2", + "Ctrl-3" : "h3", + "Ctrl-4" : "h4", + "Ctrl-5" : "h5", + "Ctrl-6" : "h6", + "Ctrl-B" : "bold", // if this is string == editormd.toolbarHandlers.xxxx + "Ctrl-D" : "datetime", + + "Ctrl-E" : function() { // emoji + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (!this.settings.emoji) + { + alert("Error: settings.emoji == false"); + return ; + } + + cm.replaceSelection(":" + selection + ":"); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + }, + "Ctrl-Alt-G" : "goto-line", + "Ctrl-H" : "hr", + "Ctrl-I" : "italic", + "Ctrl-K" : "code", + + "Ctrl-L" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + var title = (selection === "") ? "" : " \""+selection+"\""; + + cm.replaceSelection("[" + selection + "]("+title+")"); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + }, + "Ctrl-U" : "list-ul", + + "Shift-Ctrl-A" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (!this.settings.atLink) + { + alert("Error: settings.atLink == false"); + return ; + } + + cm.replaceSelection("@" + selection); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + }, + + "Shift-Ctrl-C" : "code", + "Shift-Ctrl-Q" : "quote", + "Shift-Ctrl-S" : "del", + "Shift-Ctrl-K" : "tex", // KaTeX + + "Shift-Alt-C" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection(["```", selection, "```"].join("\n")); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 3); + } + }, + + "Shift-Ctrl-Alt-C" : "code-block", + "Shift-Ctrl-H" : "html-entities", + "Shift-Alt-H" : "help", + "Shift-Ctrl-E" : "emoji", + "Shift-Ctrl-U" : "uppercase", + "Shift-Alt-U" : "ucwords", + "Shift-Ctrl-Alt-U" : "ucfirst", + "Shift-Alt-L" : "lowercase", + + "Shift-Ctrl-I" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + var title = (selection === "") ? "" : " \""+selection+"\""; + + cm.replaceSelection("![" + selection + "]("+title+")"); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 4); + } + }, + + "Shift-Ctrl-Alt-I" : "image", + "Shift-Ctrl-L" : "link", + "Shift-Ctrl-O" : "list-ol", + "Shift-Ctrl-P" : "preformatted-text", + "Shift-Ctrl-T" : "table", + "Shift-Alt-P" : "pagebreak", + "F9" : "watch", + "F10" : "preview", + "F11" : "fullscreen", + }; + + /** + * 清除字符串两边的空格 + * Clear the space of strings both sides. + * + * @param {String} str string + * @returns {String} trimed string + */ + + var trim = function(str) { + return (!String.prototype.trim) ? str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "") : str.trim(); + }; + + editormd.trim = trim; + + /** + * 所有单词首字母大写 + * Words first to uppercase + * + * @param {String} str string + * @returns {String} string + */ + + var ucwords = function (str) { + return str.toLowerCase().replace(/\b(\w)|\s(\w)/g, function($1) { + return $1.toUpperCase(); + }); + }; + + editormd.ucwords = editormd.wordsFirstUpperCase = ucwords; + + /** + * 字符串首字母大写 + * Only string first char to uppercase + * + * @param {String} str string + * @returns {String} string + */ + + var firstUpperCase = function(str) { + return str.toLowerCase().replace(/\b(\w)/, function($1){ + return $1.toUpperCase(); + }); + }; + + var ucfirst = firstUpperCase; + + editormd.firstUpperCase = editormd.ucfirst = firstUpperCase; + + editormd.urls = { + atLinkBase : "https://github.com/" + }; + + editormd.regexs = { + atLink : /@(\w+)/g, + email : /(\w+)@(\w+)\.(\w+)\.?(\w+)?/g, + emailLink : /(mailto:)?([\w\.\_]+)@(\w+)\.(\w+)\.?(\w+)?/g, + emoji : /:([\w\+-]+):/g, + emojiDatetime : /(\d{2}:\d{2}:\d{2})/g, + twemoji : /:(tw-([\w]+)-?(\w+)?):/g, + fontAwesome : /:(fa-([\w]+)(-(\w+)){0,}):/g, + editormdLogo : /:(editormd-logo-?(\w+)?):/g, + pageBreak : /^\[[=]{8,}\]$/ + }; + + // Emoji graphics files url path + editormd.emoji = { + path : "https://www.webpagefx.com/tools/emoji-cheat-sheet/graphics/emojis/", + ext : ".png" + }; + + // Twitter Emoji (Twemoji) graphics files url path + editormd.twemoji = { + path : "http://twemoji.maxcdn.com/36x36/", + ext : ".png" + }; + + /** + * 自定义marked的解析器 + * Custom Marked renderer rules + * + * @param {Array} markdownToC 传入用于接收TOC的数组 + * @returns {Renderer} markedRenderer 返回marked的Renderer自定义对象 + */ + + editormd.markedRenderer = function(markdownToC, options) { + var defaults = { + toc : true, // Table of contents + tocm : false, + tocStartLevel : 1, // Said from H1 to create ToC + pageBreak : true, + atLink : true, // for @link + emailLink : true, // for mail address auto link + taskList : false, // Enable Github Flavored Markdown task lists + emoji : false, // :emoji: , Support Twemoji, fontAwesome, Editor.md logo emojis. + tex : false, // TeX(LaTeX), based on KaTeX + flowChart : false, // flowChart.js only support IE9+ + sequenceDiagram : false, // sequenceDiagram.js only support IE9+ + }; + + var settings = $.extend(defaults, options || {}); + var marked = editormd.$marked; + var markedRenderer = new marked.Renderer(); + markdownToC = markdownToC || []; + + var regexs = editormd.regexs; + var atLinkReg = regexs.atLink; + var emojiReg = regexs.emoji; + var emailReg = regexs.email; + var emailLinkReg = regexs.emailLink; + var twemojiReg = regexs.twemoji; + var faIconReg = regexs.fontAwesome; + var editormdLogoReg = regexs.editormdLogo; + var pageBreakReg = regexs.pageBreak; + + markedRenderer.emoji = function(text) { + + text = text.replace(editormd.regexs.emojiDatetime, function($1) { + return $1.replace(/:/g, ":"); + }); + + var matchs = text.match(emojiReg); + + if (!matchs || !settings.emoji) { + return text; + } + + for (var i = 0, len = matchs.length; i < len; i++) + { + if (matchs[i] === ":+1:") { + matchs[i] = ":\\+1:"; + } + + text = text.replace(new RegExp(matchs[i]), function($1, $2){ + var faMatchs = $1.match(faIconReg); + var name = $1.replace(/:/g, ""); + + if (faMatchs) + { + for (var fa = 0, len1 = faMatchs.length; fa < len1; fa++) + { + var faName = faMatchs[fa].replace(/:/g, ""); + + return ""; + } + } + else + { + var emdlogoMathcs = $1.match(editormdLogoReg); + var twemojiMatchs = $1.match(twemojiReg); + + if (emdlogoMathcs) + { + for (var x = 0, len2 = emdlogoMathcs.length; x < len2; x++) + { + var logoName = emdlogoMathcs[x].replace(/:/g, ""); + return ""; + } + } + else if (twemojiMatchs) + { + for (var t = 0, len3 = twemojiMatchs.length; t < len3; t++) + { + var twe = twemojiMatchs[t].replace(/:/g, "").replace("tw-", ""); + return "\"twemoji-""; + } + } + else + { + var src = (name === "+1") ? "plus1" : name; + src = (src === "black_large_square") ? "black_square" : src; + src = (src === "moon") ? "waxing_gibbous_moon" : src; + + return "\":""; + } + } + }); + } + + return text; + }; + + markedRenderer.atLink = function(text) { + + if (atLinkReg.test(text)) + { + if (settings.atLink) + { + text = text.replace(emailReg, function($1, $2, $3, $4) { + return $1.replace(/@/g, "_#_@_#_"); + }); + + text = text.replace(atLinkReg, function($1, $2) { + return "" + $1 + ""; + }).replace(/_#_@_#_/g, "@"); + } + + if (settings.emailLink) + { + text = text.replace(emailLinkReg, function($1, $2, $3, $4, $5) { + return (!$2 && $.inArray($5, "jpg|jpeg|png|gif|webp|ico|icon|pdf".split("|")) < 0) ? ""+$1+"" : $1; + }); + } + + return text; + } + + return text; + }; + + markedRenderer.link = function (href, title, text) { + + if (this.options.sanitize) { + try { + var prot = decodeURIComponent(unescape(href)).replace(/[^\w:]/g,"").toLowerCase(); + } catch(e) { + return ""; + } + + if (prot.indexOf("javascript:") === 0) { + return ""; + } + } + + var out = "" + text.replace(/@/g, "@") + ""; + } + + if (title) { + out += " title=\"" + title + "\""; + } + + out += ">" + text + ""; + + return out; + }; + + markedRenderer.heading = function(text, level, raw) { + + var linkText = text; + var hasLinkReg = /\s*\]*)\>(.*)\<\/a\>\s*/; + var getLinkTextReg = /\s*\]+)\>([^\>]*)\<\/a\>\s*/g; + + if (hasLinkReg.test(text)) + { + var tempText = []; + text = text.split(/\]+)\>([^\>]*)\<\/a\>/); + + for (var i = 0, len = text.length; i < len; i++) + { + tempText.push(text[i].replace(/\s*href\=\"(.*)\"\s*/g, "")); + } + + text = tempText.join(" "); + } + + text = trim(text); + + var escapedText = text.toLowerCase().replace(/[^\w]+/g, "-"); + var toc = { + text : text, + level : level, + slug : escapedText + }; + + var isChinese = /^[\u4e00-\u9fa5]+$/.test(text); + var id = (isChinese) ? escape(text).replace(/\%/g, "") : text.toLowerCase().replace(/[^\w]+/g, "-"); + + markdownToC.push(toc); + + var headingHTML = ""; + + headingHTML += ""; + headingHTML += ""; + headingHTML += (hasLinkReg) ? this.atLink(this.emoji(linkText)) : this.atLink(this.emoji(text)); + headingHTML += ""; + + return headingHTML; + }; + + markedRenderer.pageBreak = function(text) { + if (pageBreakReg.test(text) && settings.pageBreak) + { + text = "
              "; + } + + return text; + }; + + markedRenderer.paragraph = function(text) { + var isTeXInline = /\$\$(.*)\$\$/g.test(text); + var isTeXLine = /^\$\$(.*)\$\$$/.test(text); + var isTeXAddClass = (isTeXLine) ? " class=\"" + editormd.classNames.tex + "\"" : ""; + var isToC = (settings.tocm) ? /^(\[TOC\]|\[TOCM\])$/.test(text) : /^\[TOC\]$/.test(text); + var isToCMenu = /^\[TOCM\]$/.test(text); + + if (!isTeXLine && isTeXInline) + { + text = text.replace(/(\$\$([^\$]*)\$\$)+/g, function($1, $2) { + return "" + $2.replace(/\$/g, "") + ""; + }); + } + else + { + text = (isTeXLine) ? text.replace(/\$/g, "") : text; + } + + var tocHTML = "
              " + text + "
              "; + + return (isToC) ? ( (isToCMenu) ? "
              " + tocHTML + "

              " : tocHTML ) + : ( (pageBreakReg.test(text)) ? this.pageBreak(text) : "" + this.atLink(this.emoji(text)) + "

              \n" ); + }; + + markedRenderer.code = function (code, lang, escaped) { + + if (lang === "seq" || lang === "sequence") + { + return "
              " + code + "
              "; + } + else if ( lang === "flow") + { + return "
              " + code + "
              "; + } + else if ( lang === "math" || lang === "latex" || lang === "katex") + { + return "

              " + code + "

              "; + } + else + { + + return marked.Renderer.prototype.code.apply(this, arguments); + } + }; + + markedRenderer.tablecell = function(content, flags) { + var type = (flags.header) ? "th" : "td"; + var tag = (flags.align) ? "<" + type +" style=\"text-align:" + flags.align + "\">" : "<" + type + ">"; + + return tag + this.atLink(this.emoji(content)) + "\n"; + }; + + markedRenderer.listitem = function(text) { + if (settings.taskList && /^\s*\[[x\s]\]\s*/.test(text)) + { + text = text.replace(/^\s*\[\s\]\s*/, " ") + .replace(/^\s*\[x\]\s*/, " "); + + return "
            • " + this.atLink(this.emoji(text)) + "
            • "; + } + else + { + return "
            • " + this.atLink(this.emoji(text)) + "
            • "; + } + }; + + return markedRenderer; + }; + + /** + * + * 生成TOC(Table of Contents) + * Creating ToC (Table of Contents) + * + * @param {Array} toc 从marked获取的TOC数组列表 + * @param {Element} container 插入TOC的容器元素 + * @param {Integer} startLevel Hx 起始层级 + * @returns {Object} tocContainer 返回ToC列表容器层的jQuery对象元素 + */ + + editormd.markdownToCRenderer = function(toc, container, tocDropdown, startLevel) { + + var html = ""; + var lastLevel = 0; + var classPrefix = this.classPrefix; + + startLevel = startLevel || 1; + + for (var i = 0, len = toc.length; i < len; i++) + { + var text = toc[i].text; + var level = toc[i].level; + + if (level < startLevel) { + continue; + } + + if (level > lastLevel) + { + html += ""; + } + else if (level < lastLevel) + { + html += (new Array(lastLevel - level + 2)).join("
          • "); + } + else + { + html += ""; + } + + html += "
          • " + text + "
              "; + lastLevel = level; + } + + var tocContainer = container.find(".markdown-toc"); + + if ((tocContainer.length < 1 && container.attr("previewContainer") === "false")) + { + var tocHTML = "
              "; + + tocHTML = (tocDropdown) ? "
              " + tocHTML + "
              " : tocHTML; + + container.html(tocHTML); + + tocContainer = container.find(".markdown-toc"); + } + + if (tocDropdown) + { + tocContainer.wrap("

              "); + } + + tocContainer.html("
                ").children(".markdown-toc-list").html(html.replace(/\r?\n?\\<\/ul\>/g, "")); + + return tocContainer; + }; + + /** + * + * 生成TOC下拉菜单 + * Creating ToC dropdown menu + * + * @param {Object} container 插入TOC的容器jQuery对象元素 + * @param {String} tocTitle ToC title + * @returns {Object} return toc-menu object + */ + + editormd.tocDropdownMenu = function(container, tocTitle) { + + tocTitle = tocTitle || "Table of Contents"; + + var zindex = 400; + var tocMenus = container.find("." + this.classPrefix + "toc-menu"); + + tocMenus.each(function() { + var $this = $(this); + var toc = $this.children(".markdown-toc"); + var icon = ""; + var btn = "" + icon + tocTitle + ""; + var menu = toc.children("ul"); + var list = menu.find("li"); + + toc.append(btn); + + list.first().before("
              • " + tocTitle + " " + icon + "

              • "); + + $this.mouseover(function(){ + menu.show(); + + list.each(function(){ + var li = $(this); + var ul = li.children("ul"); + + if (ul.html() === "") + { + ul.remove(); + } + + if (ul.length > 0 && ul.html() !== "") + { + var firstA = li.children("a").first(); + + if (firstA.children(".fa").length < 1) + { + firstA.append( $(icon).css({ float:"right", paddingTop:"4px" }) ); + } + } + + li.mouseover(function(){ + ul.css("z-index", zindex).show(); + zindex += 1; + }).mouseleave(function(){ + ul.hide(); + }); + }); + }).mouseleave(function(){ + menu.hide(); + }); + }); + + return tocMenus; + }; + + /** + * 简单地过滤指定的HTML标签 + * Filter custom html tags + * + * @param {String} html 要过滤HTML + * @param {String} filters 要过滤的标签 + * @returns {String} html 返回过滤的HTML + */ + + editormd.filterHTMLTags = function(html, filters) { + + if (typeof html !== "string") { + html = new String(html); + } + + if (typeof filters !== "string") { + return html; + } + + var expression = filters.split("|"); + var filterTags = expression[0].split(","); + var attrs = expression[1]; + + for (var i = 0, len = filterTags.length; i < len; i++) + { + var tag = filterTags[i]; + + html = html.replace(new RegExp("\<\s*" + tag + "\s*([^\>]*)\>([^\>]*)\<\s*\/" + tag + "\s*\>", "igm"), ""); + } + + //return html; + + if (typeof attrs !== "undefined") + { + var htmlTagRegex = /\<(\w+)\s*([^\>]*)\>([^\>]*)\<\/(\w+)\>/ig; + + if (attrs === "*") + { + html = html.replace(htmlTagRegex, function($1, $2, $3, $4, $5) { + return "<" + $2 + ">" + $4 + ""; + }); + } + else if (attrs === "on*") + { + html = html.replace(htmlTagRegex, function($1, $2, $3, $4, $5) { + var el = $("<" + $2 + ">" + $4 + ""); + var _attrs = $($1)[0].attributes; + var $attrs = {}; + + $.each(_attrs, function(i, e) { + if (e.nodeName !== '"') $attrs[e.nodeName] = e.nodeValue; + }); + + $.each($attrs, function(i) { + if (i.indexOf("on") === 0) { + delete $attrs[i]; + } + }); + + el.attr($attrs); + + var text = (typeof el[1] !== "undefined") ? $(el[1]).text() : ""; + + return el[0].outerHTML + text; + }); + } + else + { + html = html.replace(htmlTagRegex, function($1, $2, $3, $4) { + var filterAttrs = attrs.split(","); + var el = $($1); + el.html($4); + + $.each(filterAttrs, function(i) { + el.attr(filterAttrs[i], null); + }); + + return el[0].outerHTML; + }); + } + } + + return html; + }; + + /** + * 将Markdown文档解析为HTML用于前台显示 + * Parse Markdown to HTML for Font-end preview. + * + * @param {String} id 用于显示HTML的对象ID + * @param {Object} [options={}] 配置选项,可选 + * @returns {Object} div 返回jQuery对象元素 + */ + + editormd.markdownToHTML = function(id, options) { + var defaults = { + gfm : true, + toc : true, + tocm : false, + tocStartLevel : 1, + tocTitle : "目录", + tocDropdown : false, + tocContainer : "", + markdown : "", + markdownSourceCode : false, + htmlDecode : false, + autoLoadKaTeX : true, + pageBreak : true, + atLink : true, // for @link + emailLink : true, // for mail address auto link + tex : false, + taskList : false, // Github Flavored Markdown task lists + emoji : false, + flowChart : false, + sequenceDiagram : false, + previewCodeHighlight : true + }; + + editormd.$marked = marked; + + var div = $("#" + id); + var settings = div.settings = $.extend(true, defaults, options || {}); + var saveTo = div.find("textarea"); + + if (saveTo.length < 1) + { + div.append(""); + saveTo = div.find("textarea"); + } + + var markdownDoc = (settings.markdown === "") ? saveTo.val() : settings.markdown; + var markdownToC = []; + + var rendererOptions = { + toc : settings.toc, + tocm : settings.tocm, + tocStartLevel : settings.tocStartLevel, + taskList : settings.taskList, + emoji : settings.emoji, + tex : settings.tex, + pageBreak : settings.pageBreak, + atLink : settings.atLink, // for @link + emailLink : settings.emailLink, // for mail address auto link + flowChart : settings.flowChart, + sequenceDiagram : settings.sequenceDiagram, + previewCodeHighlight : settings.previewCodeHighlight, + }; + + var markedOptions = { + renderer : editormd.markedRenderer(markdownToC, rendererOptions), + gfm : settings.gfm, + tables : true, + breaks : true, + pedantic : false, + sanitize : (settings.htmlDecode) ? false : true, // 是否忽略HTML标签,即是否开启HTML标签解析,为了安全性,默认不开启 + smartLists : true, + smartypants : true + }; + + markdownDoc = new String(markdownDoc); + + var markdownParsed = marked(markdownDoc, markedOptions); + + markdownParsed = editormd.filterHTMLTags(markdownParsed, settings.htmlDecode); + + if (settings.markdownSourceCode) { + saveTo.text(markdownDoc); + } else { + saveTo.remove(); + } + + div.addClass("markdown-body " + this.classPrefix + "html-preview").append(markdownParsed); + + var tocContainer = (settings.tocContainer !== "") ? $(settings.tocContainer) : div; + + if (settings.tocContainer !== "") + { + tocContainer.attr("previewContainer", false); + } + + if (settings.toc) + { + div.tocContainer = this.markdownToCRenderer(markdownToC, tocContainer, settings.tocDropdown, settings.tocStartLevel); + + if (settings.tocDropdown || div.find("." + this.classPrefix + "toc-menu").length > 0) + { + this.tocDropdownMenu(div, settings.tocTitle); + } + + if (settings.tocContainer !== "") + { + div.find(".editormd-toc-menu, .editormd-markdown-toc").remove(); + } + } + + if (settings.previewCodeHighlight) + { + div.find("pre").addClass("prettyprint linenums"); + prettyPrint(); + } + + if (!editormd.isIE8) + { + if (settings.flowChart) { + div.find(".flowchart").flowChart(); + } + + if (settings.sequenceDiagram) { + div.find(".sequence-diagram").sequenceDiagram({theme: "simple"}); + } + } + + if (settings.tex) + { + var katexHandle = function() { + div.find("." + editormd.classNames.tex).each(function(){ + var tex = $(this); + katex.render(tex.html().replace(/</g, "<").replace(/>/g, ">"), tex[0]); + tex.find(".katex").css("font-size", "1.6em"); + }); + }; + + if (settings.autoLoadKaTeX && !editormd.$katex && !editormd.kaTeXLoaded) + { + this.loadKaTeX(function() { + editormd.$katex = katex; + editormd.kaTeXLoaded = true; + katexHandle(); + }); + } + else + { + katexHandle(); + } + } + + div.getMarkdown = function() { + return saveTo.val(); + }; + + return div; + }; + + // Editor.md themes, change toolbar themes etc. + // added @1.5.0 + editormd.themes = ["default", "dark"]; + + // Preview area themes + // added @1.5.0 + editormd.previewThemes = ["default", "dark"]; + + // CodeMirror / editor area themes + // @1.5.0 rename -> editorThemes, old version -> themes + editormd.editorThemes = [ + "default", "3024-day", "3024-night", + "ambiance", "ambiance-mobile", + "base16-dark", "base16-light", "blackboard", + "cobalt", + "eclipse", "elegant", "erlang-dark", + "lesser-dark", + "mbo", "mdn-like", "midnight", "monokai", + "neat", "neo", "night", + "paraiso-dark", "paraiso-light", "pastel-on-dark", + "rubyblue", + "solarized", + "the-matrix", "tomorrow-night-eighties", "twilight", + "vibrant-ink", + "xq-dark", "xq-light" + ]; + + editormd.loadPlugins = {}; + + editormd.loadFiles = { + js : [], + css : [], + plugin : [] + }; + + /** + * 动态加载Editor.md插件,但不立即执行 + * Load editor.md plugins + * + * @param {String} fileName 插件文件路径 + * @param {Function} [callback=function()] 加载成功后执行的回调函数 + * @param {String} [into="head"] 嵌入页面的位置 + */ + + editormd.loadPlugin = function(fileName, callback, into) { + callback = callback || function() {}; + + this.loadScript(fileName, function() { + editormd.loadFiles.plugin.push(fileName); + callback(); + }, into); + }; + + /** + * 动态加载CSS文件的方法 + * Load css file method + * + * @param {String} fileName CSS文件名 + * @param {Function} [callback=function()] 加载成功后执行的回调函数 + * @param {String} [into="head"] 嵌入页面的位置 + */ + + editormd.loadCSS = function(fileName, callback, into) { + into = into || "head"; + callback = callback || function() {}; + + var css = document.createElement("link"); + css.type = "text/css"; + css.rel = "stylesheet"; + css.onload = css.onreadystatechange = function() { + editormd.loadFiles.css.push(fileName); + callback(); + }; + + css.href = fileName + ".css"; + + if(into === "head") { + document.getElementsByTagName("head")[0].appendChild(css); + } else { + document.body.appendChild(css); + } + }; + + editormd.isIE = (navigator.appName == "Microsoft Internet Explorer"); + editormd.isIE8 = (editormd.isIE && navigator.appVersion.match(/8./i) == "8."); + + /** + * 动态加载JS文件的方法 + * Load javascript file method + * + * @param {String} fileName JS文件名 + * @param {Function} [callback=function()] 加载成功后执行的回调函数 + * @param {String} [into="head"] 嵌入页面的位置 + */ + + editormd.loadScript = function(fileName, callback, into) { + + into = into || "head"; + callback = callback || function() {}; + + var script = null; + script = document.createElement("script"); + script.id = fileName.replace(/[\./]+/g, "-"); + script.type = "text/javascript"; + script.src = fileName + ".js"; + + if (editormd.isIE8) + { + script.onreadystatechange = function() { + if(script.readyState) + { + if (script.readyState === "loaded" || script.readyState === "complete") + { + script.onreadystatechange = null; + editormd.loadFiles.js.push(fileName); + callback(); + } + } + }; + } + else + { + script.onload = function() { + editormd.loadFiles.js.push(fileName); + callback(); + }; + } + + if (into === "head") { + document.getElementsByTagName("head")[0].appendChild(script); + } else { + document.body.appendChild(script); + } + }; + + // 使用国外的CDN,加载速度有时会很慢,或者自定义URL + // You can custom KaTeX load url. + + editormd.kaTeXLoaded = false; + editormd.katexURL = { + css : "//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min", + js : "//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min" + } + /** + * 加载KaTeX文件 + * load KaTeX files + * + * @param {Function} [callback=function()] 加载成功后执行的回调函数 + */ + + editormd.loadKaTeX = function (callback) { + callback = callback || function() {}; + callback(); + }; + + /** + * 锁屏 + * lock screen + * + * @param {Boolean} lock Boolean 布尔值,是否锁屏 + * @returns {void} + */ + + editormd.lockScreen = function(lock) { + $("html,body").css("overflow", (lock) ? "hidden" : ""); + }; + + /** + * 动态创建对话框 + * Creating custom dialogs + * + * @param {Object} options 配置项键值对 Key/Value + * @returns {dialog} 返回创建的dialog的jQuery实例对象 + */ + + editormd.createDialog = function(options) { + var defaults = { + name : "", + width : 420, + height: 240, + title : "", + drag : true, + closed : true, + content : "", + mask : true, + maskStyle : { + backgroundColor : "#fff", + opacity : 0.1 + }, + lockScreen : true, + footer : true, + buttons : false + }; + + options = $.extend(true, defaults, options); + + var $this = this; + var editor = this.editor; + var classPrefix = editormd.classPrefix; + var guid = (new Date()).getTime(); + var dialogName = ( (options.name === "") ? classPrefix + "dialog-" + guid : options.name); + var mouseOrTouch = editormd.mouseOrTouch; + + var html = "
                "; + + if (options.title !== "") + { + html += "
                "; + html += "" + options.title + ""; + html += "
                "; + } + + if (options.closed) + { + html += ""; + } + + html += "
                " + options.content; + + if (options.footer || typeof options.footer === "string") + { + html += "
                " + ( (typeof options.footer === "boolean") ? "" : options.footer) + "
                "; + } + + html += "
                "; + + html += "
                "; + html += "
                "; + html += "
                "; + + editor.append(html); + + var dialog = editor.find("." + dialogName); + + dialog.lockScreen = function(lock) { + if (options.lockScreen) + { + $("html,body").css("overflow", (lock) ? "hidden" : ""); + $this.resize(); + } + + return dialog; + }; + + dialog.showMask = function() { + if (options.mask) + { + editor.find("." + classPrefix + "mask").css(options.maskStyle).css("z-index", editormd.dialogZindex - 1).show(); + } + return dialog; + }; + + dialog.hideMask = function() { + if (options.mask) + { + editor.find("." + classPrefix + "mask").hide(); + } + + return dialog; + }; + + dialog.loading = function(show) { + var loading = dialog.find("." + classPrefix + "dialog-mask"); + loading[(show) ? "show" : "hide"](); + + return dialog; + }; + + dialog.lockScreen(true).showMask(); + + dialog.show().css({ + zIndex : editormd.dialogZindex, + border : (editormd.isIE8) ? "1px solid #ddd" : "", + width : (typeof options.width === "number") ? options.width + "px" : options.width, + height : (typeof options.height === "number") ? options.height + "px" : options.height + }); + + var dialogPosition = function(){ + dialog.css({ + top : ($(window).height() - dialog.height()) / 2 + "px", + left : ($(window).width() - dialog.width()) / 2 + "px" + }); + }; + + dialogPosition(); + + $(window).resize(dialogPosition); + + dialog.children("." + classPrefix + "dialog-close").bind(mouseOrTouch("click", "touchend"), function() { + dialog.hide().lockScreen(false).hideMask(); + }); + + if (typeof options.buttons === "object") + { + var footer = dialog.footer = dialog.find("." + classPrefix + "dialog-footer"); + + for (var key in options.buttons) + { + var btn = options.buttons[key]; + var btnClassName = classPrefix + key + "-btn"; + + footer.append(""); + btn[1] = $.proxy(btn[1], dialog); + footer.children("." + btnClassName).bind(mouseOrTouch("click", "touchend"), btn[1]); + } + } + + if (options.title !== "" && options.drag) + { + var posX, posY; + var dialogHeader = dialog.children("." + classPrefix + "dialog-header"); + + if (!options.mask) { + dialogHeader.bind(mouseOrTouch("click", "touchend"), function(){ + editormd.dialogZindex += 2; + dialog.css("z-index", editormd.dialogZindex); + }); + } + + dialogHeader.mousedown(function(e) { + e = e || window.event; //IE + posX = e.clientX - parseInt(dialog[0].style.left); + posY = e.clientY - parseInt(dialog[0].style.top); + + document.onmousemove = moveAction; + }); + + var userCanSelect = function (obj) { + obj.removeClass(classPrefix + "user-unselect").off("selectstart"); + }; + + var userUnselect = function (obj) { + obj.addClass(classPrefix + "user-unselect").on("selectstart", function(event) { // selectstart for IE + return false; + }); + }; + + var moveAction = function (e) { + e = e || window.event; //IE + + var left, top, nowLeft = parseInt(dialog[0].style.left), nowTop = parseInt(dialog[0].style.top); + + if( nowLeft >= 0 ) { + if( nowLeft + dialog.width() <= $(window).width()) { + left = e.clientX - posX; + } else { + left = $(window).width() - dialog.width(); + document.onmousemove = null; + } + } else { + left = 0; + document.onmousemove = null; + } + + if( nowTop >= 0 ) { + top = e.clientY - posY; + } else { + top = 0; + document.onmousemove = null; + } + + + document.onselectstart = function() { + return false; + }; + + userUnselect($("body")); + userUnselect(dialog); + dialog[0].style.left = left + "px"; + dialog[0].style.top = top + "px"; + }; + + document.onmouseup = function() { + userCanSelect($("body")); + userCanSelect(dialog); + + document.onselectstart = null; + document.onmousemove = null; + }; + + dialogHeader.touchDraggable = function() { + var offset = null; + var start = function(e) { + var orig = e.originalEvent; + var pos = $(this).parent().position(); + + offset = { + x : orig.changedTouches[0].pageX - pos.left, + y : orig.changedTouches[0].pageY - pos.top + }; + }; + + var move = function(e) { + e.preventDefault(); + var orig = e.originalEvent; + + $(this).parent().css({ + top : orig.changedTouches[0].pageY - offset.y, + left : orig.changedTouches[0].pageX - offset.x + }); + }; + + this.bind("touchstart", start).bind("touchmove", move); + }; + + dialogHeader.touchDraggable(); + } + + editormd.dialogZindex += 2; + + return dialog; + }; + + /** + * 鼠标和触摸事件的判断/选择方法 + * MouseEvent or TouchEvent type switch + * + * @param {String} [mouseEventType="click"] 供选择的鼠标事件 + * @param {String} [touchEventType="touchend"] 供选择的触摸事件 + * @returns {String} EventType 返回事件类型名称 + */ + + editormd.mouseOrTouch = function(mouseEventType, touchEventType) { + mouseEventType = mouseEventType || "click"; + touchEventType = touchEventType || "touchend"; + + var eventType = mouseEventType; + + try { + document.createEvent("TouchEvent"); + eventType = touchEventType; + } catch(e) {} + + return eventType; + }; + + /** + * 日期时间的格式化方法 + * Datetime format method + * + * @param {String} [format=""] 日期时间的格式,类似PHP的格式 + * @returns {String} datefmt 返回格式化后的日期时间字符串 + */ + + editormd.dateFormat = function(format) { + format = format || ""; + + var addZero = function(d) { + return (d < 10) ? "0" + d : d; + }; + + var date = new Date(); + var year = date.getFullYear(); + var year2 = year.toString().slice(2, 4); + var month = addZero(date.getMonth() + 1); + var day = addZero(date.getDate()); + var weekDay = date.getDay(); + var hour = addZero(date.getHours()); + var min = addZero(date.getMinutes()); + var second = addZero(date.getSeconds()); + var ms = addZero(date.getMilliseconds()); + var datefmt = ""; + + var ymd = year2 + "-" + month + "-" + day; + var fymd = year + "-" + month + "-" + day; + var hms = hour + ":" + min + ":" + second; + + switch (format) + { + case "UNIX Time" : + datefmt = date.getTime(); + break; + + case "UTC" : + datefmt = date.toUTCString(); + break; + + case "yy" : + datefmt = year2; + break; + + case "year" : + case "yyyy" : + datefmt = year; + break; + + case "month" : + case "mm" : + datefmt = month; + break; + + case "cn-week-day" : + case "cn-wd" : + var cnWeekDays = ["日", "一", "二", "三", "四", "五", "六"]; + datefmt = "星期" + cnWeekDays[weekDay]; + break; + + case "week-day" : + case "wd" : + var weekDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + datefmt = weekDays[weekDay]; + break; + + case "day" : + case "dd" : + datefmt = day; + break; + + case "hour" : + case "hh" : + datefmt = hour; + break; + + case "min" : + case "ii" : + datefmt = min; + break; + + case "second" : + case "ss" : + datefmt = second; + break; + + case "ms" : + datefmt = ms; + break; + + case "yy-mm-dd" : + datefmt = ymd; + break; + + case "yyyy-mm-dd" : + datefmt = fymd; + break; + + case "yyyy-mm-dd h:i:s ms" : + case "full + ms" : + datefmt = fymd + " " + hms + " " + ms; + break; + + case "full" : + case "yyyy-mm-dd h:i:s" : + default: + datefmt = fymd + " " + hms; + break; + } + + return datefmt; + }; + + return editormd; + +})); diff --git a/paicoding-ui/src/main/resources/static/editormd/editormd.min.js b/paicoding-ui/src/main/resources/static/editormd/editormd.min.js new file mode 100644 index 000000000..f810e34ff --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/editormd.min.js @@ -0,0 +1,3 @@ +/*! Editor.md v1.5.0 | editormd.min.js | Open source online markdown editor. | MIT License | By: Pandao | https://github.com/pandao/editor.md | 2015-06-09 */ +!function(e){"use strict";"function"==typeof require&&"object"==typeof exports&&"object"==typeof module?module.exports=e:"function"==typeof define?define.amd||define(["jquery"],e):window.editormd=e()}(function(){"use strict";var e="undefined"!=typeof jQuery?jQuery:Zepto;if("undefined"!=typeof e){var t=function(e,i){return new t.fn.init(e,i)};t.title=t.$name="Editor.md",t.version="1.5.0",t.homePage="https://pandao.github.io/editor.md/",t.classPrefix="editormd-",t.toolbarModes={full:["undo","redo","|","bold","del","italic","quote","ucwords","uppercase","lowercase","|","h1","h2","h3","h4","h5","h6","|","list-ul","list-ol","hr","|","link","reference-link","image","code","preformatted-text","code-block","table","datetime","emoji","html-entities","pagebreak","|","goto-line","watch","preview","fullscreen","clear","search","|","help","info"],simple:["undo","redo","|","bold","del","italic","quote","uppercase","lowercase","|","h1","h2","h3","h4","h5","h6","|","list-ul","list-ol","hr","|","watch","preview","fullscreen","|","help","info"],mini:["undo","redo","|","watch","preview","|","help","info"]},t.defaults={mode:"gfm",name:"",value:"",theme:"",editorTheme:"default",previewTheme:"",markdown:"",appendMarkdown:"",width:"100%",height:"100%",path:"./lib/",pluginPath:"",delay:300,autoLoadModules:!0,watch:!0,placeholder:"Enjoy Markdown! coding now...",gotoLine:!0,codeFold:!1,autoHeight:!1,autoFocus:!0,autoCloseTags:!0,searchReplace:!0,syncScrolling:!0,readOnly:!1,tabSize:4,indentUnit:4,lineNumbers:!0,lineWrapping:!0,autoCloseBrackets:!0,showTrailingSpace:!0,matchBrackets:!0,indentWithTabs:!0,styleSelectedText:!0,matchWordHighlight:!0,styleActiveLine:!0,dialogLockScreen:!0,dialogShowMask:!0,dialogDraggable:!0,dialogMaskBgColor:"#fff",dialogMaskOpacity:.1,fontSize:"13px",saveHTMLToTextarea:!1,disabledKeyMaps:[],onload:function(){},onresize:function(){},onchange:function(){},onwatch:null,onunwatch:null,onpreviewing:function(){},onpreviewed:function(){},onfullscreen:function(){},onfullscreenExit:function(){},onscroll:function(){},onpreviewscroll:function(){},imageUpload:!1,imageFormats:["jpg","jpeg","gif","png","bmp","webp"],imageUploadURL:"",crossDomainUpload:!1,uploadCallbackURL:"",toc:!0,tocm:!1,tocTitle:"",tocDropdown:!1,tocContainer:"",tocStartLevel:1,htmlDecode:!1,pageBreak:!0,atLink:!0,emailLink:!0,taskList:!1,emoji:!1,tex:!1,flowChart:!1,sequenceDiagram:!1,previewCodeHighlight:!0,toolbar:!0,toolbarAutoFixed:!0,toolbarIcons:"full",toolbarTitles:{},toolbarHandlers:{ucwords:function(){return t.toolbarHandlers.ucwords},lowercase:function(){return t.toolbarHandlers.lowercase}},toolbarCustomIcons:{lowercase:'a',ucwords:'Aa'},toolbarIconsClass:{undo:"fa-undo",redo:"fa-repeat",bold:"fa-bold",del:"fa-strikethrough",italic:"fa-italic",quote:"fa-quote-left",uppercase:"fa-font",h1:t.classPrefix+"bold",h2:t.classPrefix+"bold",h3:t.classPrefix+"bold",h4:t.classPrefix+"bold",h5:t.classPrefix+"bold",h6:t.classPrefix+"bold","list-ul":"fa-list-ul","list-ol":"fa-list-ol",hr:"fa-minus",link:"fa-link","reference-link":"fa-anchor",image:"fa-picture-o",code:"fa-code","preformatted-text":"fa-file-code-o","code-block":"fa-file-code-o",table:"fa-table",datetime:"fa-clock-o",emoji:"fa-smile-o","html-entities":"fa-copyright",pagebreak:"fa-newspaper-o","goto-line":"fa-terminal",watch:"fa-eye-slash",unwatch:"fa-eye",preview:"fa-desktop",search:"fa-search",fullscreen:"fa-arrows-alt",clear:"fa-eraser",help:"fa-question-circle",info:"fa-info-circle"},toolbarIconTexts:{},lang:{name:"zh-cn",description:"开源在线Markdown编辑器
                Open source online Markdown editor.",tocTitle:"目录",toolbar:{undo:"撤销(Ctrl+Z)",redo:"重做(Ctrl+Y)",bold:"粗体",del:"删除线",italic:"斜体",quote:"引用",ucwords:"将每个单词首字母转成大写",uppercase:"将所选转换成大写",lowercase:"将所选转换成小写",h1:"标题1",h2:"标题2",h3:"标题3",h4:"标题4",h5:"标题5",h6:"标题6","list-ul":"无序列表","list-ol":"有序列表",hr:"横线",link:"链接","reference-link":"引用链接",image:"添加图片",code:"行内代码","preformatted-text":"预格式文本 / 代码块(缩进风格)","code-block":"代码块(多语言风格)",table:"添加表格",datetime:"日期时间",emoji:"Emoji表情","html-entities":"HTML实体字符",pagebreak:"插入分页符","goto-line":"跳转到行",watch:"关闭实时预览",unwatch:"开启实时预览",preview:"全窗口预览HTML(按 Shift + ESC还原)",fullscreen:"全屏(按ESC还原)",clear:"清空",search:"搜索",help:"使用帮助",info:"关于"+t.title},buttons:{enter:"确定",cancel:"取消",close:"关闭"},dialog:{link:{title:"添加链接",url:"链接地址",urlTitle:"链接标题",urlEmpty:"错误:请填写链接地址。"},referenceLink:{title:"添加引用链接",name:"引用名称",url:"链接地址",urlId:"链接ID",urlTitle:"链接标题",nameEmpty:"错误:引用链接的名称不能为空。",idEmpty:"错误:请填写引用链接的ID。",urlEmpty:"错误:请填写引用链接的URL地址。"},image:{title:"添加图片",url:"图片地址",link:"图片链接",alt:"图片描述",uploadButton:"本地上传",imageURLEmpty:"错误:图片地址不能为空。",uploadFileEmpty:"错误:上传的图片不能为空。",formatNotAllowed:"错误:只允许上传图片文件,允许上传的图片文件格式有:"},preformattedText:{title:"添加预格式文本或代码块",emptyAlert:"错误:请填写预格式文本或代码的内容。"},codeBlock:{title:"添加代码块",selectLabel:"代码语言:",selectDefaultText:"请选择代码语言",otherLanguage:"其他语言",unselectedLanguageAlert:"错误:请选择代码所属的语言类型。",codeEmptyAlert:"错误:请填写代码内容。"},htmlEntities:{title:"HTML 实体字符"},help:{title:"使用帮助"}}}},t.classNames={tex:t.classPrefix+"tex"},t.dialogZindex=99999,t.$katex=null,t.$marked=null,t.$CodeMirror=null,t.$prettyPrint=null;var i,o;t.prototype=t.fn={state:{watching:!1,loaded:!1,preview:!1,fullscreen:!1},init:function(i,o){o=o||{},"object"==typeof i&&(o=i);var r=this.classPrefix=t.classPrefix,n=this.settings=e.extend(!0,t.defaults,o);i="object"==typeof i?n.id:i;var a=this.editor=e("#"+i);this.id=i,this.lang=n.lang;var s=this.classNames={textarea:{html:r+"html-textarea",markdown:r+"markdown-textarea"}};n.pluginPath=""===n.pluginPath?n.path+"../plugins/":n.pluginPath,this.state.watching=n.watch?!0:!1,a.hasClass("editormd")||a.addClass("editormd"),a.css({width:"number"==typeof n.width?n.width+"px":n.width,height:"number"==typeof n.height?n.height+"px":n.height}),n.autoHeight&&a.css("height","auto");var l=this.markdownTextarea=a.children("textarea");l.length<1&&(a.append(""),l=this.markdownTextarea=a.children("textarea")),l.addClass(s.textarea.markdown).attr("placeholder",n.placeholder),("undefined"==typeof l.attr("name")||""===l.attr("name"))&&l.attr("name",""!==n.name?n.name:i+"-markdown-doc");var c=[n.readOnly?"":'',n.saveHTMLToTextarea?'':"",'
                ','
                ','
                '].join("\n");return a.append(c).addClass(r+"vertical"),""!==n.theme&&a.addClass(r+"theme-"+n.theme),this.mask=a.children("."+r+"mask"),this.containerMask=a.children("."+r+"container-mask"),""!==n.markdown&&l.val(n.markdown),""!==n.appendMarkdown&&l.val(l.val()+n.appendMarkdown),this.htmlTextarea=a.children("."+s.textarea.html),this.preview=a.children("."+r+"preview"),this.previewContainer=this.preview.children("."+r+"preview-container"),""!==n.previewTheme&&this.preview.addClass(r+"preview-theme-"+n.previewTheme),"function"==typeof define&&define.amd&&("undefined"!=typeof katex&&(t.$katex=katex),n.searchReplace&&!n.readOnly&&(t.loadCSS(n.path+"codemirror/addon/dialog/dialog"),t.loadCSS(n.path+"codemirror/addon/search/matchesonscrollbar"))),"function"==typeof define&&define.amd||!n.autoLoadModules?("undefined"!=typeof CodeMirror&&(t.$CodeMirror=CodeMirror),"undefined"!=typeof marked&&(t.$marked=marked),this.setCodeMirror().setToolbar().loadedDisplay()):this.loadQueues(),this},loadQueues:function(){var e=this,i=this.settings,o=i.path,r=function(){return t.isIE8?void e.loadedDisplay():void(i.flowChart||i.sequenceDiagram?t.loadScript(o+"raphael.min",function(){t.loadScript(o+"underscore.min",function(){!i.flowChart&&i.sequenceDiagram?t.loadScript(o+"sequence-diagram.min",function(){e.loadedDisplay()}):i.flowChart&&!i.sequenceDiagram?t.loadScript(o+"flowchart.min",function(){t.loadScript(o+"jquery.flowchart.min",function(){e.loadedDisplay()})}):i.flowChart&&i.sequenceDiagram&&t.loadScript(o+"flowchart.min",function(){t.loadScript(o+"jquery.flowchart.min",function(){t.loadScript(o+"sequence-diagram.min",function(){e.loadedDisplay()})})})})}):e.loadedDisplay())};return t.loadCSS(o+"codemirror/codemirror.min"),i.searchReplace&&!i.readOnly&&(t.loadCSS(o+"codemirror/addon/dialog/dialog"),t.loadCSS(o+"codemirror/addon/search/matchesonscrollbar")),i.codeFold&&t.loadCSS(o+"codemirror/addon/fold/foldgutter"),t.loadScript(o+"codemirror/codemirror.min",function(){t.$CodeMirror=CodeMirror,t.loadScript(o+"codemirror/modes.min",function(){t.loadScript(o+"codemirror/addons.min",function(){return e.setCodeMirror(),"gfm"!==i.mode&&"markdown"!==i.mode?(e.loadedDisplay(),!1):(e.setToolbar(),void t.loadScript(o+"marked.min",function(){t.$marked=marked,i.previewCodeHighlight?t.loadScript(o+"prettify.min",function(){r()}):r()}))})})}),this},setTheme:function(e){var t=this.editor,i=this.settings.theme,o=this.classPrefix+"theme-";return t.removeClass(o+i).addClass(o+e),this.settings.theme=e,this},setEditorTheme:function(e){var i=this.settings;return i.editorTheme=e,"default"!==e&&t.loadCSS(i.path+"codemirror/theme/"+i.editorTheme),this.cm.setOption("theme",e),this},setCodeMirrorTheme:function(e){return this.setEditorTheme(e),this},setPreviewTheme:function(e){var t=this.preview,i=this.settings.previewTheme,o=this.classPrefix+"preview-theme-";return t.removeClass(o+i).addClass(o+e),this.settings.previewTheme=e,this},setCodeMirror:function(){var e=this.settings,i=this.editor;"default"!==e.editorTheme&&t.loadCSS(e.path+"codemirror/theme/"+e.editorTheme);var o={mode:e.mode,theme:e.editorTheme,tabSize:e.tabSize,dragDrop:!1,autofocus:e.autoFocus,autoCloseTags:e.autoCloseTags,readOnly:e.readOnly?"nocursor":!1,indentUnit:e.indentUnit,lineNumbers:e.lineNumbers,lineWrapping:e.lineWrapping,extraKeys:{"Ctrl-Q":function(e){e.foldCode(e.getCursor())}},foldGutter:e.codeFold,gutters:["CodeMirror-linenumbers","CodeMirror-foldgutter"],matchBrackets:e.matchBrackets,indentWithTabs:e.indentWithTabs,styleActiveLine:e.styleActiveLine,styleSelectedText:e.styleSelectedText,autoCloseBrackets:e.autoCloseBrackets,showTrailingSpace:e.showTrailingSpace,highlightSelectionMatches:e.matchWordHighlight?{showToken:"onselected"===e.matchWordHighlight?!1:/\w/}:!1};return this.codeEditor=this.cm=t.$CodeMirror.fromTextArea(this.markdownTextarea[0],o),this.codeMirror=this.cmElement=i.children(".CodeMirror"),""!==e.value&&this.cm.setValue(e.value),this.codeMirror.css({fontSize:e.fontSize,width:e.watch?"50%":"100%"}),e.autoHeight&&(this.codeMirror.css("height","auto"),this.cm.setOption("viewportMargin",1/0)),e.lineNumbers||this.codeMirror.find(".CodeMirror-gutters").css("border-right","none"),this},getCodeMirrorOption:function(e){return this.cm.getOption(e)},setCodeMirrorOption:function(e,t){return this.cm.setOption(e,t),this},addKeyMap:function(e,t){return this.cm.addKeyMap(e,t),this},removeKeyMap:function(e){return this.cm.removeKeyMap(e),this},gotoLine:function(t){var i=this.settings;if(!i.gotoLine)return this;var o=this.cm,r=(this.editor,o.lineCount()),n=this.preview;if("string"==typeof t&&("last"===t&&(t=r),"first"===t&&(t=1)),"number"!=typeof t)return alert("Error: The line number must be an integer."),this;if(t=parseInt(t)-1,t>r)return alert("Error: The line number range 1-"+r),this;o.setCursor({line:t,ch:0});var a=o.getScrollInfo(),s=a.clientHeight,l=o.charCoords({line:t,ch:0},"local");if(o.scrollTo(null,(l.top+l.bottom-s)/2),i.watch){var c=this.codeMirror.find(".CodeMirror-scroll")[0],h=e(c).height(),d=c.scrollTop,u=d/c.scrollHeight;n.scrollTop(0===d?0:d+h>=c.scrollHeight-16?n[0].scrollHeight:n[0].scrollHeight*u)}return o.focus(),this},extend:function(){return"undefined"!=typeof arguments[1]&&("function"==typeof arguments[1]&&(arguments[1]=e.proxy(arguments[1],this)),this[arguments[0]]=arguments[1]),"object"==typeof arguments[0]&&"undefined"==typeof arguments[0].length&&e.extend(!0,this,arguments[0]),this},set:function(t,i){return"undefined"!=typeof i&&"function"==typeof i&&(i=e.proxy(i,this)),this[t]=i,this},config:function(t,i){var o=this.settings;return"object"==typeof t&&(o=e.extend(!0,o,t)),"string"==typeof t&&(o[t]=i),this.settings=o,this.recreate(),this},on:function(t,i){var o=this.settings;return"undefined"!=typeof o["on"+t]&&(o["on"+t]=e.proxy(i,this)),this},off:function(e){var t=this.settings;return"undefined"!=typeof t["on"+e]&&(t["on"+e]=function(){}),this},showToolbar:function(t){var i=this.settings;return i.readOnly?this:(i.toolbar&&(this.toolbar.length<1||""===this.toolbar.find("."+this.classPrefix+"menu").html())&&this.setToolbar(),i.toolbar=!0,this.toolbar.show(),this.resize(),e.proxy(t||function(){},this)(),this)},hideToolbar:function(t){var i=this.settings;return i.toolbar=!1,this.toolbar.hide(),this.resize(),e.proxy(t||function(){},this)(),this},setToolbarAutoFixed:function(t){var i=this.state,o=this.editor,r=this.toolbar,n=this.settings;"undefined"!=typeof t&&(n.toolbarAutoFixed=t);var a=function(){var t=e(window),i=t.scrollTop();return n.toolbarAutoFixed?void r.css(i-o.offset().top>10&&i
                  ';i.append(n),r=this.toolbar=i.children("."+o+"toolbar")}if(!e.toolbar)return r.hide(),this;r.show();for(var a="function"==typeof e.toolbarIcons?e.toolbarIcons():"string"==typeof e.toolbarIcons?t.toolbarModes[e.toolbarIcons]:e.toolbarIcons,s=r.find("."+this.classPrefix+"menu"),l="",c=!1,h=0,d=a.length;d>h;h++){var u=a[h];if("||"===u)c=!0;else if("|"===u)l+='
                • |
                • ';else{var f=/h(\d)/.test(u),g=u;"watch"!==u||e.watch||(g="unwatch");var p=e.lang.toolbar[g],m=e.toolbarIconTexts[g],w=e.toolbarIconsClass[g];p="undefined"==typeof p?"":p,m="undefined"==typeof m?"":m,w="undefined"==typeof w?"":w;var v=c?'
                • ':"
                • ";"undefined"!=typeof e.toolbarCustomIcons[u]&&"function"!=typeof e.toolbarCustomIcons[u]?v+=e.toolbarCustomIcons[u]:(v+='',v+=''+(f?u.toUpperCase():""===w?m:"")+"",v+=""),v+="
                • ",l=c?v+l:l+v}}return s.html(l),s.find('[title="Lowercase"]').attr("title",e.lang.toolbar.lowercase),s.find('[title="ucwords"]').attr("title",e.lang.toolbar.ucwords),this.setToolbarHandler(),this.setToolbarAutoFixed(),this},dialogLockScreen:function(){return e.proxy(t.dialogLockScreen,this)(),this},dialogShowMask:function(i){return e.proxy(t.dialogShowMask,this)(i),this},getToolbarHandles:function(e){var i=this.toolbarHandlers=t.toolbarHandlers;return e&&"undefined"!=typeof toolbarIconHandlers[e]?i[e]:i},setToolbarHandler:function(){var i=this,o=this.settings;if(!o.toolbar||o.readOnly)return this;var r=this.toolbar,n=this.cm,a=this.classPrefix,s=this.toolbarIcons=r.find("."+a+"menu > li > a"),l=this.getToolbarHandles();return s.bind(t.mouseOrTouch("click","touchend"),function(t){var r=e(this).children(".fa"),a=r.attr("name"),s=n.getCursor(),c=n.getSelection();return""!==a?(i.activeIcon=r,"undefined"!=typeof l[a]?e.proxy(l[a],i)(n):"undefined"!=typeof o.toolbarHandlers[a]&&e.proxy(o.toolbarHandlers[a],i)(n,r,s,c),"link"!==a&&"reference-link"!==a&&"image"!==a&&"code-block"!==a&&"preformatted-text"!==a&&"watch"!==a&&"preview"!==a&&"search"!==a&&"fullscreen"!==a&&"info"!==a&&n.focus(),!1):void 0}),this},createDialog:function(i){return e.proxy(t.createDialog,this)(i)},createInfoDialog:function(){var e=this,i=this.editor,o=this.classPrefix,r=['
                  ','
                  ','

                  '+t.title+"v"+t.version+"

                  ","

                  "+this.lang.description+"

                  ",'

                  '+t.homePage+'

                  ','

                  Copyright © 2015 Pandao, The MIT License.

                  ',"
                  ",'',"
                  "].join("\n");i.append(r);var n=this.infoDialog=i.children("."+o+"dialog-info");return n.find("."+o+"dialog-close").bind(t.mouseOrTouch("click","touchend"),function(){e.hideInfoDialog()}),n.css("border",t.isIE8?"1px solid #ddd":"").css("z-index",t.dialogZindex).show(),this.infoDialogPosition(),this},infoDialogPosition:function(){var t=this.infoDialog,i=function(){t.css({top:(e(window).height()-t.height())/2+"px",left:(e(window).width()-t.width())/2+"px"})};return i(),e(window).resize(i),this},showInfoDialog:function(){e("html,body").css("overflow-x","hidden");var i=this.editor,o=this.settings,r=this.infoDialog=i.children("."+this.classPrefix+"dialog-info");return r.length<1&&this.createInfoDialog(),this.lockScreen(!0),this.mask.css({opacity:o.dialogMaskOpacity,backgroundColor:o.dialogMaskBgColor}).show(),r.css("z-index",t.dialogZindex).show(),this.infoDialogPosition(),this},hideInfoDialog:function(){return e("html,body").css("overflow-x",""),this.infoDialog.hide(),this.mask.hide(),this.lockScreen(!1),this},lockScreen:function(e){return t.lockScreen(e),this.resize(),this},recreate:function(){var e=this.editor,t=this.settings;return this.codeMirror.remove(),this.setCodeMirror(),t.readOnly||(e.find(".editormd-dialog").length>0&&e.find(".editormd-dialog").remove(),t.toolbar&&(this.getToolbarHandles(),this.setToolbar())),this.loadedDisplay(!0),this},previewCodeHighlight:function(){var e=this.settings,t=this.previewContainer;return e.previewCodeHighlight&&(t.find("pre").addClass("prettyprint linenums"),"undefined"!=typeof prettyPrint&&prettyPrint()),this},katexRender:function(){return null===i?this:(this.previewContainer.find("."+t.classNames.tex).each(function(){var i=e(this);t.$katex.render(i.text(),i[0]),i.find(".katex").css("font-size","1.6em")}),this)},flowChartAndSequenceDiagramRender:function(){var i=this,r=this.settings,n=this.previewContainer;if(t.isIE8)return this;if(r.flowChart){if(null===o)return this;n.find(".flowchart").flowChart()}r.sequenceDiagram&&n.find(".sequence-diagram").sequenceDiagram({theme:"simple"});var a=i.preview,s=i.codeMirror,l=s.find(".CodeMirror-scroll"),c=l.height(),h=l.scrollTop(),d=h/l[0].scrollHeight,u=0;a.find(".markdown-toc-list").each(function(){u+=e(this).height()});var f=a.find(".editormd-toc-menu").height();return f=f?f:0,a.scrollTop(0===h?0:h+c>=l[0].scrollHeight-16?a[0].scrollHeight:(a[0].scrollHeight+u+f)*d),this},registerKeyMaps:function(i){var o=this,r=this.cm,n=this.settings,a=t.toolbarHandlers,s=n.disabledKeyMaps;if(i=i||null){for(var l in i)if(e.inArray(l,s)<0){var c={};c[l]=i[l],r.addKeyMap(i)}}else{for(var h in t.keyMaps){var d=t.keyMaps[h],u="string"==typeof d?e.proxy(a[d],o):e.proxy(d,o);if(e.inArray(h,["F9","F10","F11"])<0&&e.inArray(h,s)<0){var f={};f[h]=u,r.addKeyMap(f)}}e(window).keydown(function(t){var i={120:"F9",121:"F10",122:"F11"};if(e.inArray(i[t.keyCode],s)<0)switch(t.keyCode){case 120:return e.proxy(a.watch,o)(),!1;case 121:return e.proxy(a.preview,o)(),!1;case 122:return e.proxy(a.fullscreen,o)(),!1}})}return this},bindScrollEvent:function(){var i=this,o=this.preview,r=this.settings,n=this.codeMirror,a=t.mouseOrTouch;if(!r.syncScrolling)return this;var s=function(){n.find(".CodeMirror-scroll").bind(a("scroll","touchmove"),function(t){var n=e(this).height(),a=e(this).scrollTop(),s=a/e(this)[0].scrollHeight,l=0;o.find(".markdown-toc-list").each(function(){l+=e(this).height()});var c=o.find(".editormd-toc-menu").height();c=c?c:0,o.scrollTop(0===a?0:a+n>=e(this)[0].scrollHeight-16?o[0].scrollHeight:(o[0].scrollHeight+l+c)*s),e.proxy(r.onscroll,i)(t)})},l=function(){n.find(".CodeMirror-scroll").unbind(a("scroll","touchmove"))},c=function(){o.bind(a("scroll","touchmove"),function(t){var o=e(this).height(),a=e(this).scrollTop(),s=a/e(this)[0].scrollHeight,l=n.find(".CodeMirror-scroll");l.scrollTop(0===a?0:a+o>=e(this)[0].scrollHeight?l[0].scrollHeight:l[0].scrollHeight*s),e.proxy(r.onpreviewscroll,i)(t)})},h=function(){o.unbind(a("scroll","touchmove"))};return n.bind({mouseover:s,mouseout:l,touchstart:s,touchend:l}),"single"===r.syncScrolling?this:(o.bind({mouseover:c,mouseout:h,touchstart:c,touchend:h}),this)},bindChangeEvent:function(){var e=this,t=this.cm,o=this.settings;return o.syncScrolling?(t.on("change",function(t,r){o.watch&&e.previewContainer.css("padding",o.autoHeight?"20px 20px 50px 40px":"20px"),i=setTimeout(function(){clearTimeout(i),e.save(),i=null},o.delay)}),this):this},loadedDisplay:function(t){t=t||!1;var i=this,o=this.editor,r=this.preview,n=this.settings;return this.containerMask.hide(),this.save(),n.watch&&r.show(),o.data("oldWidth",o.width()).data("oldHeight",o.height()),this.resize(),this.registerKeyMaps(),e(window).resize(function(){i.resize()}),this.bindScrollEvent().bindChangeEvent(),t||e.proxy(n.onload,this)(),this.state.loaded=!0,this},width:function(e){return this.editor.css("width","number"==typeof e?e+"px":e),this.resize(),this},height:function(e){return this.editor.css("height","number"==typeof e?e+"px":e),this.resize(),this},resize:function(t,i){t=t||null,i=i||null;var o=this.state,r=this.editor,n=this.preview,a=this.toolbar,s=this.settings,l=this.codeMirror;if(t&&r.css("width","number"==typeof t?t+"px":t),!s.autoHeight||o.fullscreen||o.preview?(i&&r.css("height","number"==typeof i?i+"px":i),o.fullscreen&&r.height(e(window).height()),s.toolbar&&!s.readOnly?l.css("margin-top",a.height()+1).height(r.height()-a.height()):l.css("margin-top",0).height(r.height())):(r.css("height","auto"),l.css("height","auto")),s.watch)if(l.width(r.width()/2),n.width(o.preview?r.width():r.width()/2),this.previewContainer.css("padding",s.autoHeight?"20px 20px 50px 40px":"20px"),s.toolbar&&!s.readOnly?n.css("top",a.height()+1):n.css("top",0),!s.autoHeight||o.fullscreen||o.preview){var c=s.toolbar&&!s.readOnly?r.height()-a.height():r.height();n.height(c)}else n.height("");else l.width(r.width()),n.hide();return o.loaded&&e.proxy(s.onresize,this)(),this},save:function(){if(null===i)return this;var r=this,n=this.state,a=this.settings,s=this.cm,l=s.getValue(),c=this.previewContainer;if("gfm"!==a.mode&&"markdown"!==a.mode)return this.markdownTextarea.val(l),this;var h=t.$marked,d=this.markdownToC=[],u=this.markedRendererOptions={toc:a.toc,tocm:a.tocm,tocStartLevel:a.tocStartLevel,pageBreak:a.pageBreak,taskList:a.taskList,emoji:a.emoji,tex:a.tex,atLink:a.atLink,emailLink:a.emailLink,flowChart:a.flowChart,sequenceDiagram:a.sequenceDiagram,previewCodeHighlight:a.previewCodeHighlight},f=this.markedOptions={renderer:t.markedRenderer(d,u),gfm:!0,tables:!0,breaks:!0,pedantic:!1,sanitize:a.htmlDecode?!1:!0,smartLists:!0,smartypants:!0};h.setOptions(f);var g=t.$marked(l,f);if(g=t.filterHTMLTags(g,a.htmlDecode),this.markdownTextarea.text(l),s.save(),a.saveHTMLToTextarea&&this.htmlTextarea.text(g),a.watch||!a.watch&&n.preview){if(c.html(g),this.previewCodeHighlight(),a.toc){var p=""===a.tocContainer?c:e(a.tocContainer),m=p.find("."+this.classPrefix+"toc-menu");p.attr("previewContainer",""===a.tocContainer?"true":"false"),""!==a.tocContainer&&m.length>0&&m.remove(),t.markdownToCRenderer(d,p,a.tocDropdown,a.tocStartLevel),(a.tocDropdown||p.find("."+this.classPrefix+"toc-menu").length>0)&&t.tocDropdownMenu(p,""!==a.tocTitle?a.tocTitle:this.lang.tocTitle),""!==a.tocContainer&&c.find(".markdown-toc").css("border","none")}a.tex&&(!t.kaTeXLoaded&&a.autoLoadModules?t.loadKaTeX(function(){t.$katex=katex,t.kaTeXLoaded=!0,r.katexRender()}):(t.$katex=katex,this.katexRender())),(a.flowChart||a.sequenceDiagram)&&(o=setTimeout(function(){clearTimeout(o),r.flowChartAndSequenceDiagramRender(),o=null},10)),n.loaded&&e.proxy(a.onchange,this)()}return this},focus:function(){return this.cm.focus(),this},setCursor:function(e){return this.cm.setCursor(e),this},getCursor:function(){return this.cm.getCursor()},setSelection:function(e,t){return this.cm.setSelection(e,t),this},getSelection:function(){return this.cm.getSelection()},setSelections:function(e){return this.cm.setSelections(e),this},getSelections:function(){return this.cm.getSelections()},replaceSelection:function(e){return this.cm.replaceSelection(e),this},insertValue:function(e){return this.replaceSelection(e),this},appendMarkdown:function(e){var t=(this.settings,this.cm);return t.setValue(t.getValue()+e),this},setMarkdown:function(e){return this.cm.setValue(e||this.settings.markdown),this},getMarkdown:function(){return this.cm.getValue()},getValue:function(){return this.cm.getValue()},setValue:function(e){return this.cm.setValue(e),this},clear:function(){return this.cm.setValue(""),this},getHTML:function(){return this.settings.saveHTMLToTextarea?this.htmlTextarea.val():(alert("Error: settings.saveHTMLToTextarea == false"),!1)},getTextareaSavedHTML:function(){return this.getHTML()},getPreviewedHTML:function(){return this.settings.watch?this.previewContainer.html():(alert("Error: settings.watch == false"),!1)},watch:function(t){var o=this.settings;if(e.inArray(o.mode,["gfm","markdown"])<0)return this;if(this.state.watching=o.watch=!0,this.preview.show(),this.toolbar){var r=o.toolbarIconsClass.watch,n=o.toolbarIconsClass.unwatch,a=this.toolbar.find(".fa[name=watch]");a.parent().attr("title",o.lang.toolbar.watch),a.removeClass(n).addClass(r)}return this.codeMirror.css("border-right","1px solid #ddd").width(this.editor.width()/2),i=0,this.save().resize(),o.onwatch||(o.onwatch=t||function(){}),e.proxy(o.onwatch,this)(),this},unwatch:function(t){var i=this.settings;if(this.state.watching=i.watch=!1,this.preview.hide(),this.toolbar){var o=i.toolbarIconsClass.watch,r=i.toolbarIconsClass.unwatch,n=this.toolbar.find(".fa[name=watch]");n.parent().attr("title",i.lang.toolbar.unwatch),n.removeClass(o).addClass(r)}return this.codeMirror.css("border-right","none").width(this.editor.width()),this.resize(),i.onunwatch||(i.onunwatch=t||function(){}),e.proxy(i.onunwatch,this)(),this},show:function(t){t=t||function(){};var i=this;return this.editor.show(0,function(){e.proxy(t,i)()}),this},hide:function(t){t=t||function(){};var i=this;return this.editor.hide(0,function(){e.proxy(t,i)()}),this},previewing:function(){var i=this,o=this.editor,r=this.preview,n=this.toolbar,a=this.settings,s=this.codeMirror,l=this.previewContainer;if(e.inArray(a.mode,["gfm","markdown"])<0)return this;a.toolbar&&n&&(n.toggle(),n.find(".fa[name=preview]").toggleClass("active")),s.toggle();var c=function(e){e.shiftKey&&27===e.keyCode&&i.previewed()};"none"===s.css("display")?(this.state.preview=!0,this.state.fullscreen&&r.css("background","#fff"),o.find("."+this.classPrefix+"preview-close-btn").show().bind(t.mouseOrTouch("click","touchend"),function(){i.previewed()}),a.watch?l.css("padding",""):this.save(),l.addClass(this.classPrefix+"preview-active"),r.show().css({position:"",top:0,width:o.width(),height:a.autoHeight&&!this.state.fullscreen?"auto":o.height()}),this.state.loaded&&e.proxy(a.onpreviewing,this)(),e(window).bind("keyup",c)):(e(window).unbind("keyup",c),this.previewed())},previewed:function(){var i=this.editor,o=this.preview,r=this.toolbar,n=this.settings,a=this.previewContainer,s=i.find("."+this.classPrefix+"preview-close-btn");return this.state.preview=!1,this.codeMirror.show(),n.toolbar&&r.show(),o[n.watch?"show":"hide"](),s.hide().unbind(t.mouseOrTouch("click","touchend")),a.removeClass(this.classPrefix+"preview-active"),n.watch&&a.css("padding","20px"),o.css({background:null,position:"absolute",width:i.width()/2,height:n.autoHeight&&!this.state.fullscreen?"auto":i.height()-r.height(),top:n.toolbar?r.height():0}),this.state.loaded&&e.proxy(n.onpreviewed,this)(),this},fullscreen:function(){var t=this,i=this.state,o=this.editor,r=(this.preview,this.toolbar),n=this.settings,a=this.classPrefix+"fullscreen";r&&r.find(".fa[name=fullscreen]").parent().toggleClass("active");var s=function(e){e.shiftKey||27!==e.keyCode||i.fullscreen&&t.fullscreenExit()};return o.hasClass(a)?(e(window).unbind("keyup",s),this.fullscreenExit()):(i.fullscreen=!0,e("html,body").css("overflow","hidden"),o.css({width:e(window).width(),height:e(window).height()}).addClass(a),this.resize(),e.proxy(n.onfullscreen,this)(),e(window).bind("keyup",s)),this},fullscreenExit:function(){var t=this.editor,i=this.settings,o=this.toolbar,r=this.classPrefix+"fullscreen";return this.state.fullscreen=!1,o&&o.find(".fa[name=fullscreen]").parent().removeClass("active"),e("html,body").css("overflow",""),t.css({width:t.data("oldWidth"),height:t.data("oldHeight")}).removeClass(r),this.resize(),e.proxy(i.onfullscreenExit,this)(),this},executePlugin:function(i,o){var r=this,n=this.cm,a=this.settings;return o=a.pluginPath+o,"function"==typeof define?"undefined"==typeof this[i]?(alert("Error: "+i+" plugin is not found, you are not load this plugin."),this):(this[i](n),this):(e.inArray(o,t.loadFiles.plugin)<0?t.loadPlugin(o,function(){t.loadPlugins[i]=r[i],r[i](n)}):e.proxy(t.loadPlugins[i],this)(n),this)},search:function(e){var t=this.settings;return t.searchReplace?(t.readOnly||this.cm.execCommand(e||"find"),this):(alert("Error: settings.searchReplace == false"),this)},searchReplace:function(){return this.search("replace"),this},searchReplaceAll:function(){return this.search("replaceAll"),this}},t.fn.init.prototype=t.fn,t.dialogLockScreen=function(){var t=this.settings||{dialogLockScreen:!0};t.dialogLockScreen&&(e("html,body").css("overflow","hidden"),this.resize())},t.dialogShowMask=function(t){var i=this.editor,o=this.settings||{dialogShowMask:!0};t.css({top:(e(window).height()-t.height())/2+"px",left:(e(window).width()-t.width())/2+"px"}),o.dialogShowMask&&i.children("."+this.classPrefix+"mask").css("z-index",parseInt(t.css("z-index"))-1).show()},t.toolbarHandlers={undo:function(){this.cm.undo()},redo:function(){this.cm.redo()},bold:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();e.replaceSelection("**"+i+"**"),""===i&&e.setCursor(t.line,t.ch+2)},del:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();e.replaceSelection("~~"+i+"~~"),""===i&&e.setCursor(t.line,t.ch+2)},italic:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();e.replaceSelection("*"+i+"*"),""===i&&e.setCursor(t.line,t.ch+1)},quote:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();0!==t.ch?(e.setCursor(t.line,0),e.replaceSelection("> "+i),e.setCursor(t.line,t.ch+2)):e.replaceSelection("> "+i)},ucfirst:function(){var e=this.cm,i=e.getSelection(),o=e.listSelections();e.replaceSelection(t.firstUpperCase(i)),e.setSelections(o)},ucwords:function(){var e=this.cm,i=e.getSelection(),o=e.listSelections();e.replaceSelection(t.wordsFirstUpperCase(i)),e.setSelections(o)},uppercase:function(){var e=this.cm,t=e.getSelection(),i=e.listSelections();e.replaceSelection(t.toUpperCase()),e.setSelections(i)},lowercase:function(){var e=this.cm,t=(e.getCursor(),e.getSelection()),i=e.listSelections();e.replaceSelection(t.toLowerCase()),e.setSelections(i)},h1:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();0!==t.ch?(e.setCursor(t.line,0),e.replaceSelection("# "+i),e.setCursor(t.line,t.ch+2)):e.replaceSelection("# "+i)},h2:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();0!==t.ch?(e.setCursor(t.line,0), +e.replaceSelection("## "+i),e.setCursor(t.line,t.ch+3)):e.replaceSelection("## "+i)},h3:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();0!==t.ch?(e.setCursor(t.line,0),e.replaceSelection("### "+i),e.setCursor(t.line,t.ch+4)):e.replaceSelection("### "+i)},h4:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();0!==t.ch?(e.setCursor(t.line,0),e.replaceSelection("#### "+i),e.setCursor(t.line,t.ch+5)):e.replaceSelection("#### "+i)},h5:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();0!==t.ch?(e.setCursor(t.line,0),e.replaceSelection("##### "+i),e.setCursor(t.line,t.ch+6)):e.replaceSelection("##### "+i)},h6:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();0!==t.ch?(e.setCursor(t.line,0),e.replaceSelection("###### "+i),e.setCursor(t.line,t.ch+7)):e.replaceSelection("###### "+i)},"list-ul":function(){var e=this.cm,t=(e.getCursor(),e.getSelection());if(""===t)e.replaceSelection("- "+t);else{for(var i=t.split("\n"),o=0,r=i.length;r>o;o++)i[o]=""===i[o]?"":"- "+i[o];e.replaceSelection(i.join("\n"))}},"list-ol":function(){var e=this.cm,t=(e.getCursor(),e.getSelection());if(""===t)e.replaceSelection("1. "+t);else{for(var i=t.split("\n"),o=0,r=i.length;r>o;o++)i[o]=""===i[o]?"":o+1+". "+i[o];e.replaceSelection(i.join("\n"))}},hr:function(){{var e=this.cm,t=e.getCursor();e.getSelection()}e.replaceSelection((0!==t.ch?"\n\n":"\n")+"------------\n\n")},tex:function(){if(!this.settings.tex)return alert("settings.tex === false"),this;var e=this.cm,t=e.getCursor(),i=e.getSelection();e.replaceSelection("$$"+i+"$$"),""===i&&e.setCursor(t.line,t.ch+2)},link:function(){this.executePlugin("linkDialog","link-dialog/link-dialog")},"reference-link":function(){this.executePlugin("referenceLinkDialog","reference-link-dialog/reference-link-dialog")},pagebreak:function(){if(!this.settings.pageBreak)return alert("settings.pageBreak === false"),this;{var e=this.cm;e.getSelection()}e.replaceSelection("\r\n[========]\r\n")},image:function(){this.executePlugin("imageDialog","image-dialog/image-dialog")},code:function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();e.replaceSelection("`"+i+"`"),""===i&&e.setCursor(t.line,t.ch+1)},"code-block":function(){this.executePlugin("codeBlockDialog","code-block-dialog/code-block-dialog")},"preformatted-text":function(){this.executePlugin("preformattedTextDialog","preformatted-text-dialog/preformatted-text-dialog")},table:function(){this.executePlugin("tableDialog","table-dialog/table-dialog")},datetime:function(){var e=this.cm,i=(e.getSelection(),new Date,this.settings.lang.name),o=t.dateFormat()+" "+t.dateFormat("zh-cn"===i||"zh-tw"===i?"cn-week-day":"week-day");e.replaceSelection(o)},emoji:function(){this.executePlugin("emojiDialog","emoji-dialog/emoji-dialog")},"html-entities":function(){this.executePlugin("htmlEntitiesDialog","html-entities-dialog/html-entities-dialog")},"goto-line":function(){this.executePlugin("gotoLineDialog","goto-line-dialog/goto-line-dialog")},watch:function(){this[this.settings.watch?"unwatch":"watch"]()},preview:function(){this.previewing()},fullscreen:function(){this.fullscreen()},clear:function(){this.clear()},search:function(){this.search()},help:function(){this.executePlugin("helpDialog","help-dialog/help-dialog")},info:function(){this.showInfoDialog()}},t.keyMaps={"Ctrl-1":"h1","Ctrl-2":"h2","Ctrl-3":"h3","Ctrl-4":"h4","Ctrl-5":"h5","Ctrl-6":"h6","Ctrl-B":"bold","Ctrl-D":"datetime","Ctrl-E":function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();return this.settings.emoji?(e.replaceSelection(":"+i+":"),void(""===i&&e.setCursor(t.line,t.ch+1))):void alert("Error: settings.emoji == false")},"Ctrl-Alt-G":"goto-line","Ctrl-H":"hr","Ctrl-I":"italic","Ctrl-K":"code","Ctrl-L":function(){var e=this.cm,t=e.getCursor(),i=e.getSelection(),o=""===i?"":' "'+i+'"';e.replaceSelection("["+i+"]("+o+")"),""===i&&e.setCursor(t.line,t.ch+1)},"Ctrl-U":"list-ul","Shift-Ctrl-A":function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();return this.settings.atLink?(e.replaceSelection("@"+i),void(""===i&&e.setCursor(t.line,t.ch+1))):void alert("Error: settings.atLink == false")},"Shift-Ctrl-C":"code","Shift-Ctrl-Q":"quote","Shift-Ctrl-S":"del","Shift-Ctrl-K":"tex","Shift-Alt-C":function(){var e=this.cm,t=e.getCursor(),i=e.getSelection();e.replaceSelection(["```",i,"```"].join("\n")),""===i&&e.setCursor(t.line,t.ch+3)},"Shift-Ctrl-Alt-C":"code-block","Shift-Ctrl-H":"html-entities","Shift-Alt-H":"help","Shift-Ctrl-E":"emoji","Shift-Ctrl-U":"uppercase","Shift-Alt-U":"ucwords","Shift-Ctrl-Alt-U":"ucfirst","Shift-Alt-L":"lowercase","Shift-Ctrl-I":function(){var e=this.cm,t=e.getCursor(),i=e.getSelection(),o=""===i?"":' "'+i+'"';e.replaceSelection("!["+i+"]("+o+")"),""===i&&e.setCursor(t.line,t.ch+4)},"Shift-Ctrl-Alt-I":"image","Shift-Ctrl-L":"link","Shift-Ctrl-O":"list-ol","Shift-Ctrl-P":"preformatted-text","Shift-Ctrl-T":"table","Shift-Alt-P":"pagebreak",F9:"watch",F10:"preview",F11:"fullscreen"};var r=function(e){return String.prototype.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")};t.trim=r;var n=function(e){return e.toLowerCase().replace(/\b(\w)|\s(\w)/g,function(e){return e.toUpperCase()})};t.ucwords=t.wordsFirstUpperCase=n;var a=function(e){return e.toLowerCase().replace(/\b(\w)/,function(e){return e.toUpperCase()})};return t.firstUpperCase=t.ucfirst=a,t.urls={atLinkBase:"https://github.com/"},t.regexs={atLink:/@(\w+)/g,email:/(\w+)@(\w+)\.(\w+)\.?(\w+)?/g,emailLink:/(mailto:)?([\w\.\_]+)@(\w+)\.(\w+)\.?(\w+)?/g,emoji:/:([\w\+-]+):/g,emojiDatetime:/(\d{2}:\d{2}:\d{2})/g,twemoji:/:(tw-([\w]+)-?(\w+)?):/g,fontAwesome:/:(fa-([\w]+)(-(\w+)){0,}):/g,editormdLogo:/:(editormd-logo-?(\w+)?):/g,pageBreak:/^\[[=]{8,}\]$/},t.emoji={path:"http://www.emoji-cheat-sheet.com/graphics/emojis/",ext:".png"},t.twemoji={path:"http://twemoji.maxcdn.com/36x36/",ext:".png"},t.markedRenderer=function(i,o){var n={toc:!0,tocm:!1,tocStartLevel:1,pageBreak:!0,atLink:!0,emailLink:!0,taskList:!1,emoji:!1,tex:!1,flowChart:!1,sequenceDiagram:!1},a=e.extend(n,o||{}),s=t.$marked,l=new s.Renderer;i=i||[];var c=t.regexs,h=c.atLink,d=c.emoji,u=c.email,f=c.emailLink,g=c.twemoji,p=c.fontAwesome,m=c.editormdLogo,w=c.pageBreak;return l.emoji=function(e){e=e.replace(t.regexs.emojiDatetime,function(e){return e.replace(/:/g,":")});var i=e.match(d);if(!i||!a.emoji)return e;for(var o=0,r=i.length;r>o;o++)":+1:"===i[o]&&(i[o]=":\\+1:"),e=e.replace(new RegExp(i[o]),function(e,i){var o=e.match(p),r=e.replace(/:/g,"");if(o)for(var n=0,a=o.length;a>n;n++){var s=o[n].replace(/:/g,"");return''}else{var l=e.match(m),c=e.match(g);if(l)for(var h=0,d=l.length;d>h;h++){var u=l[h].replace(/:/g,"");return''}else{if(!c){var f="+1"===r?"plus1":r;return f="black_large_square"===f?"black_square":f,f="moon"===f?"waxing_gibbous_moon":f,':'+r+':'}for(var w=0,v=c.length;v>w;w++){var k=c[w].replace(/:/g,"").replace("tw-","");return'twemoji-'+k+''}}}});return e},l.atLink=function(i){return h.test(i)?(a.atLink&&(i=i.replace(u,function(e,t,i,o){return e.replace(/@/g,"_#_@_#_")}),i=i.replace(h,function(e,i){return''+e+""}).replace(/_#_@_#_/g,"@")),a.emailLink&&(i=i.replace(f,function(t,i,o,r,n){return!i&&e.inArray(n,"jpg|jpeg|png|gif|webp|ico|icon|pdf".split("|"))<0?''+t+"":t})),i):i},l.link=function(e,t,i){if(this.options.sanitize){try{var o=decodeURIComponent(unescape(e)).replace(/[^\w:]/g,"").toLowerCase()}catch(r){return""}if(0===o.indexOf("javascript:"))return""}var n=''+i.replace(/@/g,"@")+""):(t&&(n+=' title="'+t+'"'),n+=">"+i+"")},l.heading=function(e,t,o){var n=e,a=/\s*\]*)\>(.*)\<\/a\>\s*/;if(a.test(e)){var s=[];e=e.split(/\]+)\>([^\>]*)\<\/a\>/);for(var l=0,c=e.length;c>l;l++)s.push(e[l].replace(/\s*href\=\"(.*)\"\s*/g,""));e=s.join(" ")}e=r(e);var h=e.toLowerCase().replace(/[^\w]+/g,"-"),d={text:e,level:t,slug:h},u=/^[\u4e00-\u9fa5]+$/.test(e),f=u?escape(e).replace(/\%/g,""):e.toLowerCase().replace(/[^\w]+/g,"-");i.push(d);var g="';return g+='',g+='',g+=this.atLink(a?this.emoji(n):this.emoji(e)),g+=""},l.pageBreak=function(e){return w.test(e)&&a.pageBreak&&(e='
                  '),e},l.paragraph=function(e){var i=/\$\$(.*)\$\$/g.test(e),o=/^\$\$(.*)\$\$$/.test(e),r=o?' class="'+t.classNames.tex+'"':"",n=a.tocm?/^(\[TOC\]|\[TOCM\])$/.test(e):/^\[TOC\]$/.test(e),s=/^\[TOCM\]$/.test(e);e=!o&&i?e.replace(/(\$\$([^\$]*)\$\$)+/g,function(e,i){return''+i.replace(/\$/g,"")+""}):o?e.replace(/\$/g,""):e;var l='
                  '+e+"
                  ";return n?s?'
                  '+l+"

                  ":l:w.test(e)?this.pageBreak(e):""+this.atLink(this.emoji(e))+"

                  \n"},l.code=function(e,i,o){return"seq"===i||"sequence"===i?'
                  '+e+"
                  ":"flow"===i?'
                  '+e+"
                  ":"math"===i||"latex"===i||"katex"===i?'

                  '+e+"

                  ":s.Renderer.prototype.code.apply(this,arguments)},l.tablecell=function(e,t){var i=t.header?"th":"td",o=t.align?"<"+i+' style="text-align:'+t.align+'">':"<"+i+">";return o+this.atLink(this.emoji(e))+"\n"},l.listitem=function(e){return a.taskList&&/^\s*\[[x\s]\]\s*/.test(e)?(e=e.replace(/^\s*\[\s\]\s*/,' ').replace(/^\s*\[x\]\s*/,' '),'
                • '+this.atLink(this.emoji(e))+"
                • "):"
                • "+this.atLink(this.emoji(e))+"
                • "},l},t.markdownToCRenderer=function(e,t,i,o){var r="",n=0,a=this.classPrefix;o=o||1;for(var s=0,l=e.length;l>s;s++){var c=e[s].text,h=e[s].level;o>h||(r+=h>n?"":n>h?new Array(n-h+2).join("
              • "):"",r+='
              • '+c+"
                  ",n=h)}var d=t.find(".markdown-toc");if(d.length<1&&"false"===t.attr("previewContainer")){var u='
                  ';u=i?'
                  '+u+"
                  ":u,t.html(u),d=t.find(".markdown-toc")}return i&&d.wrap('

                  '),d.html('
                    ').children(".markdown-toc-list").html(r.replace(/\r?\n?\\<\/ul\>/g,"")),d},t.tocDropdownMenu=function(t,i){i=i||"Table of Contents";var o=400,r=t.find("."+this.classPrefix+"toc-menu");return r.each(function(){var t=e(this),r=t.children(".markdown-toc"),n='',a=''+n+i+"",s=r.children("ul"),l=s.find("li");r.append(a),l.first().before("
                  • "+i+" "+n+"

                  • "),t.mouseover(function(){s.show(),l.each(function(){var t=e(this),i=t.children("ul");if(""===i.html()&&i.remove(),i.length>0&&""!==i.html()){var r=t.children("a").first();r.children(".fa").length<1&&r.append(e(n).css({"float":"right",paddingTop:"4px"}))}t.mouseover(function(){i.css("z-index",o).show(),o+=1}).mouseleave(function(){i.hide()})})}).mouseleave(function(){s.hide()})}),r},t.filterHTMLTags=function(t,i){if("string"!=typeof t&&(t=new String(t)),"string"!=typeof i)return t;for(var o=i.split("|"),r=o[0].split(","),n=o[1],a=0,s=r.length;s>a;a++){var l=r[a];t=t.replace(new RegExp("]*)>([^>]*)","igm"),"")}if("undefined"!=typeof n){var c=/\<(\w+)\s*([^\>]*)\>([^\>]*)\<\/(\w+)\>/gi;t="*"===n?t.replace(c,function(e,t,i,o,r){return"<"+t+">"+o+""}):"on*"===n?t.replace(c,function(t,i,o,r,n){var a=e("<"+i+">"+r+""),s=e(t)[0].attributes,l={};e.each(s,function(e,t){'"'!==t.nodeName&&(l[t.nodeName]=t.nodeValue)}),e.each(l,function(e){0===e.indexOf("on")&&delete l[e]}),a.attr(l);var c="undefined"!=typeof a[1]?e(a[1]).text():"";return a[0].outerHTML+c}):t.replace(c,function(t,i,o,r){var a=n.split(","),s=e(t);return s.html(r),e.each(a,function(e){s.attr(a[e],null)}),s[0].outerHTML})}return t},t.markdownToHTML=function(i,o){var r={gfm:!0,toc:!0,tocm:!1,tocStartLevel:1,tocTitle:"目录",tocDropdown:!1,tocContainer:"",markdown:"",markdownSourceCode:!1,htmlDecode:!1,autoLoadKaTeX:!0,pageBreak:!0,atLink:!0,emailLink:!0,tex:!1,taskList:!1,emoji:!1,flowChart:!1,sequenceDiagram:!1,previewCodeHighlight:!0};t.$marked=marked;var n=e("#"+i),a=n.settings=e.extend(!0,r,o||{}),s=n.find("textarea");s.length<1&&(n.append(""),s=n.find("textarea"));var l=""===a.markdown?s.val():a.markdown,c=[],h={toc:a.toc,tocm:a.tocm,tocStartLevel:a.tocStartLevel,taskList:a.taskList,emoji:a.emoji,tex:a.tex,pageBreak:a.pageBreak,atLink:a.atLink,emailLink:a.emailLink,flowChart:a.flowChart,sequenceDiagram:a.sequenceDiagram,previewCodeHighlight:a.previewCodeHighlight},d={renderer:t.markedRenderer(c,h),gfm:a.gfm,tables:!0,breaks:!0,pedantic:!1,sanitize:a.htmlDecode?!1:!0,smartLists:!0,smartypants:!0};l=new String(l);var u=marked(l,d);u=t.filterHTMLTags(u,a.htmlDecode),a.markdownSourceCode?s.text(l):s.remove(),n.addClass("markdown-body "+this.classPrefix+"html-preview").append(u);var f=""!==a.tocContainer?e(a.tocContainer):n;if(""!==a.tocContainer&&f.attr("previewContainer",!1),a.toc&&(n.tocContainer=this.markdownToCRenderer(c,f,a.tocDropdown,a.tocStartLevel),(a.tocDropdown||n.find("."+this.classPrefix+"toc-menu").length>0)&&this.tocDropdownMenu(n,a.tocTitle),""!==a.tocContainer&&n.find(".editormd-toc-menu, .editormd-markdown-toc").remove()),a.previewCodeHighlight&&(n.find("pre").addClass("prettyprint linenums"),prettyPrint()),t.isIE8||(a.flowChart&&n.find(".flowchart").flowChart(),a.sequenceDiagram&&n.find(".sequence-diagram").sequenceDiagram({theme:"simple"})),a.tex){var g=function(){n.find("."+t.classNames.tex).each(function(){var t=e(this);katex.render(t.html().replace(/</g,"<").replace(/>/g,">"),t[0]),t.find(".katex").css("font-size","1.6em")})};!a.autoLoadKaTeX||t.$katex||t.kaTeXLoaded?g():this.loadKaTeX(function(){t.$katex=katex,t.kaTeXLoaded=!0,g()})}return n.getMarkdown=function(){return s.val()},n},t.themes=["default","dark"],t.previewThemes=["default","dark"],t.editorThemes=["default","3024-day","3024-night","ambiance","ambiance-mobile","base16-dark","base16-light","blackboard","cobalt","eclipse","elegant","erlang-dark","lesser-dark","mbo","mdn-like","midnight","monokai","neat","neo","night","paraiso-dark","paraiso-light","pastel-on-dark","rubyblue","solarized","the-matrix","tomorrow-night-eighties","twilight","vibrant-ink","xq-dark","xq-light"],t.loadPlugins={},t.loadFiles={js:[],css:[],plugin:[]},t.loadPlugin=function(e,i,o){i=i||function(){},this.loadScript(e,function(){t.loadFiles.plugin.push(e),i()},o)},t.loadCSS=function(e,i,o){o=o||"head",i=i||function(){};var r=document.createElement("link");r.type="text/css",r.rel="stylesheet",r.onload=r.onreadystatechange=function(){t.loadFiles.css.push(e),i()},r.href=e+".css","head"===o?document.getElementsByTagName("head")[0].appendChild(r):document.body.appendChild(r)},t.isIE="Microsoft Internet Explorer"==navigator.appName,t.isIE8=t.isIE&&"8."==navigator.appVersion.match(/8./i),t.loadScript=function(e,i,o){o=o||"head",i=i||function(){};var r=null;r=document.createElement("script"),r.id=e.replace(/[\./]+/g,"-"),r.type="text/javascript",r.src=e+".js",t.isIE8?r.onreadystatechange=function(){r.readyState&&("loaded"===r.readyState||"complete"===r.readyState)&&(r.onreadystatechange=null,t.loadFiles.js.push(e),i())}:r.onload=function(){t.loadFiles.js.push(e),i()},"head"===o?document.getElementsByTagName("head")[0].appendChild(r):document.body.appendChild(r)},t.katexURL={css:"//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min",js:"//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min"},t.kaTeXLoaded=!1,t.loadKaTeX=function(e){t.loadCSS(t.katexURL.css,function(){t.loadScript(t.katexURL.js,e||function(){})})},t.lockScreen=function(t){e("html,body").css("overflow",t?"hidden":"")},t.createDialog=function(i){var o={name:"",width:420,height:240,title:"",drag:!0,closed:!0,content:"",mask:!0,maskStyle:{backgroundColor:"#fff",opacity:.1},lockScreen:!0,footer:!0,buttons:!1};i=e.extend(!0,o,i);var r=this,n=this.editor,a=t.classPrefix,s=(new Date).getTime(),l=""===i.name?a+"dialog-"+s:i.name,c=t.mouseOrTouch,h='
                    ';""!==i.title&&(h+='
                    ",h+=''+i.title+"",h+="
                    "),i.closed&&(h+=''),h+='
                    '+i.content,(i.footer||"string"==typeof i.footer)&&(h+='"),h+="
                    ",h+='
                    ',h+='
                    ',h+="
                    ",n.append(h);var d=n.find("."+l);d.lockScreen=function(t){return i.lockScreen&&(e("html,body").css("overflow",t?"hidden":""),r.resize()),d},d.showMask=function(){return i.mask&&n.find("."+a+"mask").css(i.maskStyle).css("z-index",t.dialogZindex-1).show(),d},d.hideMask=function(){return i.mask&&n.find("."+a+"mask").hide(),d},d.loading=function(e){var t=d.find("."+a+"dialog-mask");return t[e?"show":"hide"](),d},d.lockScreen(!0).showMask(),d.show().css({zIndex:t.dialogZindex,border:t.isIE8?"1px solid #ddd":"",width:"number"==typeof i.width?i.width+"px":i.width,height:"number"==typeof i.height?i.height+"px":i.height});var u=function(){d.css({top:(e(window).height()-d.height())/2+"px",left:(e(window).width()-d.width())/2+"px"})};if(u(),e(window).resize(u),d.children("."+a+"dialog-close").bind(c("click","touchend"),function(){d.hide().lockScreen(!1).hideMask()}),"object"==typeof i.buttons){var f=d.footer=d.find("."+a+"dialog-footer");for(var g in i.buttons){var p=i.buttons[g],m=a+g+"-btn";f.append('"),p[1]=e.proxy(p[1],d),f.children("."+m).bind(c("click","touchend"),p[1])}}if(""!==i.title&&i.drag){var w,v,k=d.children("."+a+"dialog-header");i.mask||k.bind(c("click","touchend"),function(){t.dialogZindex+=2,d.css("z-index",t.dialogZindex)}),k.mousedown(function(e){e=e||window.event,w=e.clientX-parseInt(d[0].style.left),v=e.clientY-parseInt(d[0].style.top),document.onmousemove=y});var b=function(e){e.removeClass(a+"user-unselect").off("selectstart")},x=function(e){e.addClass(a+"user-unselect").on("selectstart",function(e){return!1})},y=function(t){t=t||window.event;var i,o,r=parseInt(d[0].style.left),n=parseInt(d[0].style.top);r>=0?r+d.width()<=e(window).width()?i=t.clientX-w:(i=e(window).width()-d.width(),document.onmousemove=null):(i=0,document.onmousemove=null),n>=0?o=t.clientY-v:(o=0,document.onmousemove=null),document.onselectstart=function(){return!1},x(e("body")),x(d),d[0].style.left=i+"px",d[0].style.top=o+"px"};document.onmouseup=function(){b(e("body")),b(d),document.onselectstart=null,document.onmousemove=null},k.touchDraggable=function(){var t=null,i=function(i){var o=i.originalEvent,r=e(this).parent().position();t={x:o.changedTouches[0].pageX-r.left,y:o.changedTouches[0].pageY-r.top}},o=function(i){i.preventDefault();var o=i.originalEvent;e(this).parent().css({top:o.changedTouches[0].pageY-t.y,left:o.changedTouches[0].pageX-t.x})};this.bind("touchstart",i).bind("touchmove",o)},k.touchDraggable()}return t.dialogZindex+=2,d},t.mouseOrTouch=function(e,t){e=e||"click",t=t||"touchend";var i=e;try{document.createEvent("TouchEvent"),i=t}catch(o){}return i},t.dateFormat=function(e){e=e||"";var t=function(e){return 10>e?"0"+e:e},i=new Date,o=i.getFullYear(),r=o.toString().slice(2,4),n=t(i.getMonth()+1),a=t(i.getDate()),s=i.getDay(),l=t(i.getHours()),c=t(i.getMinutes()),h=t(i.getSeconds()),d=t(i.getMilliseconds()),u="",f=r+"-"+n+"-"+a,g=o+"-"+n+"-"+a,p=l+":"+c+":"+h;switch(e){case"UNIX Time":u=i.getTime();break;case"UTC":u=i.toUTCString();break;case"yy":u=r;break;case"year":case"yyyy":u=o;break;case"month":case"mm":u=n;break;case"cn-week-day":case"cn-wd":var m=["日","一","二","三","四","五","六"];u="星期"+m[s];break;case"week-day":case"wd":var w=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];u=w[s];break;case"day":case"dd":u=a;break;case"hour":case"hh":u=l;break;case"min":case"ii":u=c;break;case"second":case"ss":u=h;break;case"ms":u=d;break;case"yy-mm-dd":u=f;break;case"yyyy-mm-dd":u=g;break;case"yyyy-mm-dd h:i:s ms":case"full + ms":u=g+" "+p+" "+d;break;case"full":case"yyyy-mm-dd h:i:s":default:u=g+" "+p}return u},t}}); \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/@links.html b/paicoding-ui/src/main/resources/static/editormd/examples/@links.html new file mode 100644 index 000000000..2cc6a1062 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/@links.html @@ -0,0 +1,135 @@ + + + + + @links - Editor.md examples + + + + + +
                    +
                    +

                    @links

                    +

                    Github Flavored Markdown extras syntax

                    +
                    +
                    + +
                    +
                    + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/auto-height.html b/paicoding-ui/src/main/resources/static/editormd/examples/auto-height.html new file mode 100644 index 000000000..4a37c4319 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/auto-height.html @@ -0,0 +1,55 @@ + + + + + Auto height - Editor.md examples + + + + + +
                    +
                    +

                    Auto height test

                    +
                    +
                    + +
                    +
                    + +
                    +
                    + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/change-mode.html b/paicoding-ui/src/main/resources/static/editormd/examples/change-mode.html new file mode 100644 index 000000000..e25798af1 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/change-mode.html @@ -0,0 +1,508 @@ + + + + + Chnage mode - Editor.md examples + + + + + + +
                    +
                    +

                    Chnage mode

                    +

                    Become to the code editor

                    +

                    Modes :   Themes : + +

                    +
                    +
                    + + +
                    +
                    + + + + + + + + + + +
                    +
                    + +
                    +
                    + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/code-fold.html b/paicoding-ui/src/main/resources/static/editormd/examples/code-fold.html new file mode 100644 index 000000000..e2774bcc8 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/code-fold.html @@ -0,0 +1,44 @@ + + + + + Code folding - Editor.md examples + + + + + +
                    +
                    +

                    Code folding

                    +

                    Switch code folding : Press Ctrl + Q / Command + Q

                    +
                    +
                    + +
                    +
                    + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/css/style.css b/paicoding-ui/src/main/resources/static/editormd/examples/css/style.css new file mode 100644 index 000000000..0150e3b9a --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/css/style.css @@ -0,0 +1,94 @@ +* { + padding: 0; + margin: 0; +} + +*, *:before, *:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td,hr,button,article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{ + margin: 0; + padding: 0; +} + +article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary { + display: block; +} + +audio, canvas, video { + display: inline-block; +} + +img { + border: none; + vertical-align: middle; +} + +ul, ol { + /*list-style: none;*/ +} + +.clear { + *zoom: 1; /* for IE 6/7 */ +} + +.clear:before, .clear:after { + height: 0; + content: ""; + font-size: 0; + display: table; + line-height: 0; /* for Opera */ + visibility: hidden; +} + +.clear:after { + clear: both; +} + +body { + font-size: 14px; + color: #666; + font-family: "Microsoft YaHei", "微软雅黑", Helvetica, Tahoma, STXihei, "华文细黑", STHeiti, "Helvetica Neue", Helvetica, Tahoma, "Droid Sans", "wenquanyi micro hei", FreeSans, Arimo, Arial, SimSun, "宋体", Heiti, "黑体", sans-serif; + background: #fff; + text-align: center; +} + +#layout { + text-align: left; +} + +#layout > header, .btns { + padding: 15px 0; + width: 90%; + margin: 0 auto; +} + +.btns { + padding-top: 0; +} + +.btns button { + padding: 2px 8px; +} + +#layout > header > h1 { + font-size: 20px; + margin-bottom: 10px; +} + +.btns button, .btn { + padding: 8px 10px; + background: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 3px; + border-radius: 3px; + cursor: pointer; + -webkit-transition: background 300ms ease-out; + transition: background 300ms ease-out; +} + +.btns button:hover, .btn:hover { + background: #f6f6f6; +} \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/custom-keyboard-shortcuts.html b/paicoding-ui/src/main/resources/static/editormd/examples/custom-keyboard-shortcuts.html new file mode 100644 index 000000000..3afc27ba4 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/custom-keyboard-shortcuts.html @@ -0,0 +1,118 @@ + + + + + Custom keyboard shortcuts - Editor.md examples + + + + + +
                    +
                    +

                    Custom keyboard shortcuts

                    +
                    +
                    + +
                    +
                    + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/custom-toolbar.html b/paicoding-ui/src/main/resources/static/editormd/examples/custom-toolbar.html new file mode 100644 index 000000000..89177dae8 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/custom-toolbar.html @@ -0,0 +1,178 @@ + + + + + 自定义工具栏 - Editor.md examples + + + + + +
                    +
                    +

                    自定义工具栏

                    +

                    Custom toolbar (icons handler)

                    +
                    +
                    + +
                    +
                    + + + + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/define-plugin.html b/paicoding-ui/src/main/resources/static/editormd/examples/define-plugin.html new file mode 100644 index 000000000..8c867e72c --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/define-plugin.html @@ -0,0 +1,151 @@ + + + + + Define extention plugins for Editor.md - Editor.md examples + + + + + +
                    +
                    +

                    Define extention plugins for Editor.md

                    +
                    +
                    + +
                    +
                    + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/delay-renderer-preview.html b/paicoding-ui/src/main/resources/static/editormd/examples/delay-renderer-preview.html new file mode 100644 index 000000000..ad343a2a0 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/delay-renderer-preview.html @@ -0,0 +1,56 @@ + + + + + Delay Rerender & Preview - Editor.md examples + + + + + +
                    +
                    +

                    Delay Rerender & Preview

                    +

                    P.S. If you input the content too much and too fast, You can setting the delay value.

                    +

                    P.S. 适用于输入内容太多太快的情形,但要是一个合理的值,不然会显得预览太慢。打字慢会相对显得慢,打字快时则相对显得快。

                    +
                    +
                    + +
                    +
                    + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/dynamic-create-editormd.html b/paicoding-ui/src/main/resources/static/editormd/examples/dynamic-create-editormd.html new file mode 100644 index 000000000..5644e0982 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/dynamic-create-editormd.html @@ -0,0 +1,47 @@ + + + + + 动态创建 Editor.md - Editor.md examples + + + + + +
                    +
                    +

                    动态创建 Editor.md

                    +

                    Dynamic create Editor.md

                    +
                    +
                    + + +
                    +
                    +
                    + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/emoji.html b/paicoding-ui/src/main/resources/static/editormd/examples/emoji.html new file mode 100644 index 000000000..a5a6ea642 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/emoji.html @@ -0,0 +1,191 @@ + + + + + Emoji - Editor.md examples + + + + + + +
                    +
                    +

                    Emoji 表情

                    +

                    Supports:

                    + +
                    +
                    + +
                    +
                    + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/extends.html b/paicoding-ui/src/main/resources/static/editormd/examples/extends.html new file mode 100644 index 000000000..96018603d --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/extends.html @@ -0,0 +1,153 @@ + + + + + Expanded Editor.md - Editor.md examples + + + + + +
                    +
                    +

                    Expanded Editor.md

                    +

                    Expanded of member methods and properties

                    +
                    +
                    + +
                    +
                    + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/external-use.html b/paicoding-ui/src/main/resources/static/editormd/examples/external-use.html new file mode 100644 index 000000000..32e02e26a --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/external-use.html @@ -0,0 +1,119 @@ + + + + + External use - Editor.md examples + + + + + +
                    +
                    +

                    External use

                    +

                    External use of toolbar handlers / modal dialog

                    +
                    +
                    + + + + + + + + +
                    +
                    + +
                    +
                    + + + + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/flowchart.html b/paicoding-ui/src/main/resources/static/editormd/examples/flowchart.html new file mode 100644 index 000000000..5149cb7fb --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/flowchart.html @@ -0,0 +1,53 @@ + + + + + FlowChart - Editor.md examples + + + + + +
                    +
                    +

                    FlowChart 流程图

                    +

                    Based on flowchart.js:http://adrai.github.io/flowchart.js/

                    +
                    +
                    + +
                    +
                    + + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/form-get-value.html b/paicoding-ui/src/main/resources/static/editormd/examples/form-get-value.html new file mode 100644 index 000000000..5433d4535 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/form-get-value.html @@ -0,0 +1,92 @@ + + + + + Form get textarea value - Editor.md examples + + + + + +
                    +
                    +

                    表单取值

                    +

                    Form get textarea value.

                    +
                    +
                    +
                    + + +
                    +
                    + +
                    +
                    +
                    + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/full.html b/paicoding-ui/src/main/resources/static/editormd/examples/full.html new file mode 100644 index 000000000..6fe08183a --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/full.html @@ -0,0 +1,231 @@ + + + + + Full example - Editor.md examples + + + + + + +
                    +
                    +

                    完整示例

                    +

                    Full example

                    +
                      +
                    • Enable HTML tags decode
                    • +
                    • Enable TeX, Flowchart, Sequence Diagram, Emoji, FontAwesome, Task lists
                    • +
                    • Enable Image upload
                    • +
                    • Enable [TOCM], Search Replace, Code fold
                    • +
                    +
                    +
                    + + + + + + + + + + + + + +
                    +
                    +
                    + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/goto-line.html b/paicoding-ui/src/main/resources/static/editormd/examples/goto-line.html new file mode 100644 index 000000000..7eba47b08 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/goto-line.html @@ -0,0 +1,84 @@ + + + + + Goto line - Editor.md examples + + + + + +
                    +
                    +

                    Goto line

                    +
                    +
                    + + + + + + + +
                    +
                    +
                    + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/html-preview-markdown-to-html-custom-toc-container.html b/paicoding-ui/src/main/resources/static/editormd/examples/html-preview-markdown-to-html-custom-toc-container.html new file mode 100644 index 000000000..bdf1bd6eb --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/html-preview-markdown-to-html-custom-toc-container.html @@ -0,0 +1,180 @@ + + + + + HTML Preview (markdown to html) - Editor.md examples + + + + + + +
                    +
                    +

                    Markdown转HTML的显示处理之自定义 ToC 容器

                    +

                    即:非编辑情况下的HTML预览

                    +

                    HTML Preview (markdown to html and custom ToC container)

                    +
                    +
                    + +
                    + +
                    + +
                    +
                    + +
                    +
                    + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/html-preview-markdown-to-html.html b/paicoding-ui/src/main/resources/static/editormd/examples/html-preview-markdown-to-html.html new file mode 100644 index 000000000..ad1cf590f --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/html-preview-markdown-to-html.html @@ -0,0 +1,142 @@ + + + + + HTML Preview(markdown to html) - Editor.md examples + + + + + + +
                    +
                    +

                    Markdown转HTML的显示处理

                    +

                    即:非编辑情况下的HTML预览

                    +

                    HTML Preview(markdown to html)

                    +
                    +
                    + +
                    +
                    + +
                    +
                    + +
                    +
                    + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/html-tags-decode.html b/paicoding-ui/src/main/resources/static/editormd/examples/html-tags-decode.html new file mode 100644 index 000000000..34de0d32c --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/html-tags-decode.html @@ -0,0 +1,119 @@ + + + + + 识别和解析 HTML 标签 - Editor.md examples + + + + + +
                    +
                    +

                    识别和解析HTML标签

                    +

                    HTML tags (filter) decode, You can increase safety by filtering the danger label.

                    +

                    注:虽然此功能能极大地扩展 Markdown 语法,但也面临着安全上的风险,所以默认是不开启的。

                    +

                    Update: 可以通过设置 `settings.htmlDecode = "style,script,iframe|on*"`来实现过滤指定标签及属性的解析,提高安全性;

                    +
                    +
                    + + + + +
                    +
                    + +
                    +
                    + + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/image-cross-domain-upload.html b/paicoding-ui/src/main/resources/static/editormd/examples/image-cross-domain-upload.html new file mode 100644 index 000000000..5a635454e --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/image-cross-domain-upload.html @@ -0,0 +1,109 @@ + + + + + 图片跨域上传示例 - Editor.md examples + + + + + +
                    +
                    +

                    图片跨域上传示例

                    +

                    Image cross-domain upload example.

                    +
                    +
                    + +
                    +
                    + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/image-upload.html b/paicoding-ui/src/main/resources/static/editormd/examples/image-upload.html new file mode 100644 index 000000000..e6fa69bc6 --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/image-upload.html @@ -0,0 +1,68 @@ + + + + + 图片上传示例 - Editor.md examples + + + + + +
                    +
                    +

                    图片上传示例

                    +

                    Image upload example

                    +
                    +
                    + +
                    +
                    + + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/images/4.jpg b/paicoding-ui/src/main/resources/static/editormd/examples/images/4.jpg new file mode 100644 index 000000000..948f88c0b Binary files /dev/null and b/paicoding-ui/src/main/resources/static/editormd/examples/images/4.jpg differ diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/images/7.jpg b/paicoding-ui/src/main/resources/static/editormd/examples/images/7.jpg new file mode 100644 index 000000000..c1806731c Binary files /dev/null and b/paicoding-ui/src/main/resources/static/editormd/examples/images/7.jpg differ diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/images/8.jpg b/paicoding-ui/src/main/resources/static/editormd/examples/images/8.jpg new file mode 100644 index 000000000..f56e66eb6 Binary files /dev/null and b/paicoding-ui/src/main/resources/static/editormd/examples/images/8.jpg differ diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/images/editormd-screenshot.png b/paicoding-ui/src/main/resources/static/editormd/examples/images/editormd-screenshot.png new file mode 100644 index 000000000..f63f633ba Binary files /dev/null and b/paicoding-ui/src/main/resources/static/editormd/examples/images/editormd-screenshot.png differ diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/index.html b/paicoding-ui/src/main/resources/static/editormd/examples/index.html new file mode 100644 index 000000000..1d717e9fe --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/index.html @@ -0,0 +1,356 @@ + + + + + Editor.md examples + + + + + + + +
                    + +

                    Basic

                    + +

                    + TOP + 自定义 Customs +

                    + +

                    + TOP + Markdown Extras +

                    + +

                    + TOP + Image Upload +

                    + +

                    + TOP + 事件处理 Events handle +

                    + +
                    + +
                    + + + + \ No newline at end of file diff --git a/paicoding-ui/src/main/resources/static/editormd/examples/js/jquery.min.js b/paicoding-ui/src/main/resources/static/editormd/examples/js/jquery.min.js new file mode 100644 index 000000000..b36821bec --- /dev/null +++ b/paicoding-ui/src/main/resources/static/editormd/examples/js/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.11.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.1",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b=a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="
                    ",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h; +if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="
                    a",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function ab(){return!0}function bb(){return!1}function cb(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),hb=/^\s+/,ib=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,jb=/<([\w:]+)/,kb=/\s*$/g,rb={option:[1,""],legend:[1,"
                    ","
                    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
                    "],tr:[2,"","
                    "],col:[2,"","
                    "],td:[3,"","
                    "],_default:k.htmlSerialize?[0,"",""]:[1,"X
                    ","
                    "]},sb=db(y),tb=sb.appendChild(y.createElement("div"));rb.optgroup=rb.option,rb.tbody=rb.tfoot=rb.colgroup=rb.caption=rb.thead,rb.th=rb.td;function ub(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ub(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function vb(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wb(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xb(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function yb(a){var b=pb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function zb(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Ab(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Bb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xb(b).text=a.text,yb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!gb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(tb.innerHTML=a.outerHTML,tb.removeChild(f=tb.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ub(f),h=ub(a),g=0;null!=(e=h[g]);++g)d[g]&&Bb(e,d[g]);if(b)if(c)for(h=h||ub(a),d=d||ub(f),g=0;null!=(e=h[g]);g++)Ab(e,d[g]);else Ab(a,f);return d=ub(f,"script"),d.length>0&&zb(d,!i&&ub(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=db(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(lb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(jb.exec(f)||["",""])[1].toLowerCase(),l=rb[i]||rb._default,h.innerHTML=l[1]+f.replace(ib,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&hb.test(f)&&p.push(b.createTextNode(hb.exec(f)[0])),!k.tbody){f="table"!==i||kb.test(f)?""!==l[1]||kb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ub(p,"input"),vb),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ub(o.appendChild(f),"script"),g&&zb(h),c)){e=0;while(f=h[e++])ob.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ub(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&zb(ub(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ub(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fb,""):void 0;if(!("string"!=typeof a||mb.test(a)||!k.htmlSerialize&&gb.test(a)||!k.leadingWhitespace&&hb.test(a)||rb[(jb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ib,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ub(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ub(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&nb.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ub(i,"script"),xb),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ub(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,yb),j=0;f>j;j++)d=g[j],ob.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qb,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Cb,Db={};function Eb(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fb(a){var b=y,c=Db[a];return c||(c=Eb(a,b),"none"!==c&&c||(Cb=(Cb||m("