commit 3e0da32c18f051ce3562f496b0cdd272667c8cc1
Author: liumangmang <362165265@qq.com>
Date: Mon Dec 15 23:16:59 2025 +0800
第一次提交
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6146024
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,31 @@
+# Node.js
+node_modules/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+package-lock.json
+
+# VuePress
+src/.vuepress/.cache/
+src/.vuepress/.temp/
+src/.vuepress/dist/
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+logs/
+*.log
+
+# Environment
+.env
+.env.local
+.env.*.local
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..10fabd9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,395 @@
+Attribution 4.0 International
+
+=======================================================================
+
+Creative Commons Corporation ("Creative Commons") is not a law firm and
+does not provide legal services or legal advice. Distribution of
+Creative Commons public licenses does not create a lawyer-client or
+other relationship. Creative Commons makes its licenses and related
+information available on an "as-is" basis. Creative Commons gives no
+warranties regarding its licenses, any material licensed under their
+terms and conditions, or any related information. Creative Commons
+disclaims all liability for damages resulting from their use to the
+fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and
+conditions that creators and other rights holders may use to share
+original works of authorship and other material subject to copyright
+and certain other rights specified in the public license below. The
+following considerations are for informational purposes only, are not
+exhaustive, and do not form part of our licenses.
+
+ Considerations for licensors: Our public licenses are
+ intended for use by those authorized to give the public
+ permission to use material in ways otherwise restricted by
+ copyright and certain other rights. Our licenses are
+ irrevocable. Licensors should read and understand the terms
+ and conditions of the license they choose before applying it.
+ Licensors should also secure all rights necessary before
+ applying our licenses so that the public can reuse the
+ material as expected. Licensors should clearly mark any
+ material not subject to the license. This includes other CC-
+ licensed material, or material used under an exception or
+ limitation to copyright. More considerations for licensors:
+ wiki.creativecommons.org/Considerations_for_licensors
+
+ Considerations for the public: By using one of our public
+ licenses, a licensor grants the public permission to use the
+ licensed material under specified terms and conditions. If
+ the licensor's permission is not necessary for any reason--for
+ example, because of any applicable exception or limitation to
+ copyright--then that use is not regulated by the license. Our
+ licenses grant only permissions under copyright and certain
+ other rights that a licensor has authority to grant. Use of
+ the licensed material may still be restricted for other
+ reasons, including because others have copyright or other
+ rights in the material. A licensor may make special requests,
+ such as asking that all changes be marked or described.
+ Although not required by our licenses, you are encouraged to
+ respect those requests where reasonable. More considerations
+ for the public:
+ wiki.creativecommons.org/Considerations_for_licensees
+
+=======================================================================
+
+Creative Commons Attribution 4.0 International Public License
+
+By exercising the Licensed Rights (defined below), You accept and agree
+to be bound by the terms and conditions of this Creative Commons
+Attribution 4.0 International Public License ("Public License"). To the
+extent this Public License may be interpreted as a contract, You are
+granted the Licensed Rights in consideration of Your acceptance of
+these terms and conditions, and the Licensor grants You such rights in
+consideration of benefits the Licensor receives from making the
+Licensed Material available under these terms and conditions.
+
+
+Section 1 -- Definitions.
+
+ a. Adapted Material means material subject to Copyright and Similar
+ Rights that is derived from or based upon the Licensed Material
+ and in which the Licensed Material is translated, altered,
+ arranged, transformed, or otherwise modified in a manner requiring
+ permission under the Copyright and Similar Rights held by the
+ Licensor. For purposes of this Public License, where the Licensed
+ Material is a musical work, performance, or sound recording,
+ Adapted Material is always produced where the Licensed Material is
+ synched in timed relation with a moving image.
+
+ b. Adapter's License means the license You apply to Your Copyright
+ and Similar Rights in Your contributions to Adapted Material in
+ accordance with the terms and conditions of this Public License.
+
+ c. Copyright and Similar Rights means copyright and/or similar rights
+ closely related to copyright including, without limitation,
+ performance, broadcast, sound recording, and Sui Generis Database
+ Rights, without regard to how the rights are labeled or
+ categorized. For purposes of this Public License, the rights
+ specified in Section 2(b)(1)-(2) are not Copyright and Similar
+ Rights.
+
+ d. Effective Technological Measures means those measures that, in the
+ absence of proper authority, may not be circumvented under laws
+ fulfilling obligations under Article 11 of the WIPO Copyright
+ Treaty adopted on December 20, 1996, and/or similar international
+ agreements.
+
+ e. Exceptions and Limitations means fair use, fair dealing, and/or
+ any other exception or limitation to Copyright and Similar Rights
+ that applies to Your use of the Licensed Material.
+
+ f. Licensed Material means the artistic or literary work, database,
+ or other material to which the Licensor applied this Public
+ License.
+
+ g. Licensed Rights means the rights granted to You subject to the
+ terms and conditions of this Public License, which are limited to
+ all Copyright and Similar Rights that apply to Your use of the
+ Licensed Material and that the Licensor has authority to license.
+
+ h. Licensor means the individual(s) or entity(ies) granting rights
+ under this Public License.
+
+ i. Share means to provide material to the public by any means or
+ process that requires permission under the Licensed Rights, such
+ as reproduction, public display, public performance, distribution,
+ dissemination, communication, or importation, and to make material
+ available to the public including in ways that members of the
+ public may access the material from a place and at a time
+ individually chosen by them.
+
+ j. Sui Generis Database Rights means rights other than copyright
+ resulting from Directive 96/9/EC of the European Parliament and of
+ the Council of 11 March 1996 on the legal protection of databases,
+ as amended and/or succeeded, as well as other essentially
+ equivalent rights anywhere in the world.
+
+ k. You means the individual or entity exercising the Licensed Rights
+ under this Public License. Your has a corresponding meaning.
+
+
+Section 2 -- Scope.
+
+ a. License grant.
+
+ 1. Subject to the terms and conditions of this Public License,
+ the Licensor hereby grants You a worldwide, royalty-free,
+ non-sublicensable, non-exclusive, irrevocable license to
+ exercise the Licensed Rights in the Licensed Material to:
+
+ a. reproduce and Share the Licensed Material, in whole or
+ in part; and
+
+ b. produce, reproduce, and Share Adapted Material.
+
+ 2. Exceptions and Limitations. For the avoidance of doubt, where
+ Exceptions and Limitations apply to Your use, this Public
+ License does not apply, and You do not need to comply with
+ its terms and conditions.
+
+ 3. Term. The term of this Public License is specified in Section
+ 6(a).
+
+ 4. Media and formats; technical modifications allowed. The
+ Licensor authorizes You to exercise the Licensed Rights in
+ all media and formats whether now known or hereafter created,
+ and to make technical modifications necessary to do so. The
+ Licensor waives and/or agrees not to assert any right or
+ authority to forbid You from making technical modifications
+ necessary to exercise the Licensed Rights, including
+ technical modifications necessary to circumvent Effective
+ Technological Measures. For purposes of this Public License,
+ simply making modifications authorized by this Section 2(a)
+ (4) never produces Adapted Material.
+
+ 5. Downstream recipients.
+
+ a. Offer from the Licensor -- Licensed Material. Every
+ recipient of the Licensed Material automatically
+ receives an offer from the Licensor to exercise the
+ Licensed Rights under the terms and conditions of this
+ Public License.
+
+ b. No downstream restrictions. You may not offer or impose
+ any additional or different terms or conditions on, or
+ apply any Effective Technological Measures to, the
+ Licensed Material if doing so restricts exercise of the
+ Licensed Rights by any recipient of the Licensed
+ Material.
+
+ 6. No endorsement. Nothing in this Public License constitutes or
+ may be construed as permission to assert or imply that You
+ are, or that Your use of the Licensed Material is, connected
+ with, or sponsored, endorsed, or granted official status by,
+ the Licensor or others designated to receive attribution as
+ provided in Section 3(a)(1)(A)(i).
+
+ b. Other rights.
+
+ 1. Moral rights, such as the right of integrity, are not
+ licensed under this Public License, nor are publicity,
+ privacy, and/or other similar personality rights; however, to
+ the extent possible, the Licensor waives and/or agrees not to
+ assert any such rights held by the Licensor to the limited
+ extent necessary to allow You to exercise the Licensed
+ Rights, but not otherwise.
+
+ 2. Patent and trademark rights are not licensed under this
+ Public License.
+
+ 3. To the extent possible, the Licensor waives any right to
+ collect royalties from You for the exercise of the Licensed
+ Rights, whether directly or through a collecting society
+ under any voluntary or waivable statutory or compulsory
+ licensing scheme. In all other cases the Licensor expressly
+ reserves any right to collect such royalties.
+
+
+Section 3 -- License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the
+following conditions.
+
+ a. Attribution.
+
+ 1. If You Share the Licensed Material (including in modified
+ form), You must:
+
+ a. retain the following if it is supplied by the Licensor
+ with the Licensed Material:
+
+ i. identification of the creator(s) of the Licensed
+ Material and any others designated to receive
+ attribution, in any reasonable manner requested by
+ the Licensor (including by pseudonym if
+ designated);
+
+ ii. a copyright notice;
+
+ iii. a notice that refers to this Public License;
+
+ iv. a notice that refers to the disclaimer of
+ warranties;
+
+ v. a URI or hyperlink to the Licensed Material to the
+ extent reasonably practicable;
+
+ b. indicate if You modified the Licensed Material and
+ retain an indication of any previous modifications; and
+
+ c. indicate the Licensed Material is licensed under this
+ Public License, and include the text of, or the URI or
+ hyperlink to, this Public License.
+
+ 2. You may satisfy the conditions in Section 3(a)(1) in any
+ reasonable manner based on the medium, means, and context in
+ which You Share the Licensed Material. For example, it may be
+ reasonable to satisfy the conditions by providing a URI or
+ hyperlink to a resource that includes the required
+ information.
+
+ 3. If requested by the Licensor, You must remove any of the
+ information required by Section 3(a)(1)(A) to the extent
+ reasonably practicable.
+
+ 4. If You Share Adapted Material You produce, the Adapter's
+ License You apply must not prevent recipients of the Adapted
+ Material from complying with this Public License.
+
+
+Section 4 -- Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that
+apply to Your use of the Licensed Material:
+
+ a. for the avoidance of doubt, Section 2(a)(1) grants You the right
+ to extract, reuse, reproduce, and Share all or a substantial
+ portion of the contents of the database;
+
+ b. if You include all or a substantial portion of the database
+ contents in a database in which You have Sui Generis Database
+ Rights, then the database in which You have Sui Generis Database
+ Rights (but not its individual contents) is Adapted Material; and
+
+ c. You must comply with the conditions in Section 3(a) if You Share
+ all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not
+replace Your obligations under this Public License where the Licensed
+Rights include other Copyright and Similar Rights.
+
+
+Section 5 -- Disclaimer of Warranties and Limitation of Liability.
+
+ a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
+ EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
+ AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
+ ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
+ IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
+ WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
+ ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
+ KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
+ ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
+
+ b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
+ TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
+ NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
+ INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
+ COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
+ USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
+ ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
+ DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
+ IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
+
+ c. The disclaimer of warranties and limitation of liability provided
+ above shall be interpreted in a manner that, to the extent
+ possible, most closely approximates an absolute disclaimer and
+ waiver of all liability.
+
+
+Section 6 -- Term and Termination.
+
+ a. This Public License applies for the term of the Copyright and
+ Similar Rights licensed here. However, if You fail to comply with
+ this Public License, then Your rights under this Public License
+ terminate automatically.
+
+ b. Where Your right to use the Licensed Material has terminated under
+ Section 6(a), it reinstates:
+
+ 1. automatically as of the date the violation is cured, provided
+ it is cured within 30 days of Your discovery of the
+ violation; or
+
+ 2. upon express reinstatement by the Licensor.
+
+ For the avoidance of doubt, this Section 6(b) does not affect any
+ right the Licensor may have to seek remedies for Your violations
+ of this Public License.
+
+ c. For the avoidance of doubt, the Licensor may also offer the
+ Licensed Material under separate terms or conditions or stop
+ distributing the Licensed Material at any time; however, doing so
+ will not terminate this Public License.
+
+ d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
+ License.
+
+
+Section 7 -- Other Terms and Conditions.
+
+ a. The Licensor shall not be bound by any additional or different
+ terms or conditions communicated by You unless expressly agreed.
+
+ b. Any arrangements, understandings, or agreements regarding the
+ Licensed Material not stated herein are separate from and
+ independent of the terms and conditions of this Public License.
+
+
+Section 8 -- Interpretation.
+
+ a. For the avoidance of doubt, this Public License does not, and
+ shall not be interpreted to, reduce, limit, restrict, or impose
+ conditions on any use of the Licensed Material that could lawfully
+ be made without permission under this Public License.
+
+ b. To the extent possible, if any provision of this Public License is
+ deemed unenforceable, it shall be automatically reformed to the
+ minimum extent necessary to make it enforceable. If the provision
+ cannot be reformed, it shall be severed from this Public License
+ without affecting the enforceability of the remaining terms and
+ conditions.
+
+ c. No term or condition of this Public License will be waived and no
+ failure to comply consented to unless expressly agreed to by the
+ Licensor.
+
+ d. Nothing in this Public License constitutes or may be interpreted
+ as a limitation upon, or waiver of, any privileges and immunities
+ that apply to the Licensor or You, including from the legal
+ processes of any jurisdiction or authority.
+
+
+=======================================================================
+
+Creative Commons is not a party to its public licenses.
+Notwithstanding, Creative Commons may elect to apply one of its public
+licenses to material it publishes and in those instances will be
+considered the “Licensor.” The text of the Creative Commons public
+licenses is dedicated to the public domain under the CC0 Public Domain
+Dedication. Except for the limited purpose of indicating that material
+is shared under a Creative Commons public license or as otherwise
+permitted by the Creative Commons policies published at
+creativecommons.org/policies, Creative Commons does not authorize the
+use of the trademark "Creative Commons" or any other trademark or logo
+of Creative Commons without its prior written consent including,
+without limitation, in connection with any unauthorized modifications
+to any of its public licenses or any other arrangements,
+understandings, or agreements concerning use of licensed material. For
+the avoidance of doubt, this paragraph does not form part of the public
+licenses.
+
+Creative Commons may be contacted at creativecommons.org.
diff --git a/README.en.md b/README.en.md
new file mode 100644
index 0000000..057e83b
--- /dev/null
+++ b/README.en.md
@@ -0,0 +1,36 @@
+# 博客
+
+#### Description
+一个专注于分享编程知识、技术趋势和开发经验的开源博客,旨在帮助开发者提升技能,促进技术交流与学习。
+
+#### Software Architecture
+Software architecture description
+
+#### Installation
+
+1. xxxx
+2. xxxx
+3. xxxx
+
+#### Instructions
+
+1. xxxx
+2. xxxx
+3. xxxx
+
+#### Contribution
+
+1. Fork the repository
+2. Create Feat_xxx branch
+3. Commit your code
+4. Create Pull Request
+
+
+#### Gitee Feature
+
+1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
+2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
+3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
+4. The most valuable open source project [GVP](https://gitee.com/gvp)
+5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
+6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cdb29df
--- /dev/null
+++ b/README.md
@@ -0,0 +1,37 @@
+# 博客
+
+#### 介绍
+一个专注于分享编程知识、技术趋势和开发经验的开源博客,旨在帮助开发者提升技能,促进技术交流与学习。
+
+#### 软件架构
+软件架构说明
+
+
+#### 安装教程
+
+1. xxxx
+2. xxxx
+3. xxxx
+
+#### 使用说明
+
+1. xxxx
+2. xxxx
+3. xxxx
+
+#### 参与贡献
+
+1. Fork 本仓库
+2. 新建 Feat_xxx 分支
+3. 提交代码
+4. 新建 Pull Request
+
+
+#### 特技
+
+1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
+2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
+3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
+4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
+5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
+6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..cc6f444
--- /dev/null
+++ b/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "mangmangblog",
+ "description": "我的个人博客",
+ "version": "2.0.0",
+ "license": "MIT",
+ "type": "module",
+ "scripts": {
+ "docs:build": "vuepress-vite build src",
+ "docs:clean-dev": "vuepress-vite dev src --clean-cache",
+ "docs:dev": "vuepress-vite dev src",
+ "docs:update-package": "npx vp-update",
+ "update:browsers": "npx update-browserslist-db@latest"
+ },
+ "devDependencies": {
+ "@vuepress/bundler-vite": "2.0.0-rc.22",
+ "@vuepress/plugin-git": "^2.0.0-rc.99",
+ "sass-embedded": "^1.87.0",
+ "vue": "^3.5.13",
+ "vuepress": "2.0.0-rc.22",
+ "vuepress-theme-hope": "2.0.0-rc.85"
+ }
+}
diff --git a/src/.vuepress/components/BlogHero.vue b/src/.vuepress/components/BlogHero.vue
new file mode 100644
index 0000000..1ad48a1
--- /dev/null
+++ b/src/.vuepress/components/BlogHero.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/.vuepress/config.ts b/src/.vuepress/config.ts
new file mode 100644
index 0000000..28392af
--- /dev/null
+++ b/src/.vuepress/config.ts
@@ -0,0 +1,26 @@
+import { defineUserConfig } from "vuepress";
+import { getDirname, path } from "vuepress/utils";
+
+
+const __dirname = getDirname(import.meta.url);
+
+import theme from "./theme.js";
+
+export default defineUserConfig({
+ base: "/",
+
+ lang: "zh-CN",
+ title: "氓氓小栈",
+ description: "氓氓小栈",
+ theme,
+ alias: {
+ "@theme-hope/modules/blog/components/BlogHero": path.resolve(
+ __dirname,
+ "./components/BlogHero.vue",
+ ),
+ },
+
+
+ // 和 PWA 一起启用
+ // shouldPrefetch: false,
+});
diff --git a/src/.vuepress/navbar.ts b/src/.vuepress/navbar.ts
new file mode 100644
index 0000000..35a4c70
--- /dev/null
+++ b/src/.vuepress/navbar.ts
@@ -0,0 +1,25 @@
+import { navbar } from "vuepress-theme-hope";
+
+export default navbar([
+ "/",
+ {
+ text: "编程",
+ icon: "mdi:code-tags",
+ link: "/programming/",
+ },
+ {
+ text: "工作",
+ icon: "mdi:briefcase",
+ link: "/work/",
+ },
+ {
+ text: "应用",
+ icon: "mdi:application",
+ link: "/apps/",
+ },
+ {
+ text: "工具箱",
+ icon: "mdi:toolbox",
+ link: "/tools/",
+ },
+]);
diff --git a/src/.vuepress/public/assets/icon/apple-icon-152.png b/src/.vuepress/public/assets/icon/apple-icon-152.png
new file mode 100644
index 0000000..434fcc3
Binary files /dev/null and b/src/.vuepress/public/assets/icon/apple-icon-152.png differ
diff --git a/src/.vuepress/public/assets/icon/chrome-192.png b/src/.vuepress/public/assets/icon/chrome-192.png
new file mode 100644
index 0000000..6645f52
Binary files /dev/null and b/src/.vuepress/public/assets/icon/chrome-192.png differ
diff --git a/src/.vuepress/public/assets/icon/chrome-512.png b/src/.vuepress/public/assets/icon/chrome-512.png
new file mode 100644
index 0000000..a146f00
Binary files /dev/null and b/src/.vuepress/public/assets/icon/chrome-512.png differ
diff --git a/src/.vuepress/public/assets/icon/chrome-mask-192.png b/src/.vuepress/public/assets/icon/chrome-mask-192.png
new file mode 100644
index 0000000..530977a
Binary files /dev/null and b/src/.vuepress/public/assets/icon/chrome-mask-192.png differ
diff --git a/src/.vuepress/public/assets/icon/chrome-mask-512.png b/src/.vuepress/public/assets/icon/chrome-mask-512.png
new file mode 100644
index 0000000..a4f90ae
Binary files /dev/null and b/src/.vuepress/public/assets/icon/chrome-mask-512.png differ
diff --git a/src/.vuepress/public/assets/icon/guide-maskable.png b/src/.vuepress/public/assets/icon/guide-maskable.png
new file mode 100644
index 0000000..75449b6
Binary files /dev/null and b/src/.vuepress/public/assets/icon/guide-maskable.png differ
diff --git a/src/.vuepress/public/assets/icon/ms-icon-144.png b/src/.vuepress/public/assets/icon/ms-icon-144.png
new file mode 100644
index 0000000..2464124
Binary files /dev/null and b/src/.vuepress/public/assets/icon/ms-icon-144.png differ
diff --git a/src/.vuepress/public/assets/images/cover1.jpg b/src/.vuepress/public/assets/images/cover1.jpg
new file mode 100644
index 0000000..06f33cf
Binary files /dev/null and b/src/.vuepress/public/assets/images/cover1.jpg differ
diff --git a/src/.vuepress/public/assets/images/cover2.jpg b/src/.vuepress/public/assets/images/cover2.jpg
new file mode 100644
index 0000000..edf5b38
Binary files /dev/null and b/src/.vuepress/public/assets/images/cover2.jpg differ
diff --git a/src/.vuepress/public/assets/images/cover3.jpg b/src/.vuepress/public/assets/images/cover3.jpg
new file mode 100644
index 0000000..ad79fc9
Binary files /dev/null and b/src/.vuepress/public/assets/images/cover3.jpg differ
diff --git a/src/.vuepress/public/bg/bgImage.jpg b/src/.vuepress/public/bg/bgImage.jpg
new file mode 100644
index 0000000..7cac8b5
Binary files /dev/null and b/src/.vuepress/public/bg/bgImage.jpg differ
diff --git a/src/.vuepress/public/favicon.ico b/src/.vuepress/public/favicon.ico
new file mode 100644
index 0000000..7a99168
Binary files /dev/null and b/src/.vuepress/public/favicon.ico differ
diff --git a/src/.vuepress/public/logo.png b/src/.vuepress/public/logo.png
new file mode 100644
index 0000000..ccd732e
Binary files /dev/null and b/src/.vuepress/public/logo.png differ
diff --git a/src/.vuepress/public/logo.svg b/src/.vuepress/public/logo.svg
new file mode 100644
index 0000000..364d184
--- /dev/null
+++ b/src/.vuepress/public/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/.vuepress/public/logo/logo.png b/src/.vuepress/public/logo/logo.png
new file mode 100644
index 0000000..d6501fb
Binary files /dev/null and b/src/.vuepress/public/logo/logo.png differ
diff --git a/src/.vuepress/public/logo/logoDark.png b/src/.vuepress/public/logo/logoDark.png
new file mode 100644
index 0000000..a1f75ca
Binary files /dev/null and b/src/.vuepress/public/logo/logoDark.png differ
diff --git a/src/.vuepress/public/logo/transparentLogo.png b/src/.vuepress/public/logo/transparentLogo.png
new file mode 100644
index 0000000..4c3104e
Binary files /dev/null and b/src/.vuepress/public/logo/transparentLogo.png differ
diff --git a/src/.vuepress/sidebar.ts b/src/.vuepress/sidebar.ts
new file mode 100644
index 0000000..8dbaede
--- /dev/null
+++ b/src/.vuepress/sidebar.ts
@@ -0,0 +1,186 @@
+import { sidebar } from "vuepress-theme-hope";
+
+export default sidebar({
+ "/programming/": [
+ {
+ text: "前端",
+ collapsible: true,
+ expanded: false,
+ icon: "mdi:vuejs",
+ prefix: "frontend/",
+ children: [
+ {
+ text: "Vue",
+ icon: "mdi:vuejs",
+ collapsible: true,
+ prefix: "vue/",
+ children: "structure",
+ },
+ {
+ text: "CSS",
+ icon: "mdi:language-css3",
+ collapsible: true,
+ prefix: "css/",
+ children: "structure",
+ },
+ {
+ text: "HTML",
+ icon: "mdi:language-html5",
+ collapsible: true,
+ prefix: "html/",
+ children: "structure",
+ },
+ ]
+ },
+ {
+ text: "后端",
+ collapsible: true,
+ expanded: false,
+ icon: "mdi:code-tags",
+ prefix: "backend/",
+ children: [
+ {
+ text: "Java",
+ icon: "mdi:language-java",
+ collapsible: true,
+ prefix: "java/",
+ children: [
+ {
+ text: "框架",
+ icon: "mdi:code-braces",
+ collapsible: true,
+ prefix: "框架/",
+ children: "structure",
+ },
+ {
+ text: "功能整理",
+ icon: "mdi:puzzle",
+ collapsible: true,
+ prefix: "功能整理/",
+ children: [
+ "01XJar.md",
+ "02Maven.md",
+ "03WebSocket和HTTP关系.md",
+ "05防止表单和参数重复提交.md",
+ "06Spring Boot JAR 瘦身与加密.md",
+ ],
+ },
+ {
+ text: "试题",
+ icon: "mdi:comment-question",
+ collapsible: true,
+ prefix: "AI试题/",
+ children: "structure",
+ },
+ ]
+ },
+ {
+ text: "Go",
+ icon: "mdi:language-go",
+ collapsible: true,
+ prefix: "go/",
+ children: [
+ {
+ text: "Go基础语法",
+ icon: "mdi:book-open-outline",
+ collapsible: true,
+ prefix: "Go基础语法/",
+ children: [
+ "01Hello World.md",
+ "02变量与类型.md",
+ "03slice和map.md",
+ "04Struct、方法与接收者类型详解.md",
+ "05接口.md",
+ "06错误机制.md",
+ "07从零实现 Mini 日志库.md",
+ ],
+ },
+ ],
+ },
+ {
+ text: "C++",
+ icon: "mdi:language-cpp",
+ collapsible: true,
+ prefix: "c++/",
+ children: "structure",
+ },
+ ]
+ },
+ {
+ text: "Linux",
+ collapsible: true,
+ expanded: false,
+ icon: "mdi:linux",
+ prefix: "linux/",
+ children: [
+ {
+ text: "基础",
+ icon: "mdi:console",
+ collapsible: true,
+ prefix: "基础/",
+ children: "structure",
+ },
+ {
+ text: "Linux_Mint",
+ icon: "simple-icons:linuxmint",
+ collapsible: true,
+ prefix: "Linux_Mint/",
+ children: "structure",
+ },
+ {
+ text: "凝思",
+ icon: "mdi:server",
+ collapsible: true,
+ prefix: "凝思/",
+ children: "structure",
+ },
+ ]
+ },
+ {
+ text: "Docker",
+ collapsible: true,
+ expanded: false,
+ icon: "mdi:docker",
+ prefix: "docker/",
+ children: "structure",
+ }
+ ],
+ "/work/": [
+ {
+ text: "工作日志",
+ collapsible: true,
+ expanded: false,
+ icon: "mdi:file-document-outline",
+ prefix: "log/",
+ children: "structure",
+ },
+ {
+ text: "常用记录",
+ icon: "mdi:star",
+ link: "/work/常用.md",
+ }
+ ],
+ "/apps/": [
+ {
+ text: "自建应用",
+ icon: "mdi:apps",
+ collapsible: true,
+ children: "structure",
+ }
+ ],
+ "/tools/": [
+ {
+ text: "工具箱",
+ icon: "mdi:toolbox",
+ collapsible: true,
+ children: [
+ "01gkd.md",
+ "02WSL2.md",
+ "03Scoop.md",
+ "04gitee-ssh.md",
+ "05Google.md",
+ "06MobaXterm.md"
+ ],
+ },
+ ]
+});
diff --git a/src/.vuepress/styles/config.scss b/src/.vuepress/styles/config.scss
new file mode 100644
index 0000000..801637b
--- /dev/null
+++ b/src/.vuepress/styles/config.scss
@@ -0,0 +1,2 @@
+// you can change config here
+$theme-color: #DC143C;
diff --git a/src/.vuepress/styles/index.scss b/src/.vuepress/styles/index.scss
new file mode 100644
index 0000000..7e64ce0
--- /dev/null
+++ b/src/.vuepress/styles/index.scss
@@ -0,0 +1,13 @@
+// place your custom styles here
+// .vuepress/styles/index.scss
+
+/* .vuepress/styles/index.scss */
+
+.hitokoto-text {
+ color: #DC143C !important; // 替换为你想要的颜色
+}
+
+.hitokoto-author {
+ color: #DC143C!important; // 可选:也可自定义作者名颜色
+}
+
diff --git a/src/.vuepress/styles/palette.scss b/src/.vuepress/styles/palette.scss
new file mode 100644
index 0000000..d271cb0
--- /dev/null
+++ b/src/.vuepress/styles/palette.scss
@@ -0,0 +1 @@
+// you can change colors here
diff --git a/src/.vuepress/theme.ts b/src/.vuepress/theme.ts
new file mode 100644
index 0000000..b673857
--- /dev/null
+++ b/src/.vuepress/theme.ts
@@ -0,0 +1,116 @@
+import {hopeTheme} from "vuepress-theme-hope";
+
+import navbar from "./navbar.js";
+import sidebar from "./sidebar.js";
+//VuePress Theme Hope主题的博客
+export default hopeTheme(
+ {
+ // 网站域名配置(用于SEO和RSS等功能)
+ hostname: "https://mangmang.fun/",
+
+ // 作者信息
+ author: {
+ name: "LiuMangMang",
+ },
+
+ // 网站许可证
+ license: "CC 4.0",
+
+ // 导航栏配置(从 navbar.js 文件导入)
+ navbar,
+
+ // 侧边栏配置(从 sidebar.js 文件导入)
+ sidebar,
+
+ // 网站logo设置
+ logo: "logo/transparentLogo.png", // 亮色模式下的logo
+ logoDark: "logo/transparentLogo.png", // 暗色模式下的logo
+
+ // 支持简写仓库名称,会解析到 GitHub 上,同时也可以是一个完整的 URL
+ // repo: "https://gitea.mangmang.fun/mangmang/blog",
+ // 默认从 `repo` 内容中推断为以下之一:
+ // "GitHub" / "GitLab" / "Gitee" / "Bitbucket" / "Source"
+ repoLabel: "Gitea",
+ repoDisplay: true,
+
+ // 是否显示编辑链接
+ editLink: true,
+
+ // 文档源码目录
+ docsDir: "src",
+
+ // 页脚配置
+ footer: "蜀ICP备2025176018号-1 | Powered by VuePress | Theme by Hope",
+ displayFooter: true,
+
+ // 是否显示最后更新时间
+ lastUpdated: false,
+
+ // 深色模式配置(toggle表示用户可以切换)
+ darkmode: "toggle",
+
+ // 博客相关配置
+ blog: {
+ description: "一个后端开发者",
+ intro: "/intro.html"
+ },
+
+ // Markdown增强功能配置
+ markdown: {
+ align: true, // 启用文本对齐
+ attrs: true, // 启用属性支持
+ codeTabs: true, // 启用代码选项卡
+ component: true, // 启用组件支持
+ demo: true, // 启用演示支持
+ figure: true, // 启用图片描述
+ gfm: true, // 启用GitHub风格的Markdown
+ imgLazyload: true, // 启用图片懒加载
+ imgSize: true, // 启用图片尺寸设置
+ include: true, // 启用文件包含
+ mark: true, // 启用标记高亮
+ plantuml: true, // 启用PlantUML图表
+ spoiler: true, // 启用剧透标记
+
+ // 文本样式化配置
+ stylize: [
+ {
+ matcher: "Recommended",
+ replacer: ({tag}) => {
+ if (tag === "em")
+ return {
+ tag: "Badge",
+ attrs: {type: "tip"},
+ content: "Recommended",
+ };
+ },
+ },
+ ],
+
+ sub: true, // 启用下标
+ sup: true, // 启用上标
+ tabs: true, // 启用选项卡
+ tasklist: true, // 启用任务列表
+ vPre: true, // 启用v-pre支持
+ },
+
+ // 插件配置
+ plugins: {
+ // 启用博客功能
+ blog: true,
+
+ // 组件配置
+ components: {
+ components: ["Badge", "VPCard"],
+ },
+
+ // 图标配置
+ icon: {
+ prefix: "fa6-solid:",
+ },
+ },
+ },
+ {
+ // 自定义主题配置
+ custom: true
+ }
+);
\ No newline at end of file
diff --git a/src/README.md b/src/README.md
new file mode 100644
index 0000000..db7514e
--- /dev/null
+++ b/src/README.md
@@ -0,0 +1,48 @@
+---
+home: true
+layout: BlogHome
+hero: true
+title: 博客主页
+heroText: ''
+tagline: ''
+#heroImage: logo/transparentLogo.png
+#heroImageDark: logo/transparentLogo.png
+#heroAlt: 可爱小熊猫
+bgImage: bg/day.png
+bgImageDark: bg/night.png
+heroFullScreen: true
+icon: house
+
+#projects:
+# - icon: folder-open
+# name: 项目名称
+# desc: 项目详细描述
+# link: https://你的项目链接
+#
+# - icon: link
+# name: 链接名称
+# desc: 链接详细描述
+# link: https://链接地址
+#
+# - icon: book
+# name: 书籍名称
+# desc: 书籍详细描述
+# link: https://你的书籍链接
+#
+# - icon: newspaper
+# name: 文章名称
+# desc: 文章详细描述
+# link: https://你的文章链接
+#
+# - icon: user-group
+# name: 伙伴名称
+# desc: 伙伴详细介绍
+# link: https://你的伙伴链接
+#
+# - icon: https://theme-hope-assets.vuejs.press/logo.svg
+# name: 自定义项目
+# desc: 自定义详细介绍
+# link: https://你的自定义链接
+
+#footer: Powered by VuePress | Theme by Hope
+---
diff --git a/src/apps/01Jellyfin.md b/src/apps/01Jellyfin.md
new file mode 100644
index 0000000..fc28e6b
--- /dev/null
+++ b/src/apps/01Jellyfin.md
@@ -0,0 +1,97 @@
+---
+title: Jellyfin音乐目录结构规范总结
+icon: music
+date: 2025-05-23
+category:
+ - 应用
+tag:
+ - jellyfin
+---
+
+
+Jellyfin音乐目录结构规范总结
+
+
+
+# Jellyfin音乐目录结构规范总结
+
+Jellyfin作为一款优秀的媒体服务器,对音乐文件的组织有着特定的规范和建议,以确保最佳的识别和播放体验。本文将详细总结Jellyfin音乐目录的结构规范,包括专辑组织、多碟片处理、歌词文件管理、图片资源使用以及文件格式兼容性等方面。
+
+## 专辑组织基本原则
+
+Jellyfin的音乐库管理遵循"一个文件夹一个专辑"的基本原则。每个专辑必须被包含在单独的文件夹中,这是Jellyfin识别专辑的基础。值得注意的是,Jellyfin并不严格要求您如何组织这些专辑文件夹,只要确保每个专辑都有其独立的文件夹即可。例如,您可以按艺术家创建上层文件夹,然后在其中放置该艺术家的各个专辑文件夹;也可以直接在音乐根目录下放置各个专辑文件夹。
+
+文件命名方面,Jellyfin主要依赖音频文件中嵌入的元数据进行识别,而非文件名本身。这意味着即使文件名不规范,只要音频文件中包含正确的元数据信息(如曲目标题、艺术家、专辑名等),Jellyfin仍能正确识别。只有在找不到其他元数据的情况下,Jellyfin才会使用文件名作为曲目标题。
+
+尽管如此,文件命名时仍应避免使用特殊字符,因为它们可能导致问题。已知会引起问题的字符包括:`<`、`>`、`:`、`"`、`/`、`\`、`|`、`?`、`*`。这些字符在文件系统中有特殊含义,可能导致文件访问或识别错误。
+
+## 多碟片专辑处理
+
+对于包含多张碟片的专辑,Jellyfin主要通过音频文件元数据中的"碟片编号"和"总碟片数"字段进行识别。处理多碟片专辑时,有两种组织方式:
+
+第一种方式是将所有碟片的曲目放在同一个专辑文件夹中,通过元数据区分不同碟片的曲目。这种方式简单直接,完全依赖元数据进行组织。
+
+第二种方式是在专辑文件夹内为每张碟片创建单独的子文件夹(如"Disc 1"、"Disc 2"等),然后将相应碟片的曲目放入对应子文件夹。这种方式在视觉上更加清晰,但需要注意的是,嵌入的元数据优先级仍高于文件夹结构,因此确保元数据正确尤为重要。
+
+无论采用哪种组织方式,确保音频文件中包含正确的碟片信息元数据是关键,这样Jellyfin才能准确识别和组织多碟片专辑的内容。
+
+## 歌词文件管理
+
+Jellyfin支持为音乐文件添加歌词,实现边听歌边看歌词的功能。歌词文件必须与对应的音频文件位于同一文件夹中,且文件名(不含扩展名)必须完全匹配。例如,音频文件"01 Death Eternal.mp3"对应的歌词文件应为"01 Death Eternal.lrc"、"01 Death Eternal.elrc"或"01 Death Eternal.txt"。
+
+Jellyfin支持两种类型的歌词文件:同步歌词和非同步歌词。同步歌词是交互式的,允许用户点击任意行跳转到歌曲中对应的时间点。同步歌词文件通常包含时间戳信息,格式如下:
+
+```
+[ar: 艺术家名称]
+[ti: 歌曲标题]
+[al: 专辑名称]
+[by: 作者]
+[length: 2:57]
+[00:10.89]第一行歌词
+[00:14.58]第二行歌词
+...
+```
+
+创建同步歌词可以手动完成,但这可能耗时且不够精确,建议使用如MiniLyrics等歌词同步软件辅助完成。
+
+非同步歌词则更容易实现,但用户跟唱时可能不够方便。非同步歌词文件格式简单,仅包含按顺序排列的歌词文本,没有时间戳信息:
+
+```
+第一行歌词
+第二行歌词
+...
+```
+
+## 图片资源管理
+
+Jellyfin音乐库中的图片资源主要有三种类型:封面图(Primary)、背景图(Backdrop)和标志图(Logo)。这些图片可以来自不同来源,但外部图片文件优先级最高。
+
+外部图片文件应放置在媒体文件旁边,即与音频文件位于同一文件夹中。如果没有提供封面图,Jellyfin会回退使用第一个带有嵌入专辑图片的音轨。如果没有可用的背景图或标志图,Jellyfin会回退使用专辑艺术家的相应图片。
+
+不同类型的图片文件可以使用以下命名:
+- 封面图(Primary):folder、poster、cover、default
+- 背景图(Backdrop):backdrop、fanart、background、art、extrafanart
+- 标志图(Logo):logo
+
+对于背景图,可以提供多张图片实现循环切换效果。只需在文件名后直接或在连字符后添加数字即可,如"backdrop1.jpg"、"backdrop-2.jpg"等。
+
+## 文件格式兼容性
+
+Jellyfin支持大多数常见的音乐格式,但有一些例外情况需要注意:
+
+1. 仅包含音频的MP4文件:".mp4"文件不会被识别为音乐,应将其重命名为".m4a"。
+2. 仅包含音频的MKV/WebM文件:".mkv"和".webm"文件不会被识别为音乐,应将其重命名为".mka"。
+3. WebA文件:".weba"文件不受支持,应将其重命名为".mka"。
+4. 带有嵌入WebP图片或ID3标签的".flac"文件可能在基于Chromium的浏览器(Chrome、Edge、Opera、Brave等)或Firefox中无法播放。如遇此问题,请在客户端设置中启用"始终重新封装FLAC音频文件"选项。
+5. 使用ID3v1标签的文件大多数字段长度限制为30字节,超出部分将被截断。建议升级到ID3v2.4。
+
+对于有问题的文件,可以使用ffmpeg将其重新封装为".mka"容器,命令如下:
+```
+ffmpeg -i <输入文件> -c:a copy <输出文件>.mka
+```
+
+需要注意的是,重新封装过程可能会导致元数据混乱,嵌入的图片可能无法保留,因此可能需要手动或使用其他程序恢复元数据。
+
+## 总结
+
+Jellyfin的音乐目录结构规范主要围绕"一个文件夹一个专辑"的核心原则展开,同时提供了灵活的组织方式。正确的元数据是Jellyfin识别音乐内容的关键,而外部资源如歌词和图片则需要遵循特定的命名和放置规则。了解并遵循这些规范,可以帮助用户构建一个组织良好、功能完善的Jellyfin音乐库,获得最佳的音乐欣赏体验。
diff --git a/src/apps/07RustDesk自建远程桌面服务器.md b/src/apps/07RustDesk自建远程桌面服务器.md
new file mode 100644
index 0000000..b2a990e
--- /dev/null
+++ b/src/apps/07RustDesk自建远程桌面服务器.md
@@ -0,0 +1,258 @@
+---
+icon: tabler:device-desktop-analytics
+date: 2025-07-04
+category:
+ - 实用工具
+tag:
+ - RustDesk
+title: RustDesk自建远程桌面服务器
+---
+在日常工作中,我们经常需要远程控制电脑,比如远程家中电脑进行下载、远程办公,或者帮助朋友解决技术问题。除了向日葵、QQ远程、ToDesk等商业软件,RustDesk提供了一个开源、安全、高效的替代方案。
+
+# RustDesk自建远程桌面服务器
+
+## 为什么选择RustDesk?
+
+在日常工作中,我们经常需要远程控制电脑,比如远程家中电脑进行下载、远程办公,或者帮助朋友解决技术问题。除了向日葵、QQ远程、ToDesk等商业软件,RustDesk提供了一个开源、安全、高效的替代方案。
+
+### RustDesk的主要优势
+
+- **安全性**:使用安全的加密通信协议,确保数据传输安全
+- **跨平台**:支持Windows、macOS、Linux、Android等多个平台
+- **高性能**:使用Rust编写,具有高效和并发的特点
+- **低成本**:一台1C1G的服务器就能满足个人或小团队使用
+
+## 服务器准备
+
+### 服务器配置要求
+
+- **Linux服务器**:推荐1C1G配置(如AWS EC2或Lightsail)
+- **Windows服务器**:建议至少2C4G配置
+- **网络要求**:需要开放特定端口
+
+### 需要开放的端口
+
+- **TCP端口**:21115, 21116, 21117, 21118, 21119
+- **UDP端口**:21116
+
+## 服务器初始化
+
+### 1. 连接服务器
+
+使用SSH连接服务器:
+```bash
+ssh admin@<服务器IP> -i
+```
+
+### 2. 更新系统
+
+```bash
+# 更新软件包
+sudo apt update -y
+
+# 设置时区(可选)
+sudo timedatectl set-timezone Asia/Shanghai
+```
+
+### 3. 创建用户(可选)
+
+```bash
+# 创建新用户
+sudo useradd -m rustdesk
+
+# 设置密码
+sudo passwd rustdesk
+
+# 切换用户
+sudo su - rustdesk
+```
+
+## RustDesk服务端部署
+
+### 1. 下载服务端程序
+
+```bash
+# 创建目录
+mkdir ~/rustdesk && cd ~/rustdesk
+
+# 下载最新版本(请到GitHub查看最新版本号)
+wget https://github.com/rustdesk/rustdesk-server/releases/download/1.1.8-2/rustdesk-server-linux-amd64.zip
+
+# 解压
+unzip rustdesk-server-linux-amd64.zip
+mv amd64 server
+cd server
+```
+
+### 2. 测试运行
+
+安装screen用于管理进程:
+```bash
+sudo apt install screen
+```
+
+启动hbbs(ID服务):
+```bash
+screen -S hbbs
+./hbbs
+# 按Ctrl+A然后按D退出screen
+```
+
+启动hbbr(中继服务):
+```bash
+screen -S hbbr
+./hbbr
+# 按Ctrl+A然后按D退出screen
+```
+
+### 3. 获取公钥
+
+运行后会生成公钥文件,查看并复制公钥内容:
+```bash
+cat id_ed25519.pub
+```
+
+保存这个公钥,客户端配置时需要使用。
+
+## 注册为系统服务
+
+### 创建hbbs服务
+
+创建服务文件:
+```bash
+sudo nano /etc/systemd/system/rustdesk-hbbs.service
+```
+
+写入以下内容:
+```ini
+[Unit]
+Description=RustDesk Hbbs
+After=network.target
+
+[Service]
+User=rustdesk
+Type=simple
+WorkingDirectory=/home/rustdesk/rustdesk/server
+ExecStart=/home/rustdesk/rustdesk/server/hbbs
+ExecStop=/bin/kill -TERM $MAINPID
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
+```
+
+### 创建hbbr服务
+
+```bash
+sudo nano /etc/systemd/system/rustdesk-hbbr.service
+```
+
+写入以下内容:
+```ini
+[Unit]
+Description=RustDesk Hbbr
+After=network.target
+
+[Service]
+User=rustdesk
+Type=simple
+WorkingDirectory=/home/rustdesk/rustdesk/server
+ExecStart=/home/rustdesk/rustdesk/server/hbbr
+ExecStop=/bin/kill -TERM $MAINPID
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
+```
+
+### 启动服务
+
+```bash
+# 重载systemd配置
+sudo systemctl daemon-reload
+
+# 启动服务
+sudo systemctl start rustdesk-hbbs
+sudo systemctl start rustdesk-hbbr
+
+# 设置开机自启
+sudo systemctl enable rustdesk-hbbs
+sudo systemctl enable rustdesk-hbbr
+
+# 查看服务状态
+sudo systemctl status rustdesk-hbbs
+sudo systemctl status rustdesk-hbbr
+```
+
+## 客户端配置
+
+### 1. 下载客户端
+
+访问[RustDesk官方发布页面](https://github.com/rustdesk/rustdesk/releases)下载对应系统的客户端。
+
+### 2. 配置客户端
+
+1. 打开RustDesk客户端
+2. 点击右上角的三个点,进入设置
+3. 选择"网络"选项
+4. 解锁网络配置
+5. 填入配置信息:
+ - **ID服务器**:你的服务器IP地址
+ - **中继服务器**:你的服务器IP地址
+ - **公钥**:之前复制的公钥内容
+
+### 3. 测试连接
+
+配置完成后,客户端会显示一个ID号码。在另一台设备上安装并配置好RustDesk客户端,输入这个ID即可建立连接。
+
+## 进阶配置
+
+### 启用强制加密
+
+如果希望强制使用密钥验证,可以在启动服务时添加参数:
+
+修改服务文件中的ExecStart行:
+```ini
+ExecStart=/home/rustdesk/rustdesk/server/hbbs -k _
+ExecStart=/home/rustdesk/rustdesk/server/hbbr -k _
+```
+
+这样客户端必须配置正确的公钥才能连接。
+
+### 使用域名
+
+如果你有域名,可以在客户端配置中使用域名替代IP地址,但请注意:
+- 域名必须直接解析到服务器IP
+- 不支持CDN加速的域名
+
+## 常见问题解决
+
+### 1. 连接失败
+
+检查以下几点:
+- 服务器防火墙是否开放了必要端口
+- 服务是否正常运行:`sudo systemctl status rustdesk-hbbs rustdesk-hbbr`
+- 客户端配置是否正确
+
+### 2. 性能问题
+
+- 确保服务器网络带宽充足
+- 可以考虑升级服务器配置
+- 检查是否有其他程序占用资源
+
+### 3. 安全考虑
+
+- 建议启用强制加密
+- 定期更新RustDesk版本
+- 使用防火墙限制访问来源
+
+## 总结
+
+通过本教程,你已经成功搭建了自己的RustDesk远程桌面服务器。这个方案具有以下优势:
+
+1. **完全掌控**:数据不经过第三方服务器
+2. **成本低廉**:最低1C1G服务器即可满足需求
+3. **安全可靠**:开源代码,支持端到端加密
+4. **性能优秀**:基于Rust的高效实现
+
+现在你可以享受安全、高效的远程桌面服务了!如果遇到问题,建议查看官方文档或者在GitHub上寻求帮助。
\ No newline at end of file
diff --git a/src/apps/README.md b/src/apps/README.md
new file mode 100644
index 0000000..eaca969
--- /dev/null
+++ b/src/apps/README.md
@@ -0,0 +1,13 @@
+---
+title: 自建应用
+index: false
+icon: mdi:apps
+category:
+ - 应用
+---
+
+## 自建应用列表
+
+记录个人搭建的各类应用服务。
+
+
diff --git a/src/demo/README.md b/src/demo/README.md
new file mode 100644
index 0000000..31ede9b
--- /dev/null
+++ b/src/demo/README.md
@@ -0,0 +1,9 @@
+---
+title: 主要功能与配置演示
+index: false
+icon: laptop-code
+category:
+ - 使用指南
+---
+
+
diff --git a/src/demo/disable.md b/src/demo/disable.md
new file mode 100644
index 0000000..ff54b57
--- /dev/null
+++ b/src/demo/disable.md
@@ -0,0 +1,42 @@
+---
+title: 布局与功能禁用
+icon: gears
+order: 4
+category:
+ - 使用指南
+tag:
+ - 禁用
+
+navbar: false
+sidebar: false
+
+breadcrumb: false
+pageInfo: false
+contributors: false
+editLink: false
+lastUpdated: false
+prev: false
+next: false
+comment: false
+footer: false
+
+backtotop: false
+---
+
+你可以通过设置页面的 Frontmatter,在页面禁用功能与布局。
+
+
+
+本页面就是一个示例,禁用了如下功能:
+
+- 导航栏
+- 侧边栏
+- 路径导航
+- 页面信息
+- 贡献者
+- 编辑此页链接
+- 更新时间
+- 上一篇/下一篇 链接
+- 评论
+- 页脚
+- 返回顶部按钮
diff --git a/src/demo/encrypt.md b/src/demo/encrypt.md
new file mode 100644
index 0000000..20d8d23
--- /dev/null
+++ b/src/demo/encrypt.md
@@ -0,0 +1,15 @@
+---
+icon: lock
+category:
+ - 使用指南
+tag:
+ - 加密
+---
+
+# 密码加密的文章
+
+实际的文章内容。
+
+段落 1 文字段落 1 文字段落 1 文字段落 1 文字段落 1 文字段落 1 文字段落 1 文字段落 1 文字段落 1 文字段落 1 文字段落 1 文字段落 1 文字。
+
+段落 2 文字段落 2 文字段落 2 文字段落 2 文字段落 2 文字段落 2 文字段落 2 文字段落 2 文字段落 2 文字段落 2 文字段落 2 文字段落 2 文字段落 2 文字段落 2 文字。
diff --git a/src/demo/layout.md b/src/demo/layout.md
new file mode 100644
index 0000000..b89b60a
--- /dev/null
+++ b/src/demo/layout.md
@@ -0,0 +1,31 @@
+---
+title: 布局
+icon: object-group
+order: 2
+category:
+ - 指南
+tag:
+ - 布局
+---
+
+布局包括:
+
+- [导航栏](https://theme-hope.vuejs.press/zh/guide/layout/navbar.html)
+- [侧边栏](https://theme-hope.vuejs.press/zh/guide/layout/sidebar.html)
+- [页脚](https://theme-hope.vuejs.press/zh/guide/layout/footer.html)
+
+同时每个页面包含:
+
+- [路径导航](https://theme-hope.vuejs.press/zh/guide/layout/breadcrumb.html)
+- [标题和页面信息](https://theme-hope.vuejs.press/zh/guide/feature/page-info.html)
+- [TOC (文章标题列表)](https://theme-hope.vuejs.press/zh/guide/layout/page.html#标题列表)
+- [贡献者、更新时间等页面元信息](https://theme-hope.vuejs.press/guide/feature/meta.html)
+- [评论](https://theme-hope.vuejs.press/zh/guide/feature/comment.html)
+
+主题也带有以下元素:
+
+- [夜间模式按钮](https://theme-hope.vuejs.press/zh/guide/interface/darkmode.html)
+- [返回顶部按钮](https://theme-hope.vuejs.press/guide/interface/others.html#返回顶部按钮)
+- [打印按钮](https://theme-hope.vuejs.press/guide/interface/others.html#打印按钮)
+
+你可以在主题选项和页面的 frontmatter 中自定义它们。
diff --git a/src/demo/markdown.md b/src/demo/markdown.md
new file mode 100644
index 0000000..33e65b9
--- /dev/null
+++ b/src/demo/markdown.md
@@ -0,0 +1,323 @@
+---
+title: Markdown 展示
+icon: fa6-brands:markdown
+order: 2
+category:
+ - 使用指南
+tag:
+ - Markdown
+---
+
+VuePress 主要从 Markdown 文件生成页面。因此,你可以使用它轻松生成文档或博客站点。
+
+你需要创建并编写 Markdown,以便 VuePress 可以根据文件结构将它们转换为不同的页面。
+
+
+
+## Markdown 介绍
+
+如果你是一个新手,还不会编写 Markdown,请先阅读 [Markdown 介绍](https://theme-hope.vuejs.press/zh/cookbook/markdown/) 和 [Markdown 演示](https://theme-hope.vuejs.press/zh/cookbook/markdown/demo.html)。
+
+## Markdown 配置
+
+VuePress 通过 Frontmatter 为每个 Markdown 页面引入配置。
+
+::: important Frontmatter
+
+Frontmatter 是 VuePress 中很重要的一个概念,请阅读 [Frontmatter 介绍](https://theme-hope.vuejs.press/zh/cookbook/vuepress/page.html#front-matter) 了解详情。
+
+:::
+
+## Markdown 扩展
+
+VuePress 会使用 [markdown-it](https://github.com/markdown-it/markdown-it) 来解析 Markdown 内容,因此可以借助于 markdown-it 插件来实现 [语法扩展](https://github.com/markdown-it/markdown-it#syntax-extensions) 。
+
+### VuePress 扩展
+
+为了丰富文档写作,VuePress 对 Markdown 语法进行了扩展。
+
+关于这些扩展,请阅读 [VuePress 中的 Markdown 扩展](https://theme-hope.vuejs.press/zh/cookbook/vuepress/markdown.html)。
+
+### 主题扩展
+
+通过 VuePress 插件,主题扩展了更多 Markdown 语法,提供更加丰富的写作功能。
+
+#### 选项卡
+
+::: tabs#fruit
+
+@tab apple
+
+Apple
+
+@tab banana
+
+Banana
+
+@tab orange
+
+Orange
+
+:::
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/content/tabs.html)
+
+#### 脚注
+
+此文字有脚注[^first].
+
+[^first]: 这是脚注内容
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/content/footnote.html)
+
+#### 导入文件
+
+
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/content/include.html)
+
+#### TeX 语法
+
+$$
+\frac {\partial^r} {\partial \omega^r} \left(\frac {y^{\omega}} {\omega}\right)
+= \left(\frac {y^{\omega}} {\omega}\right) \left\{(\log y)^r + \sum_{i=1}^r \frac {(-1)^i r \cdots (r-i+1) (\log y)^{r-i}} {\omega^i} \right\}
+$$
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/grammar/math.html)
+
+#### 任务列表
+
+- [x] 计划 1
+- [ ] 计划 2
+
+[查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/grammar/tasklist.html)
+
+### 图片增强
+
+支持为图片设置颜色模式和大小。
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/grammar/image.html)
+
+#### 上下角标
+
+19^th^ H~2~O
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/stylize/sup-sub.html)
+
+#### 组件
+
+```component VPCard
+title: Mr.Hope
+desc: Where there is light, there is hope
+logo: https://mister-hope.com/logo.svg
+link: https://mister-hope.com
+background: rgba(253, 230, 138, 0.15)
+```
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/component/grammar.html)
+
+#### 提示容器
+
+::: v-pre
+
+安全的在 Markdown 中使用 {{ variable }}。
+
+:::
+
+::: info 自定义标题
+
+信息容器,包含 `代码` 与 [链接](#提示容器)。
+
+```js
+const a = 1;
+```
+
+:::
+
+::: tip 自定义标题
+
+提示容器
+
+:::
+
+::: warning 自定义标题
+
+警告容器
+
+:::
+
+::: caution 自定义标题
+
+危险容器
+
+:::
+
+::: details 自定义标题
+
+详情容器
+
+:::
+
+- [GitHub 警示](https://theme-hope.vuejs.press/zh/guide/markdown/stylize/alert.html)
+- [提示框](https://theme-hope.vuejs.press/zh/guide/markdown/stylize/hint.html)
+
+#### 自定义对齐
+
+::: center
+
+我是居中的
+
+:::
+
+::: right
+
+我在右对齐
+
+:::
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/stylize/align.html)
+
+#### 属性支持
+
+一个拥有 ID 的 **单词**{#word}。
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/stylize/attrs.html)
+
+#### 标记
+
+你可以标记 ==重要的内容== 。
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/stylize/mark.html)
+
+#### 剧透
+
+VuePress Theme Hope !!十分强大!!.
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/stylize/spoiler.html)
+
+#### 样式化
+
+向 Mr.Hope 捐赠一杯咖啡。 _Recommended_
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/stylize/stylize.html)
+
+#### 图表
+
+
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/chart/chartjs.html)
+
+#### ECharts
+
+
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/chart/echarts.html)
+
+#### 流程图
+
+
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/chart/flowchart.html)
+
+#### MarkMap
+
+
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/chart/markmap.html)
+
+#### Mermaid
+
+
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/chart/mermaid.html)
+
+#### PlantUML
+
+@startuml
+Alice -> Bob: 认证请求
+
+alt 成功情况
+
+ Bob -> Alice: 认证接受
+
+else 某种失败情况
+
+ Bob -> Alice: 认证失败
+ group 我自己的标签
+ Alice -> Log : 开始记录攻击日志
+ loop 1000次
+ Alice -> Bob: DNS 攻击
+ end
+ Alice -> Log : 结束记录攻击日志
+ end
+
+else 另一种失败
+
+ Bob -> Alice: 请重复
+
+end
+@enduml
+
+- [View Detail](https://theme-hope.vuejs.press/zh/guide/markdown/chart/plantuml.html)
+
+#### 代码块
+
+::: code-tabs
+
+@tab pnpm
+
+```bash
+pnpm add -D vuepress-theme-hope
+```
+
+@tab yarn
+
+```bash
+yarn add -D vuepress-theme-hope
+```
+
+@tab:active npm
+
+```bash
+npm i -D vuepress-theme-hope
+```
+
+:::
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/code/code-tabs.html)
+
+#### 代码演示
+
+
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/code/demo.html)
+
+#### 交互演示
+
+
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/code/playground.html)
+
+#### Kotlin 交互演示
+
+
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/code/kotlin-playground.html)
+
+#### Sandpack 交互演示
+
+
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/code/sandpack.html)
+
+#### Vue 交互演示
+
+
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/code/vue-playground.html)
+
+#### 幻灯片
+
+
+
+- [查看详情](https://theme-hope.vuejs.press/zh/guide/markdown/content/revealjs.html)
diff --git a/src/demo/page.md b/src/demo/page.md
new file mode 100644
index 0000000..254a156
--- /dev/null
+++ b/src/demo/page.md
@@ -0,0 +1,125 @@
+---
+# 这是文章的标题
+title: 页面配置
+# 你可以自定义封面图片
+cover: /assets/images/cover1.jpg
+# 这是页面的图标
+icon: file
+# 这是侧边栏的顺序
+order: 3
+# 设置作者
+author: Ms.Hope
+# 设置写作时间
+date: 2020-01-01
+# 一个页面可以有多个分类
+category:
+ - 使用指南
+# 一个页面可以有多个标签
+tag:
+ - 页面配置
+ - 使用指南
+# 此页面会在文章列表置顶
+#sticky: true
+# 此页面会出现在星标文章中
+star: true
+# 你可以自定义页脚
+footer: 这是测试显示的页脚
+# 你可以自定义版权信息
+copyright: 无版权
+---
+
+`more` 注释之前的内容被视为文章摘要。
+
+
+
+## 页面标题
+
+The first H1 title in Markdown will be regarded as page title.
+
+Markdown 中的第一个 H1 标题会被视为页面标题。
+
+你可以在 Markdown 的 Frontmatter 中设置页面标题。
+
+```md
+---
+title: 页面标题
+---
+```
+
+## 页面信息
+
+你可以在 Markdown 的 Frontmatter 中设置页面信息。
+
+- 作者设置为 Ms.Hope。
+- 写作日期为 2020 年 1 月 1 日
+- 分类为 “使用指南”
+- 标签为 “页面配置” 和 “使用指南”
+
+## 页面内容
+
+你可以自由在这里书写你的 Markdown。
+
+::: tip 图片引入
+
+- 你可以将图片和 Markdown 文件放置在一起使用相对路径进行引用。
+- 对于 `.vuepress/public` 文件夹的图片,请使用绝对链接 `/` 进行引用。
+
+:::
+
+## 组件
+
+每个 Markdown 页面都会被转换为一个 Vue 组件,这意味着你可以在 Markdown 中使用 Vue 语法:
+
+{{ 1 + 1 }}
+
+
+
+
+
+
+
+你也可以创建并引入你自己的组件。
+
+
+
+
+
+---
+
+主题包含一些有用的组件。这里是一些例子:
+
+- 文字结尾应该有深蓝色的 徽章文字 徽章。
+
+- 一个卡片:
+
+ ```component VPCard
+ title: Mr.Hope
+ desc: Where there is light, there is hope
+ logo: https://mister-hope.com/logo.svg
+ link: https://mister-hope.com
+ background: rgba(253, 230, 138, 0.15)
+ ```
diff --git a/src/intro.md b/src/intro.md
new file mode 100644
index 0000000..cd72b21
--- /dev/null
+++ b/src/intro.md
@@ -0,0 +1,12 @@
+---
+icon: circle-info
+cover: /assets/images/cover3.jpg
+date: 2025-05-06
+---
+
+# 介绍页
+
+## 自我介绍
+
+
+> 一个瞎鼓捣得程序员
\ No newline at end of file
diff --git a/src/programming/README.md b/src/programming/README.md
new file mode 100644
index 0000000..13b1813
--- /dev/null
+++ b/src/programming/README.md
@@ -0,0 +1,9 @@
+---
+title: 编程
+index: false
+icon: laptop-code
+category:
+ - 编程
+---
+
+
\ No newline at end of file
diff --git a/src/programming/backend/README.md b/src/programming/backend/README.md
new file mode 100644
index 0000000..f4b7e5c
--- /dev/null
+++ b/src/programming/backend/README.md
@@ -0,0 +1,9 @@
+---
+title: 其他后端语言
+index: false
+icon: mdi:code-tags
+category:
+ - 后端
+---
+
+
diff --git a/src/programming/backend/c++/1 b/src/programming/backend/c++/1
new file mode 100644
index 0000000..e69de29
diff --git a/src/programming/backend/go/Go基础语法/01Hello World.md b/src/programming/backend/go/Go基础语法/01Hello World.md
new file mode 100644
index 0000000..5614825
--- /dev/null
+++ b/src/programming/backend/go/Go基础语法/01Hello World.md
@@ -0,0 +1,178 @@
+---
+icon: mdi:language-go
+date: 2025-05-22
+title: Go 基础语法 - Hello World
+category:
+ - Go
+ - 后端
+ - 编程
+ - 教程
+ - 入门
+ - 开发环境
+ - Linux
+ - GoLand
+ - VSCode
+ - 命令行
+ - Go Module
+ - Go fmt
+ - Go build
+ - Go run
+ - GOPATH
+ - GOROOT
+ - Linux Mint
+ - Go 1.22.2
+ - Hello World
+---
+
+好的!根据你提供的环境(Linux Mint XFCE、Go 1.22.2 已安装、项目目录为 `/home/liumangmang/GolandProjects`),我将为你梳理 **完整但简洁** 的步骤,包括验证安装、配置 Go module、编写 Hello World、以及运行 `go fmt` 和 `go build`。
+
+
+
+---
+
+## ✅ 步骤 1:验证 Go 安装
+
+打开终端,运行:
+
+```bash
+go version
+```
+
+预期输出:
+```
+go version go1.22.2 linux/amd64
+```
+
+同时检查 GOPATH 和 GOROOT(Go 1.11+ 默认支持 module,GOPATH 不再强制):
+
+```bash
+go env GOROOT GOPATH
+```
+
+通常输出类似:
+```
+/usr/local/go
+/home/liumangmang/go
+```
+
+> 📌 提示:Go 1.22 默认启用 module 模式,无需设置 `GO111MODULE=on`。
+
+---
+
+## ✅ 步骤 2:创建项目并初始化 Go Module
+
+进入你的项目目录:
+
+```bash
+cd /home/liumangmang/GolandProjects
+mkdir hello-go && cd hello-go
+```
+
+初始化一个 Go module(模块名建议用类似域名的格式,比如 `example.com/hello-go`,本地开发可随意):
+
+```bash
+go mod init hello-go
+```
+
+这会生成 `go.mod` 文件,内容类似:
+
+```go
+module hello-go
+
+go 1.22
+```
+
+---
+
+## ✅ 步骤 3:编写 Hello World
+
+创建主程序文件:
+
+```bash
+nano main.go
+```
+
+输入以下代码:
+
+```go
+package main
+
+import "fmt"
+
+func main() {
+ fmt.Println("Hello, Go from Linux Mint!")
+}
+```
+
+保存并退出(`Ctrl+O` → 回车 → `Ctrl+X`)。
+
+---
+
+## ✅ 步骤 4:格式化代码(go fmt)
+
+运行格式化命令:
+
+```bash
+go fmt
+```
+
+> 如果代码已规范,可能无输出。`go fmt` 会自动修正缩进、空格等风格问题。
+
+你可以故意写成不规范格式测试一下,例如:
+
+```go
+func main(){fmt.
+Println("Hello")}
+```
+
+再运行 `go fmt`,它会自动修复。
+
+---
+
+## ✅ 步骤 5:构建和运行程序
+
+### 构建二进制文件:
+
+```bash
+go build
+```
+
+成功后会在当前目录生成一个可执行文件(在 Linux 下默认名为 `hello-go`,即 module 名):
+
+```bash
+./hello-go
+```
+
+输出:
+```
+Hello, Go from Linux Mint!
+```
+
+### 或者直接运行(不生成二进制):
+
+```bash
+go run .
+```
+
+同样输出:
+```
+Hello, Go from Linux Mint!
+```
+
+---
+
+## ✅ 额外建议(GoLand 集成)
+
+- 在 GoLand 中打开 `/home/liumangmang/GolandProjects/hello-go` 目录。
+- GoLand 会自动识别 `go.mod` 并启用 module 支持。
+- 你可以通过 IDE 的绿色 ▶️ 按钮直接运行 `main.go`。
+- `go fmt` 在 GoLand 中默认在保存时自动触发(可在 Settings → Tools → File Watchers 中配置)。
+
+---
+
+✅ 到此,你已完成:
+- Go 环境验证
+- Module 初始化
+- Hello World 编写
+- 代码格式化(`go fmt`)
+- 编译与运行(`go build` / `go run`)
\ No newline at end of file
diff --git a/src/programming/backend/go/Go基础语法/02变量与类型.md b/src/programming/backend/go/Go基础语法/02变量与类型.md
new file mode 100644
index 0000000..f95011b
--- /dev/null
+++ b/src/programming/backend/go/Go基础语法/02变量与类型.md
@@ -0,0 +1,208 @@
+---
+icon: mdi:variable
+date: 2025-05-22
+title: Go 基础语法 - 变量与类型
+category:
+ - Go
+ - 后端
+ - 编程
+ - 教程
+ - 入门
+ - 变量
+ - 类型
+ - 指针
+ - new
+ - make
+ - 简短声明
+ - 基本类型
+ - var
+---
+
+当然可以!以下是基于你当前环境(Linux Mint XFCE、Go 1.22.2、项目目录 `/home/liumangmang/GolandProjects`)整理的 **Go 语言基础语法实践步骤**,涵盖:
+
+- 基本类型
+- 变量声明
+- `:=` 简短声明
+- 指针
+- `new` vs `make`
+
+我们将通过一个可运行的示例程序来演示这些概念。
+
+
+
+---
+
+## ✅ 步骤 1:创建练习项目
+
+打开终端:
+
+```bash
+cd /home/liumangmang/GolandProjects
+mkdir go-basics && cd go-basics
+go mod init go-basics
+```
+
+---
+
+## ✅ 步骤 2:编写示例代码(main.go)
+
+使用你喜欢的编辑器(或 GoLand)创建 `main.go`:
+
+```bash
+nano main.go
+```
+
+粘贴以下完整示例代码(包含注释说明):
+
+```go
+package main
+
+import "fmt"
+
+func main() {
+ // ========== 1. 基本类型 ==========
+ var a int = 42
+ var b float64 = 3.14
+ var c bool = true
+ var d string = "Hello, Go!"
+
+ fmt.Println("基本类型:")
+ fmt.Printf("int: %d\n", a)
+ fmt.Printf("float64: %.2f\n", b)
+ fmt.Printf("bool: %t\n", c)
+ fmt.Printf("string: %s\n", d)
+
+ // ========== 2. 变量声明方式 ==========
+ // 方式1:var 声明(可带或不带初始值)
+ var x int
+ x = 100
+ var y = 200 // 类型自动推导
+
+ // 方式2:批量声明
+ var (
+ name = "Liu"
+ age = 30
+ height = 175.5
+ isAdmin = true
+ )
+
+ fmt.Println("\n变量声明:")
+ fmt.Println("x =", x, ", y =", y)
+ fmt.Println("name:", name, "age:", age, "height:", height, "isAdmin:", isAdmin)
+
+ // ========== 3. := 简短声明(仅在函数内部可用)==========
+ z := 999 // 自动推导为 int
+ message := "Use := !" // 自动推导为 string
+ fmt.Println("\n简短声明 (:=):")
+ fmt.Println("z =", z, ", message =", message)
+
+ // 注意::= 必须至少声明一个新变量
+ // 下面这行会报错:no new variables on left side of :=
+ // z := 888 // ❌ 错误!
+
+ // 但可以这样混合新旧变量:
+ w, z := "new", 888 // w 是新的,z 是重新赋值(shadowing)
+ fmt.Println("w =", w, ", z =", z)
+
+ // ========== 4. 指针 ==========
+ p := &a // p 是指向 a 的指针(*int 类型)
+ fmt.Println("\n指针:")
+ fmt.Printf("a 的值: %d\n", a)
+ fmt.Printf("a 的地址: %p\n", &a)
+ fmt.Printf("p 的值(即 a 的地址): %p\n", p)
+ fmt.Printf("*p(解引用): %d\n", *p)
+
+ *p = 1000 // 通过指针修改 a 的值
+ fmt.Println("修改后 a =", a)
+
+ // ========== 5. new vs make ==========
+ // new(T) → 分配零值内存,返回 *T
+ ptrInt := new(int) // *int,值为 0
+ *ptrInt = 42
+ fmt.Println("\nnew(int):", *ptrInt)
+
+ // make 仅用于 slice, map, chan —— 返回初始化后的 T(不是指针!)
+ s := make([]int, 3) // 长度为3的 slice,元素为 [0 0 0]
+ m := make(map[string]int) // 空 map
+ ch := make(chan int, 2) // 缓冲通道
+
+ s[0] = 10
+ m["count"] = 1
+ ch <- 100
+
+ fmt.Println("make(slice):", s)
+ fmt.Println("make(map):", m)
+ fmt.Println("make(chan) 示例(略去接收)")
+
+ // ⚠️ 不能对 slice/map/chan 使用 new!
+ // bad := new([]int) // 这是一个 *[]int,但底层数组未初始化,不能直接用!
+}
+```
+
+保存并退出(`Ctrl+O` → Enter → `Ctrl+X`)。
+
+---
+
+## ✅ 步骤 3:格式化 & 运行
+
+```bash
+# 格式化代码(虽然我们写得很规范,但习惯要好)
+go fmt
+
+# 运行程序
+go run .
+```
+
+你应该看到类似输出:
+
+```
+基本类型:
+int: 42
+float64: 3.14
+bool: true
+string: Hello, Go!
+
+变量声明:
+x = 100 , y = 200
+name: Liu age: 30 height: 175.5 isAdmin: true
+
+简短声明 (:=):
+z = 999 , message = Use := !
+w = new , z = 888
+
+指针:
+a 的值: 42
+a 的地址: 0xc0000b4008
+p 的值(即 a 的地址): 0xc0000b4008
+*p(解引用): 42
+修改后 a = 1000
+
+new(int): 42
+make(slice): [10 0 0]
+make(map): map[count:1]
+make(chan) 示例(略去接收)
+```
+
+---
+
+## 🔍 关键知识点总结(对比 Java)
+
+| 概念 | Go 行为 | Java 对比 |
+|------|--------|----------|
+| **变量声明** | `var x int = 1` 或 `x := 1` | `int x = 1;`(必须指定类型或用 var + 初始化) |
+| **:=** | 函数内快速声明+初始化,至少一个新变量 | Java 无直接等价(Java 10+ 有 `var`,但非声明新作用域) |
+| **指针** | 显式支持 `*T` 和 `&x` | Java 无显式指针(对象引用类似,但不可运算) |
+| **new** | 分配零值内存,返回指针 `*T` | 类似 `new Object()`,但 Go 的 `new` 只用于值类型 |
+| **make** | 专用于 slice/map/chan 的初始化(返回 T,非指针) | Java 中 `new ArrayList<>()` 等构造器承担类似职责 |
+
+> 💡 提醒:Go 中 **slice / map / chan 是引用类型**,传递时是“引用语义”,但它们本身不是指针。`make` 是它们的正确初始化方式。
+
+---
+
+## ✅ 下一步建议
+
+- 在 GoLand 中打开这个项目,尝试修改变量、加断点调试指针。
+- 尝试注释掉 `*p = 1000`,观察 `a` 是否变化。
+- 尝试 `bad := new([]int); bad[0] = 1`,看是否会 panic(会!因为底层未分配)。
+
+如果你希望我继续讲解 **结构体、方法、接口、goroutine** 等内容,随时告诉我!祝你 Go 学习顺利 🚀
\ No newline at end of file
diff --git a/src/programming/backend/go/Go基础语法/03slice和map.md b/src/programming/backend/go/Go基础语法/03slice和map.md
new file mode 100644
index 0000000..783f288
--- /dev/null
+++ b/src/programming/backend/go/Go基础语法/03slice和map.md
@@ -0,0 +1,266 @@
+---
+icon: mdi:table-arrow-down
+date: 2025-05-22
+title: Go 基础语法 - Slice 和 Map
+category:
+ - Go
+ - 后端
+ - 编程
+ - 教程
+ - 数据结构
+ - slice
+ - map
+ - 扩容机制
+ - 深拷贝
+ - 浅拷贝
+ - 并发安全
+ - sync.Map
+---
+
+当然可以!以下是基于你当前环境(Linux Mint XFCE、Go 1.22.2、项目目录 `/home/liumangmang/GolandProjects`)整理的 **Go 语言 slice 与 map 深入实践步骤**,涵盖:
+
+- **slice 的扩容机制**
+- **slice 的深拷贝 vs 浅拷贝**
+- **map 的基本使用**
+- **map 的并发写风险与安全方案**
+
+我们将通过一个可运行的示例程序,在你的环境中一步步验证这些行为。
+
+
+
+---
+
+## ✅ 步骤 1:创建新项目
+
+```bash
+cd /home/liumangmang/GolandProjects
+mkdir go-slice-map && cd go-slice-map
+go mod init go-slice-map
+```
+
+---
+
+## ✅ 步骤 2:编写演示代码(main.go)
+
+创建 `main.go`:
+
+```bash
+nano main.go
+```
+
+粘贴以下完整代码(含详细注释):
+
+```go
+package main
+
+import (
+ "fmt"
+ "runtime"
+ "sync"
+ "time"
+)
+
+func main() {
+ fmt.Println("=== 1. Slice 扩容机制 ===")
+ demoSliceGrowth()
+
+ fmt.Println("\n=== 2. Slice 深拷贝 vs 浅拷贝 ===")
+ demoSliceCopy()
+
+ fmt.Println("\n=== 3. Map 基本使用 ===")
+ demoMapUsage()
+
+ fmt.Println("\n=== 4. Map 并发写风险(会 panic!)===")
+ // ⚠️ 下面这行默认注释掉,避免程序崩溃
+ // demoUnsafeConcurrentMap()
+
+ fmt.Println("\n=== 5. 安全的并发 map(sync.Map)===")
+ demoSafeConcurrentMap()
+}
+
+// 1. 演示 slice 扩容机制
+func demoSliceGrowth() {
+ var s []int
+ fmt.Printf("初始: len=%d, cap=%d, addr=%p\n", len(s), cap(s), s)
+
+ for i := 0; i < 10; i++ {
+ s = append(s, i)
+ fmt.Printf("append %d: len=%d, cap=%d, addr=%p\n", i, len(s), cap(s), s)
+ }
+ // 观察:当容量不足时,底层数组会重新分配,地址改变
+}
+
+// 2. 深拷贝 vs 浅拷贝
+func demoSliceCopy() {
+ original := []int{1, 2, 3, 4, 5}
+ shallow := original // 浅拷贝:共享底层数组
+ deep := make([]int, len(original))
+ copy(deep, original) // 深拷贝:独立底层数组
+
+ fmt.Println("修改前:")
+ fmt.Println("original:", original)
+ fmt.Println("shallow :", shallow)
+ fmt.Println("deep :", deep)
+
+ // 修改原 slice
+ original[0] = 999
+
+ fmt.Println("\n修改 original[0] = 999 后:")
+ fmt.Println("original:", original) // [999, 2, 3, 4, 5]
+ fmt.Println("shallow :", shallow) // [999, 2, 3, 4, 5] ← 被影响!
+ fmt.Println("deep :", deep) // [1, 2, 3, 4, 5] ← 不受影响
+}
+
+// 3. Map 基本使用
+func demoMapUsage() {
+ m := make(map[string]int)
+ m["apple"] = 5
+ m["banana"] = 3
+
+ fmt.Println("map 内容:", m)
+ fmt.Println("apple 数量:", m["apple"])
+
+ // 检查 key 是否存在
+ if count, ok := m["orange"]; ok {
+ fmt.Println("orange 存在,数量:", count)
+ } else {
+ fmt.Println("orange 不存在")
+ }
+
+ // 删除 key
+ delete(m, "apple")
+ fmt.Println("删除 apple 后:", m)
+}
+
+// 4. 不安全的并发 map 写入(会导致 panic)
+func demoUnsafeConcurrentMap() {
+ m := make(map[int]int)
+ var wg sync.WaitGroup
+
+ for i := 0; i < 100; i++ {
+ wg.Add(1)
+ go func(key int) {
+ defer wg.Done()
+ m[key] = key * 10 // 并发写 → fatal error: concurrent map writes
+ }(i)
+ }
+
+ wg.Wait()
+ fmt.Println("unsafe map done (should not reach here)")
+}
+
+// 5. 使用 sync.Map 实现安全并发
+func demoSafeConcurrentMap() {
+ var sm sync.Map
+ var wg sync.WaitGroup
+
+ for i := 0; i < 100; i++ {
+ wg.Add(1)
+ go func(key int) {
+ defer wg.Done()
+ sm.Store(key, key*10)
+ }(i)
+ }
+
+ wg.Wait()
+
+ // 读取部分值验证
+ sm.Range(func(key, value any) bool {
+ if key.(int) < 5 { // 只打印前几个
+ fmt.Printf("key=%v, value=%v\n", key, value)
+ }
+ return true // 继续遍历
+ })
+}
+```
+
+保存退出(`Ctrl+O` → Enter → `Ctrl+X`)。
+
+---
+
+## ✅ 步骤 3:格式化并运行
+
+```bash
+go fmt
+go run .
+```
+
+### 预期输出(节选):
+
+```
+=== 1. Slice 扩容机制 ===
+初始: len=0, cap=0, addr=0x0
+append 0: len=1, cap=1, addr=0xc0000180c0
+append 1: len=2, cap=2, addr=0xc0000180e0
+append 2: len=3, cap=4, addr=0xc00001c0a0 ← 容量翻倍,地址变了!
+...
+
+=== 2. Slice 深拷贝 vs 浅拷贝 ===
+修改前:
+original: [1 2 3 4 5]
+shallow : [1 2 3 4 5]
+deep : [1 2 3 4 5]
+
+修改 original[0] = 999 后:
+original: [999 2 3 4 5]
+shallow : [999 2 3 4 5]
+deep : [1 2 3 4 5]
+
+=== 3. Map 基本使用 ===
+map 内容: map[apple:5 banana:3]
+apple 数量: 5
+orange 不存在
+删除 apple 后: map[banana:3]
+
+=== 4. Map 并发写风险(会 panic!)===
+
+=== 5. 安全的并发 map(sync.Map)===
+key=0, value=0
+key=1, value=10
+key=2, value=20
+key=3, value=30
+key=4, value=40
+```
+
+> 🔒 注意:`demoUnsafeConcurrentMap()` 默认被注释。如果你想**亲眼看到 panic**,可以取消注释它再运行——程序会崩溃并输出:
+>
+> ```
+> fatal error: concurrent map writes
+> ```
+
+---
+
+## 📚 关键知识点总结
+
+### 🔸 Slice 扩容机制(Go 1.22)
+- 当 `cap < 1024`:扩容为 **2 倍**
+- 当 `cap >= 1024`:扩容为 **1.25 倍**(实际策略可能微调,但趋势是渐进增长)
+- 扩容时会**分配新底层数组**,旧 slice 和新 slice **不再共享数据**
+
+### 🔸 深拷贝 vs 浅拷贝
+| 方式 | 是否共享底层数组 | 是否安全独立修改 |
+|------|------------------|------------------|
+| `s2 := s1` | ✅ 是 | ❌ 否 |
+| `copy(s2, s1)` | ❌ 否 | ✅ 是 |
+
+> 💡 提醒:`copy` 要求目标 slice 已分配内存(用 `make`)!
+
+### 🔸 Map 并发安全
+- **普通 map 不是并发安全的**:多个 goroutine 同时写会 panic
+- **解决方案**:
+ - 使用 `sync.Mutex` 或 `sync.RWMutex` 保护 map
+ - 或直接使用 `sync.Map`(适用于读多写少场景)
+
+> ⚠️ `sync.Map` 性能不一定比加锁的 map 高,仅在特定场景推荐使用。
+
+---
+
+## ✅ 在 GoLand 中调试建议
+
+1. 打开项目 `/home/liumangmang/GolandProjects/go-slice-map`
+2. 在 `demoSliceGrowth()` 中设置断点,观察每次 `append` 后地址变化
+3. 尝试取消注释 `demoUnsafeConcurrentMap()`,运行看 panic(有助于理解并发风险)
+
+---
+
+如果你希望继续学习 **channel、goroutine、context、错误处理** 或 **Go 与 Java 对比(如 slice vs ArrayList)**,欢迎随时告诉我!祝你 Go 编程愉快 🚀
\ No newline at end of file
diff --git a/src/programming/backend/go/Go基础语法/04Struct、方法与接收者类型详解.md b/src/programming/backend/go/Go基础语法/04Struct、方法与接收者类型详解.md
new file mode 100644
index 0000000..5c22c2d
--- /dev/null
+++ b/src/programming/backend/go/Go基础语法/04Struct、方法与接收者类型详解.md
@@ -0,0 +1,187 @@
+---
+icon: mdi:shape-outline
+date: 2025-05-22
+title: Go 基础语法 - Struct、方法与接收者类型详解
+category:
+ - Go
+ - 后端
+ - 编程
+ - 教程
+ - 面向对象
+ - Struct
+ - 方法
+ - 接收者
+ - 值接收者
+ - 指针接收者
+ - 面向对象编程
+---
+
+当然可以!以下是专为你定制的实践指南,基于你的环境:
+
+- **系统**:Linux Mint XFCE
+- **Go 版本**:go1.22.2 linux/amd64
+- **项目目录**:`/home/liumangmang/GolandProjects`
+
+
+
+## 📌 标题:
+# Go 面向对象基石:Struct、方法与接收者类型详解(值 vs 指针)
+
+---
+
+## ✅ 步骤 1:创建练习项目
+
+打开终端,执行:
+
+```bash
+cd /home/liumangmang/GolandProjects
+mkdir go-struct-methods && cd go-struct-methods
+go mod init go-struct-methods
+```
+
+---
+
+## ✅ 步骤 2:编写演示代码(main.go)
+
+创建并编辑 `main.go`:
+
+```bash
+nano main.go
+```
+
+粘贴以下完整示例代码(含详细注释和对比):
+
+```go
+package main
+
+import "fmt"
+
+// 定义一个 Person 结构体
+type Person struct {
+ Name string
+ Age int
+}
+
+// ========== 值接收者方法 ==========
+func (p Person) SayHello() {
+ fmt.Printf("Hello, I'm %s (值接收者)\n", p.Name)
+}
+
+// 值接收者无法修改原始结构体字段
+func (p Person) SetName(name string) {
+ p.Name = name // 修改的是副本!
+ fmt.Printf("在值接收者中设置 Name = %s\n", p.Name)
+}
+
+// ========== 指针接收者方法 ==========
+func (p *Person) SayHelloPtr() {
+ fmt.Printf("Hello, I'm %s (指针接收者)\n", p.Name)
+}
+
+// 指针接收者可以真正修改原始结构体
+func (p *Person) SetNamePtr(name string) {
+ p.Name = name // 修改的是原始对象!
+ fmt.Printf("在指针接收者中设置 Name = %s\n", p.Name)
+}
+
+// ========== 对比调用行为 ==========
+func main() {
+ fmt.Println("=== 1. 使用值类型变量调用方法 ===")
+ p1 := Person{Name: "Alice", Age: 30}
+ p1.SayHello() // OK
+ p1.SetName("Alicia") // ❌ 不会改变 p1.Name
+ fmt.Printf("调用值接收者 SetName 后,p1.Name = %s\n", p1.Name)
+
+ p1.SayHelloPtr() // ✅ Go 自动取地址 (&p1)
+ p1.SetNamePtr("Anna") // ✅ 真正修改了 p1
+ fmt.Printf("调用指针接收者 SetNamePtr 后,p1.Name = %s\n", p1.Name)
+
+ fmt.Println("\n=== 2. 使用指针类型变量调用方法 ===")
+ p2 := &Person{Name: "Bob", Age: 25}
+ p2.SayHello() // ✅ Go 自动解引用 (*p2)
+ p2.SayHelloPtr() // ✅ 直接调用
+
+ p2.SetName("Bobby") // ❌ 副本修改,无效
+ fmt.Printf("调用值接收者 SetName 后,p2.Name = %s\n", p2.Name)
+
+ p2.SetNamePtr("Robert") // ✅ 真正修改
+ fmt.Printf("调用指针接收者 SetNamePtr 后,p2.Name = %s\n", p2.Name)
+
+ fmt.Println("\n=== 3. 关键规则总结 ===")
+ fmt.Println("- 值接收者:操作副本,不能修改原结构体")
+ fmt.Println("- 指针接收者:操作原始数据,可修改")
+ fmt.Println("- Go 会自动处理 & 和 * 转换(只要变量可寻址)")
+ fmt.Println("- 如果方法需要修改字段,请使用指针接收者!")
+}
+```
+
+保存并退出(`Ctrl+O` → Enter → `Ctrl+X`)。
+
+---
+
+## ✅ 步骤 3:格式化并运行
+
+```bash
+go fmt
+go run .
+```
+
+### 预期输出:
+
+```
+=== 1. 使用值类型变量调用方法 ===
+Hello, I'm Alice (值接收者)
+在值接收者中设置 Name = Alicia
+调用值接收者 SetName 后,p1.Name = Alice
+Hello, I'm Alice (指针接收者)
+在指针接收者中设置 Name = Anna
+调用指针接收者 SetNamePtr 后,p1.Name = Anna
+
+=== 2. 使用指针类型变量调用方法 ===
+Hello, I'm Bob (值接收者)
+Hello, I'm Bob (指针接收者)
+在值接收者中设置 Name = Bobby
+调用值接收者 SetName 后,p2.Name = Bob
+在指针接收者中设置 Name = Robert
+调用指针接收者 SetNamePtr 后,p2.Name = Robert
+
+=== 3. 关键规则总结 ===
+- 值接收者:操作副本,不能修改原结构体
+- 指针接收者:操作原始数据,可修改
+- Go 会自动处理 & 和 * 转换(只要变量可寻址)
+- 如果方法需要修改字段,请使用指针接收者!
+```
+
+---
+
+## 🔍 核心知识点解析(对比 Java)
+
+| 概念 | Go 行为 | Java 类比 |
+|------|--------|----------|
+| **Struct** | 值类型,默认按值传递 | 类似 `class`,但 Go 的 struct 是值语义(除非用指针) |
+| **方法** | 绑定到类型(struct 或其他) | 类似 Java 的实例方法 |
+| **值接收者** | 方法内操作的是副本 | 类似 Java 中传入不可变对象(但 Java 对象默认是引用) |
+| **指针接收者** | 方法内可修改原始对象 | 更接近 Java 实例方法的行为(因为 Java 对象总是引用) |
+
+> 💡 **重要提示**:
+> - 在 Go 中,**所有参数和接收者都是“按值传递”**。
+> - 指针接收者之所以能修改原数据,是因为“值”是一个地址,通过该地址可以访问原始内存。
+
+---
+
+## ✅ 在 GoLand 中进一步探索
+
+1. 打开项目:`/home/liumangmang/GolandProjects/go-struct-methods`
+2. 将鼠标悬停在 `p1.SetName` 和 `p1.SetNamePtr` 上,观察 IDE 提示的接收者类型
+3. 尝试将 `SetName` 改为指针接收者,看输出变化
+4. 使用调试器(Debug)查看 `p1` 内存地址是否在方法调用中被共享
+
+---
+
+## 🧭 下一步建议
+
+- 学习 **接口(interface)** 如何与 struct + 方法配合实现多态
+- 探索 **嵌入(embedding)** 替代继承
+- 对比 Java 的 getter/setter 与 Go 的字段直接访问(Go 无 private/public 关键字,靠首字母大小写控制可见性)
+
+如果你希望我继续讲解 **接口、组合、错误处理** 或 **Go 风格的面向对象设计**,请随时告诉我!祝你编码愉快 🚀
\ No newline at end of file
diff --git a/src/programming/backend/go/Go基础语法/05接口.md b/src/programming/backend/go/Go基础语法/05接口.md
new file mode 100644
index 0000000..8b957be
--- /dev/null
+++ b/src/programming/backend/go/Go基础语法/05接口.md
@@ -0,0 +1,255 @@
+---
+icon: mdi:link-variant
+date: 2025-05-22
+title: Go 基础语法 - 接口(鸭子类型)、空接口与类型断言实战
+category:
+ - Go
+ - 后端
+ - 编程
+ - 教程
+ - 接口
+ - 多态
+ - 鸭子类型
+ - 空接口
+ - 类型断言
+ - any
+ - interface{}
+---
+
+当然可以!以下是专为你量身定制的实践指南,完全基于你的开发环境:
+
+- **操作系统**:Linux Mint XFCE
+- **Go 版本**:go1.22.2 linux/amd64
+- **项目目录**:`/home/liumangmang/GolandProjects`
+
+
+
+## 📌 标题:
+# Go 的多态之道:接口(鸭子类型)、空接口与类型断言实战
+
+---
+
+## ✅ 步骤 1:创建新项目
+
+打开终端,执行以下命令:
+
+```bash
+cd /home/liumangmang/GolandProjects
+mkdir go-interfaces && cd go-interfaces
+go mod init go-interfaces
+```
+
+---
+
+## ✅ 步骤 2:编写演示代码(main.go)
+
+创建并编辑 `main.go`:
+
+```bash
+nano main.go
+```
+
+粘贴以下完整示例代码(含详细注释,覆盖鸭子类型、空接口、类型断言):
+
+```go
+package main
+
+import "fmt"
+
+// ========== 1. 定义接口(鸭子类型)==========
+type Speaker interface {
+ Speak() string
+}
+
+type Dog struct{}
+func (d Dog) Speak() string {
+ return "Woof!"
+}
+
+type Cat struct{}
+func (c Cat) Speak() string {
+ return "Meow~"
+}
+
+type Robot struct{}
+func (r Robot) Speak() string {
+ return "Beep boop."
+}
+
+// ========== 2. 空接口(interface{})==========
+// 在 Go 1.18+ 中,推荐使用 any(它是 interface{} 的别名)
+func printAnything(v any) {
+ fmt.Printf("接收到: %v (类型: %T)\n", v, v)
+}
+
+// ========== 3. 类型断言 ==========
+func describeSpeaker(s Speaker) {
+ fmt.Println("它说:", s.Speak())
+
+ // 类型断言:判断具体类型
+ if d, ok := s.(Dog); ok {
+ fmt.Println("这是一只狗!", d)
+ } else if c, ok := s.(Cat); ok {
+ fmt.Println("这是一只猫!", c)
+ }
+}
+
+// 使用 switch 进行类型断言(更优雅)
+func identify(v any) {
+ switch x := v.(type) {
+ case string:
+ fmt.Printf("字符串: %s\n", x)
+ case int:
+ fmt.Printf("整数: %d\n", x)
+ case Speaker:
+ fmt.Printf("会说话的东西: %s\n", x.Speak())
+ default:
+ fmt.Printf("未知类型: %T\n", x)
+ }
+}
+
+// ========== 主函数 ==========
+func main() {
+ fmt.Println("=== 1. 鸭子类型:只要会 Speak(),就是 Speaker ===")
+ animals := []Speaker{Dog{}, Cat{}, Robot{}}
+ for _, a := range animals {
+ fmt.Println(a.Speak())
+ }
+
+ fmt.Println("\n=== 2. 空接口(any)可接收任意类型 ===")
+ printAnything(42)
+ printAnything("Hello")
+ printAnything(Dog{})
+
+ fmt.Println("\n=== 3. 类型断言:从接口还原具体类型 ===")
+ describeSpeaker(Dog{})
+ describeSpeaker(Cat{})
+
+ fmt.Println("\n=== 4. 类型 switch:安全高效的类型判断 ===")
+ identify("Gopher")
+ identify(100)
+ identify(Robot{})
+ identify(true) // 未知类型
+
+ fmt.Println("\n=== 5. 安全 vs 不安全的类型断言 ===")
+ var i any = "hello"
+ s := i.(string) // 不安全:如果类型不对,会 panic
+ fmt.Println("不安全断言结果:", s)
+
+ // 安全方式
+ if val, ok := i.(int); ok {
+ fmt.Println("是整数:", val)
+ } else {
+ fmt.Println("不是整数!")
+ }
+}
+```
+
+保存并退出(`Ctrl+O` → Enter → `Ctrl+X`)。
+
+---
+
+## ✅ 步骤 3:格式化并运行
+
+```bash
+go fmt
+go run .
+```
+
+### 预期输出:
+
+```
+=== 1. 鸭子类型:只要会 Speak(),就是 Speaker ===
+Woof!
+Meow~
+Beep boop.
+
+=== 2. 空接口(any)可接收任意类型 ===
+接收到: 42 (类型: int)
+接收到: Hello (类型: string)
+接收到: {} (类型: main.Dog)
+
+=== 3. 类型断言:从接口还原具体类型 ===
+它说: Woof!
+这是一只狗! {}
+它说: Meow~
+这是一只猫! {}
+
+=== 4. 类型 switch:安全高效的类型判断 ===
+字符串: Gopher
+整数: 100
+会说话的东西: Beep boop.
+未知类型: bool
+
+=== 5. 安全 vs 不安全的类型断言 ===
+不安全断言结果: hello
+不是整数!
+```
+
+---
+
+## 🔍 核心概念解析
+
+### 🦆 1. 鸭子类型(Duck Typing)
+> “如果它走起来像鸭子,叫起来也像鸭子,那它就是鸭子。”
+
+- Go 的接口是 **隐式实现** 的:只要结构体有 `Speak()` 方法,就自动满足 `Speaker` 接口。
+- **无需显式声明** `implements`(对比 Java)。
+
+### 🕳️ 2. 空接口 `interface{}`(或 `any`)
+- 可以存储任意类型的值(类似 Java 的 `Object`,但更灵活)。
+- 常用于:
+ - 通用函数参数(如 `fmt.Println`)
+ - JSON 解析(`map[string]any`)
+- **代价**:失去类型安全,需配合类型断言使用。
+
+### 🔍 3. 类型断言
+语法:
+```go
+value, ok := interfaceVar.(ConcreteType)
+```
+- **安全方式**:用 `, ok` 检查是否成功,避免 panic。
+- **不安全方式**:直接 `v := i.(T)`,类型不符时程序崩溃。
+
+### 🔄 4. 类型 switch
+```go
+switch v := x.(type) {
+case int:
+ // v 是 int
+case string:
+ // v 是 string
+}
+```
+- 更简洁、高效地处理多种类型。
+
+---
+
+## 💡 对比 Java
+
+| Go | Java |
+|----|------|
+| `interface{}` / `any` | `Object` |
+| 隐式实现接口 | 显式 `implements` |
+| 类型断言 | `instanceof` + 强转 |
+| 鸭子类型 | 接口必须显式实现 |
+
+> Go 的接口设计更轻量、灵活,强调“行为”而非“继承关系”。
+
+---
+
+## ✅ 在 GoLand 中探索建议
+
+1. 打开项目:`/home/liumangmang/GolandProjects/go-interfaces`
+2. 将光标放在 `Speaker` 上,按 `Ctrl+H`(或右键 → Find Usages),查看所有实现者
+3. 尝试删除 `Robot` 的 `Speak()` 方法,观察编译错误(或无错误?因为没被用到!)
+4. 在 `identify` 函数中添加新类型(如 `[]int`),看类型 switch 如何处理
+
+---
+
+## 🧭 下一步学习方向
+
+- 接口组合(`io.Reader`, `io.Writer`)
+- 错误处理与 `error` 接口
+- 如何设计小而美的接口(Go 哲学:“Accept interfaces, return structs”)
+
+如果你希望我继续讲解 **错误处理、panic/recover、泛型入门** 或 **Go 标准库常用接口**,欢迎随时告诉我!祝你 Go 之旅越来越顺 🚀
\ No newline at end of file
diff --git a/src/programming/backend/go/Go基础语法/06错误机制.md b/src/programming/backend/go/Go基础语法/06错误机制.md
new file mode 100644
index 0000000..fc296c8
--- /dev/null
+++ b/src/programming/backend/go/Go基础语法/06错误机制.md
@@ -0,0 +1,253 @@
+---
+icon: mdi:alert-circle-outline
+date: 2025-05-22
+title: Go 基础语法 - 错误处理全解
+category:
+ - Go
+ - 后端
+ - 编程
+ - 教程
+ - 错误处理
+ - error
+ - defer
+ - panic
+ - recover
+ - fmt.Errorf
+ - errors.Is
+ - errors.As
+---
+
+当然可以!以下是专为你定制的实践指南,完全基于你的开发环境:
+
+- **操作系统**:Linux Mint XFCE
+- **Go 版本**:go1.22.2 linux/amd64
+- **项目目录**:`/home/liumangmang/GolandProjects`
+
+
+
+## 📌 标题:
+# Go 错误处理全解:error、defer、panic/recover 与错误封装实战
+
+---
+
+## ✅ 步骤 1:创建新项目
+
+打开终端,执行:
+
+```bash
+cd /home/liumangmang/GolandProjects
+mkdir go-error-handling && cd go-error-handling
+go mod init go-error-handling
+```
+
+---
+
+## ✅ 步骤 2:编写演示代码(main.go)
+
+创建并编辑 `main.go`:
+
+```bash
+nano main.go
+```
+
+粘贴以下完整示例代码(涵盖 error 返回、defer 清理、panic 恢复、fmt.Errorf 封装):
+
+```go
+package main
+
+import (
+ "errors"
+ "fmt"
+ "os"
+)
+
+// ========== 1. 自定义错误 + fmt.Errorf 封装 ==========
+func divide(a, b float64) (float64, error) {
+ if b == 0 {
+ // 使用 fmt.Errorf 包装原始错误,添加上下文
+ return 0, fmt.Errorf("divide by zero: cannot divide %.2f by %.2f", a, b)
+ }
+ return a / b, nil
+}
+
+// 更复杂的错误链(Go 1.13+ 支持 %w)
+var ErrNegativeInput = errors.New("input must be non-negative")
+
+func sqrt(x float64) (float64, error) {
+ if x < 0 {
+ // 使用 %w 包装错误,支持 errors.Is 和 errors.As
+ return 0, fmt.Errorf("invalid input for sqrt: %w", ErrNegativeInput)
+ }
+ return x * x, nil // 注意:这里故意写成平方,方便测试
+}
+
+// ========== 2. defer 的典型用途 ==========
+func readFile(filename string) error {
+ fmt.Println("尝试打开文件:", filename)
+ file, err := os.Open(filename)
+ if err != nil {
+ return fmt.Errorf("无法打开文件 %s: %w", filename, err)
+ }
+ // defer 确保文件在函数退出前关闭(即使 panic)
+ defer func() {
+ fmt.Println("defer: 关闭文件")
+ file.Close()
+ }()
+
+ // 模拟读取
+ fmt.Println("文件已打开,正在读取...")
+ return nil
+}
+
+// ========== 3. panic 与 recover ==========
+func riskyFunction(n int) {
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Printf("recover 捕获到 panic: %v\n", r)
+ }
+ }()
+ if n < 0 {
+ panic("n 不能为负数!")
+ }
+ fmt.Println("riskyFunction 正常执行,n =", n)
+}
+
+// recover 只在 defer 中有效!
+func noRecover() {
+ // ❌ 这样写无法捕获 panic
+ recover()
+ panic("不会被捕获")
+}
+
+// ========== 4. 综合示例:安全计算 ==========
+func safeCompute(a, b float64) {
+ defer fmt.Println("safeCompute 结束\n---")
+
+ fmt.Printf("计算 %.2f / %.2f\n", a, b)
+ result, err := divide(a, b)
+ if err != nil {
+ fmt.Println("错误:", err)
+ return
+ }
+ fmt.Printf("结果: %.2f\n", result)
+
+ fmt.Println("尝试对结果开方(实际是平方)")
+ sq, err := sqrt(result)
+ if err != nil {
+ if errors.Is(err, ErrNegativeInput) {
+ fmt.Println("检测到特定错误:输入为负")
+ }
+ fmt.Println("sqrt 错误:", err)
+ return
+ }
+ fmt.Printf("平方结果: %.2f\n", sq)
+}
+
+// ========== 主函数 ==========
+func main() {
+ fmt.Println("=== 1. 基本 error 处理 ===")
+ safeCompute(10, 2)
+ safeCompute(10, 0) // 触发除零错误
+
+ fmt.Println("\n=== 2. defer 文件操作 ===")
+ readFile("/nonexistent/file.txt") // 文件不存在,触发错误
+ readFile("/etc/hostname") // 存在的文件(Linux 系统通常有)
+
+ fmt.Println("\n=== 3. panic 与 recover ===")
+ riskyFunction(5)
+ riskyFunction(-1) // 触发 panic,但被 recover 捕获
+
+ fmt.Println("\n=== 4. 错误封装与识别 ===")
+ _, err := sqrt(-4)
+ if err != nil {
+ fmt.Println("原始错误信息:", err)
+ // 使用 errors.Is 判断是否包含特定错误
+ if errors.Is(err, ErrNegativeInput) {
+ fmt.Println("✅ 成功识别自定义错误 ErrNegativeInput")
+ }
+ }
+
+ // ⚠️ 取消注释下面这行会 crash(recover 不在 defer 中无效)
+ // noRecover()
+}
+```
+
+保存并退出(`Ctrl+O` → Enter → `Ctrl+X`)。
+
+---
+
+## ✅ 步骤 3:格式化并运行
+
+```bash
+go fmt
+go run .
+```
+
+### 预期输出(节选):
+
+```
+=== 1. 基本 error 处理 ===
+计算 10.00 / 2.00
+结果: 5.00
+尝试对结果开方(实际是平方)
+平方结果: 25.00
+safeCompute 结束
+---
+计算 10.00 / 0.00
+错误: divide by zero: cannot divide 10.00 by 0.00
+safeCompute 结束
+---
+
+=== 2. defer 文件操作 ===
+尝试打开文件: /nonexistent/file.txt
+defer: 关闭文件
+尝试打开文件: /etc/hostname
+文件已打开,正在读取...
+defer: 关闭文件
+
+=== 3. panic 与 recover ===
+riskyFunction 正常执行,n = 5
+recover 捕获到 panic: n 不能为负数!
+safeCompute 结束
+---
+
+=== 4. 错误封装与识别 ===
+原始错误信息: invalid input for sqrt: input must be non-negative
+✅ 成功识别自定义错误 ErrNegativeInput
+```
+
+---
+
+## 🔍 核心知识点总结
+
+| 机制 | 说明 | 最佳实践 |
+|------|------|--------|
+| **`error`** | Go 的错误是值,不是异常 | 函数最后一个返回值通常是 `error`,必须显式检查 |
+| **`fmt.Errorf`** | 创建带格式的错误;用 `%w` 包装(Go 1.13+) | 用于添加上下文,支持 `errors.Is`/`errors.As` |
+| **`defer`** | 延迟执行,常用于资源清理(文件、锁、连接) | 确保 cleanup 逻辑总被执行,即使 panic |
+| **`panic`** | 立即停止当前 goroutine,开始栈展开 | 仅用于不可恢复的严重错误(如程序状态损坏) |
+| **`recover`** | 在 defer 中调用,可捕获 panic | 用于守护关键服务不崩溃(如 HTTP 服务器中间件) |
+
+> 💡 **重要原则**:
+> - **不要滥用 panic**:Go 推崇“显式错误处理”,而非异常。
+> - **永远检查 error**:忽略 error 是 Go 新手常见错误。
+> - **用 `%w` 而不是 `%v` 包装错误**:这样才能用 `errors.Is` 判断。
+
+---
+
+## ✅ 在 GoLand 中调试建议
+
+1. 打开项目:`/home/liumangmang/GolandProjects/go-error-handling`
+2. 在 `divide` 函数中设置断点,观察错误如何逐层返回
+3. 尝试取消注释 `noRecover()`,运行看程序崩溃(理解 recover 必须在 defer 中)
+4. 使用 **Evaluate Expression** 功能测试 `errors.Is(err, ErrNegativeInput)`
+
+---
+
+## 🧭 下一步学习方向
+
+- 自定义 error 类型(实现 `Error() string` 方法)
+- 使用 `errors.Unwrap()` 手动解包错误链
+- 在 Web 服务中统一错误处理中间件
+
+如果你希望我继续讲解 **泛型入门、context 使用、并发模式(worker pool)** 或 **Go 单元测试(testing 包)**,欢迎随时告诉我!祝你写出健壮又优雅的 Go 代码 🚀
\ No newline at end of file
diff --git a/src/programming/backend/go/Go基础语法/07从零实现 Mini 日志库.md b/src/programming/backend/go/Go基础语法/07从零实现 Mini 日志库.md
new file mode 100644
index 0000000..86ac628
--- /dev/null
+++ b/src/programming/backend/go/Go基础语法/07从零实现 Mini 日志库.md
@@ -0,0 +1,275 @@
+---
+icon: mdi:file-document-edit-outline
+date: 2025-05-22
+title: Go 基础语法 - 从零实现 Mini 日志库
+category:
+ - Go
+ - 后端
+ - 编程
+ - 教程
+ - 日志库
+ - 实践项目
+ - 标准库
+---
+
+
+
+当然可以!以下是专为你定制的实战项目,完全基于你的开发环境:
+
+- **操作系统**:Linux Mint XFCE
+- **Go 版本**:go1.22.2 linux/amd64
+- **项目目录**:`/home/liumangmang/GolandProjects`
+
+---
+
+## 📌 标题:
+# 从零实现 Mini 日志库:Go 结构体、接口与 fmt 的完美结合
+
+---
+
+## ✅ 步骤 1:创建项目结构
+
+打开终端,执行以下命令:
+
+```bash
+cd /home/liumangmang/GolandProjects
+mkdir go-mini-logger && cd go-mini-logger
+go mod init go-mini-logger
+```
+
+我们将实现一个支持多级别(Info/Warn/Error)、可输出到控制台或文件、并可通过接口扩展的轻量日志库。
+
+---
+
+## ✅ 步骤 2:定义日志接口(logger.go)
+
+创建 `logger.go`:
+
+```bash
+nano logger.go
+```
+
+粘贴以下代码:
+
+```go
+package main
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "time"
+)
+
+// LogLevel 日志级别
+type LogLevel int
+
+const (
+ LevelInfo LogLevel = iota
+ LevelWarn
+ LevelError
+)
+
+func (l LogLevel) String() string {
+ switch l {
+ case LevelInfo:
+ return "INFO"
+ case LevelWarn:
+ return "WARN"
+ case LevelError:
+ return "ERROR"
+ default:
+ return "UNKNOWN"
+ }
+}
+
+// Logger 接口:定义日志行为
+type Logger interface {
+ Log(level LogLevel, format string, args ...any)
+ Info(format string, args ...any)
+ Warn(format string, args ...any)
+ Error(format string, args ...any)
+ SetMinLevel(level LogLevel)
+}
+
+// defaultLogger 结构体:默认实现
+type defaultLogger struct {
+ minLevel LogLevel
+ writer io.Writer
+}
+
+// NewLogger 创建新日志实例,默认输出到 os.Stdout
+func NewLogger() Logger {
+ return &defaultLogger{
+ minLevel: LevelInfo,
+ writer: os.Stdout,
+ }
+}
+
+// NewFileLogger 创建写入文件的日志实例
+func NewFileLogger(filename string) (Logger, error) {
+ file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
+ if err != nil {
+ return nil, fmt.Errorf("无法创建日志文件 %s: %w", filename, err)
+ }
+ return &defaultLogger{
+ minLevel: LevelInfo,
+ writer: file,
+ }, nil
+}
+
+// SetMinLevel 设置最低日志级别
+func (l *defaultLogger) SetMinLevel(level LogLevel) {
+ l.minLevel = level
+}
+
+// Log 核心日志方法
+func (l *defaultLogger) Log(level LogLevel, format string, args ...any) {
+ if level < l.minLevel {
+ return // 低于最小级别,丢弃
+ }
+
+ // 构建日志消息:[时间] [级别] 消息
+ timestamp := time.Now().Format("2006-01-02 15:04:05")
+ message := fmt.Sprintf(format, args...)
+ line := fmt.Sprintf("[%s] [%s] %s\n", timestamp, level.String(), message)
+
+ // 写入目标(控制台或文件)
+ fmt.Fprint(l.writer, line)
+}
+
+// 快捷方法
+func (l *defaultLogger) Info(format string, args ...any) { l.Log(LevelInfo, format, args...) }
+func (l *defaultLogger) Warn(format string, args ...any) { l.Log(LevelWarn, format, args...) }
+func (l *defaultLogger) Error(format string, args ...any) { l.Log(LevelError, format, args...) }
+```
+
+保存退出(`Ctrl+O` → Enter → `Ctrl+X`)。
+
+---
+
+## ✅ 步骤 3:编写使用示例(main.go)
+
+创建 `main.go`:
+
+```bash
+nano main.go
+```
+
+粘贴以下代码:
+
+```go
+package main
+
+import (
+ "fmt"
+ "os"
+)
+
+func main() {
+ fmt.Println("=== 1. 控制台日志 ===")
+ consoleLogger := NewLogger()
+ consoleLogger.Info("程序启动,PID: %d", os.Getpid())
+ consoleLogger.Warn("磁盘使用率超过 80%")
+ consoleLogger.Error("数据库连接失败: timeout")
+
+ fmt.Println("\n=== 2. 设置最低级别为 WARN ===")
+ consoleLogger.SetMinLevel(LevelWarn)
+ consoleLogger.Info("这条不会显示") // 被过滤
+ consoleLogger.Warn("这条会显示") // 显示
+ consoleLogger.Error("错误也会显示") // 显示
+
+ fmt.Println("\n=== 3. 文件日志(写入 app.log)===")
+ fileLogger, err := NewFileLogger("app.log")
+ if err != nil {
+ consoleLogger.Error("无法创建文件日志: %v", err)
+ return
+ }
+
+ fileLogger.Info("应用开始处理请求")
+ fileLogger.Warn("用户 %s 尝试越权操作", "liumangmang")
+ fileLogger.Error("写入数据库时发生唯一键冲突")
+
+ fmt.Println("日志已写入 app.log,请查看内容:")
+ os.ReadFile("app.log") // 不打印,仅确保写入
+ fmt.Println("✅ 查看 app.log 可验证文件日志内容")
+}
+```
+
+保存退出。
+
+---
+
+## ✅ 步骤 4:格式化并运行
+
+```bash
+go fmt
+go run .
+```
+
+### 预期终端输出:
+
+```
+=== 1. 控制台日志 ===
+[2025-11-25 21:30:05] [INFO] 程序启动,PID: 12345
+[2025-11-25 21:30:05] [WARN] 磁盘使用率超过 80%
+[2025-11-25 21:30:05] [ERROR] 数据库连接失败: timeout
+
+=== 2. 设置最低级别为 WARN ===
+[2025-11-25 21:30:05] [WARN] 这条会显示
+[2025-11-25 21:30:05] [ERROR] 错误也会显示
+
+=== 3. 文件日志(写入 app.log)===
+日志已写入 app.log,请查看内容:
+✅ 查看 app.log 可验证文件日志内容
+```
+
+同时,当前目录会生成 `app.log` 文件。你可以查看它:
+
+```bash
+cat app.log
+```
+
+内容类似:
+
+```
+[2025-11-25 21:30:05] [INFO] 应用开始处理请求
+[2025-11-25 21:30:05] [WARN] 用户 liumangmang 尝试越权操作
+[2025-11-25 21:30:05] [ERROR] 写入数据库时发生唯一键冲突
+```
+
+---
+
+## 🔍 设计亮点解析
+
+| Go 特性 | 在本项目中的体现 |
+|--------|----------------|
+| **结构体** | `defaultLogger` 封装 writer 和 minLevel |
+| **接口** | `Logger` 接口解耦调用方与实现,便于未来扩展(如网络日志、JSON 格式等) |
+| **fmt 包** | 使用 `fmt.Sprintf` 格式化消息,`fmt.Fprint` 写入任意 `io.Writer` |
+| **io.Writer** | 支持无缝切换输出目标(控制台、文件、网络流) |
+| **可扩展性** | 只需实现 `Logger` 接口,即可插入新的日志后端 |
+
+> 💡 **为什么用接口?**
+> 未来你可以轻松添加 `JSONLogger`、`SyslogLogger` 等,而**无需修改业务代码**——只需传入不同的 `Logger` 实现。
+
+---
+
+## ✅ 在 GoLand 中进一步探索
+
+1. 打开项目:`/home/liumangmang/GolandProjects/go-mini-logger`
+2. 右键点击 `Logger` 接口 → **Go to → Implementations**,查看所有实现(目前只有 `defaultLogger`)
+3. 尝试新增一个 `SilentLogger`(不输出任何内容),验证接口的灵活性
+4. 在 `main.go` 中将 `consoleLogger` 替换为 `fileLogger`,观察行为变化
+
+---
+
+## 🧭 扩展建议(课后练习)
+
+- 添加 `WithField(key, value string)` 方法支持结构化日志
+- 支持日志轮转(log rotation)
+- 增加 `Debug` 级别,并通过 build tag 控制是否编译
+
+---
+
+如果你希望我继续带你实现 **带上下文(context)的日志、JSON 格式输出、或集成 zap/logrus 对比分析**,欢迎随时告诉我!祝你编码愉快,日志清晰 🚀
\ No newline at end of file
diff --git a/src/programming/backend/go/Go基础语法/08Go 并发入门:Goroutine 基础与 GPM 调度模型实战解析.md b/src/programming/backend/go/Go基础语法/08Go 并发入门:Goroutine 基础与 GPM 调度模型实战解析.md
new file mode 100644
index 0000000..ea02a10
--- /dev/null
+++ b/src/programming/backend/go/Go基础语法/08Go 并发入门:Goroutine 基础与 GPM 调度模型实战解析.md
@@ -0,0 +1,235 @@
+title: Go 并发入门:Goroutine 基础与 GPM 调度模型实战解析
+icon: go
+date: 2025-12-11
+category:
+ - Go
+ - 并发编程
+tag:
+ - Goroutine
+ - GPM模型
+ - 调度器
+star: true
+---
+
+当然可以!以下是专为你定制的学习实践指南,完全基于你的开发环境:
+
+- **操作系统**:Linux Mint XFCE
+- **Go 版本**:go1.22.2 linux/amd64
+- **项目目录**:`/home/liumangmang/GolandProjects`
+
+
+
+---
+
+## 📌 标题:
+# Go 并发入门:Goroutine 基础与 GPM 调度模型实战解析
+
+---
+
+## ✅ 步骤 1:创建练习项目
+
+打开终端,执行:
+
+```bash
+cd /home/liumangmang/GolandProjects
+mkdir go-goroutine-gpm && cd go-goroutine-gpm
+go mod init go-goroutine-gpm
+```
+
+---
+
+## ✅ 步骤 2:编写 Goroutine 基础示例(main.go)
+
+创建 `main.go`:
+
+```bash
+nano main.go
+```
+
+粘贴以下代码,涵盖 goroutine 启动、并发行为、以及 GPM 模型的观察方法:
+
+```go
+package main
+
+import (
+ "fmt"
+ "runtime"
+ "sync"
+ "time"
+)
+
+func say(s string, id int) {
+ for i := 0; i < 3; i++ {
+ fmt.Printf("[%d] %s: step %d\n", id, s, i)
+ time.Sleep(100 * time.Millisecond) // 模拟工作
+ }
+}
+
+func main() {
+ fmt.Println("=== 1. 单线程顺序执行 ===")
+ say("Hello", 1)
+ say("World", 2)
+
+ fmt.Println("\n=== 2. 使用 Goroutine 并发执行 ===")
+ go say("Goroutine-A", 1)
+ go say("Goroutine-B", 2)
+
+ // 主 goroutine 等待 1 秒,否则程序会提前退出
+ time.Sleep(1 * time.Second)
+
+ fmt.Println("\n=== 3. 使用 sync.WaitGroup 安全等待 ===")
+ var wg sync.WaitGroup
+
+ for i := 1; i <= 3; i++ {
+ wg.Add(1)
+ go func(id int) {
+ defer wg.Done()
+ fmt.Printf("Worker %d started\n", id)
+ time.Sleep(time.Duration(id*200) * time.Millisecond)
+ fmt.Printf("Worker %d finished\n", id)
+ }(i)
+ }
+
+ wg.Wait() // 阻塞直到所有 goroutine 完成
+ fmt.Println("All workers done!")
+
+ fmt.Println("\n=== 4. 查看当前 GOMAXPROCS 和 CPU 核心数 ===")
+ fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())
+ fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0)) // 0 表示不修改,仅查询
+
+ fmt.Println("\n=== 5. 手动设置 GOMAXPROCS(通常不需要)===")
+ old := runtime.GOMAXPROCS(2)
+ fmt.Printf("旧 GOMAXPROCS: %d, 新设为: 2\n", old)
+ runtime.GOMAXPROCS(old) // 恢复原值
+
+ fmt.Println("\n=== 6. 观察 Goroutine 数量变化 ===")
+ fmt.Printf("启动前 Goroutine 数: %d\n", runtime.NumGoroutine())
+
+ var wg2 sync.WaitGroup
+ for i := 0; i < 5; i++ {
+ wg2.Add(1)
+ go func(n int) {
+ defer wg2.Done()
+ time.Sleep(200 * time.Millisecond)
+ }(i)
+ }
+
+ time.Sleep(50 * time.Millisecond) // 让 goroutine 启动
+ fmt.Printf("启动后 Goroutine 数: %d\n", runtime.NumGoroutine())
+
+ wg2.Wait()
+ fmt.Printf("全部完成后 Goroutine 数: %d\n", runtime.NumGoroutine())
+}
+```
+
+保存并退出(`Ctrl+O` → Enter → `Ctrl+X`)。
+
+---
+
+## ✅ 步骤 3:格式化并运行
+
+```bash
+go fmt
+go run .
+```
+
+### 预期输出(顺序可能略有不同):
+
+```
+=== 1. 单线程顺序执行 ===
+[1] Hello: step 0
+[1] Hello: step 1
+[1] Hello: step 2
+[2] World: step 0
+[2] World: step 1
+[2] World: step 2
+
+=== 2. 使用 Goroutine 并发执行 ===
+[Goroutine-A] step 0
+[Goroutine-B] step 0
+[Goroutine-A] step 1
+[Goroutine-B] step 1
+[Goroutine-A] step 2
+[Goroutine-B] step 2
+
+=== 3. 使用 sync.WaitGroup 安全等待 ===
+Worker 1 started
+Worker 2 started
+Worker 3 started
+Worker 1 finished
+Worker 2 finished
+Worker 3 finished
+All workers done!
+
+=== 4. 查看当前 GOMAXPROCS 和 CPU 核心数 ===
+CPU 核心数: 8
+GOMAXPROCS: 8
+
+=== 5. 手动设置 GOMAXPROCS(通常不需要)===
+旧 GOMAXPROCS: 8, 新设为: 2
+
+=== 6. 观察 Goroutine 数量变化 ===
+启动前 Goroutine 数: 1
+启动后 Goroutine 数: 6
+全部完成后 Goroutine 数: 1
+```
+
+> 💡 注意:Goroutine 输出顺序是**不确定的**,这正是并发的体现!
+
+---
+
+## 🔍 核心概念解析:GPM 调度模型
+
+Go 的调度器采用 **GPM 模型**,三者含义如下:
+
+| 组件 | 全称 | 作用 |
+|------|------|------|
+| **G** | Goroutine | 用户级轻量协程,由 Go 运行时管理 |
+| **P** | Processor | 逻辑处理器,持有 G 的本地队列,数量 = `GOMAXPROCS` |
+| **M** | Machine | 操作系统线程,真正执行代码的实体 |
+
+### 调度流程简述:
+1. 每个 **P** 绑定一个 **M**(OS 线程)
+2. **G** 被分配到某个 **P** 的本地队列
+3. **M** 执行 **P** 上的 **G**
+4. 若 **G** 阻塞(如 I/O),**M** 会与 **P** 解绑,**P** 可被其他 **M** 接管,保证 CPU 不空闲
+
+> ✅ **优势**:
+> - Goroutine 创建成本极低(初始栈仅 2KB)
+> - 即使百万级 Goroutine,也能高效调度
+> - 阻塞不会阻塞 OS 线程(网络 I/O 由 netpoller 处理)
+
+---
+
+## 🧪 实验建议(在 GoLand 中)
+
+1. **查看 Goroutine 堆栈**:
+ 在调试模式下,点击 **Goroutines** 面板,实时查看所有 goroutine 状态。
+
+2. **修改 GOMAXPROCS**:
+ 尝试 `runtime.GOMAXPROCS(1)`,观察并发是否变成“伪并发”(仍能切换,但只用 1 个 CPU)。
+
+3. **制造阻塞**:
+ 在 goroutine 中加入 `time.Sleep` 或 `http.Get`,观察调度器如何复用 M。
+
+---
+
+## ⚠️ 常见误区提醒
+
+| 误区 | 正确理解 |
+|------|--------|
+| “Goroutine = OS 线程” | ❌ Goroutine 是用户态协程,由 Go 调度器在少量 OS 线程上复用 |
+| “必须设置 GOMAXPROCS” | ❌ Go 1.5+ 默认等于 CPU 核心数,通常无需手动设置 |
+| “Goroutine 会自动等待” | ❌ 主 goroutine 退出,程序立即终止!必须用 `WaitGroup`、channel 或 `time.Sleep` 等待 |
+
+---
+
+## 📚 延伸学习方向
+
+- 使用 `go tool trace` 可视化调度行为
+- 学习 **channel** 实现 goroutine 通信(避免共享内存)
+- 理解 **context** 如何优雅取消 goroutine
+
+---
+
+如果你希望我接下来带你实现 **channel 通信、select 多路复用、或 worker pool 模式**,欢迎随时告诉我!祝你轻松掌握 Go 并发编程 🚀
\ No newline at end of file
diff --git a/src/programming/backend/go/go基础入门.md b/src/programming/backend/go/go基础入门.md
new file mode 100644
index 0000000..ce510a3
--- /dev/null
+++ b/src/programming/backend/go/go基础入门.md
@@ -0,0 +1,643 @@
+---
+icon: mdi:language-go
+date: 2025-05-22
+category:
+ - 后端开发
+ - Go语言
+tag:
+ - Go
+ - 编程语言
+ - 入门教程
+star: true
+---
+
+# Go 语言基础入门
+
+Go(又称 Golang)是由 Google 开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。它于 2009 年首次发布,以其简洁、高效和强大的并发处理能力而闻名。
+
+## 目录
+1. [Go 语言简介](#go-语言简介)
+2. [环境搭建](#环境搭建)
+3. [第一个 Go 程序](#第一个-go-程序)
+4. [基本语法](#基本语法)
+5. [数据类型](#数据类型)
+6. [变量和常量](#变量和常量)
+7. [运算符](#运算符)
+8. [控制结构](#控制结构)
+9. [函数](#函数)
+10. [数组和切片](#数组和切片)
+11. [映射(Map)](#映射map)
+12. [结构体](#结构体)
+13. [接口](#接口)
+14. [并发编程](#并发编程)
+15. [错误处理](#错误处理)
+16. [包管理](#包管理)
+
+## Go 语言简介
+
+Go 语言的设计目标是:
+- 简单易学:语法简洁,去除了许多其他语言的复杂特性
+- 高效执行:编译型语言,运行速度快
+- 内置并发支持:通过 goroutine 和 channel 实现轻量级并发
+- 强大的标准库:提供了丰富的内置库
+- 跨平台支持:支持多种操作系统和架构
+
+## 环境搭建
+
+### 下载安装
+
+1. 访问 [Go 官方网站](https://golang.org/dl/) 下载适合你操作系统的安装包
+2. Windows 用户下载 `.msi` 文件,macOS 用户下载 `.pkg` 文件,Linux 用户下载 `.tar.gz` 文件
+
+### Windows 安装步骤
+1. 双击下载的 `.msi` 文件开始安装
+2. 默认会安装到 `C:\Go\` 目录下
+3. 安装完成后会自动配置环境变量
+
+### Linux 安装步骤
+```bash
+# 下载 Go 安装包
+wget https://golang.org/dl/go1.19.linux-amd64.tar.gz
+
+# 解压到 /usr/local 目录
+sudo tar -C /usr/local -xzf go1.19.linux-amd64.tar.gz
+
+# 配置环境变量
+echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
+source ~/.bashrc
+```
+
+### 验证安装
+打开终端或命令提示符,输入以下命令验证是否安装成功:
+```bash
+go version
+```
+如果显示版本信息,则说明安装成功。
+
+### 设置工作目录
+Go 项目推荐使用模块化管理,在任意目录下创建你的项目文件夹:
+```bash
+mkdir myproject
+cd myproject
+go mod init myproject
+```
+
+## 第一个 Go 程序
+
+创建一个名为 `main.go` 的文件:
+
+```go
+package main
+
+import "fmt"
+
+func main() {
+ fmt.Println("Hello, World!")
+}
+```
+
+运行程序:
+```bash
+go run main.go
+```
+
+或者编译后运行:
+```bash
+go build main.go
+./main
+```
+
+代码解释:
+- `package main`:声明这是一个可执行程序的主包
+- `import "fmt"`:导入格式化输入输出包
+- `func main()`:程序入口函数
+- `fmt.Println()`:打印文本并换行
+
+## 基本语法
+
+### 注释
+```go
+// 单行注释
+
+/*
+多行注释
+可以跨越多行
+*/
+```
+
+### 标识符命名规则
+- 只能包含字母、数字和下划线
+- 不能以数字开头
+- 区分大小写
+- 不能是关键字
+
+### 关键字
+Go 语言有 25 个关键字:
+```
+break default func interface select
+case defer go map struct
+chan else goto package switch
+const fallthrough if range type
+continue for import return var
+```
+
+## 数据类型
+
+Go 语言的数据类型分为四大类:
+
+### 基本类型
+1. **布尔型**:bool(true/false)
+2. **数字类型**:
+ - 整数型:int8, int16, int32, int64, uint8, uint16, uint32, uint64, int, uint, uintptr
+ - 浮点型:float32, float64
+ - 复数型:complex64, complex128
+3. **字符串类型**:string
+4. **派生类型**:
+ - 指针类型(Pointer)
+ - 数组类型
+ - 结构化类型(struct)
+ - Channel 类型
+ - 函数类型
+ - 切片类型
+ - 接口类型(interface)
+ - Map 类型
+
+### 类型示例
+```go
+var a bool = true // 布尔型
+var b int = 10 // 整型
+var c float32 = 3.14 // 浮点型
+var d string = "Hello" // 字符串
+var e complex64 = 3+4i // 复数型
+```
+
+## 变量和常量
+
+### 变量声明
+
+四种声明变量的方式:
+
+```go
+// 1. 指定变量类型,声明后若不赋值则使用默认值
+var v1 int
+
+// 2. 根据值自行判定变量类型
+var v2 = 10
+
+// 3. 省略 var,注意 := 左侧的变量必须是未声明过的
+v3 := 10
+
+// 4. 多变量声明
+var v4, v5 int = 1, 2
+var v6, v7 = 3, "hello"
+v8, v9 := 5, true
+```
+
+### 变量作用域
+- **局部变量**:在函数内声明,只在函数内有效
+- **全局变量**:在函数外声明,在整个包内有效
+
+### 常量声明
+```go
+const PI = 3.14159
+const NAME = "Golang"
+
+// 常量组
+const (
+ Monday = 1
+ Tuesday = 2
+ Wednesday = 3
+)
+```
+
+### iota
+iota 是一个特殊的常量,用于生成一组相似的常量值:
+```go
+const (
+ a = iota // 0
+ b // 1
+ c // 2
+ d // 3
+)
+```
+
+## 运算符
+
+### 算术运算符
+| 运算符 | 描述 |
+|--------|------|
+| + | 相加 |
+| - | 相减 |
+| * | 相乘 |
+| / | 相除 |
+| % | 求余 |
+| ++ | 自增 |
+| -- | 自减 |
+
+### 关系运算符
+| 运算符 | 描述 |
+|--------|------|
+| == | 相等 |
+| != | 不相等 |
+| > | 大于 |
+| < | 小于 |
+| >= | 大于等于 |
+| <= | 小于等于 |
+
+### 逻辑运算符
+| 运算符 | 描述 |
+|--------|------|
+| && | 逻辑与 |
+| \|\| | 逻辑或 |
+| ! | 逻辑非 |
+
+### 位运算符
+| 运算符 | 描述 |
+|--------|------|
+| & | 按位与 |
+| \| | 按位或 |
+| ^ | 按位异或 |
+| << | 左移 |
+| >> | 右移 |
+
+## 控制结构
+
+### 条件语句 if
+
+```go
+if condition {
+ // code
+}
+
+// 带初始化语句
+if x := 10; x > 5 {
+ fmt.Println("x 大于 5")
+} else {
+ fmt.Println("x 小于等于 5")
+}
+```
+
+### 循环语句 for
+
+Go 只有一种循环结构:for 循环
+
+```go
+// 基本 for 循环
+for i := 0; i < 10; i++ {
+ fmt.Println(i)
+}
+
+// while 形式的循环
+i := 0
+for i < 10 {
+ fmt.Println(i)
+ i++
+}
+
+// 无限循环
+for {
+ // 死循环
+}
+```
+
+### 选择语句 switch
+
+```go
+switch day {
+case 1:
+ fmt.Println("Monday")
+case 2:
+ fmt.Println("Tuesday")
+default:
+ fmt.Println("Unknown day")
+}
+
+// 不带条件的 switch
+switch {
+case score >= 90:
+ fmt.Println("优秀")
+case score >= 80:
+ fmt.Println("良好")
+default:
+ fmt.Println("一般")
+}
+```
+
+## 函数
+
+### 函数定义
+```go
+func functionName(parameterName type) returnType {
+ // 函数体
+ return returnValue
+}
+```
+
+### 示例
+```go
+// 简单函数
+func add(a int, b int) int {
+ return a + b
+}
+
+// 多返回值函数
+func swap(x, y string) (string, string) {
+ return y, x
+}
+
+// 可变参数函数
+func sum(nums ...int) int {
+ total := 0
+ for _, num := range nums {
+ total += num
+ }
+ return total
+}
+```
+
+### 函数调用
+```go
+result := add(3, 5)
+a, b := swap("hello", "world")
+total := sum(1, 2, 3, 4, 5)
+```
+
+### defer 语句
+defer 用于延迟执行函数,通常用于资源清理:
+```go
+func readFile() {
+ file, err := os.Open("file.txt")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer file.Close() // 函数结束前关闭文件
+
+ // 处理文件...
+}
+```
+
+## 数组和切片
+
+### 数组
+数组是固定长度的序列:
+```go
+// 声明数组
+var arr1 [5]int // 长度为 5 的整型数组
+arr2 := [5]int{1, 2, 3, 4, 5} // 初始化数组
+arr3 := [...]int{1, 2, 3} // 根据元素个数确定长度
+
+// 访问数组元素
+fmt.Println(arr2[0]) // 输出 1
+arr2[1] = 10 // 修改元素
+```
+
+### 切片
+切片是对数组的抽象,长度可变:
+```go
+// 创建切片
+slice1 := []int{1, 2, 3} // 直接创建切片
+slice2 := make([]int, 5) // 创建长度为 5 的切片
+slice3 := make([]int, 3, 5) // 长度为 3,容量为 5
+
+// 从数组创建切片
+arr := [5]int{1, 2, 3, 4, 5}
+slice4 := arr[1:4] // 包含索引 1 到 3 的元素
+
+// 切片操作
+slice5 := append(slice1, 4, 5) // 添加元素
+len(slice1) // 获取长度
+cap(slice1) // 获取容量
+```
+
+## 映射(Map)
+
+Map 是一种无序的键值对集合:
+
+```go
+// 创建 map
+var m1 map[string]int // 声明但未初始化
+m2 := make(map[string]int) // 使用 make 初始化
+m3 := map[string]int{ // 直接初始化
+ "apple": 5,
+ "banana": 3,
+}
+
+// 操作 map
+m2["orange"] = 10 // 添加键值对
+value := m2["orange"] // 获取值
+delete(m2, "orange") // 删除键值对
+
+// 检查键是否存在
+if val, ok := m2["apple"]; ok {
+ fmt.Println("apple:", val)
+} else {
+ fmt.Println("apple not found")
+}
+```
+
+## 结构体
+
+结构体是一种用户自定义的数据类型,允许我们组合不同类型的字段:
+
+```go
+// 定义结构体
+type Person struct {
+ Name string
+ Age int
+ Email string
+}
+
+// 创建结构体实例
+person1 := Person{Name: "张三", Age: 25, Email: "zhangsan@example.com"}
+person2 := Person{"李四", 30, "lisi@example.com"}
+person3 := Person{} // 所有字段使用零值
+
+// 访问结构体字段
+fmt.Println(person1.Name)
+person1.Age = 26
+
+// 结构体指针
+person4 := &Person{Name: "王五", Age: 28}
+fmt.Println(person4.Name) // 自动解引用
+```
+
+### 方法
+方法是带有接收者的函数:
+
+```go
+// 为结构体定义方法
+func (p Person) SayHello() {
+ fmt.Printf("Hello, I'm %s\n", p.Name)
+}
+
+func (p *Person) SetAge(age int) {
+ p.Age = age
+}
+
+// 调用方法
+person1.SayHello()
+person1.SetAge(27)
+```
+
+## 接口
+
+接口是一组方法签名的集合:
+
+```go
+// 定义接口
+type Shape interface {
+ Area() float64
+ Perimeter() float64
+}
+
+// 实现接口
+type Rectangle struct {
+ Width, Height float64
+}
+
+func (r Rectangle) Area() float64 {
+ return r.Width * r.Height
+}
+
+func (r Rectangle) Perimeter() float64 {
+ return 2 * (r.Width + r.Height)
+}
+
+// 使用接口
+var s Shape = Rectangle{Width: 10, Height: 5}
+fmt.Println("Area:", s.Area())
+fmt.Println("Perimeter:", s.Perimeter())
+```
+
+## 并发编程
+
+Go 语言内置了强大的并发支持,主要包括 goroutine 和 channel。
+
+### Goroutine
+Goroutine 是轻量级线程:
+
+```go
+// 启动 goroutine
+go func() {
+ fmt.Println("Hello from goroutine")
+}()
+
+// 启动多个 goroutine
+for i := 0; i < 5; i++ {
+ go func(n int) {
+ fmt.Printf("Goroutine %d\n", n)
+ }(i)
+}
+```
+
+### Channel
+Channel 用于 goroutine 之间通信:
+
+```go
+// 创建 channel
+ch := make(chan int)
+
+// 发送数据到 channel
+go func() {
+ ch <- 42
+}()
+
+// 从 channel 接收数据
+value := <-ch
+fmt.Println(value)
+
+// 带缓冲的 channel
+bufferedCh := make(chan int, 3)
+bufferedCh <- 1
+bufferedCh <- 2
+bufferedCh <- 3
+```
+
+### Select
+Select 用于在多个 channel 操作中进行选择:
+
+```go
+select {
+case msg1 := <-ch1:
+ fmt.Println("Received", msg1)
+case msg2 := <-ch2:
+ fmt.Println("Received", msg2)
+case <-time.After(time.Second):
+ fmt.Println("Timeout")
+}
+```
+
+## 错误处理
+
+Go 语言通过返回错误值来进行错误处理:
+
+```go
+// 错误处理示例
+func divide(a, b float64) (float64, error) {
+ if b == 0 {
+ return 0, errors.New("division by zero")
+ }
+ return a / b, nil
+}
+
+// 使用错误
+result, err := divide(10, 0)
+if err != nil {
+ fmt.Println("Error:", err)
+} else {
+ fmt.Println("Result:", result)
+}
+```
+
+### 自定义错误
+```go
+type MyError struct {
+ Msg string
+ Code int
+}
+
+func (e *MyError) Error() string {
+ return fmt.Sprintf("Error %d: %s", e.Code, e.Msg)
+}
+```
+
+## 包管理
+
+### 创建模块
+```bash
+go mod init myproject
+```
+
+### 添加依赖
+```bash
+go get github.com/gin-gonic/gin
+```
+
+### 查看依赖
+```bash
+go mod tidy
+go list -m all
+```
+
+### go.mod 文件示例
+```
+module myproject
+
+go 1.19
+
+require (
+ github.com/gin-gonic/gin v1.8.1
+)
+```
+
+## 总结
+
+Go 语言以其简洁的语法、强大的并发支持和高效的执行性能成为现代软件开发的重要选择。通过本文档的学习,你应该掌握了 Go 语言的基本概念和语法,可以开始编写简单的 Go 程序了。
+
+建议接下来的学习路径:
+1. 练习更多 Go 语言编程题目
+2. 学习标准库的使用
+3. 了解 Go 语言的最佳实践
+4. 尝试构建实际项目
+
+Happy coding with Go!
\ No newline at end of file
diff --git a/src/programming/backend/java/AI试题/20250522.md b/src/programming/backend/java/AI试题/20250522.md
new file mode 100644
index 0000000..a02bc30
--- /dev/null
+++ b/src/programming/backend/java/AI试题/20250522.md
@@ -0,0 +1,168 @@
+---
+icon: mdi:clipboard-text
+date: 2025-05-22
+title: "20250522"
+category:
+ - Java
+tag:
+ - 面试题
+---
+
+
+
+
+
+### 选择题
+1. 以下关于 Java 中异常处理的说法,正确的是( )
+ A. try 块后必须跟 catch 块
+ B. try 块后可以不跟任何 catch 块,但必须跟 finally 块
+ C. 一个 try 块可以有多个 catch 块,且异常捕获顺序无关紧要
+ D. finally 块中的代码无论是否发生异常都会执行
+
+2. 下列关于 Java 多线程的说法,错误的是( )
+ A. 创建线程可以通过继承 Thread 类或实现 Runnable 接口
+ B. 线程的 start() 方法会调用 run() 方法
+ C. 多个线程可以同时访问同一个共享资源而不会产生任何问题
+ D. 可以使用 synchronized 关键字来实现线程同步
+
+### 填空题
+1. Java 中用于创建对象的关键字是__________。
+2. Java 中实现多态的两种方式是__________和__________。
+
+### 简答题
+1. 请简述 Java 中的垃圾回收机制及其作用。
+2. 简述 Spring 框架中 IoC(控制反转)和 DI(依赖注入)的概念及它们之间的关系。
+
+### 编程题
+1. 编写一个 Java 程序,实现一个简单的栈(Stack)数据结构,包含入栈(push)、出栈(pop)和查看栈顶元素(peek)的方法。
+
+### 场景题
+1. 在一个分布式系统中,有多个服务需要共享缓存数据。当缓存数据更新时,如何确保各个服务能及时获取到最新的缓存数据?请描述你的解决方案。
+
+### 论述题
+1. 论述 Java 中微服务架构的优势和挑战,并结合实际案例进行说明。
+
+### 综合题
+1. 假设你正在开发一个电商系统,该系统包含商品管理、订单管理和用户管理三个模块。请设计一个 Java 程序架构,说明各个模块之间的关系以及如何使用 Java 技术实现它们之间的交互。
+
+### 选择题答案
+1. **答案**:D
+ **解析**:A选项,try块后可以不跟catch块,但必须跟finally块或者catch块,所以A错误;B选项,try块后可以既不跟catch块也不跟finally块,不过这样就失去了异常处理的意义,所以B错误;C选项,一个try块可以有多个catch块,但异常捕获顺序是有要求的,子类异常的catch块要放在父类异常的catch块之前,所以C错误;D选项,finally块中的代码无论是否发生异常都会执行,这是finally块的特性,所以D正确。
+
+2. **答案**:C
+ **解析**:A选项,在Java中创建线程可以通过继承Thread类或实现Runnable接口,这是常见的创建线程的方式,所以A正确;B选项,线程的start()方法会启动线程,然后会调用run()方法执行线程的任务,所以B正确;C选项,多个线程同时访问同一个共享资源时,如果没有进行适当的同步控制,会产生数据不一致等问题,也就是线程安全问题,所以C错误;D选项,synchronized关键字可以用来实现线程同步,保证同一时间只有一个线程可以访问被synchronized修饰的代码块或方法,所以D正确。
+
+### 填空题答案
+1. **答案**:new
+ **解析**:在Java中,使用new关键字来创建对象。例如`Object obj = new Object();`,这里的new关键字会在堆内存中为对象分配空间,并调用对象的构造方法进行初始化。
+2. **答案**:方法重载;方法重写
+ **解析**:方法重载是指在同一个类中,有多个方法具有相同的方法名,但参数列表不同(参数的类型、个数或顺序不同)。通过方法重载,我们可以根据不同的参数来调用不同的方法,这是实现多态的一种方式。方法重写是指子类重写父类的方法,要求方法名、参数列表和返回值类型都相同。在运行时,根据对象的实际类型来调用相应的方法,这也是实现多态的重要方式。
+
+### 简答题答案
+1. **答案**:Java 中的垃圾回收机制是 Java 虚拟机(JVM)提供的一种自动内存管理机制。它会自动检测和回收那些不再被引用的对象所占用的内存空间。其作用主要有:一是减轻程序员的负担,程序员不需要手动释放对象的内存,避免了因忘记释放内存而导致的内存泄漏问题;二是提高内存的使用效率,及时回收不再使用的内存,使得内存可以被其他对象使用。
+ **解析**:垃圾回收机制的核心是通过垃圾回收器(GC)来实现的。GC 会定期或在内存不足时启动,它会遍历对象的引用关系,找出那些没有被任何引用指向的对象,将这些对象标记为垃圾对象,然后回收它们所占用的内存。在 Java 中,对象的生命周期由 JVM 自动管理,程序员只需要关注对象的创建和使用,而不需要关心对象的销毁,这大大提高了开发效率和程序的稳定性。
+
+2. **答案**:IoC(控制反转)是一种设计思想,它将对象的创建和依赖关系的管理从代码中转移到外部容器中。在传统的编程方式中,对象的创建和依赖关系是在代码中硬编码的,而在 IoC 模式下,对象的创建和依赖关系的配置由外部容器负责。DI(依赖注入)是 IoC 的一种具体实现方式,它通过外部容器将对象所依赖的其他对象注入到该对象中。IoC 和 DI 的关系是:DI 是实现 IoC 的一种手段,通过 DI 可以更好地实现 IoC 的思想。
+ **解析**:在 Spring 框架中,IoC 容器(如 ApplicationContext)负责管理对象的创建和依赖关系。通过配置文件(如 XML 配置文件)或注解(如 @Autowired),可以将对象之间的依赖关系注入到相应的对象中。例如,一个类 A 依赖于类 B,在传统编程中,类 A 会在自己的代码中创建类 B 的实例,而在 Spring 中,类 A 只需要声明对类 B 的依赖,Spring 容器会自动将类 B 的实例注入到类 A 中。这样可以降低代码的耦合度,提高代码的可维护性和可测试性。
+
+### 编程题答案
+1. **示例代码**:
+```java
+import java.util.EmptyStackException;
+
+// 自定义栈类
+class MyStack {
+ private int[] stack;
+ private int top;
+ private int capacity;
+
+ // 构造函数,初始化栈的容量
+ public MyStack(int capacity) {
+ this.capacity = capacity;
+ this.stack = new int[capacity];
+ this.top = -1;
+ }
+
+ // 入栈操作
+ public void push(int item) {
+ if (top == capacity - 1) {
+ throw new StackOverflowError("Stack is full");
+ }
+ stack[++top] = item;
+ }
+
+ // 出栈操作
+ public int pop() {
+ if (top == -1) {
+ throw new EmptyStackException();
+ }
+ return stack[top--];
+ }
+
+ // 查看栈顶元素
+ public int peek() {
+ if (top == -1) {
+ throw new EmptyStackException();
+ }
+ return stack[top];
+ }
+
+ // 判断栈是否为空
+ public boolean isEmpty() {
+ return top == -1;
+ }
+}
+
+public class StackExample {
+ public static void main(String[] args) {
+ MyStack stack = new MyStack(5);
+ stack.push(1);
+ stack.push(2);
+ stack.push(3);
+ System.out.println("Top element: " + stack.peek());
+ System.out.println("Popped element: " + stack.pop());
+ System.out.println("Top element after pop: " + stack.peek());
+ }
+}
+```
+**解析**:首先定义了一个`MyStack`类,它包含一个整数数组`stack`用于存储栈中的元素,`top`变量表示栈顶的位置,`capacity`表示栈的容量。构造函数`MyStack(int capacity)`用于初始化栈的容量和数组。`push(int item)`方法用于将元素入栈,如果栈已满则抛出`StackOverflowError`异常。`pop()`方法用于将栈顶元素出栈,如果栈为空则抛出`EmptyStackException`异常。`peek()`方法用于查看栈顶元素,如果栈为空则抛出`EmptyStackException`异常。`isEmpty()`方法用于判断栈是否为空。在`main`方法中,创建了一个栈对象,并进行了入栈、查看栈顶元素和出栈等操作。
+
+### 场景题答案
+1. **答案**:可以采用以下解决方案来确保各个服务能及时获取到最新的缓存数据:
+ - 使用消息队列:当缓存数据更新时,更新服务向消息队列发送一条消息,各个需要使用缓存数据的服务订阅该消息队列。当接收到消息后,这些服务会主动去获取最新的缓存数据。
+ - 缓存失效机制:更新服务在更新缓存数据时,同时将旧的缓存数据标记为失效。各个服务在访问缓存时,如果发现缓存数据失效,会重新从数据源获取最新的数据并更新缓存。
+ - 定时刷新:各个服务设置定时任务,定期从缓存中获取最新的数据。这种方式可以保证在一定时间内各个服务能获取到最新的缓存数据,但可能存在一定的延迟。
+ - 缓存更新通知:更新服务在更新缓存数据后,直接向各个服务发送更新通知,各个服务接收到通知后,立即去获取最新的缓存数据。
+
+ **解析**:使用消息队列的好处是解耦了缓存更新和服务获取数据的过程,提高了系统的可扩展性和可靠性。缓存失效机制可以确保服务在访问缓存时能获取到最新的数据,但需要注意失效标记的管理。定时刷新的方式简单易行,但可能会造成不必要的资源浪费和数据延迟。缓存更新通知可以实时通知各个服务,但需要确保通知的可靠性和及时性。在实际应用中,可以根据系统的特点和需求选择合适的解决方案,也可以将多种方案结合使用。
+
+### 论述题答案
+1. **答案**:Java 中微服务架构的优势主要有:
+ - 可扩展性:微服务架构将一个大型应用拆分成多个小型的、自治的服务,每个服务可以独立进行扩展。例如,一个电商系统中的商品服务和订单服务可以根据各自的业务需求进行独立的水平扩展,提高系统的性能和处理能力。
+ - 可维护性:每个微服务都有自己独立的代码库和开发团队,开发人员可以更专注于自己负责的服务,降低了代码的复杂度,提高了代码的可维护性。例如,当需要对商品服务进行修改时,不会影响到其他服务的正常运行。
+ - 技术多样性:不同的微服务可以使用不同的技术栈来实现,根据服务的特点选择最合适的技术。例如,对于实时性要求较高的订单服务可以使用 Java 的高性能框架,而对于数据处理和分析的服务可以使用 Python 等语言。
+ - 快速部署:微服务可以独立部署,当一个服务发生变化时,只需要部署该服务即可,不需要重新部署整个应用,提高了开发和部署的效率。
+
+ 微服务架构的挑战主要有:
+ - 服务间通信:微服务之间需要进行通信,这增加了系统的复杂性。例如,需要处理网络延迟、服务故障等问题。可以使用 RESTful API、消息队列等方式来实现服务间的通信。
+ - 服务管理:随着微服务数量的增加,服务的管理变得更加困难。需要使用服务注册与发现、配置管理等工具来管理微服务。
+ - 分布式事务:在微服务架构中,一个业务操作可能涉及多个服务,需要处理分布式事务的问题。可以使用两阶段提交、补偿事务等方式来解决分布式事务问题。
+
+ 实际案例:Netflix 是一个典型的使用微服务架构的公司。它将视频流服务拆分成多个微服务,如用户认证服务、视频播放服务、推荐服务等。每个服务可以独立开发、部署和扩展,提高了系统的性能和可维护性。同时,Netflix 也面临着服务间通信、服务管理等挑战,它使用了 Eureka 进行服务注册与发现,使用 Hystrix 进行服务熔断和降级,解决了微服务架构中的一些问题。
+ **解析**:微服务架构的优势在于它能够更好地适应现代软件开发的需求,提高系统的灵活性和可扩展性。但同时也带来了一些挑战,需要开发人员掌握更多的技术和工具来解决这些问题。通过实际案例可以更直观地看到微服务架构的应用和解决问题的方法。
+
+### 综合题答案
+1. **答案**:以下是一个电商系统的 Java 程序架构设计:
+
+ **模块关系**:
+ - 用户管理模块负责用户的注册、登录、信息管理等功能。用户管理模块为商品管理模块和订单管理模块提供用户信息,例如在商品浏览和下单时需要验证用户的身份。
+ - 商品管理模块负责商品的添加、修改、删除和查询等功能。商品管理模块为订单管理模块提供商品信息,订单管理模块根据商品信息生成订单。
+ - 订单管理模块负责订单的创建、支付、发货、退款等功能。订单管理模块会更新商品的库存信息,同时会记录用户的订单历史。
+
+ **实现方式**:
+ - **分层架构**:采用 MVC(Model - View - Controller)或 MVVM(Model - View - ViewModel)分层架构,将业务逻辑、数据访问和视图展示分离。例如,在商品管理模块中,Controller 层负责接收用户的请求,Service 层负责处理业务逻辑,Dao 层负责与数据库进行交互。
+ - **数据库设计**:使用关系型数据库(如 MySQL)来存储用户信息、商品信息和订单信息。不同模块对应不同的数据库表,通过外键关联来建立表之间的关系。例如,订单表中会有用户 ID 和商品 ID 字段,分别关联用户表和商品表。
+ - **服务接口**:各个模块之间通过服务接口进行交互。可以使用 RESTful API 来实现服务接口,提高系统的可扩展性和兼容性。例如,订单管理模块可以通过调用商品管理模块的 API 来获取商品信息。
+ - **消息队列**:使用消息队列(如 Kafka 或 RabbitMQ)来实现模块之间的异步通信。例如,当订单创建成功后,订单管理模块可以向消息队列发送一条消息,商品管理模块订阅该消息,接收到消息后更新商品的库存信息。
+
+ **解析**:通过分层架构可以将不同的功能模块分离,提高代码的可维护性和可测试性。数据库设计是系统的基础,合理的数据库表结构可以提高数据的存储和查询效率。服务接口的使用可以实现模块之间的解耦,使得各个模块可以独立开发和部署。消息队列的引入可以实现模块之间的异步通信,提高系统的性能和可靠性。这种架构设计可以满足电商系统的高并发、高可用性和可扩展性的需求。
\ No newline at end of file
diff --git a/src/programming/backend/java/AI试题/20250523.md b/src/programming/backend/java/AI试题/20250523.md
new file mode 100644
index 0000000..b0924e9
--- /dev/null
+++ b/src/programming/backend/java/AI试题/20250523.md
@@ -0,0 +1,136 @@
+---
+icon: mdi:clipboard-text
+title: "20250523"
+date: 2025-05-23
+category:
+ - JAVA
+tag:
+ - 试题
+---
+
+2025-05-23 AI试题
+
+
+### 选择题
+1. 以下关于 Java 类的构造方法,说法正确的是( )
+ A. 构造方法必须有返回值类型
+ B. 一个类只能有一个构造方法
+ C. 构造方法的名称必须与类名相同
+ D. 构造方法不能使用访问修饰符
+
+2. 下列关于 Java 中 String 类的描述,错误的是( )
+ A. String 类是不可变类,一旦创建,其值不能被修改
+ B. 可以使用“+”运算符来连接两个 String 对象
+ C. String 类的对象存储在栈内存中
+ D. String 类提供了很多方法,如 equals() 用于比较两个字符串的内容是否相等
+
+### 填空题
+1. Java 中用于实现线程同步的关键字除了 synchronized 外,还有__________。
+2. 在 Java 集合框架中,__________是一个有序的、可重复的集合接口。
+
+### 简答题
+1. 简述 Java 中反射机制的概念和用途。
+2. 请说明 Java 中抽象类和接口的区别。
+
+### 编程题
+1. 编写一个 Java 程序,实现对一个整数数组进行排序,并输出排序后的数组。要求使用冒泡排序算法。
+
+### 场景题
+1. 在一个 Java Web 项目中,用户登录后,需要将用户信息存储在会话(Session)中。当用户访问其他页面时,需要验证用户是否已经登录。请描述实现该功能的步骤。
+
+### 论述题
+1. 论述 Java 中性能优化的主要方面和常用方法,并结合实际案例进行说明。
+
+### 综合题
+1. 设计一个 Java 程序,模拟一个图书馆管理系统。该系统包含图书管理、读者管理和借阅管理三个模块。图书管理模块负责图书的添加、删除和查询;读者管理模块负责读者的注册、注销和信息查询;借阅管理模块负责图书的借阅和归还操作。请说明各个模块之间的关系以及如何实现它们之间的交互。
+
+### 选择题答案
+1. **答案**:C
+ **解析**:A选项,构造方法没有返回值类型,连 void 都不能写,所以 A 错误;B选项,一个类可以有多个构造方法,这就是构造方法的重载,所以 B 错误;C选项,构造方法的名称必须与类名相同,这是构造方法的基本定义,所以 C 正确;D选项,构造方法可以使用访问修饰符,如 public、private 等,所以 D 错误。
+2. **答案**:C
+ **解析**:A选项,String 类是不可变类,一旦创建,其值不能被修改,如果对 String 对象进行修改操作,实际上是创建了一个新的 String 对象,所以 A 正确;B选项,“+”运算符可以用于连接两个 String 对象,这是 Java 中常用的字符串连接方式,所以 B 正确;C选项,String 类的对象存储在堆内存中,而不是栈内存,所以 C 错误;D选项,String 类提供了很多方法,equals() 方法用于比较两个字符串的内容是否相等,而不是比较引用是否相等,所以 D 正确。
+
+### 填空题答案
+1. **答案**:Lock
+ **解析**:在 Java 中,除了使用 synchronized 关键字实现线程同步外,还可以使用 Lock 接口及其实现类(如 ReentrantLock)来实现线程同步。Lock 提供了更灵活的锁机制,例如可以实现公平锁、可中断锁等。
+2. **答案**:List
+ **解析**:在 Java 集合框架中,List 是一个有序的、可重复的集合接口。常见的实现类有 ArrayList、LinkedList 等。List 中的元素按照插入的顺序排列,并且可以包含重复的元素。
+
+### 简答题答案
+1. **答案**:Java 中的反射机制是指在运行时动态地获取类的信息、创建对象、调用方法和访问属性等。其用途主要有:一是在框架开发中,如 Spring 框架通过反射机制实现依赖注入;二是在开发工具中,如 IDE 可以通过反射机制获取类的信息,提供代码提示等功能;三是在插件开发中,可以通过反射机制动态加载和调用插件类的方法。
+ **解析**:反射机制的核心是 Java 的 Class 类,通过 Class 类可以获取类的各种信息,如构造方法、方法、属性等。然后可以使用这些信息来创建对象、调用方法和访问属性。反射机制打破了传统的编译时绑定,使得程序在运行时可以更加灵活地处理类和对象。
+2. **答案**:Java 中抽象类和接口的区别主要有:
+ - 定义方式:抽象类使用 abstract 关键字修饰类,接口使用 interface 关键字定义。
+ - 成员变量:抽象类可以有普通成员变量,也可以有常量;接口中的成员变量默认都是 public static final 类型的常量。
+ - 方法:抽象类可以有抽象方法和非抽象方法;接口中的方法默认都是 public abstract 类型的抽象方法(Java 8 及以后支持默认方法和静态方法)。
+ - 继承和实现:一个类只能继承一个抽象类,但可以实现多个接口。
+ - 设计目的:抽象类是对一类事物的抽象,包含了一些通用的属性和方法;接口是对行为的抽象,定义了一组行为规范。
+ **解析**:抽象类和接口在 Java 中都用于实现多态和代码复用,但它们的设计目的和使用场景有所不同。抽象类更侧重于对类的抽象,而接口更侧重于对行为的抽象。在实际开发中,需要根据具体的需求来选择使用抽象类还是接口。
+
+### 编程题答案
+1. **示例代码**:
+```java
+public class BubbleSort {
+ public static void bubbleSort(int[] arr) {
+ int n = arr.length;
+ for (int i = 0; i < n - 1; i++) {
+ for (int j = 0; j < n - i - 1; j++) {
+ if (arr[j] > arr[j + 1]) {
+ // 交换 arr[j] 和 arr[j+1]
+ int temp = arr[j];
+ arr[j] = arr[j + 1];
+ arr[j + 1] = temp;
+ }
+ }
+ }
+ }
+
+ public static void main(String[] args) {
+ int[] arr = {64, 34, 25, 12, 22, 11, 90};
+ bubbleSort(arr);
+ System.out.println("排序后的数组:");
+ for (int num : arr) {
+ System.out.print(num + " ");
+ }
+ }
+}
+```
+**解析**:冒泡排序算法的基本思想是比较相邻的元素,如果顺序错误就把它们交换过来。外层循环控制排序的轮数,内层循环控制每一轮比较的次数。在每一轮中,将较大的元素逐步“冒泡”到数组的末尾。最后,数组就会按照从小到大的顺序排列。
+
+### 场景题答案
+1. **答案**:实现该功能的步骤如下:
+ - 用户登录验证:当用户提交登录信息时,在服务器端验证用户的用户名和密码是否正确。如果验证通过,将用户信息(如用户 ID、用户名等)存储在会话(Session)中。
+ - 会话管理:在用户访问其他页面时,首先获取当前会话对象。检查会话中是否包含用户信息,如果包含,则说明用户已经登录;如果不包含,则说明用户未登录,将用户重定向到登录页面。
+ - 会话过期处理:为了保证系统的安全性,需要设置会话的过期时间。当会话过期后,用户需要重新登录。
+
+ **解析**:在 Java Web 项目中,可以使用 HttpSession 对象来管理会话。通过 request.getSession() 方法可以获取当前会话对象。在登录验证通过后,使用 session.setAttribute() 方法将用户信息存储在会话中。在其他页面中,使用 session.getAttribute() 方法获取用户信息进行验证。同时,可以使用 session.setMaxInactiveInterval() 方法设置会话的过期时间。
+
+### 论述题答案
+1. **答案**:Java 中性能优化的主要方面和常用方法如下:
+ - **代码层面**:
+ - 避免创建过多的对象,尽量复用对象。例如,使用 StringBuilder 代替 String 进行字符串拼接,因为 String 是不可变类,每次拼接都会创建新的对象。
+ - 减少方法调用的开销,避免在循环中频繁调用方法。
+ - 合理使用数据结构,根据不同的需求选择合适的数据结构。例如,对于频繁查找操作,使用 HashMap 比使用 ArrayList 更高效。
+ - **内存管理层面**:
+ - 及时释放不再使用的对象,避免内存泄漏。例如,在使用完文件流、数据库连接等资源后,要及时关闭。
+ - 调整 JVM 的堆内存大小,根据应用程序的实际需求合理分配堆内存。
+ - **多线程层面**:
+ - 合理使用线程池,避免频繁创建和销毁线程。例如,使用 Executors 类创建线程池。
+ - 优化线程同步机制,减少锁的竞争。例如,使用读写锁代替普通的互斥锁。
+
+ 实际案例:一个电商系统在高并发场景下,用户下单时响应时间过长。通过性能优化,发现是由于在下单过程中频繁创建对象和数据库连接导致的。于是,使用对象池来复用对象,使用数据库连接池来管理数据库连接,同时调整了 JVM 的堆内存大小。经过优化后,系统的响应时间明显缩短,性能得到了显著提升。
+ **解析**:Java 性能优化需要从多个方面入手,包括代码的编写、内存的管理和多线程的使用等。通过合理的优化措施,可以提高系统的性能和响应速度,减少资源的消耗。实际案例可以帮助我们更好地理解性能优化的重要性和具体方法。
+
+### 综合题答案
+1. **答案**:
+ - **模块关系**:
+ - 读者管理模块为借阅管理模块提供读者信息,只有已注册的读者才能进行借阅和归还操作。
+ - 图书管理模块为借阅管理模块提供图书信息,借阅管理模块根据图书信息判断图书是否可借。
+ - 借阅管理模块会更新图书管理模块中的图书库存信息和读者管理模块中的读者借阅记录。
+ - **实现方式**:
+ - **数据模型**:设计数据库表来存储图书信息、读者信息和借阅记录。例如,图书表包含图书 ID、书名、作者、库存等字段;读者表包含读者 ID、姓名、联系方式等字段;借阅记录表包含借阅 ID、图书 ID、读者 ID、借阅时间、归还时间等字段。
+ - **分层架构**:采用 MVC 分层架构,将业务逻辑、数据访问和视图展示分离。每个模块都有对应的 Service 层和 Dao 层,Service 层负责处理业务逻辑,Dao 层负责与数据库进行交互。
+ - **服务接口**:各个模块之间通过服务接口进行交互。例如,借阅管理模块可以调用图书管理模块的服务接口来获取图书信息,调用读者管理模块的服务接口来验证读者信息。
+ - **异常处理**:在各个模块中添加异常处理机制,确保系统的稳定性。例如,当图书库存不足时,抛出相应的异常并进行处理。
+
+ **解析**:通过合理的模块设计和交互方式,可以实现一个功能完整、结构清晰的图书馆管理系统。数据模型的设计是系统的基础,分层架构可以提高代码的可维护性和可测试性,服务接口的使用可以实现模块之间的解耦,异常处理可以保证系统的稳定性。
\ No newline at end of file
diff --git a/src/programming/backend/java/README.md b/src/programming/backend/java/README.md
new file mode 100644
index 0000000..e46b1e3
--- /dev/null
+++ b/src/programming/backend/java/README.md
@@ -0,0 +1,9 @@
+---
+title: Java
+index: false
+icon: mdi:language-java
+category:
+ - Java
+---
+
+
diff --git a/src/programming/backend/java/功能整理/01XJar.md b/src/programming/backend/java/功能整理/01XJar.md
new file mode 100644
index 0000000..07bb96d
--- /dev/null
+++ b/src/programming/backend/java/功能整理/01XJar.md
@@ -0,0 +1,231 @@
+---
+icon: mdi:shield-lock
+date: 2025-05-13
+category:
+ - JAVA
+ - 加密
+tag:
+ - xjar
+title: XJar
+---
+
+
+XJar:保护您的Java应用程序免受反编译和源码泄露
+
+# XJar:保护您的Java应用程序免受反编译和源码泄露
+
+介绍一个强大且实用的工具——**XJar**。它专为Spring Boot JAR和原生JAR提供安全加密运行支持,能够有效防止源码泄露和反编译的风险。无论你是想保护个人项目还是企业级应用,XJar都能为你提供一个简单而高效的解决方案。
+
+GitHub: https://github.com/core-lib/xjar
+
+## 什么是XJar?
+
+XJar是一个开源工具,通过对JAR包内的资源进行加密,并结合扩展的ClassLoader,构建了一套程序加密启动和动态解密运行的机制。它特别适合Spring Boot项目,同时也支持原生JAR,旨在帮助开发者保护Java应用程序的安全性。
+
+## 功能特性
+
+XJar提供了以下核心功能,让它在加密工具中脱颖而出:
+
+- **无代码侵入**:无需修改源代码,只需对编译好的JAR包进行加密即可。
+- **完全内存解密**:资源在运行时动态解密,减少源码或字节码泄露的风险。
+- **支持所有JDK内置加解密算法**:灵活选择适合你的加密算法。
+- **资源加密灵活性**:支持选择需要加密的字节码或其他资源文件。
+- **Maven插件支持**:集成到构建流程,加密更便捷。
+- **Go启动器**:动态生成Go语言启动器,保护密码不泄露。
+
+## 环境依赖
+
+- **JDK版本**:1.7及以上
+
+## 使用步骤
+
+下面我将详细介绍如何使用XJar加密你的JAR包,步骤清晰易懂。
+
+### 1. 添加依赖
+
+首先,在你的Maven项目中添加XJar依赖,并配置jitpack.io仓库:
+
+```xml
+
+
+
+
+ jitpack.io
+ https://jitpack.io
+
+
+
+
+
+ com.github.core-lib
+ xjar
+ 4.0.2
+
+
+
+```
+
+> **小贴士**:如果只是用JUnit测试加密过程,可以将``设置为`test`。
+
+### 2. 加密源码
+
+使用XJar提供的`XCryptos.encryption()`方法对JAR包进行加密:
+
+```java
+XCryptos.encryption()
+ .from("/path/to/read/plaintext.jar") // 待加密JAR包路径
+ .use("io.xjar") // 加密密码
+ .include("/io/xjar/**/*.class") // 需要加密的字节码
+ .include("/mapper/**/*Mapper.xml") // 需要加密的资源文件
+ .exclude("/static/**/*") // 排除静态文件
+ .exclude("/conf/*") // 排除配置文件
+ .to("/path/to/save/encrypted.jar"); // 输出加密后的JAR包
+```
+
+- `include`和`exclude`支持ANT表达式或正则表达式,灵活控制加密范围。
+- 当两者同时使用时,加密范围为`include`内排除`exclude`后的资源。
+
+### 3. 编译脚本
+
+加密完成后,XJar会在输出目录生成一个`xjar.go`文件。这是Go语言编写的启动器源码,需要编译为可执行文件:
+
+- **Windows**:编译后生成`xjar.exe`
+- **Linux**:编译后生成`xjar`
+
+编译需要Go环境,但运行时无需Go支持。注意:启动器带有防篡改校验,无法通用。
+
+### 4. 启动运行
+
+使用编译好的启动器运行加密后的JAR包:
+
+```shell
+./xjar java -Xms256m -Xmx1024m -jar /path/to/encrypted.jar
+```
+
+- 启动器需放在Java命令之前。
+- 仅支持`-jar`方式启动,不支持`-cp`或`-classpath`。
+- 使用`nohup`时,需写为:`nohup ./xjar java -jar /path/to/encrypted.jar`。
+
+## 注意事项
+
+在使用XJar时,以下问题可能影响你的体验,我整理了解决方案供参考:
+
+### 1. Spring Boot Maven插件兼容性
+
+XJar不支持`spring-boot-maven-plugin`的`executable = true`和`embeddedLaunchScript`配置,需删除:
+
+```xml
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+```
+
+### 2. Spring Boot + JPA(Hibernate)报错
+
+若使用Hibernate,启动可能报错。解决方法:
+
+1. 克隆[XJar-Agent-Hibernate](https://github.com/core-lib/xjar-agent-hibernate),编译生成`xjar-agent-hibernate-${version}.jar`。
+
+2. 启动命令添加代理:
+
+ ```shell
+ ./xjar java -javaagent:xjar-agent-hibernate-${version}.jar -jar your-app.jar
+ ```
+
+### 3. 静态文件加载问题
+
+加密静态文件可能导致浏览器加载失败,建议排除加密:
+
+```java
+.exclude("/static/**/*")
+.exclude("/META-INF/resources/**/*")
+```
+
+### 4. JDK 9+模块化问题
+
+在JDK 9及以上版本,需添加参数:
+
+```shell
+./xjar java --add-opens java.base/jdk.internal.loader=ALL-UNNAMED -jar /path/to/encrypted.jar
+```
+
+### 5. 阿里云Maven镜像问题
+
+使用阿里云镜像时,需在`mirrorOf`中排除jitpack.io:
+
+```xml
+
+ alimaven
+ central,!jitpack.io
+ http://maven.aliyun.com/nexus/content/repositories/central/
+
+```
+
+## 插件集成
+
+XJar提供了[xjar-maven-plugin](https://github.com/core-lib/xjar-maven-plugin),可自动完成加密过程。
+
+### 配置示例
+
+```xml
+
+
+
+ jitpack.io
+ https://jitpack.io
+
+
+
+
+
+ com.github.core-lib
+ xjar-maven-plugin
+ 4.0.2
+
+
+
+ build
+
+ package
+
+ io.xjar
+
+
+
+
+
+
+
+```
+
+### 执行方式
+
+- **自动构建**:`mvn clean package -Dxjar.password=io.xjar`
+- **手动执行**:`mvn xjar:build -Dxjar.password=io.xjar`
+
+> **强烈建议**:不要在`pom.xml`中写死密码,通过命令行传递更安全!
+
+## 参数说明
+
+以下是`xjar-maven-plugin`的主要参数:
+
+| 参数名称 | 命令参数 | 参数说明 | 参数类型 | 缺省值 |
+| --------- | ---------------- | ------------ | -------- | -------------------------- |
+| password | -Dxjar.password | 密码字符串 | String | 必须 |
+| algorithm | -Dxjar.algorithm | 加密算法名称 | String | AES/CBC/PKCS5Padding |
+| keySize | -Dxjar.keySize | 密钥长度 | int | 128 |
+| ivSize | -Dxjar.ivSize | 密钥向量长度 | int | 128 |
+| sourceDir | -Dxjar.sourceDir | 源JAR目录 | File | ${project.build.directory} |
+| targetDir | -Dxjar.targetDir | 目标JAR目录 | File | ${project.build.directory} |
+
+更多详情见:[xjar-maven-plugin](https://github.com/core-lib/xjar-maven-plugin)。
+
+## 结尾
+
+XJar以其无侵入性、灵活性和高效性,成为保护Java应用的绝佳选择。无论是个人项目还是商业产品,它都能帮你轻松加密JAR包,确保代码安全。赶快试试吧!
\ No newline at end of file
diff --git a/src/programming/backend/java/功能整理/02Maven.md b/src/programming/backend/java/功能整理/02Maven.md
new file mode 100644
index 0000000..c86bc14
--- /dev/null
+++ b/src/programming/backend/java/功能整理/02Maven.md
@@ -0,0 +1,38 @@
+---
+icon: mdi:package-variant
+date: 2025-05-08
+category:
+ - 实用工具
+ - JAVA
+tag:
+ - maven
+title: Maven常用配置
+---
+
+
+
+maven常用配置
+
+
+# 阿里云镜像
+
+```xml
+
+ aliyunmaven
+ central
+ 阿里云公共仓库
+ https://maven.aliyun.com/repository/public
+
+```
+
+# 配置代理
+```xml
+
+ http-proxy
+ true
+ http
+ 10.6.212.9
+ 7897
+ localhost|127.0.0.1|*.local
+
+```
diff --git a/src/programming/backend/java/功能整理/03WebSocket和HTTP关系.md b/src/programming/backend/java/功能整理/03WebSocket和HTTP关系.md
new file mode 100644
index 0000000..5e426cd
--- /dev/null
+++ b/src/programming/backend/java/功能整理/03WebSocket和HTTP关系.md
@@ -0,0 +1,458 @@
+---
+icon: mdi:webhook
+date: 2025-05-08
+category:
+ - JAVA
+ - 通信协议
+tag:
+ - websocket
+ - http
+title: WebSocket和HTTP关系
+---
+
+
+# WebSocket和HTTP关系
+
+## 1. WebSocket简介
+
+WebSocket 是 HTML5 提供的一种在单个 TCP 连接上进行全双工通讯的协议。它使客户端和服务器之间的数据交换变得更加简单高效,并允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需完成一次握手,便可创建持久性的连接,实现双向数据传输。
+
+## 2. WebSocket与HTTP的关系
+
+### 2.1 协议转换过程
+
+WebSocket依赖于HTTP连接初始化,但随后会进行协议升级。具体转换过程如下:
+
+1. **初始HTTP请求**:每个WebSocket连接都始于一个HTTP请求。客户端发送标准的HTTP请求,但包含特殊的头信息,表明希望升级为WebSocket协议:
+
+```
+GET /chat HTTP/1.1
+Host: server.example.com
+Upgrade: websocket
+Connection: Upgrade
+Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
+Origin: http://example.com
+Sec-WebSocket-Version: 13
+```
+
+2. **服务器响应升级**:如果服务器支持WebSocket协议,会返回101状态码,表示协议正在切换:
+
+```
+HTTP/1.1 101 Switching Protocols
+Upgrade: websocket
+Connection: Upgrade
+Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+```
+
+3. **协议切换完成**:此时HTTP请求已完成其使命,如果协议升级成功,客户端会触发`onopen`事件;否则触发`onerror`事件。此后的所有通信都使用WebSocket协议,不再依赖HTTP。
+
+### 2.2 为什么WebSocket要依赖HTTP协议连接?
+
+WebSocket选择依赖HTTP协议有几个重要原因:
+
+1. **设计理念**:WebSocket设计之初就是为HTTP增强通信能力(尤其是全双工通信),因此在HTTP协议基础上扩展是自然的选择,能够复用HTTP的基础设施。
+
+2. **最大兼容性**:基于HTTP连接可获得最广泛的兼容支持。即使服务器不支持WebSocket,也能建立HTTP通信并返回相应错误,这比完全无响应要好得多。
+
+3. **防火墙友好**:大多数网络环境允许HTTP流量通过(80和443端口),基于HTTP的WebSocket更容易穿越防火墙和代理服务器。
+
+4. **减少实现复杂度**:复用现有的HTTP基础设施,无需从零开始构建新的协议栈。
+
+### 2.3 HTTP与WebSocket的主要区别
+
+|特性|HTTP|WebSocket|
+|---|---|---|
+|连接类型|无状态、短连接|有状态、长连接|
+|通信方式|单向(请求-响应)|双向(全双工)|
+|数据交互模式|客户端主动请求|双方均可主动发送|
+|数据传输量|每次请求都有HTTP头|握手后数据传输更轻量|
+|实时性|弱(通常需轮询)|强(可即时推送)|
+|使用场景|传统网页请求、RESTful API|聊天应用、实时数据更新、在线游戏等|
+
+## 3. WebSocket的优势与应用场景
+
+相比传统HTTP,WebSocket具有以下优势:
+
+1. **降低延迟**:一旦建立连接,通信双方可随时发送数据,无需重复建立连接。
+
+2. **减少网络流量**:WebSocket在握手后的通信中没有HTTP头的开销,数据传输更高效。
+
+3. **实时双向通信**:服务器可以主动推送信息给客户端,无需客户端轮询。
+
+4. **保持连接状态**:WebSocket是有状态协议,可维护连接上下文信息。
+
+典型应用场景:
+- 实时通讯应用(聊天室、即时通讯工具)
+- 在线协作工具(协同编辑文档)
+- 实时数据展示(股票行情、体育赛事直播)
+- 游戏应用(在线多人游戏)
+- 物联网设备通信
+
+## 4. 总结
+
+WebSocket与HTTP是相辅相成的关系,而非替代关系。WebSocket通过HTTP协议完成初始握手,随后升级为独立的协议,实现更高效的双向通信。两种协议各有优势,应根据应用场景选择合适的通信方式。在需要实时性、双向通信的场景中,WebSocket展现出明显优势;而对于简单的数据获取和传统网页浏览,HTTP仍然是最佳选择。
+
+## 5. 案例(服务端)
+### 5.1 项目结构
+
+
+### 5.2 依赖配置
+```xml
+
+ 4.0.0
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.4.3
+
+
+
+
+ websocket
+ jar
+
+ websocket
+ http://maven.apache.org
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-websocket
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+```
+
+### 5.3 主应用类
+```java
+package com.mangmang;
+
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class WebSocketDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(WebSocketDemoApplication.class, args);
+ }
+}
+```
+### 5.4 WebSocket配置类
+```java
+package com.mangmang.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.messaging.simp.config.MessageBrokerRegistry;
+import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
+import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
+import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
+
+@Configuration
+@EnableWebSocketMessageBroker
+public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
+
+ @Override
+ public void registerStompEndpoints(StompEndpointRegistry registry) {
+ // 注册STOMP协议的节点(endpoint)
+ registry.addEndpoint("/ws").setAllowedOrigins("http://10.6.212.39:5173/");
+ }
+
+ @Override
+ public void configureMessageBroker(MessageBrokerRegistry registry) {
+ // 配置消息代理,前缀为/topic的消息将会被路由到消息代理
+ registry.enableSimpleBroker("/topic");
+ // 以/app开头的消息将会被路由到@MessageMapping注解的方法中
+ registry.setApplicationDestinationPrefixes("/app");
+ }
+
+}
+```
+### 5.5 消息实体类
+```java
+package com.mangmang.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+
+import lombok.NoArgsConstructor;
+
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Message {
+ private String content;
+ private String sender;
+ private MessageType type;
+
+
+ public enum MessageType {
+ CHAT,
+ JOIN,
+ LEAVE
+ }
+}
+```
+
+### 5.6 消息控制类
+```
+package com.mangmang.controller;
+
+import com.mangmang.model.Message;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.messaging.handler.annotation.MessageMapping;
+import org.springframework.messaging.handler.annotation.Payload;
+import org.springframework.messaging.handler.annotation.SendTo;
+import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
+import org.springframework.stereotype.Controller;
+
+import java.util.Objects;
+
+@Slf4j
+@Controller
+public class MessageController {
+
+ @MessageMapping("/chat.sendMessage")
+ @SendTo("/topic/public")
+ public Message sendMessage(@Payload Message message) {
+ log.info(message.toString());
+ return message;
+ }
+
+
+ @MessageMapping("/chat.addUser")
+ @SendTo("/topic/public")
+ public Message addUser(@Payload Message message, SimpMessageHeaderAccessor headerAccessor) {
+ log.info(message.toString());
+ // 将用户名添加到WebSocket会话
+ Objects.requireNonNull(headerAccessor.getSessionAttributes()).put("username", message.getSender());
+ return message;
+ }
+}
+```
+
+### 5.7 websocket断连通知类
+```java
+package com.mangmang.listener;
+
+import com.mangmang.model.Message;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.event.EventListener;
+import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
+import org.springframework.messaging.simp.SimpMessageSendingOperations;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.messaging.SessionDisconnectEvent;
+
+import java.util.Objects;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class WebSocketEventListener {
+
+ private final SimpMessageSendingOperations messagingTemplate;
+
+ @EventListener
+ public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
+ // 获取会话属性
+ SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.wrap(event.getMessage());
+
+ // 从会话中获取用户名
+ String username = (String) Objects.requireNonNull(headerAccessor.getSessionAttributes()).get("username");
+
+ if (username != null) {
+ log.info("用户断开连接: {}", username);
+
+ // 创建一个离开消息
+ Message message = Message.builder()
+ .type(Message.MessageType.LEAVE)
+ .sender(username)
+ .content("下线了")
+ .build();
+
+ // 发送消息到公共主题
+ messagingTemplate.convertAndSend("/topic/public", message);
+ }
+ }
+}
+```
+
+## 6. 案例(客户端)
+### 6.1 封装websocket
+```ts
+import {Client} from '@stomp/stompjs';
+
+interface ChatMessage {
+ content: string;
+ sender: string;
+ type: 'CHAT' | 'JOIN' | 'LEAVE';
+}
+
+class WebSocketService {
+ private stompClient: Client | null = null;
+
+ connect(username: string, onMessageReceived: (msg: ChatMessage) => void) {
+ // 创建原生 WebSocket 连接
+ const socket = new WebSocket('ws://10.6.212.39:8099/ws'); // 确保这里是 WebSocket 协议
+
+ this.stompClient = new Client({
+ webSocketFactory: () => socket,
+ onConnect: () => {
+ console.log('STOMP connected');
+ // 订阅公共主题
+ this.stompClient?.subscribe('/topic/public', (message) => {
+ const chatMsg: ChatMessage = JSON.parse(message.body);
+ onMessageReceived(chatMsg);
+ });
+
+ // 发送用户加入消息
+ this.sendAddUserMessage(username);
+ },
+ onStompError: (frame) => {
+ console.error('Broker reported error: ', frame.headers['message']);
+ console.error('Additional details: ', frame.body);
+ },
+ // 关闭时清理资源
+ onDisconnect: () => {
+ console.log('Disconnected from STOMP');
+ this.stompClient = null;
+ }
+ });
+
+ // 激活客户端
+ this.stompClient.activate();
+ }
+
+ sendMessage(chatMessage: Omit) {
+ if (this.stompClient?.connected) {
+ this.stompClient.publish({
+ destination: '/app/chat.sendMessage',
+ body: JSON.stringify(chatMessage),
+ });
+ } else {
+ console.warn('WebSocket not connected');
+ }
+ }
+
+ sendAddUserMessage(username: string) {
+ if (this.stompClient?.connected) {
+ this.stompClient.publish({
+ destination: '/app/chat.addUser',
+ body: JSON.stringify({content: "加入聊天", sender: username, type: 'JOIN'}),
+ });
+ }
+ }
+
+ disconnect() {
+ if (this.stompClient) {
+ this.stompClient.deactivate();
+ this.stompClient = null;
+ }
+ }
+}
+
+export default new WebSocketService();
+```
+
+### 6.2 页面
+```vue
+
+
+
+
+
WebSocket 聊天室
+
+
+
+ {{ msg.sender }}: {{ msg.content }}
+
+
+
+
+
+
+
+
+
+```
\ No newline at end of file
diff --git a/src/programming/backend/java/功能整理/05防止表单和参数重复提交.md b/src/programming/backend/java/功能整理/05防止表单和参数重复提交.md
new file mode 100644
index 0000000..312507c
--- /dev/null
+++ b/src/programming/backend/java/功能整理/05防止表单和参数重复提交.md
@@ -0,0 +1,765 @@
+---
+date: 2025-05-07
+category:
+ - JAVA
+tag:
+ - 表单
+ - 重复提交
+ - 防重
+icon: lock
+---
+
+防止表单和参数重复提交案例
+
+# 防止表单和参数重复提交
+
+## 1. 编写注解
+
+```java
+/**
+ * 防止表单重复提交
+ *
+ * @author 氓氓编程
+ * @Date: 2021-06-08-16:35
+ * @Inherited @interface 自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节
+ * @Target 用于描述注解的使用范围(作用于方法上)
+ * @Retention 被描述的注解在什么范围内有效 (在运行时有效,即运行时保留)
+ */
+@Inherited
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RepeatSubmit {
+
+}
+```
+
+## 2. 自定义拦截器
+
+```java
+/**
+ * @author 茫茫编程
+ * @Date: 2021-06-08-16:29
+ */
+@Slf4j
+@Component
+public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter {
+ /**
+ * @param request 是指经过spring封装的请求对象, 包含请求地址, 头, 参数, body(流)等信息.
+ * @param response 是指经过spring封装的响应对象, 包含输入流, 响应body类型等信息.
+ * @param handler 是指controller的@Controller注解下的"完整"方法名, 是包含exception等字段信息的.
+ * @return 是否放行
+ * @throws Exception 异常
+ */
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ //instanceof是Java中的二元运算符,左边是对象,右边是类;当对象是右边类或子类所创建对象时,返回true;否则,返回false。
+ if (handler instanceof HandlerMethod) {
+ log.info("进来了");
+ //把handler强转为HandlerMethod
+ HandlerMethod handlerMethod = (HandlerMethod) handler;
+ //获取当前请求的方法
+ Method method = handlerMethod.getMethod();
+ //获取当前去请求方法上是否有注解@RepeatSubmit
+ RepeatSubmit repeatSubmit = method.getAnnotation(RepeatSubmit.class);
+ //如果方法有@RepeatSubmit注解,进入if
+ if (repeatSubmit != null) {
+ //判断是否是重复提交,重复提交进入
+ if (isRepeatSubmit(request, response)) {
+ //返回消息实体类
+ R message = R.error().message("不允许重复提交");
+ //把消息响应给客户端
+ ServletUtils.renderString(response, JSONUtil.toJsonStr(message));
+ //拦截
+ return false;
+ }
+ }
+ //注解和不重复直接放行
+ return true;
+ } else {
+ //如果handler不是HandlerMethod或子类放行
+ return super.preHandle(request, response, handler);
+ }
+ }
+
+ /**
+ * 弗雷调用该方法会使用子类的实现
+ * 验证是否重复提交由子类实现具体的防重复提交的规则
+ *
+ * @param request 请求
+ * @return 是否是重复提交
+ */
+ public abstract boolean isRepeatSubmit(HttpServletRequest request,HttpServletResponse response);
+}
+```
+
+## 3. 自定义拦截器子类
+
+```java
+/**
+ * 判断请求url和数据是否和上一次相同,
+ * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
+ *
+ * @author 氓氓编程
+ * @Date: 2021-06-08-17:27
+ */
+@Slf4j
+@Component
+public class SameUrlDataInterceptor extends RepeatSubmitInterceptor {
+ /**
+ * 重复参数
+ */
+ public final String REPEAT_PARAMS = "repeatParams";
+
+ /**
+ * 重复时间
+ */
+ public final String REPEAT_TIME = "repeatTime";
+
+ /**
+ * 间隔时间单位秒
+ * 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据
+ */
+ private int intervalTime = 120;
+
+ public SameUrlDataInterceptor(JwtUtil jwtUtil, RedisCache redisCache) {
+ this.jwtUtil = jwtUtil;
+ this.redisCache = redisCache;
+ }
+
+ public void setIntervalTime(int intervalTime) {
+ this.intervalTime = intervalTime;
+ }
+
+ private final JwtUtil jwtUtil;
+
+ /**
+ * 注入redis
+ */
+ private final RedisCache redisCache;
+
+ /**
+ * 重写父类判断是否重复的抽象方法
+ *
+ * @param request 请求
+ * @return true=重复提交 false=未重复
+ */
+ @SneakyThrows
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean isRepeatSubmit(HttpServletRequest request, HttpServletResponse response) {
+ //1.声明当前参数
+ String nowParams = null;
+ //判断请求是否为空
+ if (request != null) {
+ //把request 转为可重复获取流的RepeatedlyRequestWrapper
+ RepeatedlyRequestWrapper repeatedlyRequest = new RepeatedlyRequestWrapper(request, response);
+ //获取body参数
+ nowParams = RepeatedlyRequestWrapper.getBodyString(repeatedlyRequest);
+ }
+ //如果请求体body参数为空,获取Parameter的参数
+ if (StringUtils.isEmpty(nowParams)) {
+ assert request != null;
+ nowParams = JSONUtil.toJsonStr(request.getParameterMap());
+ log.info("body=={}", nowParams);
+ }
+ //把数据存储起来
+ Map nowMap = new HashMap<>(80);
+ nowMap.put(REPEAT_PARAMS, nowParams);
+ nowMap.put(REPEAT_TIME, System.currentTimeMillis());
+ log.info("nowMap=={}", nowMap);
+
+ // 请求地址(作为存放cache的key值)
+ String url = request.getRequestURI();
+
+ //唯一标识-获取请求头的token值
+ String submitKey = request.getHeader(jwtUtil.getHeader());
+ //如果token为空,使用请求地址作为key
+ if (StringUtils.isEmpty(submitKey)) {
+ submitKey = url;
+ }
+ //唯一标识(指定key+消息头)
+ String cacheRepeatKey = "repeat_submit:" + submitKey;
+ log.info("repeat_submit=={}", cacheRepeatKey);
+
+ //缓存中获取上次请求数据
+ Object cacheObject = redisCache.getCacheObject(cacheRepeatKey);
+ log.info("cacheObject=={}", cacheObject);
+ //如果缓存中没有数据,则存放
+ if (cacheObject == null) {
+ Map cacheMap = new HashMap<>(109);
+ cacheMap.put(url, nowMap);
+ redisCache.setCacheObject(cacheRepeatKey, cacheMap, intervalTime, TimeUnit.SECONDS);
+ } else {
+ //强转为map
+ Map preDataMap = (Map) cacheObject;
+ //判断该map是否有url作为的键
+ if (preDataMap.containsKey(url)) {
+ //根据map中的键url 获取对应的参数
+ Map preMap = (Map) preDataMap.get(url);
+ return compareParams(nowMap, preMap) && compareTime(nowMap, preMap);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 比较两次请求参数是否相同
+ *
+ * @param nowMap 现在的数据
+ * @param preMap 之前的数据
+ * @return true=相同 false=不相同
+ */
+ private boolean compareParams(Map nowMap, Map preMap) {
+ String now = (String) nowMap.get(REPEAT_PARAMS);
+ String pre = (String) preMap.get(REPEAT_PARAMS);
+ return now.equals(pre);
+ }
+
+ /**
+ * 比较两次请求时间间隔
+ *
+ * @param nowMap 现在的数据
+ * @param preMap 之前的数据
+ * @return true=相同 false=不相同
+ */
+ private boolean compareTime(Map nowMap, Map preMap) {
+ long now = (Long) nowMap.get(REPEAT_TIME);
+ long pre = (Long) preMap.get(REPEAT_TIME);
+ //如果两次间隔时间小于10秒
+ return (now - pre) < this.intervalTime * 1000L;
+ }
+}
+```
+
+## 4. 配置拦截器到web中
+
+```java
+/**
+ * @author 氓氓编程
+ */
+@Configuration
+public class CorsConfig implements WebMvcConfigurer {
+
+ private final RepeatSubmitInterceptor repeatSubmitInterceptor;
+ //构造方法注入
+ public CorsConfig(RepeatSubmitInterceptor repeatSubmitInterceptor) {
+ this.repeatSubmitInterceptor = repeatSubmitInterceptor;
+ }
+
+
+ /**
+ * 解决跨域
+ * @return CorsFilter
+ */
+ @Bean
+ public CorsFilter corsFilter() {
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ CorsConfiguration configuration = new CorsConfiguration();
+ // 设置访问源地址(允许那些地址访问服务器)
+ configuration.addAllowedOrigin("*");
+ // 设置访问源请求方法(方法)
+ configuration.addAllowedMethod("*");
+ // 设置访问源请求头(头部信息)
+ configuration.addAllowedHeader("*");
+ // 跨域需要暴露的请求头(因为跨域访问默认不能获取全部头部信息)
+ configuration.addExposedHeader("token");
+ // 注册配置
+ source.registerCorsConfiguration("/**", configuration);
+ return new CorsFilter(source);
+ }
+
+ /**
+ * 添加拦截器
+ * @param registry registry
+ */
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ registry.addInterceptor(repeatSubmitInterceptor);
+ }
+}
+
+```
+
+## 5. redis
+
+### 5.1 配置
+
+```java
+**
+ * @author 氓氓编程
+ *
+ */
+@Configuration
+public class RedisCacheConfig extends CachingConfigurerSupport {
+ @Bean
+ public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
+ RedisTemplate redisTemplate = new RedisTemplate<>();
+ redisTemplate.setConnectionFactory(redisConnectionFactory);
+ //key序列化
+ redisTemplate.setKeySerializer(new StringRedisSerializer());
+ //value序列化
+ redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
+ redisTemplate.setHashKeySerializer(new StringRedisSerializer());
+ redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
+ redisTemplate.afterPropertiesSet();
+ return redisTemplate;
+ }
+}
+```
+
+> 以下是需要用到的工具类
+
+### 5.2 redis 工具类
+
+```java
+/**
+ * Redis 工具类
+ *
+ * @author 氓氓编程
+ * @Date: 2021-06-08-17:35
+ */
+@Component
+public class RedisCache {
+
+ private final RedisTemplate redisTemplate;
+
+
+ public RedisCache(RedisTemplate redisTemplate) {
+ this.redisTemplate = redisTemplate;
+ }
+
+
+ /**
+ * 缓存基本对象,Integer、String、实体类等
+ *
+ * @param key 键
+ * @param value 值
+ */
+ public void setCacheObject(final String key, final Object value) {
+ redisTemplate.opsForValue().set(key, value);
+ }
+
+ /**
+ * 带有效时间缓存基本对象,Integer、String、实体类等
+ *
+ * @param key 键
+ * @param value 值
+ * @param timeout 有效时间
+ * @param timeUnit 有效时间单位
+ */
+ public void setCacheObject(final String key, final Object value, final Integer timeout, final TimeUnit timeUnit) {
+ redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
+ }
+
+ /**
+ * 给某个键设置有效时间
+ *
+ * @param key 需要设置有效时间的键
+ * @param timeout 设置的时间 默认是秒
+ * @return true=设置成功 false=设置失败
+ */
+ public boolean expire(final String key, final long timeout) {
+ return expire(key, timeout, TimeUnit.SECONDS);
+ }
+
+
+ /**
+ * 给某个键设置有效时间
+ *
+ * @param key 需要设置有效时间的键
+ * @param timeout 设置的时间
+ * @param timeUnit 有效时间单位
+ * @return true=设置成功 false=设置失败
+ */
+ public boolean expire(final String key, final long timeout, final TimeUnit timeUnit) {
+ Boolean isSuccess = redisTemplate.expire(key, timeout, timeUnit);
+ if (isSuccess != null) {
+ return isSuccess;
+ }
+ return false;
+ }
+
+ /**
+ * 根据键获取某个缓存的值
+ *
+ * @param key 键
+ * @return Object
+ */
+ public Object getCacheObject(final String key) {
+ return redisTemplate.opsForValue().get(key);
+ }
+
+
+ /**
+ * 根据建删除缓存
+ *
+ * @param key 键
+ * @return true=成功 false=失败
+ */
+ public boolean deleteObject(final String key) {
+ Boolean isDelete = redisTemplate.delete(key);
+ return isDelete != null && isDelete;
+ }
+
+ /**
+ * 根据键批量删除
+ *
+ * @param collection 装有键的集合
+ * @return 删除成功的数量
+ */
+ public long deleteObject(final Collection collection) {
+ Long count = redisTemplate.delete(collection);
+ return count == null ? 0 : count;
+ }
+
+ /**
+ * 缓存List集合数据
+ *
+ * @param key 键
+ * @param dataList 待缓存的List数据
+ * @return 缓存成功的数量
+ */
+ public long setCacheList(final String key, final List dataList) {
+ Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
+ return count == null ? 0 : count;
+ }
+
+ /**
+ * 获取List缓存数据
+ *
+ * @param key 键
+ * @return 缓存键值对应的数据
+ */
+ public List getCacheList(final String key) {
+ return redisTemplate.opsForList().range(key, 0, -1);
+ }
+
+ /**
+ * 缓存Set
+ *
+ * @param key 键
+ * @param dataSet 待缓存的Set数据
+ * @return 缓存数据的对象
+ */
+ public BoundSetOperations setCacheSet(final String key, final Set dataSet) {
+ BoundSetOperations setOperations = redisTemplate.boundSetOps(key);
+ Iterator it = dataSet.iterator();
+ if (it.hasNext()) {
+ setOperations.add(it.next());
+ }
+ return setOperations;
+ }
+
+
+ /**
+ * 获取Set缓存数据
+ *
+ * @param key 键
+ * @return 缓存键值对应的数据
+ */
+ public Set getCacheSet(final String key) {
+ return redisTemplate.opsForSet().members(key);
+ }
+
+ /**
+ * @param key 键
+ * @param dataMap 缓存的map数据
+ */
+ public void setCacheMap(final String key, final Map dataMap) {
+ if (dataMap != null) {
+ redisTemplate.opsForHash().putAll(key, dataMap);
+ }
+ }
+
+
+ /**
+ * 获得缓存的Map
+ *
+ * @param key 键
+ * @return 获得缓存的数据
+ */
+ public Map getCacheMap(final String key) {
+ return redisTemplate.opsForHash().entries(key);
+ }
+
+
+ /**
+ * 往Hash中存入数据
+ *
+ * @param key Redis键
+ * @param hKey Hash键
+ * @param value 值
+ */
+ public void setCacheMapValue(final String key, final String hKey, final Object value) {
+ redisTemplate.opsForHash().put(key, hKey, value);
+ }
+
+ /**
+ * 获取Hash中的数据
+ *
+ * @param key Redis键
+ * @param hKey Hash键
+ * @return Hash中的对象
+ */
+ public Object getCacheMapValue(final String key, final String hKey) {
+ return redisTemplate.opsForHash().get(key, hKey);
+ }
+
+ /**
+ * 获取多个Hash中的数据
+ *
+ * @param key Redis键
+ * @param hKeys Hash键集合
+ * @return Hash对象集合
+ */
+ public List getMultiCacheMapValue(final String key, final Collection hKeys) {
+ return redisTemplate.opsForHash().multiGet(key, hKeys);
+ }
+
+ /**
+ * 获得缓存的基本对象列表
+ *
+ * @param pattern 字符串前缀
+ * @return 对象列表
+ */
+ public Collection keys(final String pattern) {
+ return redisTemplate.keys(pattern);
+ }
+}
+```
+
+## 6. Servlet参数发送的工具类
+
+```java
+/**
+ * 客户端工具类
+ *
+ * @author 氓氓编程
+ * @Date: 2021-06-08-16:50
+ */
+public class ServletUtils {
+ /**
+ * 获取String参数
+ */
+ public static String getParameter(String name) {
+ return getRequest().getParameter(name);
+ }
+
+ /**
+ * 获取String参数,如墨没有设置一个默认值
+ */
+ public static String getParameter(String name, String defaultValue) {
+ return Convert.toStr(getRequest().getParameter(name), defaultValue);
+ }
+
+ /**
+ * 获取session
+ */
+ public static HttpSession getSession() {
+ return getRequest().getSession();
+ }
+
+ /**
+ * 获取Integer参数
+ */
+ public static Integer getParameterToInt(String name) {
+ return Convert.toInt(getRequest().getParameter(name));
+ }
+
+ /**
+ * 获取response
+ */
+ public static HttpServletResponse getResponse() {
+ return getRequestAttributes().getResponse();
+ }
+
+ /**
+ * 获取request
+ */
+ public static HttpServletRequest getRequest() {
+ return getRequestAttributes().getRequest();
+ }
+
+ /**
+ * 获取到当前的HttpServletRequest
+ *
+ * @return ServletRequestAttributes
+ */
+ public static ServletRequestAttributes getRequestAttributes() {
+ RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
+ return (ServletRequestAttributes) attributes;
+ }
+
+ /**
+ * @param response 当前请求的响应
+ * @param string 传输的文字
+ */
+ public static void renderString(HttpServletResponse response, String string) {
+ try {
+ response.setStatus(200);
+ response.setContentType("application/json");
+ response.setCharacterEncoding("UTF-8");
+ response.getWriter().print(string);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
+```
+
+## 7. 可重复读取流的RepeatedlyRequestWrapper
+
+```java
+/**
+ * 构建可重复读取inputStream的请求request
+ *
+ * @author 氓氓编程
+ * @Date: 2021-06-09-8:47
+ */
+@Slf4j
+public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
+
+ /**
+ * 存放请求体中的数据
+ */
+ private final byte[] body;
+
+ public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws UnsupportedEncodingException {
+ super(request);
+ request.setCharacterEncoding("UTF-8");
+ response.setCharacterEncoding("UTF-8");
+ this.body = getBodyString(request).getBytes(StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public BufferedReader getReader() {
+ return new BufferedReader(new InputStreamReader(getInputStream()));
+ }
+
+ @Override
+ public ServletInputStream getInputStream(){
+ //1.创建一个字节数组流存放请求体
+ final ByteArrayInputStream bodyInputStream = new ByteArrayInputStream(body);
+ //2.返回获取的body中的数据流
+ return new ServletInputStream() {
+ @Override
+ public int read(){
+ return bodyInputStream.read();
+ }
+
+ @Override
+ public int available(){
+ return body.length;
+ }
+
+ @Override
+ public boolean isFinished() {
+ return false;
+ }
+
+ @Override
+ public boolean isReady() {
+ return false;
+ }
+
+ @Override
+ public void setReadListener(ReadListener readListener) {
+
+ }
+ };
+ }
+
+
+ /**
+ * 获取请求体
+ *
+ * @param request 请求
+ * @return 字符串
+ */
+ public static String getBodyString(ServletRequest request) {
+ //1.创建一个StringBuilder
+ StringBuilder sb = new StringBuilder();
+ //2.声明一个读缓存的流
+ BufferedReader reader = null;
+ //3.获取请求中的流
+ try (InputStream inputStream = request.getInputStream()) {
+ //把请求中的流读取出来给reader
+ reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+ //声明line存放每一行的数据
+ String line;
+ //一行一行的读取数据并赋值给line,line不为空
+ while ((line = reader.readLine()) != null) {
+ //追加写入
+ sb.append(line);
+ }
+ } catch (IOException e) {
+ log.warn("获取请求体中数据出现问题");
+ } finally {
+ //如果reader不为空,关闭流
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ log.error(ExceptionUtil.getMessage(e));
+ }
+ }
+ }
+ return sb.toString();
+ }
+}
+
+```
+
+## 8. 让可重复读的流生效
+
+```java
+/**
+ * 使用重写后的RepeatedlyRequestWrapper
+ *
+ * Repeatable 过滤器
+ */
+public class RepeatableFilter implements Filter {
+ /**
+ * startsWithIgnoreCase 判断开始部分是否与二参数相同。不区分大小写
+ *
+ * @param request 请求
+ * @param response 响应
+ * @param chain 放行
+ * @throws IOException io异常
+ * @throws ServletException 异常
+ */
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ //声明一个ServletRequest
+ ServletRequest requestWrapper = null;
+ //判断request是HttpServletRequest或子类并且request.getContentType()开头包含application/json
+ if (request instanceof HttpServletRequest && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
+ //创建一个可重复获取流的RepeatedlyRequestWrapper赋值给ServletRequest
+ requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);
+ }
+ //为空,直接放行
+ if (null == requestWrapper) {
+ chain.doFilter(request, response);
+ } else {
+ //赋值完毕可重复读流继续向下传
+ chain.doFilter(requestWrapper, response);
+ }
+ }
+}
+```
+
+```java
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Bean
+ public FilterRegistrationBean someFilterRegistration() {
+ FilterRegistrationBean registration = new FilterRegistrationBean();
+ registration.setFilter(new RepeatableFilter());
+ registration.addUrlPatterns("/*");
+ registration.setName("repeatableFilter");
+ registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
+ return registration;
+ }
+```
+
diff --git a/src/programming/backend/java/功能整理/06Spring Boot JAR 瘦身与加密.md b/src/programming/backend/java/功能整理/06Spring Boot JAR 瘦身与加密.md
new file mode 100644
index 0000000..b34bdca
--- /dev/null
+++ b/src/programming/backend/java/功能整理/06Spring Boot JAR 瘦身与加密.md
@@ -0,0 +1,1226 @@
+---
+icon: mdi:package-variant-closed
+date: 2025-05-13
+category:
+ - 实用工具
+ - JAVA
+ - SpringBoot
+ - JAR
+tag:
+ - JAR包瘦身
+title: Spring Boot JAR 瘦身与加密
+---
+
+
+Spring Boot JAR 瘦身与加密:构建安全高效的部署方案
+
+# Spring Boot JAR 瘦身与加密:构建安全高效的部署方案
+
+在 Spring Boot 应用程序部署过程中,我们常常面临两个主要挑战:
+
+1. **JAR 包体积过大**:Spring Boot 应用打包时会将所有依赖一起打包,导致最终 JAR 文件臃肿
+2. **代码安全性问题**:部署到客户环境或公开场合的 JAR 包可能被反编译,造成核心业务逻辑泄露
+
+为了解决这些问题,本文将介绍一套完整的解决方案,包括 JAR 包瘦身和 JAR 包加密两部分,以及配套的自定义类加载器,实现高效安全的 Spring Boot 应用部署。
+
+## 整体方案设计
+
+该方案由两个主要项目组成:
+
+1. **spring-boot-jar-slim-encrypt**:用于将 Spring Boot 应用 JAR 包瘦身和加密
+2. **spring-boot-custom-classloader**:用于加载第三方JAR
+
+### 工作流程
+
+```
+┌────────────────────┐
+│ 原始Spring Boot │
+│ JAR包 │
+└──────────┬─────────┘
+ │
+ ▼
+┌────────────────────┐ ┌────────────────────┐
+│ spring-boot-jar- │ │ │
+│ slim-encrypt工具 ├───►│ 提取依赖到libs目录 │
+└──────────┬─────────┘ └────────────────────┘
+ │
+ ▼
+┌────────────────────┐
+│ 瘦身后的JAR包 │
+└──────────┬─────────┘
+ │
+ ▼
+┌────────────────────┐
+│ XJar加密处理 │
+└──────────┬─────────┘
+ │
+ ▼
+┌────────────────────┐
+│ 加密后的JAR包 │
+│ (.xjar) │
+└──────────┬─────────┘
+ │
+ │ 部署
+ ▼
+┌─────────────────────────────────────────┐
+│ 运行时环境 │
+│ ┌─────────────────┐ ┌───────────────┐ │
+│ │ 加密JAR (.xjar) │ │ 提取的依赖库 │ │
+│ └────────┬────────┘ └───────┬───────┘ │
+│ │ │ │
+│ │ │ │
+│ ▼ ▼ │
+│ ┌─────────────────────────────────┐ │
+│ │ PlainTextClassLoader │ │
+│ │ (自定义类加载器) │ │
+│ └─────────────┬─────────────┬─────┘ │
+│ │ │ │
+│ ▼ ▼ │
+│ ┌─────────────────┐ ┌───────────────┐ │
+│ │ 解密JAR内容 │ │ 加载外部依赖 │ │
+│ └─────────────────┘ └───────────────┘ │
+│ │
+└─────────────────────────────────────────┘
+```
+
+1. 使用 spring-boot-jar-slim-encrypt 工具提取原始 JAR 包中的依赖
+2. 将依赖库单独存储在 libs 目录
+3. 对精简后的 JAR 包进行加密
+4. 使用自定义类加载器加载外部依赖
+5. 使用xJar进行解密
+
+## 项目一:spring-boot-custom-classloader
+
+这是一个自定义类加载器项目,它实现了类加载器:
+
+### 1. JarClassLoader 接口
+
+```java
+package com.mangmang;
+
+import java.io.File;
+
+public interface JarClassLoader {
+ String JAR_EXTENSION = ".jar";
+
+ /**
+ * 从指定目录加载所有JAR文件
+ *
+ * @param jarDir 包含要加载的JAR文件的目录路径
+ * @throws IllegalArgumentException 如果jarDir为null或不存在
+ */
+ default void loadJar(String jarDir) {
+ if (jarDir == null || jarDir.trim().isEmpty()) {
+ throw new IllegalArgumentException("JAR目录路径不能为空");
+ }
+
+ File directory = new File(jarDir);
+ if (!directory.exists() || !directory.isDirectory()) {
+ throw new IllegalArgumentException("指定路径不是有效目录: " + jarDir);
+ }
+
+ File[] jarFiles = directory.listFiles(this::isJarFile);
+ if (jarFiles == null) {
+ return;
+ }
+
+ for (File jarFile : jarFiles) {
+ System.out.println("加载 》" + jarFile.getName());
+ scanJarFile(jarFile);
+ }
+ }
+
+ /**
+ * 递归扫描文件或目录以查找JAR文件
+ *
+ * @param file 要扫描的文件或目录
+ * @throws IllegalArgumentException 如果file为null
+ */
+ default void scanJarFile(File file) {
+ if (file == null) {
+ throw new IllegalArgumentException("文件不能为null");
+ }
+
+ if (!file.exists()) {
+ return;
+ }
+
+ if (isJarFile(file)) {
+ addJARFile(file);
+ } else if (file.isDirectory()) {
+ File[] files = file.listFiles();
+ if (files != null) {
+ for (File f : files) {
+ scanJarFile(f);
+ }
+ }
+ }
+ }
+
+ /**
+ * 检查文件是否为JAR文件
+ */
+ default boolean isJarFile(File file) {
+ return file.isFile() && file.getName().endsWith(JAR_EXTENSION);
+ }
+
+ /**
+ * 将JAR文件添加到类加载器
+ *
+ * @param jar 要添加的JAR文件
+ * @throws IllegalArgumentException 如果jar为null或不是有效的JAR文件
+ */
+ void addJARFile(File jar);
+}
+```
+
+这是一个接口,定义了 JAR 文件加载的核心方法:
+
+- `loadJar(String jarDir)`: 从指定目录加载所有 JAR 文件
+- `scanJarFile(File file)`: 递归扫描文件或目录查找 JAR 文件
+- `isJarFile(File file)`: 检查文件是否为 JAR 文件
+- `addJARFile(File jar)`: 将 JAR 文件添加到类加载器
+
+### 2. PlainTextClassLoader 实现
+
+```java
+package com.mangmang;
+
+import org.springframework.boot.context.event.ApplicationStartingEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.lang.NonNull;
+
+import java.io.File;
+import java.lang.management.ManagementFactory;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 明文类加载器
+ *
+ * 该类实现了自定义类加载器接口和Spring应用启动事件监听器接口。
+ * 主要功能是在应用启动时,从JVM启动参数中查找指定路径的JAR文件并加载。
+ * 通过反射机制将JAR文件动态添加到当前线程的类加载器中,实现运行时加载额外的类库。
+ *
+ *
+ * 使用方法:
+ * 1. 在JVM启动参数中添加 -Dexternal.jars.path=你的JAR文件目录路径
+ * 2. 系统将自动加载该目录下所有的JAR文件
+ *
+ *
+ * 示例:
+ * java -Dexternal.jars.path.path=/path/to/jars -jar your-application.jar
+ *
+ */
+public class PlainTextClassLoader implements JarClassLoader, ApplicationListener {
+
+ private final String findPath = "external.jars.path"; // 查找路径的键名
+ private final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); // 获取当前线程的类加载器
+ private final List jarFiles = new ArrayList<>(); // 存储已加载的JAR文件列表
+
+ /**
+ * 构造函数
+ *
+ * 在初始化时执行以下操作:
+ * 1. 设置当前线程的类加载器
+ * 2. 输出启动日志信息
+ * 3. 从JVM启动参数中检索包含"external.jars.path.path"的参数
+ * 4. 提取路径值并调用loadJar方法加载指定目录下的JAR文件
+ *
+ */
+ public PlainTextClassLoader() {
+ // 设置当前线程的类加载器
+ Thread.currentThread().setContextClassLoader(classLoader);
+
+ // 打印启动信息
+ System.out.println("启动自定义明文类加载器");
+
+ // 查找并加载外部JAR文件
+ loadExternalJarsFromSystemProperties();
+ }
+
+ /**
+ * 从系统属性中查找并加载外部JAR文件
+ */
+ private void loadExternalJarsFromSystemProperties() {
+ List inputArguments = ManagementFactory.getRuntimeMXBean().getInputArguments();
+
+ // 查找包含指定路径参数的启动参数
+ inputArguments.stream()
+ .filter(arg -> arg.contains(findPath))
+ .map(this::extractPathFromArgument)
+ .filter(Objects::nonNull)
+ .forEach(this::loadJar);
+ }
+
+ /**
+ * 从JVM参数中提取路径值
+ *
+ * @param argument JVM启动参数
+ * @return 提取的路径值,如果提取失败则返回null
+ */
+ private String extractPathFromArgument(String argument) {
+ String prefix = "-D" + findPath + "=";
+ if (argument.startsWith(prefix)) {
+ String path = argument.replace(prefix, "");
+ if (!path.isEmpty()) {
+ return path;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 处理应用程序启动事件
+ *
+ * 当Spring应用启动时会触发此方法。
+ * 目前该方法为空实现,可以在此添加应用启动时需要执行的代码。
+ *
+ *
+ * @param event Spring应用启动事件对象
+ */
+
+ @Override
+ public void onApplicationEvent(@NonNull ApplicationStartingEvent event) {
+ // 应用程序启动事件的处理方法,目前为空
+ }
+
+ /**
+ * 将JAR文件添加到类加载器
+ *
+ * 通过反射机制调用URLClassLoader的addURL方法,将指定的JAR文件URL添加到当前类加载器。
+ * 添加成功后,JAR文件中的类可以被当前JVM加载和使用。
+ * 同时将已加载的JAR文件记录到jarFiles列表中。
+ *
+ *
+ * @param jar 要添加到类加载器的JAR文件对象
+ * @throws RuntimeException 如果添加过程中发生任何异常,将抛出RuntimeException
+ */
+ @Override
+ public void addJARFile(File jar) {
+ if (jar == null) {
+ throw new IllegalArgumentException("JAR文件不能为null");
+ }
+
+ try {
+ addUrlToClassLoader(jar);
+ jarFiles.add(jar);
+ System.out.println(jarFiles);
+ } catch (Exception e) {
+ throw new RuntimeException("添加JAR文件到类加载器失败: " + jar.getName(), e);
+ }
+ }
+
+ /**
+ * 通过反射将JAR文件URL添加到类加载器
+ *
+ * @param jar 要添加的JAR文件
+ * @throws Exception 如果反射操作失败
+ */
+ private void addUrlToClassLoader(File jar) throws Exception {
+ Method addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
+ if (!addUrlMethod.isAccessible()) {
+ addUrlMethod.setAccessible(true);
+ }
+ URL jarUrl = jar.toURI().toURL();
+ addUrlMethod.invoke(classLoader, jarUrl);
+ }
+```
+
+这是一个明文类加载器,实现了 `JarClassLoader` 接口和 Spring 的 `ApplicationListener` 接口,用于在 Spring Boot 应用启动时加载外部 JAR 文件:
+
+主要特点:
+
+- 在 Spring Boot 应用启动时自动执行
+- 通过 JVM 参数 `-Dexternal.jars.path=你的JAR文件目录路径` 指定外部 JAR 文件目录
+- 使用反射机制将 JAR 文件 URL 添加到当前线程的类加载器中
+
+使用示例:
+
+```bash
+java -Dexternal.jars.path=/path/to/jars -jar your-application.jar
+```
+
+### 3. MANIFEAT.MF
+
+>src/main/resources/META-INF/spring.factories
+
+```text
+org.springframework.context.ApplicationListener=\
+com.mangmang.PlainTextClassLoader
+```
+
+### 4. pom.xml
+
+```xml
+
+ 4.0.0
+
+ spring-boot-custom-classloader
+ jar
+
+ spring-boot-custom-classloader
+ http://maven.apache.org
+
+
+ UTF-8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ 2.6.7
+
+
+
+
+```
+
+## 项目二:spring-boot-jar-slim-encrypt
+
+这个工具用于压缩和加密 Spring Boot JAR 文件,主要包含以下两个核心类:
+
+### 1. JarUtil 工具类
+
+```java
+package com.mangmang;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.io.FileUtil;
+import io.xjar.XCryptos;
+import io.xjar.XEncryption;
+import org.apache.commons.compress.utils.IOUtils;
+import org.apache.commons.compress.utils.Sets;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+
+public class JarUtil {
+ /**
+ * 表示UTF-8字符编码的常量。
+ * 该变量用于在各种文件操作中强制使用UTF-8编码,
+ * 确保在整个应用程序中一致地处理文本数据。
+ */
+ private static final String UTF_8 = "UTF-8";
+ /**
+ * 常量BOOT_INF_LIB表示JAR文件中通常存储依赖库的默认目录路径。
+ * 该路径主要用于在压缩或排除等操作中识别和处理库文件。
+ */
+ private static final String BOOT_INF_LIB = "BOOT-INF/lib";
+ /**
+ * 用于标识JAR(Java归档)文件的文件扩展名。
+ * 该常量表示JAR文件的标准扩展名,通常用于
+ * 文件过滤、命名或在目录或归档中识别JAR文件的操作。
+ */
+ private static final String JAR_EXTENSION = ".jar";
+ /**
+ * 定义在管理JAR过程中生成的需求文件的后缀,
+ * 特别是在处理依赖项或排除项时使用。
+ * 该字符串用作特定的文件名模式,用于保存与特定服务
+ * 相关的排除依赖项或其他需求的列表。
+ * 默认值为"-requirements.txt"。
+ */
+ private static final String REQUIREMENTS_SUFFIX = "-requirements.txt";
+ /**
+ * 预定义的、不可修改的特定jar文件名集合,被视为
+ * "安全"或"始终包含"的文件。这些jar文件通常在
+ * 处理或压缩操作中免于排除过滤。
+ * 该集合包含以下jar标识符:
+ * - "spring"
+ * - "logback-core"
+ * - "tomcat"
+ * 该变量用于根据jar文件名决定是否包含特定jar文件的操作中。
+ * 它作为应用程序关键或必要jar的白名单。
+ */
+ private static final Set WHITE_LIST_JARS = Sets.newHashSet("spring", "logback-core", "tomcat");
+
+ /**
+ * 通过排除和包含指定的条目来压缩给定的源JAR文件,并将结果写入目标JAR文件。
+ * 它处理源JAR的条目,应用排除和包含规则,还可以将某些条目提取到指定的目录中。
+ * 在此过程中创建一个临时文件,成功完成后将其重命名为目标JAR文件。
+ *
+ * @param serviceName 正在处理的服务名称,主要用于日志记录和创建其他相关文件。
+ * @param sourceJar 要压缩的源JAR文件。
+ * @param includes 指定应保留哪些条目的包含模式集合。可能会自动添加额外的默认包含项。
+ * @param exclusions 指定应排除哪些条目的排除模式集合。
+ * @param targetJar 将写入压缩JAR的文件。
+ * @param libDir 某些被排除的条目可能被提取到的目录(如适用)。
+ */
+ public static void compress(String serviceName, File sourceJar, Set includes, Set exclusions, File targetJar, String libDir) {
+ includes.addAll(WHITE_LIST_JARS);
+ File tempJar = new File(targetJar.getAbsolutePath() + ".tmp");
+ Set excludedJars = new HashSet<>();
+
+ if (processJarEntries(sourceJar, tempJar, includes, exclusions, libDir, excludedJars)) {
+ finalizeCompression(serviceName, targetJar, tempJar, excludedJars, libDir);
+ } else {
+ boolean delete = tempJar.delete();
+ System.out.println("删除临时文件:{" + delete + "}");
+ }
+ }
+
+/**
+ * 处理源JAR文件中的条目以生成临时JAR文件,
+ * 同时根据包含和排除规则过滤条目。如果需要,
+ * 还会将指定的JAR条目提取到库目录中。
+ *
+ * @param sourceJar 要处理的源JAR文件
+ * @param tempJar 要创建的临时JAR文件
+ * @param includes 定义要包含的条目的模式集合
+ * @param exclusions 定义要排除的条目的模式集合
+ * @param libDir 特定JAR应该被提取到的目录,如果不需要提取则为null
+ * @param excludedJars 用于存储被排除的JAR条目名称的集合
+ * @return 如果处理成功完成则返回true,否则返回false
+ */
+ private static boolean processJarEntries(File sourceJar, File tempJar, Set includes,
+ Set exclusions, String libDir, Set excludedJars) {
+ try (JarFile jar = new JarFile(sourceJar);
+ JarOutputStream tempJarStream = new JarOutputStream(Files.newOutputStream(tempJar.toPath()))) {
+ for (Enumeration entries = jar.entries(); entries.hasMoreElements(); ) {
+ JarEntry entry = entries.nextElement();
+ String entryName = entry.getName();
+
+ if (shouldExcludeEntry(entryName, includes, exclusions)) {
+ if (libDir != null && !libDir.isEmpty()) {
+ extractJarToLib(jar, entry, libDir, excludedJars);
+ }
+ continue;
+ }
+
+ copyEntryToJar(jar, entry, tempJarStream);
+ }
+ return true;
+ } catch (Exception ex) {
+ System.out.println("处理异常:" + ex.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 根据预定义的标准确定是否应排除特定的jar条目。
+ * 该方法评估条目是否属于BOOT-INF/lib目录,是否具有".jar"扩展名,
+ * 以及是否不满足由includes和exclusions集合定义的包含/排除条件。
+ *
+ * @param entryName 要检查的jar条目名称
+ * @param includes jar包含条件的集合
+ * @param exclusions jar排除条件的集合
+ * @return 如果应排除该条目则返回true,否则返回false
+ */
+ private static boolean shouldExcludeEntry(String entryName, Set includes, Set exclusions) {
+ if (!entryName.startsWith(BOOT_INF_LIB)) {
+ return false;
+ }
+ String jarName = entryName.substring(entryName.lastIndexOf("/") + 1);
+ return jarName.endsWith(JAR_EXTENSION) && !isWhiteJar(jarName, includes, exclusions);
+ }
+
+ /**
+ * 从JAR文件中提取指定的JAR条目到指定的库目录。
+ * 如果条目对应于JAR文件且在库目录中尚不存在,
+ * 则将其复制到该目录,并将其名称添加到被排除的JAR集合中。
+ *
+ * @param jar 包含要提取的条目的JAR文件
+ * @param entry 要提取的JAR条目
+ * @param libDir 提取的JAR文件将被复制到的目录
+ * @param excludedJars 处理过程中被排除的JAR文件名的集合
+ * @throws IOException 如果在从文件系统读取或写入时发生I/O错误
+ */
+ private static void extractJarToLib(JarFile jar, JarEntry entry, String libDir,
+ Set excludedJars) throws IOException {
+ String jarName = entry.getName().substring(entry.getName().lastIndexOf("/") + 1);
+ File outputFile = new File(libDir, jarName);
+
+ if (!outputFile.exists()) {
+ FileUtil.touch(outputFile);
+ }
+
+ try (InputStream input = jar.getInputStream(entry);
+ FileOutputStream output = new FileOutputStream(outputFile)) {
+ IOUtils.copy(input, output);
+ excludedJars.add(jarName);
+ System.out.println("Excluding: " + outputFile.getAbsolutePath());
+ }
+ }
+
+ /**
+ * 将单个{@link JarEntry}从源{@link JarFile}复制到目标{@link JarOutputStream}。
+ *
+ * @param jar 包含要复制的条目的源{@link JarFile}
+ * @param entry 要复制的{@link JarEntry}
+ * @param output 将写入条目的目标{@link JarOutputStream}
+ * @throws IOException 如果在复制过程中发生I/O错误
+ */
+ private static void copyEntryToJar(JarFile jar, JarEntry entry, JarOutputStream output) throws IOException {
+ try (InputStream input = jar.getInputStream(entry)) {
+ output.putNextEntry(entry);
+ IOUtils.copy(input, output);
+ }
+ }
+
+ /**
+ * 通过处理目标和临时JAR文件完成压缩过程,
+ * 并可选择将排除的JAR列表写入需求文件。
+ *
+ * @param serviceName 与压缩过程关联的服务名称
+ * @param targetJar 要创建或更新的目标JAR文件
+ * @param tempJar 压缩过程中使用的临时JAR文件
+ * @param excludedJars 压缩过程中排除的JAR文件名的集合
+ * @param libDir 存储库文件的目录
+ */
+ private static void finalizeCompression(String serviceName, File targetJar, File tempJar, Set excludedJars, String libDir) {
+ boolean deleteTarget = targetJar.delete();
+ System.out.println("删除目标文件结果:" + deleteTarget);
+ boolean rename = tempJar.renameTo(targetJar);
+ System.out.println("临时文件重命名结果:" + rename);
+
+ if (CollectionUtil.isNotEmpty(excludedJars)) {
+ File requirementsFile = new File(libDir, serviceName + REQUIREMENTS_SUFFIX);
+ FileUtil.writeLines(excludedJars, requirementsFile, UTF_8);
+ }
+ }
+
+ /**
+ * 确定给定的jar文件名是否匹配任何指定的包含模式
+ * 且不是排除集的一部分。
+ *
+ * @param jarName 要检查的jar文件名
+ * @param includes 表示包含模式的字符串集合;如果jar名称包含
+ * 这些模式中的任何一个,则被视为匹配
+ * @param exclusions 表示要排除的jar名称的字符串集合;如果jar名称
+ * 存在于此集合中,则被视为排除
+ * @return 如果jar名称匹配任何包含模式且不是
+ * 排除集的一部分,则返回{@code true},否则返回{@code false}
+ */
+ private static boolean isWhiteJar(String jarName, Set includes, Set exclusions) {
+ if (exclusions.contains(jarName)) {
+ return false;
+ }
+ for (String include : includes) {
+ if (jarName.contains(include)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 使用提供的密码将指定的原始JAR文件加密为加密的JAR文件。
+ * 支持包含和排除模式,用于选择性地加密JAR中的文件条目。
+ *
+ * @param rawFile 要加密的输入JAR文件
+ * @param xjarFile 输出的加密JAR文件
+ * @param pass 用于加密的密码,如果为空则生成默认密码
+ * @param includes 指定要包含在加密中的文件条目的包含模式数组
+ * @param excludes 指定要从加密中排除的文件条目的排除模式数组
+ * @throws Exception 如果在加密过程中发生错误
+ */
+ public static void encrypt(File rawFile, File xjarFile, String pass, String[] includes, String[] excludes) throws Exception {
+ XEncryption xe = XCryptos.encryption().from(rawFile.getAbsolutePath());
+ xe.use((pass == null || pass.trim().isEmpty() || pass.startsWith("默认")) ? "0755isa" : pass);
+ if (includes != null) {
+ for (String include : includes) {
+ xe.include(include);
+ }
+ }
+ if (excludes != null) {
+ for (String exclude : excludes) {
+ xe.exclude(exclude);
+ }
+ }
+ xe.to(xjarFile);
+ }
+}
+```
+
+这个工具类提供了两个主要功能:
+
+#### JAR 包压缩功能
+
+`compress` 方法实现了 JAR 瘦身功能:
+
+- 根据包含列表和排除列表过滤 JAR 中的依赖
+- 将被排除的依赖提取到指定的库目录
+- 生成一个记录排除依赖的需求文件
+- 创建一个只包含必要依赖的精简 JAR 文件
+
+#### JAR 包加密功能
+
+`encrypt` 方法利用 XJar 库实现了 JAR 加密:
+
+- 支持指定加密密码
+- 通过包含和排除模式选择性地加密 JAR 中的内容
+- 生成加密后的 XJar 文件
+
+### 2. 主应用
+
+```java
+package com.mangmang;
+
+import cn.hutool.core.io.FileUtil;
+import org.dom4j.Document;
+import org.dom4j.Element;
+import org.dom4j.io.SAXReader;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * SpringBoot JAR文件压缩和加密工具的主类。
+ *
+ * 此类提供了一个基于XJar加密技术的框架,用于压缩和加密JAR文件。
+ * 它支持根据XML配置文件中的规则包含或排除特定的依赖项,并将结果保存为加密的JAR文件。
+ * 程序从原始JAR文件开始,根据需要压缩它们,然后应用加密。
+ *
+ */
+public class SpringBootJarSlimEncryptApplication {
+
+ /**
+ * 类的Logger实例,用于记录程序执行过程中的各种级别的日志信息。
+ */
+ private static final Logger LOGGER = Logger.getLogger(SpringBootJarSlimEncryptApplication.class.getName());
+
+ /**
+ * 包含程序所有配置常量的内部静态类。
+ * 这些常量定义了输入/输出目录、加密设置和其他程序所需的各种配置参数。
+ */
+ private static class Config {
+ /**
+ * 存储依赖项XML文件的目录路径。
+ * 默认为"./xml/",可通过系统属性"xml.dir"覆盖。
+ */
+ static final String DEPENDENCY_XML_DIR = System.getProperty("xml.dir", "./config/xml/");
+
+ /**
+ * 存储原始JAR文件的目录路径。
+ * 默认为"./rawJars/",可通过系统属性"raw.dir"覆盖。
+ */
+ static final String RAW_JAR_DIR = System.getProperty("raw.dir", "./config/rawJars/");
+
+ /**
+ * 存储压缩后JAR文件的目录路径。
+ * 默认为"./compressJars/",可通过系统属性"compress.dir"覆盖。
+ */
+ static final String COMPRESS_JAR_DIR = System.getProperty("compress.dir", "./config/compressJars/");
+
+ /**
+ * 存储提取的库文件的目录路径。
+ * 默认为"./libs/",可通过系统属性"libs.dir"覆盖。
+ */
+ static final String LIB_DIR = System.getProperty("libs.dir", "./config/libs/");
+
+ /**
+ * 存储加密后的XJar文件的目录路径。
+ * 默认为"./xJars/",可通过系统属性"xjar.dir"覆盖。
+ */
+ static final String X_JAR_DIR = System.getProperty("xjar.dir", "./config/xJars/");
+
+ /**
+ * 控制是否启用压缩功能的标志。
+ * 默认为true,可通过系统属性"compress.enable"覆盖。
+ */
+ static final boolean COMPRESS_ENABLED = Boolean.parseBoolean(System.getProperty("compress.enable", "true"));
+
+ /**
+ * 定义要包含在XJar加密中的文件模式数组。
+ * 这些文件将在加密过程中被加密。
+ */
+ static final String[] X_JAR_INCLUDES = new String[]{"/com/mangmang/**", "*.yaml", "*.yml", "mapper/**.xml"};
+
+ /**
+ * 定义要从XJar加密中排除的文件模式数组。
+ * 这些文件在加密过程中将保持未加密状态。
+ */
+ static final String[] X_JAR_EXCLUDES = new String[]{"/com/mangmang/pinyin/**"};
+
+ /**
+ * 用于X_JAR文件加密的密码。
+ */
+ static final String ENCRYPTION_PASSWORD = "0755isa";
+
+ /**
+ * 包含要排除的依赖项列表的XML文件的名称。
+ */
+ static final String EXCLUSIONS_XML = "config/exclusions.xml";
+
+ /**
+ * 包含要包含的依赖项列表的XML文件的名称。
+ */
+ static final String INCLUDES_XML = "config/includes.xml";
+ }
+ /**
+ * 应用程序的主入口点。
+ * 4. 处理所有服务,根据需要进行压缩和加密
+ *
+ *
+ * @param args 命令行参数,当前未使用
+ */
+ public static void main(String[] args) {
+ try {
+ //1. 确保所有必需的目录存在
+ ensureDirectoriesExist();
+ //2. 查找所有原始JAR服务
+ Set serviceList = findAllRawJarServices();
+ //3. 从XML配置文件加载排除和包含的JAR
+ Set exclusionJars = loadJarsFromXml(new File(Config.DEPENDENCY_XML_DIR + File.separator + Config.EXCLUSIONS_XML));
+ Set includedJars = loadJarsFromXml(new File(Config.DEPENDENCY_XML_DIR + File.separator + Config.INCLUDES_XML));
+ processAllServices(serviceList, includedJars, exclusionJars);
+ } catch (Exception e) {
+ LOGGER.log(Level.SEVERE, "处理JAR文件过程中发生错误", e);
+ }
+ }
+
+ /**
+ * 处理提供的服务列表,对每个服务应用包含和排除规则,然后处理它们。
+ *
+ * 对于列表中的每个服务,此方法会调用{@link #processService}方法,
+ * 并记录任何可能发生的错误。
+ *
+ *
+ * @param serviceList 要处理的服务名称集合
+ * @param includedJars 定义要包含的JAR的规则集合
+ * @param exclusionJars 定义要排除的JAR的规则集合
+ */
+ private static void processAllServices(Set serviceList, Set includedJars, Set exclusionJars) {
+ for (String service : serviceList) {
+ LOGGER.info("开始处理" + service);
+ try {
+ processService(service, includedJars, exclusionJars);
+ } catch (Exception e) {
+ LOGGER.log(Level.SEVERE, "处理服务 " + service + " 时发生错误", e);
+ }
+ }
+ }
+
+ /**
+ * 处理单个服务,应用压缩(如果启用)和加密操作。
+ *
+ * 根据配置,此方法将执行以下操作之一:
+ * - 如果启用了压缩:压缩原始JAR文件,然后加密压缩后的JAR
+ * - 如果禁用了压缩:直接加密原始JAR文件
+ *
+ *
+ * @param service 要处理的服务的名称
+ * @param includedJars 要包含在压缩JAR中的JAR文件集合
+ * @param exclusionJars 要从压缩JAR中排除的JAR文件集合
+ * @throws Exception 如果在处理过程中发生错误
+ */
+ private static void processService(String service, Set includedJars, Set exclusionJars) throws Exception {
+ File rawJarFile = new File(Config.RAW_JAR_DIR + File.separator + service + ".jar");
+ File xjarFile = new File(Config.X_JAR_DIR + File.separator + service + ".xjar");
+
+ if (Config.COMPRESS_ENABLED) {
+ File compressedJarFile = new File(Config.COMPRESS_JAR_DIR + File.separator + service + "-compress.jar");
+ JarUtil.compress(service, rawJarFile, includedJars, exclusionJars, compressedJarFile, Config.LIB_DIR);
+ JarUtil.encrypt(compressedJarFile, xjarFile, Config.ENCRYPTION_PASSWORD, Config.X_JAR_INCLUDES, Config.X_JAR_EXCLUDES);
+ if (xjarFile.exists()) {
+ LOGGER.info("压缩并加密" + service + "成功");
+ }
+ } else {
+ JarUtil.encrypt(rawJarFile, xjarFile, Config.ENCRYPTION_PASSWORD, Config.X_JAR_INCLUDES, Config.X_JAR_EXCLUDES);
+ if (xjarFile.exists()) {
+ LOGGER.info("加密" + service + "成功");
+ }
+ }
+ }
+
+
+
+ /**
+ * 查找RAW_JAR_DIR目录中的所有JAR文件,并返回不带.jar扩展名的服务名称集合。
+ *
+ * 此方法扫描配置的原始JAR目录,查找所有以.jar结尾的文件,
+ * 然后从文件名中删除.jar扩展名以获取服务名称。
+ *
+ *
+ * @return 原始JAR目录中找到的服务名称的集合(不带.jar扩展名)
+ */
+ private static Set findAllRawJarServices() {
+ File dir = new File(Config.RAW_JAR_DIR);
+ File[] files = dir.listFiles();
+ if (files == null) {
+ return Collections.emptySet();
+ }
+
+ return Arrays.stream(files)
+ .filter(file -> file.getName().endsWith(".jar"))
+ .map(file -> file.getName().replace(".jar", ""))
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * 确保所有必需的目录存在,如果不存在则创建它们。
+ *
+ * 此方法检查配置中定义的所有目录,并在必要时创建它们。
+ * 这些目录包括:
+ * - 依赖项XML目录
+ * - 原始JAR目录
+ * - 压缩JAR目录
+ * - 库目录
+ * - X_JAR目录
+ *
+ */
+ private static void ensureDirectoriesExist() {
+ String[] dirs = {
+ Config.DEPENDENCY_XML_DIR,
+ Config.RAW_JAR_DIR,
+ Config.COMPRESS_JAR_DIR,
+ Config.LIB_DIR,
+ Config.X_JAR_DIR
+ };
+
+ for (String dir : dirs) {
+ File file = new File(dir);
+ if (!file.exists()) {
+ FileUtil.mkdir(file);
+ }
+ }
+ }
+
+
+ /**
+ * 从XML文件中加载JAR依赖项列表。
+ *
+ * 此方法解析指定的XML文件,查找依赖项元素,并提取artifactId和可选的version,
+ * 以构建JAR文件名列表。
+ *
+ *
+ * @param xmlFile 包含依赖项列表的XML文件
+ * @return 从XML文件中提取的JAR名称集合
+ * @throws SAXException 如果在解析XML时发生错误
+ */
+ private static Set loadJarsFromXml(File xmlFile) throws SAXException {
+ Set jars = new HashSet<>();
+ if (!xmlFile.exists()) {
+ return jars;
+ }
+
+ SAXReader saxReader = new SAXReader();
+ saxReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ saxReader.setEncoding("UTF-8");
+
+ try {
+ Document document = saxReader.read(xmlFile);
+ Element rootElement = document.getRootElement();
+
+ if (!rootElement.hasContent()) {
+ return jars;
+ }
+
+ List dependencies = rootElement.elements("dependency");
+ if (dependencies.isEmpty()) {
+ return jars;
+ }
+
+ for (Element element : dependencies) {
+ Element artifactId = element.element("artifactId");
+ String artifactIdText = artifactId.getText();
+ Element version = element.element("version");
+
+ String jarName;
+ if (Objects.nonNull(version)) {
+ String versionText = version.getText();
+ jarName = artifactIdText + "-" + versionText + ".jar";
+ } else {
+ jarName = artifactIdText;
+ }
+ jars.add(jarName);
+ }
+ } catch (Exception e) {
+ LOGGER.log(Level.WARNING, "解析XML文件 " + xmlFile.getName() + " 时发生错误", e);
+ }
+
+ return jars;
+ }
+}
+```
+
+这是工具的主应用类,提供了完整的命令行接口来处理 JAR 文件的压缩和加密:
+
+主要功能:
+
+- 通过配置常量定义输入/输出目录、加密设置等
+- 支持从 XML 配置文件加载要包含和排除的依赖项
+- 批量处理多个 JAR 文件
+- 支持通过系统属性覆盖默认配置
+
+主要配置参数:
+
+- `xml.dir`: 依赖项 XML 文件目录(默认: `./config/xml/`)
+- `raw.dir`: 原始 JAR 文件目录(默认: `./config/rawJars/`)
+- `compress.dir`: 压缩后 JAR 文件目录(默认: `./config/compressJars/`)
+- `libs.dir`: 提取的库文件目录(默认: `./config/libs/`)
+- `xjar.dir`: 加密后的 XJar 文件目录(默认: `./config/xJars/`)
+- `compress.enable`: 是否启用压缩功能(默认: `true`)
+
+### 3. pom.xml
+
+```xml
+
+ 4.0.0
+
+ spring-boot-jar-slim-encrypt
+ jar
+
+ spring-boot-jar-slim-encrypt
+ http://maven.apache.org
+
+
+ UTF-8
+
+
+
+
+
+ jitpack.io
+ https://jitpack.io
+
+
+
+
+
+
+ com.github.core-lib
+ xjar
+ 4.0.0
+
+
+ org.dom4j
+ dom4j
+ 2.1.4
+
+
+ cn.hutool
+ hutool-core
+ 5.8.25
+
+
+ org.apache.commons
+ commons-lang3
+ 3.0
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.2.0
+
+
+
+
+
+
+ com.mangmang.SpringBootJarSlimEncryptApplication
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 3.3.0
+
+
+ make-assembly
+
+
+ package
+
+
+ single
+
+
+
+
+
+ jar-with-dependencies
+
+
+
+
+ com.mangmang.SpringBootJarSlimEncryptApplication
+
+
+
+
+
+
+
+
+```
+
+
+
+## 使用步骤
+
+### 步骤 1: 集成自定义类加载器
+
+在你的 Spring Boot 项目中添加自定义类加载器依赖:
+
+```xml
+
+ com.mangmang
+ spring-boot-custom-classloader
+ 1.0.0
+
+```
+
+### 步骤 2: 构建 Spring Boot 应用
+
+正常构建你的 Spring Boot 应用:
+
+```bash
+mvn clean package
+```
+
+### 步骤 3: 配置依赖排除和包含规则
+
+>默认路径为根目录下的./config/xml/
+
+创建两个 XML 文件以定义要排除和包含的依赖项:
+
+**exclusions.xml**(要排除的依赖):
+
+```xml
+
+
+
+
+ cn.hutool
+ hutool-all
+ 5.8.26
+
+
+```
+
+**includes.xml**(要保留的依赖):
+
+```xml
+
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ 2.10.3
+
+
+```
+
+### 步骤 4: 执行 JAR 瘦身和加密工具
+
+将你的 Spring Boot JAR 文件放入 `./config/rawJars/` 目录,然后运行瘦身加密工具:
+
+```bash
+java -jar spring-boot-jar-slim-encrypt.jar
+```
+
+你可以通过系统属性覆盖默认配置:
+
+```bash
+java -Dcompress.enable=true -Dlibs.dir=/custom/libs/path -jar spring-boot-jar-slim-encrypt.jar
+```
+
+### 步骤 5: 部署和运行
+
+#### 1. 精简JAR启动指令
+```shell
+java -Dexternal.jars.path.path=/path/to/jars -jar your-application.jar
+```
+
+#### 2. 加密JAR启动指令
+1. 需要进入xjar.dir(默认: `./config/xJars/`)对应目录
+2. 使用go build xjar.go 编译
+3. 启动win10示例
+ ```powershell
+ .\xjar.exe java "-Dexternal.jars.path=..\libs" -jar .\spring-boot-thin-launcher-1.0-SNAPSHOT.xjar
+ ```
+4. linux 示例
+ ```shell
+ ./xjar java -Xms256m -Xmx1024m -Dexternal.jars.path=./libs -jar /path/to/encrypted.jar
+ ```
+
+## 技术原理解析
+
+### JAR 瘦身原理
+
+1. 扫描 Spring Boot JAR 中的 `BOOT-INF/lib` 目录
+2. 根据配置的排除和包含规则过滤依赖
+3. 将被排除的依赖提取到外部目录
+4. 创建一个不包含被排除依赖的新 JAR 文件
+
+### JAR 加密原理
+
+1. 使用 XJar 库实现 JAR 文件内容的加密
+2. 只加密指定的文件模式(如 Java 类文件、配置文件等)
+3. 避免加密某些需要保持明文的资源
+
+### 自定义类加载器原理
+
+1. 在 Spring Boot 应用启动时初始化自定义类加载器
+2. 扫描指定目录下的 JAR 文件
+3. 使用反射机制将 JAR 文件 URL 添加到当前类加载器
+
+## 方案优势
+
+1. **减小 JAR 体积**:将大型依赖库外置,显著减小主 JAR 文件体积
+2. **提高安全性**:通过加密保护核心业务逻辑和敏感配置
+3. **灵活配置**:支持通过 XML 配置和系统属性灵活控制瘦身和加密过程
+4. **无缝集成**:与 Spring Boot 应用无缝集成,无需修改应用代码
+
+## 注意事项
+
+1. 确保加密密码安全保存,丢失密码将导致无法运行加密的 JAR
+2. 测试瘦身后的应用,确保所有需要的依赖都能正确加载
+3. 部署时必须将提取的依赖库和加密后的 JAR 一起部署
+4. 启动应用时必须指定外部库路径参数
+
+## 结论
+
+通过结合 JAR 瘦身、JAR 加密和自定义类加载器,我们成功构建了一套完整的 Spring Boot 应用优化和保护方案。这不仅有效减小了部署包的体积,还提高了应用的安全性,为企业级 Spring Boot 应用的部署提供了一种实用的解决方案。
+
+在实际应用中,可以根据具体需求调整配置参数,以达到最佳的平衡点。例如,可以根据应用规模和安全需求调整要排除的依赖和加密的文件模式。
+
+这套工具实现了将原本臃肿的 Spring Boot 应用拆分为核心加密 JAR 和外部依赖库的方案,使得应用部署更加灵活,也为应用分发和更新提供了更多可能性。
+
+------
+
+*注:本方案适用于需要保护核心业务逻辑或减小部署包体积的 Spring Boot 应用。对于简单应用或开源项目,可能不必使用这么复杂的方案。*
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/programming/backend/java/功能整理/assets/20210721105228.png b/src/programming/backend/java/功能整理/assets/20210721105228.png
new file mode 100644
index 0000000..ed034a9
Binary files /dev/null and b/src/programming/backend/java/功能整理/assets/20210721105228.png differ
diff --git a/src/programming/backend/java/框架/SpringSecurity.md b/src/programming/backend/java/框架/SpringSecurity.md
new file mode 100644
index 0000000..847fc48
--- /dev/null
+++ b/src/programming/backend/java/框架/SpringSecurity.md
@@ -0,0 +1,3015 @@
+---
+icon: simple-icons:spring
+title: SpringSecurity
+date: 2025-05-07
+category:
+ - JAVA
+tag:
+ - Spring
+ - SpringSecurity
+star: 10
+description: 若依的SpringSecurity案例
+# 此页面会在文章列表置顶
+sticky: true
+---
+若依的SpringSecurity案例
+
+
+
+# 一、准备
+
+## 1. 导入依赖
+
+### 1. 父依赖
+
+```xml
+
+
+ 4.0.0
+
+ 01-druid
+ 02-SpringSecurity
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.3.0.RELEASE
+
+
+
+ com.mangmang
+ learn-test
+ 0.0.1-SNAPSHOT
+ learn-test
+ pom
+
+ 依赖版本控制
+
+
+ 1.8
+ 3.4.3.1
+ 3.4.1
+ 2.3
+ 1.2.4
+ 0.9.1
+ 2.0.8
+ 1.21
+ 1.5.1-RELEASE
+
+
+
+
+
+
+
+ eu.bitwalker
+ UserAgentUtils
+ ${userAgentUtils.version}
+
+
+
+ com.github.xiaoymin
+ knife4j-spring-boot-starter
+ ${knife4j.version}
+
+
+
+
+ io.jsonwebtoken
+ jjwt
+ ${jjwt.version}
+
+
+
+
+ com.github.jeffreyning
+ mybatisplus-plus
+ ${mybatisplus-plus.version}
+
+
+
+
+ com.baomidou
+ mybatis-plus-boot-starter
+ ${mybatis-plus.version}
+
+
+
+
+ com.baomidou
+ mybatis-plus-generator
+ ${mybatis-plus-generator.version}
+
+
+
+
+ org.apache.velocity
+ velocity-engine-core
+ ${mybatis-velocity.version}
+
+
+
+
+ com.alibaba
+ druid-spring-boot-starter
+ ${druid.version}
+
+
+
+
+
+
+
+ com.alibaba
+ fastjson
+ 1.2.75
+
+
+
+
+
+ cn.hutool
+ hutool-all
+ 5.5.8
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
+
+
+
+ mysql
+ mysql-connector-java
+ runtime
+
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ 2.3.0.RELEASE
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
+
+
+```
+
+
+
+### 2. 模块使用依赖
+
+```xml
+
+
+
+ learn-test
+ com.mangmang
+ 0.0.1-SNAPSHOT
+
+ 4.0.0
+
+ 02-SpringSecurity
+
+
+ 8
+ 8
+
+
+
+
+
+ com.github.jeffreyning
+ mybatisplus-plus
+
+
+
+
+ eu.bitwalker
+ UserAgentUtils
+
+
+
+
+ com.github.xiaoymin
+ knife4j-spring-boot-starter
+
+
+
+
+ io.jsonwebtoken
+ jjwt
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+
+ com.baomidou
+ mybatis-plus-boot-starter
+
+
+
+
+ com.baomidou
+ mybatis-plus-generator
+
+
+
+
+ org.apache.velocity
+ velocity-engine-core
+
+
+
+
+ com.alibaba
+ druid-spring-boot-starter
+
+
+
+
+
+
+
+ src/main/java
+
+ **/*.xml
+
+ false
+
+
+
+
+
+```
+
+## 2. 配置YML
+
+### 1. application.yml
+
+```yml
+spring:
+ profiles:
+ active: local
+
+server:
+ port: 8000
+
+swagger:
+ enabled: true
+ pathMapping:
+
+token:
+ header: token
+ secret: asdaswqesdzxwr3123
+ expire-time: 30
+```
+
+### 2. application-local.yml
+
+```yml
+spring:
+ datasource:
+ # 配置项目数据源为druid
+ type: com.alibaba.druid.pool.DruidDataSource
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ druid:
+ url: jdbc:mysql://127.0.0.1:3306/book?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+ username: root
+ password: root
+ initial-size: 5
+ # 最小连接池数量
+ min-idle: 10
+ # 最大连接池数量
+ max-active: 20
+ # 配置获取连接等待超时的时间
+ max-wait: 60000
+ # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+ time-between-eviction-runs-millis: 60000
+ # 配置一个连接在池中最小生存的时间,单位是毫秒
+ min-evictable-idle-time-millis: 300000
+ # 配置一个连接在池中最大生存的时间,单位是毫秒
+ max-evictable-idle-time-millis: 900000
+ # 配置检测连接是否有效
+ validation-query: SELECT 1 FROM DUAL
+ # 官方推荐配置
+ test-while-idle: true
+ test-on-borrow: false
+ test-on-return: false
+
+ #监控配置
+ web-stat-filter:
+ # 是否启用StatFilter默认值true
+ enabled: true
+ stat-view-servlet:
+ enabled: true
+ # 设置白名单,不填则允许所有访问
+ allow:
+ url-pattern: /druid/*
+ # 控制台管理用户名和密码
+ login-username: root
+ login-password: liujing
+ filter:
+ stat:
+ enabled: true
+ # 慢SQL记录
+ log-slow-sql: true
+ slow-sql-millis: 1000
+ merge-sql: true
+ wall:
+ config:
+ multi-statement-allow: true
+ redis:
+ port: 6379
+ host: 127.0.0.1
+
+
+# MyBatis Plus配置
+mybatis-plus:
+ # 搜索指定包别名
+ typeAliasesPackage: com.mangmang.**.domain
+ # 配置mapper的扫描,找到所有的mapper.xml映射文件
+ mapperLocations: classpath*:mapper/**/*Mapper.xml
+ configuration:
+ # SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
+ log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
+ #开启二级缓存
+ cache-enabled: true
+ #配置默认的执行器
+ default-executor-type: reuse
+ # 开启驼峰命名
+ map-underscore-to-camel-case: true
+ # 允许 JDBC 支持自动生成主键
+ use-generated-keys: true
+ #关闭logo
+ global-config:
+ banner: false
+ db-config:
+ # 全局逻辑删除的实体字段名
+ logic-delete-field: isDeleted
+ # 逻辑已删除值(默认为 1)
+ logic-delete-value: 1
+ # 逻辑未删除值(默认为 0)
+ logic-not-delete-value: 0
+```
+
+## 3. 配置Mybatis-Plus
+
+```java
+/**
+ * @Date: 2021-08-10-17:31
+ * @Author lj
+ */
+@Configuration
+@EnableTransactionManagement
+@MapperScan("com.mangmang.security.mapper")
+public class MybatisPlusConfig {
+ @Bean
+ public MybatisPlusInterceptor mybatisPlusInterceptor() {
+ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+ // 分页插件
+ interceptor.addInnerInterceptor(paginationInnerInterceptor());
+ // 乐观锁插件
+ interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
+ // 阻断插件
+ interceptor.addInnerInterceptor(blockAttackInnerInterceptor());
+ return interceptor;
+ }
+ /**
+ * 分页插件,自动识别数据库类型 https://baomidou.com/guide/interceptor-pagination.html
+ */
+ public PaginationInnerInterceptor paginationInnerInterceptor() {
+ PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
+ // 设置数据库类型为mysql
+ paginationInnerInterceptor.setDbType(DbType.MYSQL);
+ // 设置最大单页限制数量,默认 500 条,-1 不受限制
+ paginationInnerInterceptor.setMaxLimit(-1L);
+ return paginationInnerInterceptor;
+ }
+ /**
+ * 乐观锁插件 https://baomidou.com/guide/interceptor-optimistic-locker.html
+ */
+ public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() {
+ return new OptimisticLockerInnerInterceptor();
+ }
+ /**
+ * 如果是对全表的删除或更新操作,就会终止该操作 https://baomidou.com/guide/interceptor-block-attack.html
+ */
+ public BlockAttackInnerInterceptor blockAttackInnerInterceptor() {
+ return new BlockAttackInnerInterceptor();
+ }
+
+}
+```
+
+## 4. 配置Knife4j
+
+```java
+/**
+ * @Date: 2021-08-10-18:14
+ * @Author lj
+ */
+@EnableSwagger2WebMvc
+@Configuration
+@EnableKnife4j
+public class SwaggerConfig {
+ /**
+ * 是否开启swagger
+ */
+ @Value("${swagger.enabled}")
+ private boolean enabled;
+ /**
+ * 设置请求的统一前缀
+ */
+ @Value("${swagger.pathMapping}")
+ private String pathMapping;
+ /**
+ * 创建API
+ * .pathMapping("test")设置访问路径统一前缀
+ */
+ @Bean
+ public Docket createRestApi() {
+ return new Docket(DocumentationType.SWAGGER_2)
+ // 是否启用Swagger
+ .enable(enabled)
+ // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)
+ .apiInfo(apiInfo())
+ // 设置哪些接口暴露给Swagger展示
+ .select()
+ // 扫描所有有注解的api,用这种方式更灵活
+ .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
+ .paths(PathSelectors.any())
+ .build()
+ /* 设置安全模式,swagger可以设置访问token */
+ .securitySchemes(securitySchemes())
+ .securityContexts(securityContexts())
+ .pathMapping(pathMapping);
+ }
+
+ /**
+ * 安全模式,这里指定token通过Authorization头请求头传递
+ */
+ private List securitySchemes() {
+ List apiKeyList = new ArrayList<>();
+ apiKeyList.add(new ApiKey("Authorization", "Authorization", "header"));
+ return apiKeyList;
+ }
+
+ /**
+ * 安全上下文
+ */
+ private List securityContexts() {
+ List securityContexts = new ArrayList<>();
+ securityContexts.add(
+ SecurityContext.builder()
+ .securityReferences(defaultAuth())
+ .forPaths(PathSelectors.regex("^(?!auth).*$"))
+ .build());
+ return securityContexts;
+ }
+
+ /**
+ * 默认的安全上引用
+ */
+ private List defaultAuth() {
+ AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
+ AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
+ authorizationScopes[0] = authorizationScope;
+ List securityReferences = new ArrayList<>();
+ securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
+ return securityReferences;
+ }
+
+ /**
+ * 添加摘要信息
+ */
+ private ApiInfo apiInfo() {
+ // 用ApiInfoBuilder进行定制
+ return new ApiInfoBuilder()
+ // 设置标题
+ .title("标题:后端接口")
+ // 描述
+ .description("描述:测试")
+ // 作者信息
+ .contact(new Contact("氓氓编程", null, null))
+ // 版本
+ .version("版本号:" + "0.0.1")
+ .build();
+ }
+}
+```
+
+## 5. 配置RedisTemplate序列化规则
+
+```java
+/**
+ * @author a3621
+ */
+@EnableCaching
+@Configuration
+public class RedisConfig extends CachingConfigurerSupport {
+ @Bean
+ public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
+ // 创建一个模板
+ RedisTemplate template = new RedisTemplate<>();
+ //创建一个字符串序列化器
+ RedisSerializer redisSerializer = new StringRedisSerializer();
+ //创建Jackson 序列化器
+ Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
+ //Jackson得ObjectMapper对象
+ ObjectMapper om = new ObjectMapper();
+ //这将使所有成员字段无需进一步注释即可序列化,而不仅仅是公共字段(默认设置)。
+ om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+ //开启后序列化得类会带上全类名
+ om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+ //om配置给jackson2JsonRedisSerializer
+ jackson2JsonRedisSerializer.setObjectMapper(om);
+ //RedisTemplate配置factory
+ template.setConnectionFactory(factory);
+ //key序列化方式
+ template.setKeySerializer(redisSerializer);
+ //value序列化
+ template.setValueSerializer(jackson2JsonRedisSerializer);
+ //value hashmap序列化
+ template.setHashValueSerializer(jackson2JsonRedisSerializer);
+ //返回该模板
+ return template;
+ }
+
+ @Bean
+ public CacheManager cacheManager(RedisConnectionFactory factory) {
+ //创建一个字符串序列化器
+ RedisSerializer redisSerializer = new StringRedisSerializer();
+ //创建Jackson 序列化器
+ Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
+ //解决查询缓存转换异常的问题
+ ObjectMapper om = new ObjectMapper();
+ //这将使所有成员字段无需进一步注释即可序列化,而不仅仅是公共字段(默认设置)。
+ om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+ //开启后序列化得类会带上全类名
+ om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+ //om配置给jackson2JsonRedisSerializer
+ jackson2JsonRedisSerializer.setObjectMapper(om);
+ // 配置序列化(解决乱码的问题),过期时间600秒
+ RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
+ .entryTtl(Duration.ofSeconds(600))
+ .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
+ .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
+ .disableCachingNullValues();
+ return RedisCacheManager.builder(factory)
+ .cacheDefaults(config)
+ .build();
+ }
+}
+```
+
+## 6. sql(复制到文本命名为.sql,然后导入数据库)
+
+```sql
+/*
+ Navicat Premium Data Transfer
+
+ Source Server : 127.0.0.1
+ Source Server Type : MySQL
+ Source Server Version : 50730
+ Source Host : localhost:3306
+ Source Schema : book
+
+ Target Server Type : MySQL
+ Target Server Version : 50730
+ File Encoding : 65001
+
+ Date: 29/08/2021 18:07:23
+*/
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for sys_menu
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_menu`;
+CREATE TABLE `sys_menu` (
+ `menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
+ `menu_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '菜单名称',
+ `parent_id` bigint(20) NULL DEFAULT 0 COMMENT '父菜单ID',
+ `order_num` int(4) NULL DEFAULT 0 COMMENT '显示顺序',
+ `path` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '路由地址',
+ `component` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '组件路径',
+ `is_frame` int(1) NULL DEFAULT 1 COMMENT '是否为外链(0是 1否)',
+ `is_cache` int(1) NULL DEFAULT 0 COMMENT '是否缓存(0缓存 1不缓存)',
+ `menu_type` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
+ `visible` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
+ `status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
+ `perms` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限标识',
+ `icon` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '#' COMMENT '菜单图标',
+ `create_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '创建者',
+ `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+ `update_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '更新者',
+ `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+ `remark` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '备注',
+ PRIMARY KEY (`menu_id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1061 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜单权限表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of sys_menu
+-- ----------------------------
+INSERT INTO `sys_menu` VALUES (1, '系统管理', 0, 1, 'system', NULL, 1, 0, 'M', '0', '0', '', 'system', 'admin', '2021-04-30 17:27:28', '', NULL, '系统管理目录');
+INSERT INTO `sys_menu` VALUES (2, '系统监控', 0, 2, 'monitor', NULL, 1, 0, 'M', '0', '0', '', 'monitor', 'admin', '2021-04-30 17:27:28', '', NULL, '系统监控目录');
+INSERT INTO `sys_menu` VALUES (3, '系统工具', 0, 3, 'tool', NULL, 1, 0, 'M', '0', '0', '', 'tool', 'admin', '2021-04-30 17:27:28', '', NULL, '系统工具目录');
+INSERT INTO `sys_menu` VALUES (4, '若依官网', 0, 4, 'http://ruoyi.vip', NULL, 0, 0, 'M', '0', '0', '', 'guide', 'admin', '2021-04-30 17:27:28', '', NULL, '若依官网地址');
+INSERT INTO `sys_menu` VALUES (100, '用户管理', 1, 1, 'user', 'system/user/index', 1, 0, 'C', '0', '0', 'system:user:list', 'user', 'admin', '2021-04-30 17:27:28', '', NULL, '用户管理菜单');
+INSERT INTO `sys_menu` VALUES (101, '角色管理', 1, 2, 'role', 'system/role/index', 1, 0, 'C', '0', '0', 'system:role:list', 'peoples', 'admin', '2021-04-30 17:27:28', '', NULL, '角色管理菜单');
+INSERT INTO `sys_menu` VALUES (102, '菜单管理', 1, 3, 'menu', 'system/menu/index', 1, 0, 'C', '0', '0', 'system:menu:list', 'tree-table', 'admin', '2021-04-30 17:27:28', '', NULL, '菜单管理菜单');
+INSERT INTO `sys_menu` VALUES (103, '部门管理', 1, 4, 'dept', 'system/dept/index', 1, 0, 'C', '0', '0', 'system:dept:list', 'tree', 'admin', '2021-04-30 17:27:28', '', NULL, '部门管理菜单');
+INSERT INTO `sys_menu` VALUES (104, '岗位管理', 1, 5, 'post', 'system/post/index', 1, 0, 'C', '0', '0', 'system:post:list', 'post', 'admin', '2021-04-30 17:27:28', '', NULL, '岗位管理菜单');
+INSERT INTO `sys_menu` VALUES (105, '字典管理', 1, 6, 'dict', 'system/dict/index', 1, 0, 'C', '0', '0', 'system:dict:list', 'dict', 'admin', '2021-04-30 17:27:28', '', NULL, '字典管理菜单');
+INSERT INTO `sys_menu` VALUES (106, '参数设置', 1, 7, 'config', 'system/config/index', 1, 0, 'C', '0', '0', 'system:config:list', 'edit', 'admin', '2021-04-30 17:27:28', '', NULL, '参数设置菜单');
+INSERT INTO `sys_menu` VALUES (107, '通知公告', 1, 8, 'notice', 'system/notice/index', 1, 0, 'C', '0', '0', 'system:notice:list', 'message', 'admin', '2021-04-30 17:27:28', '', NULL, '通知公告菜单');
+INSERT INTO `sys_menu` VALUES (108, '日志管理', 1, 9, 'log', '', 1, 0, 'M', '0', '0', '', 'log', 'admin', '2021-04-30 17:27:28', '', NULL, '日志管理菜单');
+INSERT INTO `sys_menu` VALUES (109, '在线用户', 2, 1, 'online', 'monitor/online/index', 1, 0, 'C', '0', '0', 'monitor:online:list', 'online', 'admin', '2021-04-30 17:27:28', '', NULL, '在线用户菜单');
+INSERT INTO `sys_menu` VALUES (110, '定时任务', 2, 2, 'job', 'monitor/job/index', 1, 0, 'C', '0', '0', 'monitor:job:list', 'job', 'admin', '2021-04-30 17:27:28', '', NULL, '定时任务菜单');
+INSERT INTO `sys_menu` VALUES (111, '数据监控', 2, 3, 'druid', 'monitor/druid/index', 1, 0, 'C', '0', '0', 'monitor:druid:list', 'druid', 'admin', '2021-04-30 17:27:28', '', NULL, '数据监控菜单');
+INSERT INTO `sys_menu` VALUES (112, '服务监控', 2, 4, 'server', 'monitor/server/index', 1, 0, 'C', '0', '0', 'monitor:server:list', 'server', 'admin', '2021-04-30 17:27:28', '', NULL, '服务监控菜单');
+INSERT INTO `sys_menu` VALUES (113, '缓存监控', 2, 5, 'cache', 'monitor/cache/index', 1, 0, 'C', '0', '0', 'monitor:cache:list', 'redis', 'admin', '2021-04-30 17:27:28', '', NULL, '缓存监控菜单');
+INSERT INTO `sys_menu` VALUES (114, '表单构建', 3, 1, 'build', 'tool/build/index', 1, 0, 'C', '0', '0', 'tool:build:list', 'build', 'admin', '2021-04-30 17:27:28', '', NULL, '表单构建菜单');
+INSERT INTO `sys_menu` VALUES (115, '代码生成', 3, 2, 'gen', 'tool/gen/index', 1, 0, 'C', '0', '0', 'tool:gen:list', 'code', 'admin', '2021-04-30 17:27:28', '', NULL, '代码生成菜单');
+INSERT INTO `sys_menu` VALUES (116, '系统接口', 3, 3, 'swagger', 'tool/swagger/index', 1, 0, 'C', '0', '0', 'tool:swagger:list', 'swagger', 'admin', '2021-04-30 17:27:28', '', NULL, '系统接口菜单');
+INSERT INTO `sys_menu` VALUES (500, '操作日志', 108, 1, 'operlog', 'monitor/operlog/index', 1, 0, 'C', '0', '0', 'monitor:operlog:list', 'form', 'admin', '2021-04-30 17:27:28', '', NULL, '操作日志菜单');
+INSERT INTO `sys_menu` VALUES (501, '登录日志', 108, 2, 'logininfor', 'monitor/logininfor/index', 1, 0, 'C', '0', '0', 'monitor:logininfor:list', 'logininfor', 'admin', '2021-04-30 17:27:28', '', NULL, '登录日志菜单');
+INSERT INTO `sys_menu` VALUES (1001, '用户查询', 100, 1, '', '', 1, 0, 'F', '0', '0', 'system:user:query', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1002, '用户新增', 100, 2, '', '', 1, 0, 'F', '0', '0', 'system:user:add', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1003, '用户修改', 100, 3, '', '', 1, 0, 'F', '0', '0', 'system:user:edit', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1004, '用户删除', 100, 4, '', '', 1, 0, 'F', '0', '0', 'system:user:remove', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1005, '用户导出', 100, 5, '', '', 1, 0, 'F', '0', '0', 'system:user:export', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1006, '用户导入', 100, 6, '', '', 1, 0, 'F', '0', '0', 'system:user:import', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1007, '重置密码', 100, 7, '', '', 1, 0, 'F', '0', '0', 'system:user:resetPwd', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1008, '角色查询', 101, 1, '', '', 1, 0, 'F', '0', '0', 'system:role:query', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1009, '角色新增', 101, 2, '', '', 1, 0, 'F', '0', '0', 'system:role:add', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1010, '角色修改', 101, 3, '', '', 1, 0, 'F', '0', '0', 'system:role:edit', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1011, '角色删除', 101, 4, '', '', 1, 0, 'F', '0', '0', 'system:role:remove', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1012, '角色导出', 101, 5, '', '', 1, 0, 'F', '0', '0', 'system:role:export', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1013, '菜单查询', 102, 1, '', '', 1, 0, 'F', '0', '0', 'system:menu:query', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1014, '菜单新增', 102, 2, '', '', 1, 0, 'F', '0', '0', 'system:menu:add', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1015, '菜单修改', 102, 3, '', '', 1, 0, 'F', '0', '0', 'system:menu:edit', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1016, '菜单删除', 102, 4, '', '', 1, 0, 'F', '0', '0', 'system:menu:remove', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1017, '部门查询', 103, 1, '', '', 1, 0, 'F', '0', '0', 'system:dept:query', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1018, '部门新增', 103, 2, '', '', 1, 0, 'F', '0', '0', 'system:dept:add', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1019, '部门修改', 103, 3, '', '', 1, 0, 'F', '0', '0', 'system:dept:edit', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1020, '部门删除', 103, 4, '', '', 1, 0, 'F', '0', '0', 'system:dept:remove', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1021, '岗位查询', 104, 1, '', '', 1, 0, 'F', '0', '0', 'system:post:query', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1022, '岗位新增', 104, 2, '', '', 1, 0, 'F', '0', '0', 'system:post:add', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1023, '岗位修改', 104, 3, '', '', 1, 0, 'F', '0', '0', 'system:post:edit', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1024, '岗位删除', 104, 4, '', '', 1, 0, 'F', '0', '0', 'system:post:remove', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1025, '岗位导出', 104, 5, '', '', 1, 0, 'F', '0', '0', 'system:post:export', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1026, '字典查询', 105, 1, '#', '', 1, 0, 'F', '0', '0', 'system:dict:query', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1027, '字典新增', 105, 2, '#', '', 1, 0, 'F', '0', '0', 'system:dict:add', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1028, '字典修改', 105, 3, '#', '', 1, 0, 'F', '0', '0', 'system:dict:edit', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1029, '字典删除', 105, 4, '#', '', 1, 0, 'F', '0', '0', 'system:dict:remove', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1030, '字典导出', 105, 5, '#', '', 1, 0, 'F', '0', '0', 'system:dict:export', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1031, '参数查询', 106, 1, '#', '', 1, 0, 'F', '0', '0', 'system:config:query', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1032, '参数新增', 106, 2, '#', '', 1, 0, 'F', '0', '0', 'system:config:add', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1033, '参数修改', 106, 3, '#', '', 1, 0, 'F', '0', '0', 'system:config:edit', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1034, '参数删除', 106, 4, '#', '', 1, 0, 'F', '0', '0', 'system:config:remove', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1035, '参数导出', 106, 5, '#', '', 1, 0, 'F', '0', '0', 'system:config:export', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1036, '公告查询', 107, 1, '#', '', 1, 0, 'F', '0', '0', 'system:notice:query', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1037, '公告新增', 107, 2, '#', '', 1, 0, 'F', '0', '0', 'system:notice:add', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1038, '公告修改', 107, 3, '#', '', 1, 0, 'F', '0', '0', 'system:notice:edit', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1039, '公告删除', 107, 4, '#', '', 1, 0, 'F', '0', '0', 'system:notice:remove', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1040, '操作查询', 500, 1, '#', '', 1, 0, 'F', '0', '0', 'monitor:operlog:query', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1041, '操作删除', 500, 2, '#', '', 1, 0, 'F', '0', '0', 'monitor:operlog:remove', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1042, '日志导出', 500, 4, '#', '', 1, 0, 'F', '0', '0', 'monitor:operlog:export', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1043, '登录查询', 501, 1, '#', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:query', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1044, '登录删除', 501, 2, '#', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:remove', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1045, '日志导出', 501, 3, '#', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:export', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1046, '在线查询', 109, 1, '#', '', 1, 0, 'F', '0', '0', 'monitor:online:query', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1047, '批量强退', 109, 2, '#', '', 1, 0, 'F', '0', '0', 'monitor:online:batchLogout', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1048, '单条强退', 109, 3, '#', '', 1, 0, 'F', '0', '0', 'monitor:online:forceLogout', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1049, '任务查询', 110, 1, '#', '', 1, 0, 'F', '0', '0', 'monitor:job:query', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1050, '任务新增', 110, 2, '#', '', 1, 0, 'F', '0', '0', 'monitor:job:add', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1051, '任务修改', 110, 3, '#', '', 1, 0, 'F', '0', '0', 'monitor:job:edit', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1052, '任务删除', 110, 4, '#', '', 1, 0, 'F', '0', '0', 'monitor:job:remove', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1053, '状态修改', 110, 5, '#', '', 1, 0, 'F', '0', '0', 'monitor:job:changeStatus', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1054, '任务导出', 110, 7, '#', '', 1, 0, 'F', '0', '0', 'monitor:job:export', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1055, '生成查询', 115, 1, '#', '', 1, 0, 'F', '0', '0', 'tool:gen:query', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1056, '生成修改', 115, 2, '#', '', 1, 0, 'F', '0', '0', 'tool:gen:edit', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1057, '生成删除', 115, 3, '#', '', 1, 0, 'F', '0', '0', 'tool:gen:remove', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1058, '导入代码', 115, 2, '#', '', 1, 0, 'F', '0', '0', 'tool:gen:import', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1059, '预览代码', 115, 4, '#', '', 1, 0, 'F', '0', '0', 'tool:gen:preview', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+INSERT INTO `sys_menu` VALUES (1060, '生成代码', 115, 5, '#', '', 1, 0, 'F', '0', '0', 'tool:gen:code', '#', 'admin', '2021-04-30 17:27:28', '', NULL, '');
+
+-- ----------------------------
+-- Table structure for sys_role
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_role`;
+CREATE TABLE `sys_role` (
+ `role_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
+ `role_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色名称',
+ `role_key` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色权限字符串',
+ `role_sort` int(4) NOT NULL COMMENT '显示顺序',
+ `data_scope` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)',
+ `menu_check_strictly` tinyint(1) NULL DEFAULT 1 COMMENT '菜单树选择项是否关联显示',
+ `dept_check_strictly` tinyint(1) NULL DEFAULT 1 COMMENT '部门树选择项是否关联显示',
+ `status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色状态(0正常 1停用)',
+ `del_flag` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
+ `create_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '创建者',
+ `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+ `update_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '更新者',
+ `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+ `remark` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+ PRIMARY KEY (`role_id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色信息表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of sys_role
+-- ----------------------------
+INSERT INTO `sys_role` VALUES (1, '超级管理员', 'admin', 1, '1', 1, 1, '0', '0', 'admin', '2021-04-30 17:27:28', '', NULL, '超级管理员');
+INSERT INTO `sys_role` VALUES (2, '普通角色', 'common', 2, '2', 1, 1, '0', '0', 'admin', '2021-04-30 17:27:28', '', NULL, '普通角色');
+
+-- ----------------------------
+-- Table structure for sys_role_menu
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_role_menu`;
+CREATE TABLE `sys_role_menu` (
+ `role_id` bigint(20) NOT NULL COMMENT '角色ID',
+ `menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
+ PRIMARY KEY (`role_id`, `menu_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色和菜单关联表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of sys_role_menu
+-- ----------------------------
+INSERT INTO `sys_role_menu` VALUES (2, 1);
+INSERT INTO `sys_role_menu` VALUES (2, 2);
+INSERT INTO `sys_role_menu` VALUES (2, 3);
+INSERT INTO `sys_role_menu` VALUES (2, 4);
+INSERT INTO `sys_role_menu` VALUES (2, 100);
+INSERT INTO `sys_role_menu` VALUES (2, 101);
+INSERT INTO `sys_role_menu` VALUES (2, 102);
+INSERT INTO `sys_role_menu` VALUES (2, 103);
+INSERT INTO `sys_role_menu` VALUES (2, 104);
+INSERT INTO `sys_role_menu` VALUES (2, 105);
+INSERT INTO `sys_role_menu` VALUES (2, 106);
+INSERT INTO `sys_role_menu` VALUES (2, 107);
+INSERT INTO `sys_role_menu` VALUES (2, 108);
+INSERT INTO `sys_role_menu` VALUES (2, 109);
+INSERT INTO `sys_role_menu` VALUES (2, 110);
+INSERT INTO `sys_role_menu` VALUES (2, 111);
+INSERT INTO `sys_role_menu` VALUES (2, 112);
+INSERT INTO `sys_role_menu` VALUES (2, 113);
+INSERT INTO `sys_role_menu` VALUES (2, 114);
+INSERT INTO `sys_role_menu` VALUES (2, 115);
+INSERT INTO `sys_role_menu` VALUES (2, 116);
+INSERT INTO `sys_role_menu` VALUES (2, 500);
+INSERT INTO `sys_role_menu` VALUES (2, 501);
+INSERT INTO `sys_role_menu` VALUES (2, 1000);
+INSERT INTO `sys_role_menu` VALUES (2, 1001);
+INSERT INTO `sys_role_menu` VALUES (2, 1002);
+INSERT INTO `sys_role_menu` VALUES (2, 1003);
+INSERT INTO `sys_role_menu` VALUES (2, 1004);
+INSERT INTO `sys_role_menu` VALUES (2, 1005);
+INSERT INTO `sys_role_menu` VALUES (2, 1006);
+INSERT INTO `sys_role_menu` VALUES (2, 1007);
+INSERT INTO `sys_role_menu` VALUES (2, 1008);
+INSERT INTO `sys_role_menu` VALUES (2, 1009);
+INSERT INTO `sys_role_menu` VALUES (2, 1010);
+INSERT INTO `sys_role_menu` VALUES (2, 1011);
+INSERT INTO `sys_role_menu` VALUES (2, 1012);
+INSERT INTO `sys_role_menu` VALUES (2, 1013);
+INSERT INTO `sys_role_menu` VALUES (2, 1014);
+INSERT INTO `sys_role_menu` VALUES (2, 1015);
+INSERT INTO `sys_role_menu` VALUES (2, 1016);
+INSERT INTO `sys_role_menu` VALUES (2, 1017);
+INSERT INTO `sys_role_menu` VALUES (2, 1018);
+INSERT INTO `sys_role_menu` VALUES (2, 1019);
+INSERT INTO `sys_role_menu` VALUES (2, 1020);
+INSERT INTO `sys_role_menu` VALUES (2, 1021);
+INSERT INTO `sys_role_menu` VALUES (2, 1022);
+INSERT INTO `sys_role_menu` VALUES (2, 1023);
+INSERT INTO `sys_role_menu` VALUES (2, 1024);
+INSERT INTO `sys_role_menu` VALUES (2, 1025);
+INSERT INTO `sys_role_menu` VALUES (2, 1026);
+INSERT INTO `sys_role_menu` VALUES (2, 1027);
+INSERT INTO `sys_role_menu` VALUES (2, 1028);
+INSERT INTO `sys_role_menu` VALUES (2, 1029);
+INSERT INTO `sys_role_menu` VALUES (2, 1030);
+INSERT INTO `sys_role_menu` VALUES (2, 1031);
+INSERT INTO `sys_role_menu` VALUES (2, 1032);
+INSERT INTO `sys_role_menu` VALUES (2, 1033);
+INSERT INTO `sys_role_menu` VALUES (2, 1034);
+INSERT INTO `sys_role_menu` VALUES (2, 1035);
+INSERT INTO `sys_role_menu` VALUES (2, 1036);
+INSERT INTO `sys_role_menu` VALUES (2, 1037);
+INSERT INTO `sys_role_menu` VALUES (2, 1038);
+INSERT INTO `sys_role_menu` VALUES (2, 1039);
+INSERT INTO `sys_role_menu` VALUES (2, 1040);
+INSERT INTO `sys_role_menu` VALUES (2, 1041);
+INSERT INTO `sys_role_menu` VALUES (2, 1042);
+INSERT INTO `sys_role_menu` VALUES (2, 1043);
+INSERT INTO `sys_role_menu` VALUES (2, 1044);
+INSERT INTO `sys_role_menu` VALUES (2, 1045);
+INSERT INTO `sys_role_menu` VALUES (2, 1046);
+INSERT INTO `sys_role_menu` VALUES (2, 1047);
+INSERT INTO `sys_role_menu` VALUES (2, 1048);
+INSERT INTO `sys_role_menu` VALUES (2, 1049);
+INSERT INTO `sys_role_menu` VALUES (2, 1050);
+INSERT INTO `sys_role_menu` VALUES (2, 1051);
+INSERT INTO `sys_role_menu` VALUES (2, 1052);
+INSERT INTO `sys_role_menu` VALUES (2, 1053);
+INSERT INTO `sys_role_menu` VALUES (2, 1054);
+INSERT INTO `sys_role_menu` VALUES (2, 1055);
+INSERT INTO `sys_role_menu` VALUES (2, 1056);
+INSERT INTO `sys_role_menu` VALUES (2, 1057);
+INSERT INTO `sys_role_menu` VALUES (2, 1058);
+INSERT INTO `sys_role_menu` VALUES (2, 1059);
+INSERT INTO `sys_role_menu` VALUES (2, 1060);
+
+-- ----------------------------
+-- Table structure for sys_user
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_user`;
+CREATE TABLE `sys_user` (
+ `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
+ `dept_id` bigint(20) NULL DEFAULT NULL COMMENT '部门ID',
+ `user_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户账号',
+ `nick_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户昵称',
+ `user_type` varchar(2) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '00' COMMENT '用户类型(00系统用户)',
+ `email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '用户邮箱',
+ `phonenumber` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '手机号码',
+ `sex` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
+ `avatar` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '头像地址',
+ `password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '密码',
+ `status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
+ `del_flag` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
+ `login_ip` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '最后登录IP',
+ `login_date` datetime NULL DEFAULT NULL COMMENT '最后登录时间',
+ `create_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '创建者',
+ `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+ `update_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '更新者',
+ `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+ `remark` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
+ PRIMARY KEY (`user_id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of sys_user
+-- ----------------------------
+INSERT INTO `sys_user` VALUES (1, 103, 'admin', '若依', '00', 'ry@163.com', '15888888888', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', '2021-04-30 17:27:28', 'admin', '2021-04-30 17:27:28', '', NULL, '管理员');
+INSERT INTO `sys_user` VALUES (2, 105, 'ry', '若依', '00', 'ry@qq.com', '15666666666', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', '2021-04-30 17:27:28', 'admin', '2021-04-30 17:27:28', '', NULL, '测试员');
+
+-- ----------------------------
+-- Table structure for sys_user_role
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_user_role`;
+CREATE TABLE `sys_user_role` (
+ `user_id` bigint(20) NOT NULL COMMENT '用户ID',
+ `role_id` bigint(20) NOT NULL COMMENT '角色ID',
+ PRIMARY KEY (`user_id`, `role_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户和角色关联表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of sys_user_role
+-- ----------------------------
+INSERT INTO `sys_user_role` VALUES (1, 1);
+INSERT INTO `sys_user_role` VALUES (2, 2);
+
+-- ----------------------------
+-- Table structure for t_user
+-- ----------------------------
+DROP TABLE IF EXISTS `t_user`;
+CREATE TABLE `t_user` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `username` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
+ `password` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
+ `email` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `username`(`username`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of t_user
+-- ----------------------------
+INSERT INTO `t_user` VALUES (1, 'admin', 'admin', 'admin@atguigu.com');
+INSERT INTO `t_user` VALUES (2, 'test', 'test', 'test@test.com');
+INSERT INTO `t_user` VALUES (3, 'wzg168', '123456', 'wzg168@qq.com');
+INSERT INTO `t_user` VALUES (4, 'bbj168', '666666', 'bbj168@qq.com');
+INSERT INTO `t_user` VALUES (5, 'abc168', '666666', 'abc168@qq.com');
+INSERT INTO `t_user` VALUES (6, 'admin250', '123456', '362165265@qq.com');
+INSERT INTO `t_user` VALUES (7, 'admin235', '123456', '362165265@qq.com');
+INSERT INTO `t_user` VALUES (8, 'aaaaaaaaa', 'aaaaaa', '362165265@qq.com');
+INSERT INTO `t_user` VALUES (9, 'aaaaa', 'aaaaa', '362165265@qq.com');
+
+-- ----------------------------
+-- Table structure for test
+-- ----------------------------
+DROP TABLE IF EXISTS `test`;
+CREATE TABLE `test` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `seqno` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+ `update_time` datetime NULL DEFAULT NULL,
+ `create_time` datetime NULL DEFAULT NULL,
+ `some` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of test
+-- ----------------------------
+INSERT INTO `test` VALUES (1, NULL, '2021-08-27 15:12:29', '2021-08-27 15:12:29', 'some');
+
+-- ----------------------------
+-- Table structure for test07
+-- ----------------------------
+DROP TABLE IF EXISTS `test07`;
+CREATE TABLE `test07` (
+ `k1` int(11) NOT NULL,
+ `k2` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
+ `col1` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+ `col2` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+ PRIMARY KEY (`k1`, `k2`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of test07
+-- ----------------------------
+INSERT INTO `test07` VALUES (1, '111', 'xxxx', NULL);
+
+-- ----------------------------
+-- Table structure for test2
+-- ----------------------------
+DROP TABLE IF EXISTS `test2`;
+CREATE TABLE `test2` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `refid` int(11) NOT NULL,
+ `some2` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of test2
+-- ----------------------------
+
+SET FOREIGN_KEY_CHECKS = 1;
+```
+
+## 7. mybatis-plus 自动生成代码
+
+# 二、SpringSecurity认证
+
+## 1. SpringSecurity的工作流程
+
+>**一个web请求会经过一条过滤器链,在经过过滤器链的过程中会完成认证与授权,如果中间发现这条请求未认证或者未授权,会根据被保护API的权限去抛出异常,然后由异常处理器去处理这些异常。**
+
+图片显示流程
+
+[//]: # ()
+
+[//]: # ()
+[//]: # ()
+
+[//]: # ()
+[//]: # ()
+
+```java
+/**
+如上图:
+ 1. 一个请求想要访问到API就会以从左到右的形式经过蓝线框框里面的过滤器
+ - 其中绿色部分是我们本篇主要讲的负责认证的过滤器
+ - 蓝色部分负责异常处理
+ - 橙色部分则是负责授权
+ 2. 图中的这两个绿色过滤器是Spring Security对form表单认证和Basic认证内置的两个Filter,JWT认证方式用不上。
+ 3. Spring Security配置中有两个叫formLogin和httpBasic的配置项,在配置中打开了它俩就对应着打开了上面的过滤器。
+ 4. formLogin对应着你form表单认证方式,即UsernamePasswordAuthenticationFilter。
+ 5. httpBasic对应着Basic认证方式,即BasicAuthenticationFilter。
+ 6. 换言之,你配置了这两种认证方式,过滤器链中才会加入它们,否则它们是不会被加到过滤器链中去的。
+ 7. 因为Spring Security自带的过滤器中是没有针对JWT这种认证方式的,所以我们会写一个JWT的认证过滤器,然后放在绿色的位置进行认证工作。
+```
+
+## 2. SpringSecurity的重要概念
+
+```apl
+1. SecurityContext:上下文对象,Authentication对象会放在里面。
+2. SecurityContextHolder:用于拿到上下文对象的静态工具类。
+3. Authentication:认证接口,定义了认证对象的数据形式。
+4. AuthenticationManager:用于校验Authentication,返回一个认证完成后• Authentication对象。
+```
+
+### 1. SecurityContext
+
+> 上下文对象,认证后的数据就放在这里面,接口定义如下:
+
+```java
+public interface SecurityContext extends Serializable {
+ // 获取Authentication对象
+ Authentication getAuthentication();
+ // 放入Authentication对象
+ void setAuthentication(Authentication authentication);
+}
+```
+
+### 2. SecurityContextHolder
+
+> 可以说是SecurityContext的工具类,用于get 或 set 或者 clear SecurityContext,**默认会把数据都存储到当前线程中**。
+
+```java
+public class SecurityContextHolder {
+ public static void clearContext() {
+ strategy.clearContext();
+ }
+ public static SecurityContext getContext() {
+ return strategy.getContext();
+ }
+ public static void setContext(SecurityContext context) {
+ strategy.setContext(context);
+ }
+}
+```
+
+### 3. Authentication
+
+> Authentication只是定义了一种在SpringSecurity进行认证过的数据的数据形式应该是怎么样的,要有权限,要有密码,要有身份信息,要有额外信息。
+
+```java
+public interface Authentication extends Principal, Serializable {
+ Collection extends GrantedAuthority> getAuthorities();
+ Object getCredentials();
+ Object getDetails();
+ Object getPrincipal();
+ boolean isAuthenticated();
+ void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
+}
+```
+
+```java
+/**
+1. getAuthorities: 获取用户权限,一般情况下获取到的是用户的角色信息。
+2. getCredentials: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。
+3. getDetails: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)。
+4. getPrincipal: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails。
+6. isAuthenticated: 获取当前 Authentication 是否已认证。
+7. setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。
+```
+
+### 4. AuthenticationManager
+
+> AuthenticationManager定义了一个认证方法,它将一个未认证的Authentication传入,返回一个已认证的Authentication,默认使用的实现类为:ProviderManager。
+
+```java
+public interface AuthenticationManager {
+ // 认证方法
+ Authentication authenticate(Authentication authentication)throws AuthenticationException;
+}
+```
+
+### 5. Spring Security进行认证的流程:
+
+```java
+/**
+1. 先是一个请求带着身份信息进来
+2. 经过AuthenticationManager的认证,
+3. 再通过SecurityContextHolder获取SecurityContext,
+4. 最后将认证后的信息放入到SecurityContext。
+```
+
+## 3. 必要组件
+
+### 1. 定义加密器Bean
+
+> 这个Bean是不必可少的,Spring Security在认证操作时会使用我们定义的这个加密器,如果没有则会出现异常。
+
+```java
+ /**
+ * spring容器中注入加密算法
+ *
+ * @return 强散列哈希加密实现
+ */
+ @Bean
+ public BCryptPasswordEncoder bCryptPasswordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+```
+
+### 2.定义AuthenticationManager
+
+>这里将Spring Security自带的authenticationManager声明成Bean,声明它的作用是用它帮我们进行认证操作,调用这个Bean的authenticate方法会由Spring Security自动帮我们做认证。
+
+```java
+ /**
+ * 解决 无法直接注入 AuthenticationManager
+ *
+ * @return return
+ * @throws Exception e
+ */
+ @Bean
+ @Override
+ protected AuthenticationManager authenticationManager() throws Exception {
+ return super.authenticationManager();
+ }
+```
+
+
+
+### 3. 实现UserDetailsService
+
+>实现UserDetailsService的抽象方法并返回一个UserDetails对象,认证过程中SpringSecurity会调用这个方法访问数据库进行对用户的搜索,逻辑什么都可以自定义,无论是从数据库中还是从缓存中,但是我们需要将我们查询出来的用户信息和权限信息组装成一个UserDetails返回。
+>
+>UserDetails也是一个定义了数据形式的接口,用于保存我们从数据库中查出来的数据,其功能主要是验证账号状态和获取权限
+
+```java
+/**
+ * 自定义用户认证逻辑并交给spring管理
+ *
+ * @Date: 2021-08-27-13:50
+ * @Author lj
+ */
+@Slf4j
+@Service("userDetailsService")
+@RequiredArgsConstructor
+public class UserDetailsServiceImpl implements UserDetailsService {
+
+ private final UserService userService;
+
+ private final PermissionService permissionService;
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ log.info("②-用户信息认证");
+ //认证 判断用户是否存在于数据库内
+ User user = userService.getOne(new QueryWrapper().eq("user_name", username));
+ if (Validator.isNull(user)) {
+ log.info("登录用户:{} 不存在.", username);
+ throw new RuntimeException("登录用户:" + username + "不存在");
+ } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
+ log.info("登录用户:{} 已被删除.", username);
+ throw new RuntimeException("登录用户:" + username + "已被删除");
+ } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+ log.info("登录用户:{} 已被停用.", username);
+ throw new RuntimeException("登录用户:" + username + "已被停用");
+ }
+ //满足以上信息认证通过
+ //设置用户权限和当前用户基本信息
+ Set permissions = permissionService.getMenuPermission(user);
+ //角色权限信息
+ Set rolePermission = permissionService.getRolePermission(user);
+ user.setRoles(rolePermission);
+ //返回安全框架需要得用户信息和自定义得一些信息
+ return new SecurityUser(permissions, user);
+ }
+}
+```
+
+### 4. TokenUtil
+
+> 由于我们是JWT的认证模式,所以我们也需要一个帮我们操作Token的工具类,一般来说它具有以下三个方法就够了
+>
+> - 创建token
+> - 验证token
+> - 反解析token中的信息
+
+```java
+package com.mangmang.security.securities.jwt;
+
+import cn.hutool.core.lang.Validator;
+import com.mangmang.security.securities.domain.SecurityUser;
+import com.mangmang.security.utils.AddressUtils;
+import com.mangmang.security.utils.Constants;
+import com.mangmang.security.utils.IpUtils;
+import eu.bitwalker.useragentutils.UserAgent;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @Date: 2021-08-26-15:00
+ * @Author lj
+ */
+@Data
+@Component
+@ConfigurationProperties("token")
+@RequiredArgsConstructor
+public class TokenService {
+
+ private final RedisTemplate redisTemplate;
+
+ /**
+ * 令牌自定义标识
+ */
+ private String header;
+
+ /**
+ * 令牌密钥
+ */
+ private String secret;
+
+ /**
+ * 令牌有效期(默认30分钟)
+ */
+ private int expireTime;
+
+
+ /**
+ * 根据token获取用户信息
+ *
+ * @param request 请求
+ * @return 用户信息
+ */
+ public SecurityUser getSecurityUser(HttpServletRequest request) {
+ //获取token
+ String token = request.getHeader(this.header);
+ if (Validator.isNotEmpty(token)) {
+ //获取用户唯一标识
+ String uniquelyIdentifies = getUniquelyIdentifiesByToken(token);
+ //返回该用户信息
+ return (SecurityUser) redisTemplate.opsForValue().get(Constants.LOGIN_TOKEN_KEY + uniquelyIdentifies);
+ }
+ return null;
+ }
+
+ /**
+ * 生成一个token
+ *
+ * @param securityUser 安全框架需要得用户信息
+ * @param request 请求
+ * @return token
+ */
+ public String createToken(SecurityUser securityUser, HttpServletRequest request) {
+ //设置用户唯一标识
+ securityUser.setUniquelyIdentifies(UUID.randomUUID().toString());
+ //获取用户ip地址
+ String ipAddr = IpUtils.getIpAddr(request);
+ securityUser.setIpaddr(ipAddr);
+ //获取客户端信息
+ UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
+ //根据ip获取用户真实地址
+ securityUser.setLoginLocation(AddressUtils.getRealAddressByIp(ipAddr));
+ //设置登录浏览器信息
+ securityUser.setBrowser(userAgent.getBrowser().getName());
+ //设置操作系统信息
+ securityUser.setOs(userAgent.getOperatingSystem().getName());
+ //用户信息存入redis
+ refreshRedisTokenTime(securityUser);
+ //token得主体信息;
+ HashMap tokenInfo = new HashMap<>(64);
+ tokenInfo.put(Constants.LOGIN_USER_KEY, securityUser.getUniquelyIdentifies());
+ //生成token
+ return Jwts.builder()
+ //jwt头使用默认
+ //有效载荷
+ .setClaims(tokenInfo)
+ //签名哈希
+ //让上面两部分数据签名通过HS256算法生成哈希,确保数据不会被篡改
+ .signWith(SignatureAlgorithm.HS256, secret)
+ .compact();
+ }
+
+
+ /**
+ * 刷新存放在redis中用户登录相关信息时间
+ */
+ public void refreshRedisTokenTime(SecurityUser securityUser) {
+ //设置当前时间为登录时间
+ securityUser.setLoginTime(System.currentTimeMillis());
+ //登录时间+30分钟为过期时间
+ securityUser.setExpireTime(securityUser.getLoginTime() + expireTime * Constants.MILLIS_MINUTE);
+ //把用户登录信息存入缓存expireTime分钟后过期
+ redisTemplate
+ .opsForValue()
+ .set(Constants.LOGIN_TOKEN_KEY + securityUser.getUniquelyIdentifies(), securityUser, expireTime, TimeUnit.MINUTES);
+ }
+
+ /**
+ * 小于10分钟更新存放在redis中token过期时间
+ * 判断redis中token过期时间是否小于10分钟
+ *
+ * @param securityUser 安全用户信息
+ */
+ public void verifyToken(SecurityUser securityUser) {
+ if (securityUser.getExpireTime() - securityUser.getLoginTime() <= Constants.MILLIS_MINUTE_TEN) {
+ refreshRedisTokenTime(securityUser);
+ }
+ }
+
+ /**
+ * 通过token获取用户唯一标识
+ *
+ * @param token token
+ * @return 用户唯一标识
+ */
+ public String getUniquelyIdentifiesByToken(String token) {
+ return (String) Jwts.parser()
+ .setSigningKey(secret)
+ .parseClaimsJws(token)
+ .getBody()
+ .get(Constants.LOGIN_USER_KEY);
+ }
+
+ /**
+ * 根据用户唯一标识删除用户信息
+ *
+ * @param uniquelyIdentifies 用户唯一标识;
+ */
+ public void deleteSecurity(String uniquelyIdentifies) {
+ if (Validator.isNotEmpty(uniquelyIdentifies)) {
+ //删除redis中数据
+ redisTemplate.delete(Constants.LOGIN_TOKEN_KEY + uniquelyIdentifies);
+ }
+ }
+}
+```
+
+### 5. 自定义UserDetails
+
+```java
+package com.mangmang.security.securities.domain;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.mangmang.security.entity.User;
+import lombok.*;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * security框架得用户信息
+ *
+ * @Date: 2021-08-26-16:18
+ * @Author lj
+ */
+@NoArgsConstructor
+@Data
+@ToString
+public class SecurityUser implements UserDetails, Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户唯一标识
+ */
+ private String uniquelyIdentifies;
+
+ /**
+ * 登录时间
+ */
+ private Long loginTime;
+
+ /**
+ * 过期时间
+ */
+ private Long expireTime;
+
+ /**
+ * 登录IP地址
+ */
+ private String ipaddr;
+
+ /**
+ * 登录地点
+ */
+ private String loginLocation;
+
+ /**
+ * 浏览器类型
+ */
+ private String browser;
+
+ /**
+ * 操作系统
+ */
+ private String os;
+
+ /**
+ * 权限列表
+ */
+ private Set permissions;
+
+ /**
+ * 用户信息
+ */
+ private User user;
+
+ @Getter(AccessLevel.NONE)
+ private String username;
+
+ /**
+ * 用户信息和权限信息
+ *
+ * @param permissions 权限信息
+ * @param user 用户信息
+ */
+ public SecurityUser(Set permissions, User user) {
+ this.permissions = permissions;
+ this.user = user;
+ }
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return null;
+ }
+
+ @JsonIgnore
+ @Override
+ public String getPassword() {
+ return user.getPassword();
+ }
+
+
+ @Override
+ public String getUsername() {
+ return user.getUserName();
+ }
+
+
+ /**
+ * 账户是否未过期,过期无法验证
+ *
+ * @return 未过期
+ */
+ @JsonIgnore
+ @Override
+ public boolean isAccountNonExpired() {
+ return true;
+ }
+
+ /**
+ * 指定用户是否解锁,锁定的用户无法进行身份验证
+ *
+ * @return 已解锁
+ */
+ @JsonIgnore
+ @Override
+ public boolean isAccountNonLocked() {
+ return true;
+ }
+
+ /**
+ * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
+ *
+ * @return 未过期
+ */
+ @JsonIgnore
+ @Override
+ public boolean isCredentialsNonExpired() {
+ return true;
+ }
+
+ /**
+ * 是否可用 ,禁用的用户不能身份验证
+ *
+ * @return 可用
+ */
+ @JsonIgnore
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+}
+
+```
+
+
+
+## 4. 具体实现
+
+### 1. 认证方法和认证源码分析
+
+> 访问一个系统,一般最先访问的是认证方法,这里我写了最简略的认证需要的几个步骤,因为实际系统中我们还要写登录记录啊,前台密码解密啊这些操作。
+
+```java
+/**
+ * security登录接口
+ *
+ * @Date: 2021-08-29-17:05
+ * @Author lj
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class LoginService {
+
+ private final AuthenticationManager authenticationManager;
+
+ private final TokenService tokenService;
+
+ private RedisTemplate redisTemplate;
+
+ public String login(String username, String password, HttpServletRequest request) {
+ Authentication authentication = null;
+ try {
+ //1. 开始认证
+ authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
+ } catch (Exception e) {
+ if (e instanceof BadCredentialsException) {
+ log.error("用户密码不匹配");
+ throw new RuntimeException("用户密码不匹配");
+ } else {
+ e.printStackTrace();
+ }
+ }
+ assert authentication != null;
+ //2. 获取认证成功后得用户信息
+ SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
+ System.out.println(securityUser);
+ //3. 缓存用户信息并生成token
+ return tokenService.createToken(securityUser, request);
+ }
+}
+```
+
+```java
+/**
+1. 传入用户名和密码创建了一个UsernamePasswordAuthenticationToken对象,这是我们前面说过的Authentication的实现类,传入用户名和密码做构造参数,这个对象就是我们创建出来的未认证的Authentication对象。使用我们先前已经声明过的Bean-authenticationManager调用它的authenticate方法进行认证,返回一个认证完成的Authentication对象。
+2. 从Authentication对象中拿到我们的UserDetails对象,之前我们说过,认证后的Authentication对象调用它的getPrincipal()方法就可以拿到我们先前数据库查询后组装出来的UserDetails对象,
+3. 然后创建token。把UserDetails对象放入缓存中,方便后面过滤器使用。
+
+```
+
+```java
+ public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+ Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
+ return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
+ });
+ //还未认证成功过去用户名
+ String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
+ boolean cacheWasUsed = true;
+ //从缓存中去查用户名为XXX的对象
+ UserDetails user = this.userCache.getUserFromCache(username);
+ //为空
+ if (user == null) {
+ cacheWasUsed = false;
+ try {
+ //调用我们重写UserDetailsService的loadUserByUsername方法
+ // 拿到我们自己组装好的UserDetails对象
+ //AbstractUserDetailsAuthenticationProvider得实现类为DaoAuthenticationProvider由于retrieveUser为抽象方法所以调用子类方法
+ user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
+ } catch (UsernameNotFoundException var6) {
+ this.logger.debug("User '" + username + "' not found");
+ if (this.hideUserNotFoundExceptions) {
+ throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
+ }
+ throw var6;
+ }
+ Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
+ }
+ try {
+ // 校验账号是否禁用
+ this.preAuthenticationChecks.check(user);
+ // 校验数据库查出来的密码,和我们传入的密码是否一致
+ this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
+ } catch (AuthenticationException var7) {
+ if (!cacheWasUsed) {
+ throw var7;
+ }
+
+ cacheWasUsed = false;
+ user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
+ this.preAuthenticationChecks.check(user);
+ this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
+ }
+ this.postAuthenticationChecks.check(user);
+ if (!cacheWasUsed) {
+ this.userCache.putUserInCache(user);
+ }
+ Object principalToReturn = user;
+ if (this.forcePrincipalAsString) {
+ principalToReturn = user.getUsername();
+ }
+ return this.createSuccessAuthentication(principalToReturn, authentication, user);
+ }
+
+```
+
+
+
+### 2. jwt认证过滤器
+
+>有了token之后,我们要把过滤器放在过滤器链中,用于解析token,因为我们没有session,所以我们每次去辨别这是哪个用户的请求的时候,都是根据请求中的token来解析出来当前是哪个用户。
+>
+>addFilterBefore的语义是添加一个Filter到XXXFilter之前,放在这里就是把`JwtAuthenticationTokenFilter`放在UsernamePasswordAuthenticationFilter之前,因为filter的执行也是有顺序的,我们必须要把我们的filter放在过滤器链中绿色的部分才会起到自动认证的效果。
+>
+>具体实现如下:
+
+```java
+/**
+ * token过滤器 验证token有效性
+ *
+ * @Date: 2021-08-29-14:08
+ * @Author lj
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
+
+ private final TokenService tokenService;
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+ log.info("①=========jwt认证");
+ //1.之前登录过 所以这里会获取到redis中完整得用户信息
+ //1. 根据token获取用户唯一标识查询用户信息
+ SecurityUser securityUser = tokenService.getSecurityUser(request);
+ //2. 获取上下文中是否有 authentication认证接口,定义了认证对象的数据形式。(本次请求得认证接口,包含当前访问接口的用户信息)
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (Validator.isNotNull(securityUser) && Validator.isNull(authentication)) {
+ //3 如果redis中数据小于10分钟刷新
+ tokenService.verifyToken(securityUser);
+ //4. 组装一个authentication对象,把它放在上下文对象中,这样后面的过滤器看到我们上下文对象中有authentication对象,就相当于我们已经认证过了
+ UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(securityUser, securityUser.getPassword(), securityUser.getAuthorities());
+ //5. 设置网页详情,ip地址,sessionId等信息
+ authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+ //6. 将authentication信息放入到上下文对象中
+ SecurityContextHolder.getContext().setAuthentication(authenticationToken);
+ }
+ //放行
+ filterChain.doFilter(request, response);
+ }
+}
+
+```
+
+> 这样的话,每一个带有正确token的请求进来之后,都会找到它的账号信息,并放在上下文对象中,我们可以使用`SecurityContextHolder`很方便的拿到上下文对象中的`Authentication`对象。完成之后,启动我们的demo,可以看到过滤器链中有以下过滤器,其中我们自定义的是第5个
+>
+> 登录👉拿到token👉请求带上token👉JWT过滤器拦截👉校验token👉将从缓存中查出来的对象放到上下文中
+
+## 5. 代码优化
+
+### 1.认证失败处理器
+
+> 当用户未登录或者token解析失败时会触发这个处理器,返回一个非法访问的结果。
+
+```java
+/**
+ * 认证失败处理类 返回未授权
+ *
+ * @Date: 2021-08-29-13:10
+ * @Author lj
+ */
+@Component
+public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {
+ @Override
+ public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
+ String msg = String.format("请求访问:%s,认证失败,无法访问系统资源", request.getRequestURI());
+ try {
+ // 设置返回的数据内容的数据类型和编码
+ response.setContentType("text/html; charset=utf-8");
+ response.getWriter().write(msg);
+ } catch (IOException ioException) {
+ ioException.printStackTrace();
+ }
+ }
+}
+
+```
+
+### 2. 自定义退出处理
+
+```java
+/**
+ * 自定义退出处理类 返回成功
+ *
+ * @Date: 2021-08-29-13:51
+ * @Author lj
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
+
+ private final TokenService tokenService;
+
+ @Override
+ public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException {
+ //获取认证得用户
+ SecurityUser securityUser = tokenService.getSecurityUser(request);
+ //不为空
+ if (Validator.isNotNull(securityUser)) {
+ //获取用户姓名
+ String userName = securityUser.getUser().getUserName();
+ //删除redis缓存数据
+ tokenService.deleteSecurity(securityUser.getUniquelyIdentifies());
+ //删除上下文中的用户信息,只删除当前线程用户数据
+ SecurityContextHolder.clearContext();
+ //记录退出日志
+ log.info("用户 {} 退出了系统", userName);
+ }
+ // 设置返回的数据内容的数据类型和编码
+ response.setContentType("text/html; charset=utf-8");
+ response.getWriter().write("用户为空");
+ }
+}
+
+```
+
+### 3. 权限不足处理器
+
+```java
+/**
+ * 权限不足处理器
+ *
+ * @Date: 2021-08-31-13:50
+ * @Author lj
+ */
+@Slf4j
+@Component
+public class InsufficientPermissionsHandler implements AccessDeniedHandler {
+ @Override
+ public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) {
+ String msg = String.format("请求访问:%s,失败,无权限,请联系管理员", request.getRequestURI());
+ response.setHeader("Cache-Control", "no-cache");
+ response.setCharacterEncoding("UTF-8");
+ response.setContentType("application/json");
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ try {
+ response.getWriter().println(msg);
+ response.getWriter().flush();
+ } catch (IOException ioException) {
+ ioException.printStackTrace();
+ }
+ }
+}
+```
+
+
+
+## 6. security完整配置
+
+```java
+/**
+ * 自动配置时会判断是否存在WebSecurityConfigurerAdapter类,
+ *
+ * @Date: 2021-08-27-13:48
+ * @Author lj
+ */
+@RequiredArgsConstructor
+@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
+
+ /**
+ * 用户认证逻辑spring自动注入加入容器实现该接口得方法
+ */
+ private final UserDetailsService userDetailsService;
+
+ /**
+ * 认证失败处理类
+ */
+ private final AuthenticationEntryPointImpl authenticationEntryPoint;
+
+
+ /**
+ * 无权限处理器
+ */
+ private final InsufficientPermissionsHandler insufficientPermissionsHandler;
+
+ /**
+ * 退出成功处理
+ */
+ private final LogoutSuccessHandlerImpl logoutSuccessHandler;
+
+ /**
+ * jwt认证过滤器
+ */
+ private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
+
+
+ /**
+ * 配置security密码加密方式和自定义认证流程
+ *
+ * @param auth 认证
+ * @throws Exception 异常
+ */
+ @Override
+ protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+ auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
+ }
+
+
+ /**
+ * spring容器中注入加密算法
+ *
+ * @return 强散列哈希加密实现
+ */
+ @Bean
+ public BCryptPasswordEncoder bCryptPasswordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ /**
+ * 解决 无法直接注入 AuthenticationManager
+ *
+ * @return return
+ * @throws Exception e
+ */
+ @Bean
+ @Override
+ protected AuthenticationManager authenticationManager() throws Exception {
+ return super.authenticationManager();
+ }
+
+
+ /**
+ * 跨域配置
+ */
+ @Bean
+ public CorsFilter corsFilter() {
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ CorsConfiguration config = new CorsConfiguration();
+ config.setAllowCredentials(true);
+ // 设置访问源地址
+ config.addAllowedOrigin("*");
+ // 设置访问源请求头
+ config.addAllowedHeader("*");
+ // 设置访问源请求方法
+ config.addAllowedMethod("*");
+ // 设置访问源请求方法
+ source.registerCorsConfiguration("/**", config);
+ return new CorsFilter(source);
+ }
+
+ /**
+ * anyRequest | 匹配所有请求路径
+ * access | SpringEl表达式结果为true时可以访问
+ * anonymous | 匿名可以访问
+ * denyAll | 用户不能访问
+ * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
+ * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
+ * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
+ * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
+ * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
+ * hasRole | 如果有参数,参数表示角色,则其角色可以访问
+ * permitAll | 用户可以任意访问
+ * rememberMe | 允许通过remember-me登录的用户访问
+ * authenticated | 用户登录后可访问
+ *
+ * @param http HttpSecurity
+ * @throws Exception e
+ */
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http // CSRF禁用,因为不使用session
+ .csrf().disable()
+
+ //认证失败处理
+ .exceptionHandling()
+ //没认证
+ .authenticationEntryPoint(authenticationEntryPoint)
+ //无权限
+ .accessDeniedHandler(insufficientPermissionsHandler)
+
+ //session管理
+ .and()
+ .sessionManagement()
+ //基于token,所以不需要session,设置为无状态
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+
+
+ //过滤请求
+ .and()
+ .authorizeRequests()
+ //静态资源用户可以任意访问
+ .antMatchers(HttpMethod.GET, URL_STATIC).permitAll()
+ //允许匿名访问得url
+ .antMatchers(URL_WHITELIST).anonymous()
+ //除以上配置路径外全部需要鉴权认证
+ .anyRequest().authenticated()
+
+ //配置完全开启x-frame-options
+ .and()
+ .headers()
+ .frameOptions()
+ .disable()
+
+ //退出成功处理
+ .and()
+ .logout()
+ .logoutUrl("/logout")
+ .logoutSuccessHandler(logoutSuccessHandler)
+
+ //添加滤器
+ .and()
+ //添加token认证过滤器到表单认证过滤器得前面
+ .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
+ //添加跨域配置
+ .addFilterBefore(corsFilter(), JwtAuthenticationTokenFilter.class)
+ .addFilterBefore(corsFilter(), LogoutFilter.class);
+ }
+
+ /**
+ * 需要放行的静态资源
+ */
+ private static final String[] URL_STATIC = {
+ "/*.html",
+ "/**/*.html",
+ "/**/*.css",
+ "/**/*.js"
+ };
+
+ /**
+ * 需要放行的url地址
+ */
+ private static final String[] URL_WHITELIST = {
+ "/swagger-ui.html",
+ "/swagger-resources/**",
+ "/webjars/**",
+ "/*/api-docs",
+ "/druid/**",
+ "/login"
+ };
+}
+
+```
+
+## 7. 其它工具类
+
+### 1. 枚举
+
+```java
+/**
+ * 用户状态
+ *
+ * @Date: 2021-08-27-17:55
+ * @Author lj
+ */
+public enum UserStatus {
+ /**
+ * 相当于创建UserStatus对象
+ * 正常
+ */
+ OK("0", "正常"),
+
+ /**
+ * 禁用
+ */
+ DISABLE("1", "停用"),
+
+ /**
+ * 删除
+ */
+ DELETED("2", "删除");
+
+
+ /**
+ * 代号
+ */
+ private final String code;
+
+ /**
+ * 信息
+ */
+ private final String info;
+
+
+ UserStatus(String code, String info) {
+ this.code = code;
+ this.info = info;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public String getInfo() {
+ return info;
+ }
+}
+```
+
+### 2. 静态常量
+
+```java
+/**
+ * 静态常量
+ *
+ * @Date: 2021-08-26-15:35
+ * @Author lj
+ */
+public class Constants {
+ /**
+ * 毫秒
+ */
+ public static final long MILLIS_SECOND = 1000;
+
+ /**
+ * 1分钟
+ */
+ public static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
+
+ /**
+ * 10分钟
+ */
+ public static final Long MILLIS_MINUTE_TEN = 10 * 60 * 1000L;
+
+ /**
+ * 未知
+ */
+ public static final String UNKNOWN = "unknown";
+
+ /**
+ * 2
+ */
+ public static final Integer TWO = 2;
+
+ /**
+ * 4
+ */
+ public static final Integer FOUR = 4;
+
+ /**
+ * GBK
+ */
+ public static final String GBK = "GBK";
+
+ /**
+ * UTF-8
+ */
+ public static final String UTF8 = "UTF-8";
+
+
+ /**
+ * redis中登录用户唯一标识得key
+ */
+ public static final String LOGIN_TOKEN_KEY = "login_tokens:";
+
+ /**
+ * token中用户唯一标识
+ */
+ public static final String LOGIN_USER_KEY = "login_user_key";
+
+ /**
+ * 401 未授权
+ */
+ public static final Integer UNAUTHORIZED = 401;
+}
+```
+
+### 3. ip解析
+
+```java
+/**
+ * @Date: 2021-08-27-8:43
+ * @Author lj
+ */
+@Slf4j
+public class IpUtils {
+ /**
+ * 获取请求得ip地址
+ *
+ * @param request 请求
+ * @return ip地址
+ */
+ public static String getIpAddr(HttpServletRequest request) {
+ if (request == null) {
+ return "unknown";
+ }
+ String ip = request.getHeader("x-forwarded-for");
+ if (ip == null || ip.length() == 0) {
+ ip = request.getHeader("Proxy-Client-IP");
+ }
+ if (ip == null || ip.length() == 0 || Constants.UNKNOWN.equalsIgnoreCase(ip)) {
+ ip = request.getHeader("X-Forwarded-For");
+ }
+ if (ip == null || ip.length() == 0 || Constants.UNKNOWN.equalsIgnoreCase(ip)) {
+ ip = request.getHeader("WL-Proxy-Client-IP");
+ }
+ if (ip == null || ip.length() == 0 || Constants.UNKNOWN.equalsIgnoreCase(ip)) {
+ ip = request.getHeader("X-Real-IP");
+ }
+
+ if (ip == null || ip.length() == 0 || Constants.UNKNOWN.equalsIgnoreCase(ip)) {
+ ip = request.getRemoteAddr();
+ }
+ return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : EscapeUtil.clean(ip);
+ }
+
+ /**
+ * 判断ip是否为内网ip
+ *
+ * @param ip ip
+ * @return 是或否
+ */
+ public static boolean internalIp(String ip) {
+ //将IPv4地址转换成字节
+ byte[] addr = textToNumericFormatV4(ip);
+
+ return internalIp(addr) || "127.0.0.1".equals(ip);
+ }
+
+ /**
+ * 判断该该字节是否为内网ip
+ *
+ * @param addr 字节内网ip
+ * @return 是或否
+ */
+ private static boolean internalIp(byte[] addr) {
+ if (Validator.isNull(addr) || addr.length < Constants.TWO) {
+ return true;
+ }
+ final byte b0 = addr[0];
+ final byte b1 = addr[1];
+ // 10.x.x.x/8
+ final byte section1 = 0x0A;
+ // 172.16.x.x/12
+ final byte section2 = (byte) 0xAC;
+ final byte section3 = (byte) 0x10;
+ final byte section4 = (byte) 0x1F;
+ // 192.168.x.x/16
+ final byte section5 = (byte) 0xC0;
+ final byte section6 = (byte) 0xA8;
+ switch (b0) {
+ case section1:
+ return true;
+ case section2:
+ if (b1 >= section3 && b1 <= section4) {
+ return true;
+ }
+ case section5:
+ if (b1 == section6) {
+ return true;
+ }
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * 将IPv4地址转换成字节
+ *
+ * @param text IPv4地址
+ * @return byte 字节
+ */
+ public static byte[] textToNumericFormatV4(String text) {
+ long number1 = 4294967295L;
+ long number2 = 255L;
+ long number3 = 16777215L;
+ long number4 = 65535L;
+
+ if (text.length() == 0) {
+ return null;
+ }
+ byte[] bytes = new byte[4];
+ String[] elements = text.split("\\.", -1);
+ try {
+ long l;
+ int i;
+ switch (elements.length) {
+ case 1:
+ l = Long.parseLong(elements[0]);
+ if ((l < 0L) || (l > number1)) {
+ return null;
+ }
+ bytes[0] = (byte) (int) (l >> 24 & 0xFF);
+ bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF);
+ bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
+ bytes[3] = (byte) (int) (l & 0xFF);
+ break;
+ case 2:
+ l = Integer.parseInt(elements[0]);
+ if ((l < 0L) || (l > number2)) {
+ return null;
+ }
+ bytes[0] = (byte) (int) (l & 0xFF);
+ l = Integer.parseInt(elements[1]);
+ if ((l < 0L) || (l > number3)) {
+ return null;
+ }
+ bytes[1] = (byte) (int) (l >> 16 & 0xFF);
+ bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
+ bytes[3] = (byte) (int) (l & 0xFF);
+ break;
+ case 3:
+ for (i = 0; i < Constants.TWO; ++i) {
+ l = Integer.parseInt(elements[i]);
+ if ((l < 0L) || (l > 255L)) {
+ return null;
+ }
+ bytes[i] = (byte) (int) (l & 0xFF);
+ }
+ l = Integer.parseInt(elements[2]);
+ if ((l < 0L) || (l > number4)) {
+ return null;
+ }
+ bytes[2] = (byte) (int) (l >> 8 & 0xFF);
+ bytes[3] = (byte) (int) (l & 0xFF);
+ break;
+ case 4:
+ for (i = 0; i < Constants.FOUR; ++i) {
+ l = Integer.parseInt(elements[i]);
+ if ((l < 0L) || (l > 255L)) {
+ return null;
+ }
+ bytes[i] = (byte) (int) (l & 0xFF);
+ }
+ break;
+ default:
+ return null;
+ }
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ return bytes;
+ }
+
+ /**
+ * 获取本机ip地址
+ *
+ * @return ip地址
+ */
+ public static String getHostIp() {
+ try {
+ return InetAddress.getLocalHost().getHostAddress();
+ } catch (UnknownHostException e) {
+ log.error(e.getMessage());
+ }
+ return "127.0.0.1";
+ }
+
+ /**
+ * 获取本机名称
+ *
+ * @return 主机名称
+ */
+ public static String getHostName() {
+ try {
+ return InetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ log.error("UnknownHostException", e);
+ }
+ return "未知";
+ }
+}
+
+```
+
+```java
+package com.mangmang.security.utils;
+
+import cn.hutool.http.HTMLFilter;
+
+/**
+ * @Date: 2021-08-27-8:44
+ * @Author lj
+ */
+public class EscapeUtil {
+ public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)";
+
+ private static final char[][] TEXT = new char[64][];
+
+ /**
+ * 清除所有HTML标签,但是不删除标签内的内容
+ *
+ * @param content 文本
+ * @return 清除标签后的文本
+ */
+ public static String clean(String content) {
+ return new HTMLFilter().filter(content);
+ }
+}
+```
+
+### 4. HTTP请求发送
+
+```java
+package com.mangmang.security.utils;
+
+import lombok.extern.slf4j.Slf4j;
+
+import javax.net.ssl.*;
+import java.io.*;
+import java.net.ConnectException;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
+import java.security.cert.X509Certificate;
+
+/**
+ * @Date: 2021-08-27-9:22
+ * @Author lj
+ */
+@Slf4j
+public class HttpUtils {
+ /**
+ * 向指定 URL 发送GET方法的请求
+ *
+ * @param url 发送请求的 URL
+ * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
+ * @return 所代表远程资源的响应结果
+ */
+ public static String sendGet(String url, String param) {
+ return sendGet(url, param, Constants.UTF8);
+ }
+
+
+ /**
+ * 向指定 URL 发送GET方法的请求
+ *
+ * @param url 发送请求的 URL
+ * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
+ * @param contentType 编码类型
+ * @return 所代表远程资源的响应结果
+ */
+ public static String sendGet(String url, String param, String contentType) {
+ StringBuilder result = new StringBuilder();
+ BufferedReader in = null;
+ try {
+ String urlNameString = url + "?" + param;
+ log.info("sendGet - {}", urlNameString);
+ URL realUrl = new URL(urlNameString);
+ URLConnection connection = realUrl.openConnection();
+ connection.setRequestProperty("accept", "*/*");
+ connection.setRequestProperty("connection", "Keep-Alive");
+ connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
+ connection.connect();
+ in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType));
+ String line;
+ while ((line = in.readLine()) != null) {
+ result.append(line);
+ }
+ log.info("recv - {}", result);
+ } catch (ConnectException e) {
+ log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e);
+ } catch (SocketTimeoutException e) {
+ log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e);
+ } catch (IOException e) {
+ log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e);
+ } catch (Exception e) {
+ log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e);
+ } finally {
+ try {
+ if (in != null) {
+ in.close();
+ }
+ } catch (Exception ex) {
+ log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
+ }
+ }
+ return result.toString();
+ }
+
+
+ /**
+ * 向指定 URL 发送POST方法的请求
+ *
+ * @param url 发送请求的 URL
+ * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
+ * @return 所代表远程资源的响应结果
+ */
+ public static String sendPost(String url, String param) {
+ PrintWriter out = null;
+ BufferedReader in = null;
+ StringBuilder result = new StringBuilder();
+ try {
+ log.info("sendPost - {}", url);
+ URL realUrl = new URL(url);
+ URLConnection conn = realUrl.openConnection();
+ conn.setRequestProperty("accept", "*/*");
+ conn.setRequestProperty("connection", "Keep-Alive");
+ conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
+ conn.setRequestProperty("Accept-Charset", "utf-8");
+ conn.setRequestProperty("contentType", "utf-8");
+ conn.setDoOutput(true);
+ conn.setDoInput(true);
+ out = new PrintWriter(conn.getOutputStream());
+ out.print(param);
+ out.flush();
+ in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
+ String line;
+ while ((line = in.readLine()) != null) {
+ result.append(line);
+ }
+ log.info("recv - {}", result);
+ } catch (ConnectException e) {
+ result.setLength(0);
+ result.append("fail");
+ log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e);
+ } catch (SocketTimeoutException e) {
+ result.setLength(0);
+ result.append("fail");
+ log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e);
+ } catch (IOException e) {
+ result.setLength(0);
+ result.append("fail");
+ log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e);
+ } catch (Exception e) {
+ result.setLength(0);
+ result.append("fail");
+ log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e);
+ } finally {
+ try {
+ if (out != null) {
+ out.close();
+ }
+ if (in != null) {
+ in.close();
+ }
+ } catch (IOException ex) {
+ log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
+ }
+ }
+ return result.toString();
+ }
+
+ public static String sendSslPost(String url, String param) {
+ StringBuilder result = new StringBuilder();
+ String urlNameString = url + "?" + param;
+ try {
+ log.info("sendSSLPost - {}", urlNameString);
+ SSLContext sc = SSLContext.getInstance("SSL");
+ sc.init(null, new TrustManager[]{new TrustAnyTrustManager()}, new java.security.SecureRandom());
+ URL console = new URL(urlNameString);
+ HttpsURLConnection conn = (HttpsURLConnection) console.openConnection();
+ conn.setRequestProperty("accept", "*/*");
+ conn.setRequestProperty("connection", "Keep-Alive");
+ conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
+ conn.setRequestProperty("Accept-Charset", "utf-8");
+ conn.setRequestProperty("contentType", "utf-8");
+ conn.setDoOutput(true);
+ conn.setDoInput(true);
+
+ conn.setSSLSocketFactory(sc.getSocketFactory());
+ conn.setHostnameVerifier(new TrustAnyHostnameVerifier());
+ conn.connect();
+ InputStream is = conn.getInputStream();
+ BufferedReader br = new BufferedReader(new InputStreamReader(is));
+ String ret;
+ while ((ret = br.readLine()) != null) {
+ if (!"".equals(ret.trim())) {
+ result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));
+ }
+ }
+ log.info("recv - {}", result);
+ conn.disconnect();
+ br.close();
+ } catch (ConnectException e) {
+ log.error("调用HttpUtils.sendSSLPost ConnectException, url=" + url + ",param=" + param, e);
+ } catch (SocketTimeoutException e) {
+ log.error("调用HttpUtils.sendSSLPost SocketTimeoutException, url=" + url + ",param=" + param, e);
+ } catch (IOException e) {
+ log.error("调用HttpUtils.sendSSLPost IOException, url=" + url + ",param=" + param, e);
+ } catch (Exception e) {
+ log.error("调用HttpsUtil.sendSSLPost Exception, url=" + url + ",param=" + param, e);
+ }
+ return result.toString();
+ }
+
+ private static class TrustAnyTrustManager implements X509TrustManager {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) {
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[]{};
+ }
+ }
+
+ private static class TrustAnyHostnameVerifier implements HostnameVerifier {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ }
+}
+```
+
+# 三 、SpringSecurity授权
+
+## 1. 获取用户权限信息
+
+```java
+package com.mangmang.security.securities.service;
+
+import com.mangmang.security.entity.User;
+import com.mangmang.security.securities.domain.SecurityUser;
+import com.mangmang.security.service.MenuService;
+import com.mangmang.security.service.RoleService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 用户权限处理
+ *
+ * @Date: 2021-08-30-17:26
+ * @Author lj
+ */
+@Component
+@RequiredArgsConstructor
+public class PermissionService {
+ private final RoleService roleService;
+
+ private final MenuService menuService;
+
+ /**
+ * 获取角色权限
+ *
+ * @param user 用户信息
+ * @return 角色权限集合
+ */
+ public Set getRolePermission(User user) {
+ // 需要返回得权限集合
+ Set roles = new HashSet<>();
+ //如果为超级用户添加超级用户
+ if (user.judgeIsAdmin()) {
+ roles.add("admin");
+ } else {
+ //否则添加其它角色
+ roles.addAll(roleService.findRolePermissionByUserId(user.getUserId()));
+ }
+ return roles;
+ }
+
+ /**
+ * 获取菜单权限
+ *
+ * @param user 用户户信息
+ * @return 菜单权限集合
+ */
+ public Set getMenuPermission(User user) {
+ // 需要返回得权限集合
+ Set menus = new HashSet<>();
+ //如果为超级用户添加超级用户
+ if (user.judgeIsAdmin()) {
+ menus.add("*:*:*");
+ } else {
+ //添加两个集合得并集
+ menus.addAll(menuService.findAllMenu(user.getUserId()));
+ }
+ return menus;
+ }
+}
+```
+
+## 2. 自定义权限判断
+
+```java
+package com.mangmang.security.securities.service;
+
+import cn.hutool.core.lang.Validator;
+import cn.hutool.core.util.StrUtil;
+import com.mangmang.security.entity.Role;
+import com.mangmang.security.securities.domain.SecurityUser;
+import com.mangmang.security.securities.jwt.TokenService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Set;
+
+/**
+ * 判断是否具有某个权限
+ * 自定义权限实现,ss取自SpringSecurity首字母
+ *
+ * @Date: 2021-08-30-18:21
+ * @Author lj
+ */
+@Service("ss")
+@RequiredArgsConstructor
+public class PermissionJudgeService {
+
+ /**
+ * 拥有所有菜单权限标识
+ */
+ private static final String ALL_PERMISSION = "*:*:*";
+
+
+ /**
+ * 拥有管理员角色权限标识
+ */
+ private static final String SUPER_ADMIN = "admin";
+
+
+ /**
+ * 分隔符
+ */
+ private static final String DELIMETER = ",";
+
+
+ /**
+ * token处理
+ */
+ private final TokenService tokenService;
+
+
+ /**
+ * 获取request
+ */
+ public static HttpServletRequest getRequest() {
+ //获取当前线程得request
+ RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
+ ServletRequestAttributes requestAttributes = (ServletRequestAttributes) attributes;
+ assert requestAttributes != null;
+ return requestAttributes.getRequest();
+ }
+
+
+ /**
+ * 验证用户是否具备某权限
+ *
+ * @param permission 需要判断的权限字符串
+ * @return 是或否
+ */
+ public boolean hasPermi(String permission) {
+ //如果为null拒绝访问
+ if (Validator.isEmpty(permission)) {
+ return false;
+ }
+ //获取用户信息
+ SecurityUser securityUser = tokenService.getSecurityUser(getRequest());
+ //如果用户或权限为空拒绝访问
+ if (Validator.isNull(securityUser) || Validator.isNull(securityUser.getPermissions())) {
+ return false;
+ }
+ return hasPermissions(securityUser.getPermissions(), permission);
+ }
+
+ /**
+ * 验证不具备某个权限
+ *
+ * @param permission 需要判断的权限字符串
+ * @return true=没有该权限 false=有该权限
+ */
+ public boolean lacksPermi(String permission) {
+ return !hasPermi(permission);
+ }
+
+
+ /**
+ * 验证用户是否具备以下任意权限
+ *
+ * @param permissions 以逗号为分隔符的权限列表
+ * @return true=有该权限 false=无该权限
+ */
+ public boolean hasAnyPermi(String permissions) {
+ if (Validator.isEmpty(permissions)) {
+ return false;
+ }
+ //获取用户信息
+ SecurityUser securityUser = tokenService.getSecurityUser(getRequest());
+ //如果用户或权限为空拒绝访问
+ if (Validator.isNull(securityUser) || Validator.isNull(securityUser.getPermissions())) {
+ return false;
+ }
+ //获取权限列表
+ Set authorities = securityUser.getPermissions();
+ //遍历权限判断是否具有某一个权限
+ for (String permission : permissions.split(DELIMETER)) {
+ if (permission != null && hasPermissions(authorities, permission)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 公用判断是否拥有某个权限
+ */
+ private boolean hasPermissions(Set permissions, String permission) {
+ return permissions.contains(ALL_PERMISSION) || permissions.contains(StrUtil.trim(permission));
+ }
+
+ /**
+ * 判断用户是否具有某一角色
+ *
+ * @param role 需要判断得角色
+ * @return true=有这个权限 false=没有这个权限
+ */
+ public boolean hasRole(String role) {
+ if (Validator.isEmpty(role)) {
+ return false;
+ }
+ SecurityUser securityUser = tokenService.getSecurityUser(getRequest());
+ if (Validator.isNull(securityUser) || Validator.isNull(securityUser.getUser().getRoles())) {
+ return false;
+ }
+
+ for (String rolePerm : securityUser.getUser().getRoles()) {
+ if (SUPER_ADMIN.equals(rolePerm) || rolePerm.equals(StrUtil.trim(role))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 验证用户是否不具备某角色,与hasRole逻辑相反。
+ *
+ * @param role 角色名称
+ * @return 用户是否不具备某角色
+ */
+ public boolean lacksRole(String role) {
+ return !hasRole(role);
+ }
+
+ /**
+ * 验证用户是否具有以下任意一个角色
+ *
+ * @param roles 以逗号为分隔符的角色列表
+ * @return 用户是否具有以下任意一个角色
+ */
+ public boolean hasAnyRoles(String roles) {
+ if (Validator.isEmpty(roles)) {
+ return false;
+ }
+ SecurityUser securityUser = tokenService.getSecurityUser(getRequest());
+ if (Validator.isNull(securityUser) || Validator.isEmpty(securityUser.getUser().getRoles())) {
+ return false;
+ }
+ //roles以逗号分割后循环有匹配得数据直接返回true
+ for (String role : roles.split(DELIMETER)) {
+ if (hasRole(role)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
+```
+
+## 3. 测试类
+
+```java
+package com.mangmang.security.controller;
+
+import com.mangmang.security.securities.service.LoginService;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * @Date: 2021-08-29-16:51
+ * @Author lj
+ */
+@RestController
+@RequiredArgsConstructor
+public class LoginController {
+
+ private final LoginService loginService;
+
+ @ApiOperation("登录接口")
+ @GetMapping("/login")
+ public String login(String username, String password, HttpServletRequest request) {
+ return loginService.login(username, password, request);
+ }
+
+ @PreAuthorize("@ss.hasPermi('system:menu:list')")
+ @ApiOperation("test是否具有system:menu:list权限")
+ @GetMapping("/vip1")
+ public String test() {
+ return "访问成功";
+ }
+
+ @PreAuthorize("@ss.hasPermi('system:menu:test')")
+ @ApiOperation("test是否具有system:menu:test权限")
+ @GetMapping("/vip2")
+ public String test2() {
+ return "访问成功";
+ }
+}
+```
+
+## 4. 查询权限得2个方法
+
+```java
+package com.mangmang.security.service.impl;
+
+
+import cn.hutool.core.lang.Validator;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.api.R;
+import com.mangmang.security.entity.Menu;
+import com.mangmang.security.entity.Role;
+import com.mangmang.security.entity.RoleMenu;
+import com.mangmang.security.entity.UserRole;
+import com.mangmang.security.mapper.MenuMapper;
+import com.mangmang.security.mapper.RoleMapper;
+import com.mangmang.security.mapper.RoleMenuMapper;
+import com.mangmang.security.mapper.UserRoleMapper;
+import com.mangmang.security.service.MenuService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ *
+ * 菜单权限表 服务实现类
+ *
+ *
+ * @author 氓氓编程
+ * @since 2021-08-26
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class MenuServiceImpl extends ServiceImpl implements MenuService {
+
+
+ private final RoleMapper roleMapper;
+
+ private final UserRoleMapper userRoleMapper;
+
+ private final RoleMenuMapper roleMenuMapper;
+
+ @Override
+ public Set findAllMenu(Long userId) {
+ //查询该用户所有角色
+ List userRoleList = userRoleMapper.selectList(new QueryWrapper().eq("user_id", userId));
+ if (userRoleList.size() <= 0) {
+ return null;
+ }
+ Set roleIdSet = userRoleList.stream().map(UserRole::getRoleId).collect(Collectors.toSet());
+
+ //查询该角色中未被禁用角色
+ QueryWrapper roleWrapper = new QueryWrapper<>();
+ roleWrapper.in("role_id", roleIdSet);
+ roleWrapper.eq("status", 0);
+ List rolesIdList = roleMapper.selectList(roleWrapper);
+ Set roleIds = rolesIdList.stream().map(Role::getRoleId).collect(Collectors.toSet());
+
+ //查询用户角色所有菜单
+ List roleMenuList = roleMenuMapper.selectList(new QueryWrapper().in("role_id", roleIds));
+ if (roleMenuList.size() <= 0) {
+ return null;
+ }
+
+ Set menuIdSet = roleMenuList.stream().map(RoleMenu::getMenuId).collect(Collectors.toSet());
+ //查询用户所有菜单中未禁用菜单
+ QueryWrapper menuWrapper = new QueryWrapper<>();
+ menuWrapper.select("perms");
+ menuWrapper.eq("status", 0);
+ menuWrapper.in("menu_id", menuIdSet);
+ List menus = baseMapper.selectList(menuWrapper);
+
+ //创建权限集合
+ Set permsSet = new HashSet<>();
+ for (Menu menu : menus) {
+ if (Validator.isNotEmpty(menu.getPerms())) {
+ permsSet.addAll(Arrays.asList(menu.getPerms().split(",")));
+ }
+ }
+ log.info("{}", permsSet);
+ return permsSet;
+ }
+}
+
+```
+
+```java
+package com.mangmang.security.service.impl;
+
+import cn.hutool.core.lang.Validator;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.mangmang.security.entity.Role;
+import com.mangmang.security.entity.UserRole;
+import com.mangmang.security.mapper.RoleMapper;
+import com.mangmang.security.mapper.UserRoleMapper;
+import com.mangmang.security.service.RoleService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.mangmang.security.service.UserRoleService;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * 角色信息表 服务实现类
+ *
+ *
+ * @author 氓氓编程
+ * @since 2021-08-26
+ */
+@Service
+@RequiredArgsConstructor
+public class RoleServiceImpl extends ServiceImpl implements RoleService {
+
+ private final UserRoleMapper userRoleMapper;
+
+ @Override
+ public Set findRolePermissionByUserId(Long userId) {
+ //获取该用户所有角色信息
+ List userRoleList = userRoleMapper.selectList(new QueryWrapper().eq("user_id", userId));
+ if (userRoleList.size() <= 0) {
+ return null;
+ }
+
+ //获取角色id
+ Set roleIdSet = userRoleList.stream().map(UserRole::getRoleId).collect(Collectors.toSet());
+
+ //查询该角色中未被禁用角色
+ QueryWrapper roleWrapper = new QueryWrapper<>();
+ roleWrapper.in("role_id", roleIdSet);
+ roleWrapper.eq("status", 0);
+ List perms = baseMapper.selectList(roleWrapper);
+
+ //遍历角色
+ Set permsSet = new HashSet<>();
+ for (Role perm : perms) {
+ if (Validator.isNotNull(perm)) {
+ permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(",")));
+ }
+ }
+ return permsSet;
+ }
+}
+```
+
diff --git a/src/programming/docker/安装.md b/src/programming/docker/安装.md
new file mode 100644
index 0000000..b446e0e
--- /dev/null
+++ b/src/programming/docker/安装.md
@@ -0,0 +1,186 @@
+---
+icon: mdi:package-variant
+date: 2025-05-22
+category:
+ - docker
+tag:
+ - 代理
+ - 手动安装
+---
+
+手动安装docker
+
+
+# Docker 安装配置指南
+
+## 一、Docker 安装
+
+### 1. 配置代理
+
+首先配置系统代理以便下载Docker安装包:
+
+```bash
+sudo vi /etc/wgetrc
+```
+
+在文件中添加以下代理配置:
+
+```tex
+https_proxy = http://10.6.212.9:7897/
+http_proxy = http://10.6.212.9:7897/
+ftp_proxy = http://10.6.212.9:7897/
+```
+
+### 2. 下载Docker
+
+```bash
+wget https://download.docker.com/linux/static/stable/x86_64/docker-24.0.5.tgz
+tar -zxvf docker-24.0.5.tgz
+```
+
+### 3. 安装Docker
+
+将Docker二进制文件复制到系统目录:
+
+```bash
+sudo cp docker/* /usr/bin/
+sudo rm -rf docker docker-24.0.5.tgz
+```
+
+### 4. 创建系统服务
+
+创建Docker系统服务文件:
+
+```bash
+sudo vim /etc/systemd/system/docker.service
+```
+
+添加以下服务配置:
+
+```ini
+[Unit]
+Description=Docker Application Container Engine
+Documentation=https://docs.docker.com
+After=network-online.target firewalld.service
+Wants=network-online.target
+
+[Service]
+Type=notify
+ExecStart=/usr/bin/dockerd
+ExecReload=/bin/kill -s HUP $MAINPID
+TimeoutSec=0
+RestartSec=2
+ExecStartPost=/usr/sbin/iptables -P FORWARD ACCEPT
+Restart=always
+TimeoutStartSec=0
+LimitNOFILE=infinity
+LimitNPROC=infinity
+LimitCORE=infinity
+Delegate=yes
+KillMode=process
+StartLimitBurst=3
+StartLimitInterval=60s
+
+[Install]
+WantedBy=multi-user.target
+```
+
+### 5. 启动和配置Docker服务
+
+```bash
+# 修改权限
+sudo chmod +x /etc/systemd/system/docker.service
+
+# 加载systemctl配置
+sudo systemctl daemon-reload
+
+# 开机自启
+sudo systemctl enable docker.service
+
+# 启动服务
+sudo systemctl start docker
+
+# 查看状态
+sudo systemctl status docker
+```
+
+## 二、Docker Compose 安装
+
+### 1. 下载Docker Compose
+
+```bash
+# 下载最新版本
+wget https://github.com/docker/compose/releases/download/v2.28.0/docker-compose-linux-x86_64
+```
+
+### 2. 安装Docker Compose
+
+```bash
+# 复制到系统目录
+sudo cp -f docker-compose-linux-x86_64 /usr/local/bin/docker-compose
+
+# 修改权限
+sudo chmod +x /usr/local/bin/docker-compose
+
+# 验证安装
+docker-compose -v
+```
+
+## 三、配置 HTTP/HTTPS 网络代理
+
+### 1. 为 dockerd 设置网络代理
+
+由于 `docker pull` 命令由 dockerd 守护进程执行,而 dockerd 守护进程由 systemd 管理,因此需要通过 systemd 来配置代理。
+
+#### 创建配置文件夹
+
+```bash
+sudo mkdir -p /etc/systemd/system/docker.service.d
+```
+
+#### 创建代理配置文件
+
+```bash
+sudo vi /etc/systemd/system/docker.service.d/http-proxy.conf
+```
+
+添加以下代理配置:
+
+```ini
+[Service]
+Environment="HTTP_PROXY=http://10.6.212.9:7897/"
+Environment="HTTPS_PROXY=http://10.6.212.9:7897/"
+Environment="NO_PROXY=localhost,127.0.0.1"
+```
+
+#### 应用配置
+
+```bash
+# 刷新配置
+sudo systemctl daemon-reload
+
+# 重启Docker服务
+sudo systemctl restart docker
+```
+
+## 注意事项
+
+1. **版本选择**:示例中使用的是 Docker 24.0.5 版本,建议根据实际需求选择合适的版本
+2. **代理配置**:代理地址 `10.6.212.9:7897` 需要根据实际网络环境进行调整
+3. **权限管理**:安装过程中涉及多个权限操作,请确保具有足够的系统权限
+4. **防火墙**:如果系统启用了防火墙,可能需要配置相关规则以允许Docker正常运行
+
+## 验证安装
+
+安装完成后,可以通过以下命令验证安装是否成功:
+
+```bash
+# 查看Docker版本
+docker --version
+
+# 查看Docker Compose版本
+docker-compose --version
+
+# 运行测试容器
+sudo docker run hello-world
+```
\ No newline at end of file
diff --git a/src/programming/frontend/README.md b/src/programming/frontend/README.md
new file mode 100644
index 0000000..b2f4d6d
--- /dev/null
+++ b/src/programming/frontend/README.md
@@ -0,0 +1,9 @@
+---
+title: 前端开发
+index: false
+icon: mdi:vuejs
+category:
+ - 前端
+---
+
+
diff --git a/src/programming/frontend/css/1 b/src/programming/frontend/css/1
new file mode 100644
index 0000000..e69de29
diff --git a/src/programming/frontend/html/1 b/src/programming/frontend/html/1
new file mode 100644
index 0000000..e69de29
diff --git a/src/programming/frontend/vue/1 b/src/programming/frontend/vue/1
new file mode 100644
index 0000000..e69de29
diff --git a/src/programming/linux/Linux_Mint/常用配置.md b/src/programming/linux/Linux_Mint/常用配置.md
new file mode 100644
index 0000000..2686d30
--- /dev/null
+++ b/src/programming/linux/Linux_Mint/常用配置.md
@@ -0,0 +1,110 @@
+---
+title: 常用配置
+icon: simple-icons:linuxmint
+date: 2025-11-21
+category:
+ - Linux
+tag:
+ - Linux Mint
+---
+
+# Linux Mint 常用配置
+
+记录 Linux Mint 系统的常用配置和操作。
+
+
+
+## 一 禁用网卡
+
+```shell
+sudo vi /etc/modprobe.d/blacklist.conf
+# Blacklist the driver for Intel Wireless 3165
+blacklist iwlwifi
+```
+
+```shell
+sudo update-initramfs -u
+sudo reboot
+```
+
+## 二 Wi-Fi 驱动重装
+
+系统更新后需要重新安装 Wi-Fi 驱动:
+
+```shell
+sudo dpkg -r ax300-wifi-adapter-linux-driver
+sudo apt install ./ax300_wifi_adapter_linux_driver.deb
+```
+
+## 三 Navicat 17 配置
+
+### 3.1 移动 AppImage 并赋予权限
+
+```bash
+sudo mkdir -p /opt/navicat17-premium-lite
+sudo mv "/home/liumangmang/下载/navicat17-premium-lite-cs-x86_64.AppImage" /opt/navicat17-premium-lite/
+sudo chmod +x /opt/navicat17-premium-lite/navicat17-premium-lite-cs-x86_64.AppImage
+```
+
+> 💡 若下载目录为英文 `Downloads`,修改路径为:`/home/liumangmang/Downloads/`
+
+### 3.2 创建应用启动器
+
+```bash
+cat > ~/.local/share/applications/navicat17.desktop < ⚠️ 首次启动需授予权限:右键 → Properties → Permissions → Allow executing as program
+
+### 3.4 设置应用图标(可选)
+
+若启动器显示空白图标,可手动提取:
+
+```bash
+cd /tmp
+/opt/navicat17-premium-lite/navicat17-premium-lite-cs-x86_64.AppImage --appimage-extract
+sudo cp squashfs-root/navicat.png /usr/share/icons/hicolor/256x256/apps/navicat.png
+sudo gtk-update-icon-cache /usr/share/icons/hicolor
+rm -rf squashfs-root
+```
+
+或放在用户目录:
+
+```bash
+mkdir -p ~/.local/share/icons
+cp /tmp/squashfs-root/navicat.png ~/.local/share/icons/
+```
+
+编辑 `.desktop` 文件:
+```ini
+Icon=navicat # 或指定完整路径
+```
+
+### 3.5 卸载
+
+```bash
+sudo rm -rf /opt/navicat17-premium-lite
+rm ~/.local/share/applications/navicat17.desktop
+rm ~/桌面/navicat17.desktop
+sudo rm -f /usr/share/icons/hicolor/256x256/apps/navicat.png
+```
\ No newline at end of file
diff --git a/src/programming/linux/凝思/常用配置.md b/src/programming/linux/凝思/常用配置.md
new file mode 100644
index 0000000..e3614f8
--- /dev/null
+++ b/src/programming/linux/凝思/常用配置.md
@@ -0,0 +1,212 @@
+
+
+Linux系统配置完全指南:VNC、分辨率、本地化与时间同步
+
+
+
+# Linux系统配置完全指南:VNC、分辨率、本地化与时间同步
+
+在日常的Linux系统管理中,我们经常需要进行各种配置来满足特定的使用需求。本文将详细介绍四个重要的系统配置方面:VNC远程桌面、显示分辨率调整、系统语言设置以及时间同步配置。这些配置技巧将帮助您更好地管理和使用Linux系统。
+
+## 一、VNC远程桌面配置
+
+VNC(Virtual Network Computing)是一种图形化远程桌面协议,允许用户远程访问和控制Linux桌面环境。
+
+### 基础配置步骤
+
+首先需要设置VNC访问密码。使用`vncpasswd`命令可以在用户主目录下的`.vnc`文件夹中生成密码文件:
+
+```bash
+vncpasswd
+```
+
+执行后会在`~/.vnc/passwd`路径下生成密码文件,文件权限通常为`-rw-------`,确保只有用户本人可以读写。
+
+### 启动VNC服务
+
+使用以下命令启动x0vncserver服务,该服务会共享当前的显示会话:
+
+```bash
+/usr/bin/x0vncserver -display :0 -passwordfile /home/sunri/.vnc/passwd
+```
+
+这里的`:0`表示本地显示器,`passwordfile`参数指定了之前创建的密码文件路径。
+
+### 客户端连接
+
+VNC服务启动后,可以通过VNC客户端连接到服务器。默认端口为5900,连接格式为:
+
+```
+IP地址:5900
+例如:192.168.100.201:5900
+```
+
+### 系统服务管理
+
+为了更好地管理VNC服务,可以将其配置为systemd服务。创建服务单元文件:
+
+```bash
+sudo vi /etc/systemd/system/x0vncserver.service
+```
+
+在文件中添加以下配置:
+
+```ini
+[Unit]
+Description=Start x0vncserver on display :0
+
+[Service]
+ExecStart=/usr/bin/x0vncserver -display :0 -passwordfile /home/sunri/.vnc/passwd
+Restart=always
+User=sunri
+Group=sunri
+
+[Install]
+WantedBy=multi-user.target
+```
+
+然后使用systemctl命令管理服务:
+
+```bash
+sudo systemctl enable x0vncserver # 启用自启动
+sudo systemctl start x0vncserver # 启动服务
+sudo systemctl status x0vncserver # 查看服务状态
+sudo systemctl stop x0vncserver # 停止服务
+```
+
+需要注意的是,由于启动优先级等原因,某些情况下可能需要手动启动服务。
+
+## 二、显示分辨率配置
+
+在Linux系统中,合适的显示分辨率对于用户体验至关重要。通过xrandr工具可以灵活地调整显示分辨率。
+
+### 查看当前分辨率信息
+
+使用xrandr命令查看当前显示设备和支持的分辨率:
+
+```bash
+xrandr
+```
+
+该命令会显示所有连接的显示设备及其支持的分辨率模式。
+
+### 生成自定义分辨率参数
+
+如果系统不支持所需的分辨率,可以使用cvt命令生成相应的显示模式参数:
+
+```bash
+cvt 1880 1000
+```
+
+输出示例:
+```
+Modeline "1920x1080_60.00" 173.00 1920 2048 2248 2576 1080 1083 1088 1120 -hsync +vsync
+```
+
+### 添加和应用新分辨率
+
+使用生成的参数创建新的显示模式:
+
+```bash
+xrandr --newmode "1920x1080_60.00" 173.00 1920 2048 2248 2576 1080 1083 1088 1120 -hsync +vsync
+```
+
+将新模式添加到指定的显示设备(请将HDMI-1替换为实际的显示设备名称):
+
+```bash
+xrandr --addmode HDMI-1 1920x1080_60.00
+```
+
+最后应用新的分辨率:
+
+```bash
+xrandr --output HDMI-1 --mode 1920x1080_60.00
+```
+
+## 三、系统语言本地化配置
+
+Linux系统支持多语言环境,通过locale配置可以设置系统语言和区域设置。
+
+### 配置系统语言
+
+执行以下命令打开语言配置界面:
+
+```bash
+sudo dpkg-reconfigure locales
+```
+
+在弹出的界面中:
+1. 使用空格键选择需要的语言选项
+2. 使用回车键确认选择
+3. 选择默认的系统语言环境
+4. 保存并退出
+
+配置完成后,系统将使用新选择的语言环境。部分应用程序可能需要重启才能显示新的语言设置。
+
+## 四、网络时间同步配置
+
+准确的系统时间对于日志记录、文件时间戳和网络通信都非常重要。NTP(Network Time Protocol)服务可以确保系统时间的准确性。
+
+### 选择合适的NTP服务器
+
+对于国内用户,建议使用国内的NTP服务器以获得更好的同步效果:
+
+**通用NTP池:**
+- `cn.pool.ntp.org`
+
+**阿里云NTP服务器:**
+- `ntp1.aliyun.com` 到 `ntp10.aliyun.com`
+
+**腾讯云NTP服务器:**
+- `time1.cloud.tencent.com` 到 `time10.cloud.tencent.com`
+
+### 配置NTP服务
+
+编辑NTP配置文件:
+
+```bash
+sudo vi /etc/ntp.conf
+```
+
+在文件中替换原有的server配置,添加国内NTP服务器:
+
+```ini
+server ntp1.aliyun.com iburst
+server ntp2.aliyun.com iburst
+server ntp3.aliyun.com iburst
+server ntp4.aliyun.com iburst
+```
+
+`iburst`参数可以加快初始同步速度。
+
+### 启动和验证NTP服务
+
+重启NTP服务使配置生效:
+
+```bash
+sudo systemctl restart ntp
+```
+
+验证时间同步状态:
+
+```bash
+sudo ntpq -p
+```
+
+该命令会显示NTP服务器列表及其状态信息。
+
+### 手动时间同步
+
+如需立即同步时间,可以使用ntpdate命令:
+
+```bash
+sudo ntpdate ntp1.aliyun.com
+```
+
+## 总结
+
+本文介绍了Linux系统的四个重要配置方面:VNC远程桌面可以实现图形化远程访问;分辨率配置确保最佳的显示效果;语言本地化让系统更适合本地用户使用;时间同步保证系统时间的准确性。
+
+这些配置技巧在日常的Linux系统管理中非常实用。掌握这些方法,可以帮助您更高效地管理和使用Linux系统,为工作和学习提供更好的环境支持。
+
+在实际应用中,建议根据具体需求调整配置参数,并定期检查服务状态,确保系统的稳定运行。
\ No newline at end of file
diff --git a/src/programming/linux/凝思/问题记录.md b/src/programming/linux/凝思/问题记录.md
new file mode 100644
index 0000000..819284e
--- /dev/null
+++ b/src/programming/linux/凝思/问题记录.md
@@ -0,0 +1,133 @@
+---
+icon: mdi:alert-circle
+date: 2025-05-22
+title: 问题记录
+category:
+ - Linux
+tag:
+ - 登录问题
+---
+
+
+
+## Linux图形登录界面循环跳转问题解决方案
+
+### 问题描述
+
+启动系统后,输入正确的账号和密码点击登录,屏幕闪烁一下后又跳回到登录界面,无法正常进入桌面环境。
+
+### 问题原因
+
+主目录下的 `.Xauthority` 文件的拥有者变成了 root 用户,导致普通用户登录时无法读取该文件。
+
+### 技术背景
+
+`.Xauthority` 是 X Window System 的认证文件:
+
+- **作用**:startx 脚本的记录文件,用于 X 服务器的身份验证
+- **工作原理**:
+ - Xserver 启动时读取 `~/.Xauthority` 文件,获取对应 display 的认证记录
+ - 客户端程序调用 `XOpenDisplay()` 时也会读取此文件,并将 magic code 发送给 Xserver
+ - Xserver 验证 magic code 正确后允许连接
+- **更新机制**:每次运行 startx 时,都会使用 xauth 的 add 命令向 `~/.Xauthority` 添加新的认证记录
+
+### 解决方案
+
+#### 方法一:修改文件拥有者(推荐)
+
+```bash
+# 进入用户主目录
+cd ~
+
+# 修改 .Xauthority 文件的拥有者为当前用户
+sudo chown username:username .Xauthority
+
+# 验证修改结果
+ls .Xauthority -l
+```
+
+成功修改后,输出应该类似:
+```
+-rw------- 1 username username 271 12月 13 09:04 .Xauthority
+```
+
+#### 方法二:删除文件(备选方案)
+
+```bash
+# 删除 .Xauthority 文件(系统会自动重新生成)
+rm ~/.Xauthority
+```
+
+**注意**:此方法未经完全验证,建议优先使用方法一。
+
+### 操作步骤
+
+1. 使用 `Ctrl + Alt + F1` 切换到文本终端
+2. 登录系统
+3. 执行上述解决方案中的命令
+4. 使用 `Ctrl + Alt + F7` 切换回图形登录界面
+5. 重新输入用户名和密码登录
+
+### 预防措施
+
+- 避免使用 root 权限修改用户主目录下的隐藏文件
+- 定期检查关键系统文件的权限设置
+- 在进行系统维护时注意保持文件所有权的正确性
+
+---
+
+## VNC环境下安装包启动失败问题解决方案
+
+### 问题描述
+
+在VNC环境中无法启动安装包或图形应用程序。
+
+### 问题原因
+
+环境变量中的显示端口(DISPLAY)与VNC实际使用的端口不一致,导致应用程序无法找到正确的显示设备。
+
+### 解决方案
+
+#### 步骤1:检查当前显示端口
+
+```bash
+# 查看当前DISPLAY环境变量设置
+echo $DISPLAY
+```
+
+常见输出示例:
+- `:1` - VNC显示端口1
+- `:0` - 本地显示端口
+- 空白 - 未设置显示端口
+
+#### 步骤2:手动设置正确的显示端口
+
+```bash
+# 设置DISPLAY环境变量与VNC连接端口一致
+export DISPLAY=:1
+
+# 验证设置是否生效
+echo $DISPLAY
+```
+
+#### 步骤3:启动应用程序
+
+```bash
+# 现在可以正常启动图形应用程序或安装包
+./your_installer.run
+# 或
+your_application
+```
+
+### 注意事项
+
+- **端口匹配**:确保 `DISPLAY` 变量值与VNC服务器的显示端口一致
+- **会话有效性**:该设置仅在当前终端会话中有效
+- **永久设置**:如需永久生效,可将 `export DISPLAY=:1` 添加到 `~/.bashrc` 或 `~/.profile` 文件中
+
+### 常见端口说明
+
+- `:0` - 本地物理显示器
+- `:1` - VNC显示端口1(通常对应5901端口)
+- `:2` - VNC显示端口2(通常对应5902端口)
+- 以此类推...
\ No newline at end of file
diff --git a/src/programming/linux/基础/01-权限.md b/src/programming/linux/基础/01-权限.md
new file mode 100644
index 0000000..cf3d5b0
--- /dev/null
+++ b/src/programming/linux/基础/01-权限.md
@@ -0,0 +1,321 @@
+---
+# 页面基础信息
+title: Linux 权限
+icon: simple-icons:linux
+date: 2024-11-14
+category:
+ - Linux 基础
+ - 系统运维
+tag:
+ - Linux
+ - 权限管理
+ - 用户与用户组
+ - 文件系统
+ - 运维实践
+
+# 主题相关增强
+star: true # 在文章列表中高亮显示
+sticky: 1 # 置顶权重(数字越大越靠前,可按需调整)
+article: true # 标记为文章(而非文档/幻灯片等)
+timeline: true # 在时间线中显示
+
+# SEO & 列表摘要
+description: >-
+ 深度解析 Linux 权限体系,从用户、用户组到 SGID 与 umask 的协同,
+ 结合共享目录实战案例,帮助你系统掌握多用户环境下的安全与协作配置。
+---
+
+深度解析 Linux 权限体系,从用户、用户组到 SGID 与 umask 的协同,
+结合共享目录实战案例,帮助你系统掌握多用户环境下的安全与协作配置。
+
+
+
+# Linux 权限体系深度解析:从用户组到共享目录的完整实践
+
+> 这是一份关于 Linux 用户与权限体系的深度整理,涵盖基础概念、核心机制与实战配置,帮助你彻底理解并灵活运用 Linux 权限管理。
+
+---
+
+## 一、Linux 权限体系基础
+
+Linux 的权限体系是系统安全和多用户协作的基石。它通过**用户**、**用户组**和**权限位**三个核心要素,精确控制谁能对文件做什么操作。
+
+### 1.1 用户类型
+
+```bash
+# 超级用户(root)
+- 拥有系统的全部权限,命令提示符为 #
+- 可修改任何文件、安装软件、管理用户
+
+# 普通用户
+- 只能操作自己有权限的文件和目录,命令提示符为 $
+
+# 切换方式
+su 用户名 # 切换用户
+sudo 命令 # 临时提升权限执行操作
+```
+
+### 1.2 文件权限结构
+
+每个文件或目录都有三类访问者和三种基本权限:
+
+**三类访问者**
+- **Owner(拥有者)**:文件的创建者或指定用户
+- **Group(所属组)**:同组用户共享一定权限
+- **Others(其他人)**:系统中所有其他用户
+
+**三种基本权限**
+- **r**(read):读权限,可查看文件内容或列出目录
+- **w**(write):写权限,可修改文件或在目录中添加/删除文件
+- **x**(execute):执行权限,可运行文件或进入目录
+
+**权限表示示例**
+```bash
+-rw-r--r--
+```
+- 文件类型:`-` 表示普通文件
+- 拥有者:`rw-` → 可读可写
+- 所属组:`r--` → 只读
+- 其他人:`r--` → 只读
+
+### 1.3 权限管理命令
+
+```bash
+chmod 755 file # 拥有者可读写执行,组和其他人可读执行
+chown 用户:组 file # 修改文件拥有者和所属组
+chgrp 组 file # 修改文件所属组
+umask 002 # 设置默认权限掩码
+```
+
+---
+
+## 二、深入理解用户组(Group)
+
+用户组是 Linux 权限体系的精髓所在,它能极大简化多用户协作场景下的权限管理。
+
+### 2.1 组的两种类型
+
+**主组(Primary Group)**
+- 每个用户必须属于一个主组
+- 用户创建文件时,文件的所属组默认就是该用户的主组
+
+**附加组(Secondary Group)**
+- 用户可以属于多个附加组
+- 附加组赋予用户额外的权限,比如访问某些共享目录
+
+### 2.2 权限与组的关系
+
+Linux 判断文件访问权限的顺序是:**Owner → Group → Others**
+
+```bash
+-rw-r----- 1 alice dev 1234 Nov 14 report.txt
+```
+- **alice**(拥有者)→ `rw-` 可读写
+- **dev组成员** → `r--` 只读
+- **其他人** → `---` 无权限
+
+**关键结论**:拥有者、组、其他人的权限是**独立设置**的,完全可能出现"拥有者有权限,但组没有权限"的情况,这在保护敏感文件时非常有用。
+
+### 2.3 组管理常用命令
+
+```bash
+groups 用户名 # 查看用户所属组
+id 用户名 # 查看用户详细信息
+
+groupadd 组名 # 创建组
+groupdel 组名 # 删除组
+
+usermod -aG 组名 用户名 # 把用户加入附加组(-a表示追加,-G指定附加组)
+```
+
+---
+
+## 三、常用权限模式对比表
+
+| 权限模式 | Owner | Group | Others | 典型场景 |
+|----------|-------|-------|--------|-----------|
+| `600` | 读写 | 无 | 无 | 私钥文件(如 `~/.ssh/id_rsa`) |
+| `644` | 读写 | 只读 | 只读 | 配置文件(如 `/etc/hosts`) |
+| `660` | 读写 | 读写 | 无 | 项目组共享文件 |
+| `700` | 读写执行 | 无 | 无 | 个人脚本或私有目录 |
+| `755` | 读写执行 | 读执行 | 读执行 | 公共程序或脚本 |
+| `775` | 读写执行 | 读写执行 | 读执行 | 团队共享目录 |
+| `777` | 读写执行 | 读写执行 | 读写执行 | 临时目录(如 `/tmp`) |
+
+---
+
+## 四、共享目录实战配置
+
+假设公司智能巡视程序位于 `/home/sunri/PRS7950`,需要让 `user1`、`user2`、`user3` 三人都能操作该目录。
+
+### 4.1 核心配置步骤
+
+```bash
+# 1. 创建专用组(推荐,而非使用 docker 组)
+sudo groupadd prsgroup
+
+# 2. 把用户加入组
+sudo usermod -aG prsgroup user1
+sudo usermod -aG prsgroup user2
+sudo usermod -aG prsgroup user3
+
+# 3. 修改目录所属组
+sudo chgrp -R prsgroup /home/sunri/PRS7950
+
+# 4. 设置 SGID 位和权限
+sudo chmod 2770 /home/sunri/PRS7950
+```
+**关键说明**:这里的 `2` 就是 **SGID 位**,它确保在该目录下创建的文件自动继承 `prsgroup` 组。
+
+### 4.2 调整 umask(可选)
+
+在用户的 `~/.bashrc` 或 `~/.profile` 中添加:
+```bash
+umask 0002
+```
+这样新建文件权限为 `664`(组成员可读写),目录为 `775`。
+
+### 4.3 验证配置
+
+```bash
+# 切换到 user2 测试
+su - user2
+cd /home/sunri/PRS7950
+touch test.txt
+ls -l test.txt # 所属组应为 prsgroup
+```
+
+---
+
+## 五、SGID 位与 umask 深度解析
+
+### 5.1 SGID 位(Set Group ID)
+
+**作用**:主要用于目录,确保目录下新建文件自动继承目录的所属组。
+
+```bash
+# 设置 SGID 位
+chmod g+s /shared_dir # 符号模式
+chmod 2770 /shared_dir # 八进制模式(推荐)
+```
+
+**典型场景**:团队共享目录,保证所有成员创建的文件都属于同一组。
+
+### 5.2 umask(用户文件创建掩码)
+
+**定义**:决定新建文件/目录的默认权限。
+
+**计算方式**:实际权限 = 默认权限 - umask
+- 文件默认:`666`(rw-rw-rw-)
+- 目录默认:`777`(rwxrwxrwx)
+
+```bash
+# 示例:umask=022
+666 - 022 = 644 (rw-r--r--) # 文件
+777 - 022 = 755 (rwxr-xr-x) # 目录
+
+# 示例:umask=002(适合团队协作)
+666 - 002 = 664 (rw-rw-r--) # 文件(组成员可写)
+777 - 002 = 775 (rwxrwxr-x) # 目录(组成员可写)
+```
+
+### 5.3 两者配合的最佳实践
+
+在共享目录场景下,**SGID + umask 002** 是黄金组合:
+- **SGID** 确保文件所属组统一
+- **umask 002** 确保组成员有写权限
+
+---
+
+## 六、特殊场景:Docker 组权限详解
+
+### 6.1 Docker 组的本质
+
+```bash
+# 安装 Docker 时会自动创建 docker 组
+# 关键:/var/run/docker.sock 套接字的组权限
+ls -l /var/run/docker.sock
+# 输出示例:srw-rw---- 1 root docker 0 Nov 14 10:20 /var/run/docker.sock
+```
+
+Docker 组通过**套接字的组权限**让用户能与 Docker 守护进程通信,**并非**通过 SGID 或 umask 实现。
+
+### 6.2 安全警示
+
+**加入 docker 组 ≈ 获得 root 权限**
+- 组成员可以启动特权容器,挂载宿主机根目录
+- 可以修改系统文件,完全控制系统
+- **生产环境应谨慎使用**,建议通过 `sudoers` 限制特定命令
+
+```bash
+# 更安全的做法:限制只能执行特定 Docker 命令
+username ALL=(ALL) /usr/bin/docker ps, /usr/bin/docker exec
+```
+
+### 6.3 常见误区澄清
+
+**误区**:docker 组成员创建的文件会自动属于 docker 组
+
+**事实**:
+- docker 组**不会**自动设置 SGID 位
+- 用户在非 SGID 目录创建文件时,文件属于主组,不是 docker 组
+- 要实现共享,仍需手动配置 SGID + umask
+
+---
+
+## 七、安全建议与最佳实践
+
+### 7.1 最小权限原则
+
+- 不要直接加入 **root 组**,使用 **sudo 机制**临时提权
+- Docker 组权限极大,仅在开发环境或可信场景使用
+- 为不同项目创建独立组(如 `prsgroup`),而非混用 docker 组
+
+### 7.2 权限配置检查清单
+
+配置共享目录时,请确认:
+- [ ] 是否创建了专用组?
+- [ ] 所有用户是否已加入该组?
+- [ ] 目录是否设置了 SGID 位?(`chmod 2770`)
+- [ ] 用户 umask 是否设为 0002?
+- [ ] 目录权限是否为 770 或 775?
+
+### 7.3 审计与监控
+
+```bash
+# 查看 sudo 操作日志
+sudo grep sudo /var/log/auth.log
+
+# 查看文件访问记录
+auditctl -w /home/sunri/PRS7950 -p wa -k prs7950_access
+```
+
+---
+
+## 八、总结
+
+Linux 权限体系的精髓在于 **用户 + 用户组 + 权限位** 的组合:
+
+1. **用户分类**:root 拥有全部权限,普通用户受权限约束
+2. **用户组**:是用户的集合,简化批量权限管理
+3. **权限独立性**:Owner、Group、Others 的权限完全独立
+4. **SGID + umask**:多用户协作的黄金组合,确保文件自动继承组且组成员可读写
+5. **Docker 组**:本质是套接字访问权限,不是文件共享机制,需谨慎使用
+
+掌握这些概念后,你不仅能理解 Linux 权限的"为什么",更能灵活应对各种多用户协作场景,实现既安全又高效的权限管理。
+
+---
+
+**参考命令速查**
+
+```bash
+# 创建共享目录完整流程
+sudo groupadd projectgroup
+sudo usermod -aG projectgroup user1 user2 user3
+mkdir /shared/project
+sudo chgrp -R projectgroup /shared/project
+sudo chmod 2770 /shared/project
+echo "umask 0002" >> ~/.bashrc
+```
+
+希望这份整理能成为你理解和运用 Linux 权限体系的可靠指南!
\ No newline at end of file
diff --git a/src/programming/linux/基础/02-Less常用指令.md b/src/programming/linux/基础/02-Less常用指令.md
new file mode 100644
index 0000000..57beef6
--- /dev/null
+++ b/src/programming/linux/基础/02-Less常用指令.md
@@ -0,0 +1,307 @@
+---
+icon: mdi:console
+date: 2025-05-22
+category:
+ - Linux
+ - 命令行工具
+tag:
+ - Linux
+ - less
+ - 命令行
+ - 文本查看
+---
+
+# Linux Less 常用指令详解
+
+less 是 Linux 系统中最常用的文本文件查看工具之一,相比 cat 和 more 命令,它提供了更强大的功能和交互性。less 允许你向前或向后浏览文件内容,搜索特定文本,以及其他实用功能。
+
+## 目录
+1. [Less 命令简介](#less-命令简介)
+2. [基本语法](#基本语法)
+3. [常用选项](#常用选项)
+4. [导航指令](#导航指令)
+5. [搜索功能](#搜索功能)
+6. [标记和跳转](#标记和跳转)
+7. [窗口调整](#窗口调整)
+8. [其他实用功能](#其他实用功能)
+9. [与其他命令结合使用](#与其他命令结合使用)
+10. [实际应用示例](#实际应用示例)
+
+## Less 命令简介
+
+less 命令是一个用于查看文本文件内容的过滤器程序。它的名字来源于"less is more"的理念,表示它是 more 命令的一个增强版本。主要特点包括:
+
+- 支持向前和向后浏览文件
+- 提供强大的搜索功能
+- 支持多种导航方式
+- 内存效率高,即使对于大文件也能快速加载
+- 支持多种文件格式
+- 提供丰富的交互式命令
+
+## 基本语法
+
+```bash
+less [选项] [文件名]
+```
+
+基本使用示例:
+```bash
+# 查看单个文件
+less filename.txt
+
+# 查看多个文件
+less file1.txt file2.txt
+
+# 从标准输入读取内容
+cat filename.txt | less
+
+# 查看命令输出结果
+ps aux | less
+```
+
+## 常用选项
+
+| 选项 | 描述 |
+|------|------|
+| -N 或 --line-numbers | 显示行号 |
+| -S 或 --chop-long-lines | 截断长行而不是换行显示 |
+| -X 或 --no-init | 退出时不清除屏幕 |
+| -F 或 --quit-if-one-screen | 如果内容少于一屏则直接退出 |
+| -r 或 --raw-control-chars | 显示原始控制字符 |
+| -R 或 --RAW-CONTROL-CHARS | 类似 -r,但只处理颜色控制字符 |
+| -i 或 --ignore-case | 搜索时忽略大小写 |
+| -I 或 --IGNORE-CASE | 更严格的忽略大小写模式 |
+| -g 或 --hilite-search | 高亮显示搜索结果 |
+| -G 或 --HILITE-SEARCH | 取消高亮显示 |
+
+使用示例:
+```bash
+# 显示行号
+less -N filename.txt
+
+# 截断长行
+less -S filename.txt
+
+# 忽略大小写搜索
+less -i filename.txt
+```
+
+## 导航指令
+
+在 less 界面中,可以使用以下按键进行导航:
+
+### 基本移动
+| 按键 | 功能 |
+|------|------|
+| 空格键 或 Page Down | 向下翻页 |
+| b 或 Page Up | 向上翻页 |
+| 回车键 或 j 或 ↓ | 向下移动一行 |
+| k 或 ↑ | 向上移动一行 |
+| d 或 Ctrl+D | 向下移动半页 |
+| u 或 Ctrl+U | 向上移动半页 |
+
+### 行级别移动
+| 按键 | 功能 |
+|------|------|
+| g 或 < 或 Home | 移动到文件开头 |
+| G 或 > 或 End | 移动到文件结尾 |
+| 数字+g | 移动到指定行号 |
+| 数字+回车 | 向下移动指定行数 |
+
+### 水平移动(当使用 -S 选项时)
+| 按键 | 功能 |
+|------|------|
+| → 或 右箭头 | 向右滚动 |
+| ← 或 左箭头 | 向左滚动 |
+| 数字+→ | 向右滚动指定列数 |
+| 数字+← | 向左滚动指定列数 |
+
+## 搜索功能
+
+less 提供了强大的文本搜索功能:
+
+### 基本搜索
+| 按键 | 功能 |
+|------|------|
+| /pattern | 向前搜索 pattern |
+| ?pattern | 向后搜索 pattern |
+| n | 查找下一个匹配项 |
+| N | 查找上一个匹配项 |
+| &pattern | 只显示匹配 pattern 的行 |
+
+### 搜索技巧
+```bash
+# 搜索包含特殊字符的内容需要转义
+/hello\.world
+
+# 不区分大小写的搜索(需要配合 -i 选项)
+/hello
+
+# 搜索以某词开头的行
+/^Start
+
+# 搜索以某词结尾的行
+/end$
+
+# 搜索包含数字的行
+/[0-9]
+```
+
+## 标记和跳转
+
+less 支持设置标记以便快速跳转:
+
+| 按键 | 功能 |
+|------|------|
+| mx | 在当前位置设置标记 x(x 为 a-z 的字母) |
+| 'x | 跳转到标记 x 的位置 |
+| '' | 返回上次浏览的位置 |
+| Ctrl+X Ctrl+X | 在两个位置间切换 |
+
+## 窗口调整
+
+| 按键 | 功能 |
+|------|------|
+| -+option | 添加选项 |
+| _+option | 显示当前选项状态 |
+| +cmd | 执行命令后进入 less |
+| = 或 Ctrl+G | 显示文件信息和状态 |
+| :e filename | 打开另一个文件 |
+| :n | 切换到下一个文件 |
+| :p | 切换到上一个文件 |
+
+## 其他实用功能
+
+### 文件信息
+| 按键 | 功能 |
+|------|------|
+| = 或 Ctrl+G | 显示文件名、行号、百分比等信息 |
+| h 或 H | 显示帮助信息 |
+| V | 显示 less 版本信息 |
+
+### 编辑功能
+| 按键 | 功能 |
+|------|------|
+| v | 使用编辑器编辑当前文件 |
+| !command | 执行 shell 命令 |
+
+### 退出和刷新
+| 按键 | 功能 |
+|------|------|
+| q 或 Q 或 ZZ | 退出 less |
+| Ctrl+C | 中断当前操作 |
+| Ctrl+L | 刷新屏幕 |
+| F | 进入跟随模式(类似 tail -f)|
+
+## 与其他命令结合使用
+
+less 经常与其他 Linux 命令结合使用:
+
+```bash
+# 查看压缩文件内容
+zcat file.gz | less
+
+# 查看日志文件的最新内容
+tail -f /var/log/syslog | less
+
+# 查看进程信息
+ps aux | less
+
+# 查看系统信息
+df -h | less
+
+# 查看网络连接
+netstat -an | less
+
+# 查看硬件信息
+lspci | less
+
+# 查看内存使用情况
+free -h | less
+```
+
+## 实际应用示例
+
+### 1. 查看系统日志
+```bash
+# 查看系统日志文件
+less /var/log/syslog
+
+# 查看最近的日志条目
+less +G /var/log/syslog
+
+# 搜索特定错误信息
+less /var/log/syslog
+/error # 在 less 中输入搜索命令
+```
+
+### 2. 分析大文件
+```bash
+# 查看大型日志文件,显示行号且截断长行
+less -SN /var/log/access.log
+
+# 查找特定 IP 地址的访问记录
+less /var/log/access.log
+/192\.168\.1\.100
+```
+
+### 3. 监控实时日志
+```bash
+# 进入 less 后按 Shift+F 进入跟随模式
+less /var/log/application.log
+# 按 Shift+F 进入跟随模式
+# 按 Ctrl+C 退出跟随模式
+```
+
+### 4. 比较文件内容
+```bash
+# 查看多个文件
+less file1.txt file2.txt
+# 使用 :n 和 :p 在文件间切换
+```
+
+### 5. 查看二进制文件
+```bash
+# 查看二进制文件内容
+less /bin/ls
+# 注意:会显示乱码,主要用于检查文件是否存在
+```
+
+## 高级技巧
+
+### 1. 自定义 less 环境
+可以通过环境变量定制 less 的行为:
+```bash
+# 在 ~/.bashrc 中添加
+export LESS="-R -N -i"
+```
+
+常用环境变量:
+- LESS:设置默认选项
+- LESSKEY:指定 lesskey 文件路径
+- LESSSECURE:启用安全模式
+
+### 2. 使用 lesskey 配置文件
+创建 ~/.lesskey 文件来自定义快捷键:
+```
+#command
+#line-edit
+```
+
+### 3. 在 less 中执行 shell 命令
+```bash
+# 在 less 中按 ! 然后输入命令
+!pwd
+!ls -la
+```
+
+## 总结
+
+less 是 Linux 系统中非常强大且实用的文本查看工具,掌握其各种快捷键和功能可以大大提高工作效率。通过本文档的学习,你应该能够:
+
+1. 熟练使用基本的导航和搜索功能
+2. 灵活运用各种选项来满足不同的查看需求
+3. 结合其他命令进行复杂的文本分析
+4. 利用高级功能提高工作效率
+
+建议在日常工作中多加练习这些命令,逐渐熟悉并掌握 less 的各种功能。
\ No newline at end of file
diff --git a/src/programming/linux/基础/nginx基础入门.md b/src/programming/linux/基础/nginx基础入门.md
new file mode 100644
index 0000000..adf25f2
--- /dev/null
+++ b/src/programming/linux/基础/nginx基础入门.md
@@ -0,0 +1,506 @@
+---
+title: Nginx 基础入门
+icon: logos:nginx
+date: 2025-12-11
+category:
+ - Linux
+ - 基础
+tag:
+ - Nginx
+ - Web服务器
+ - 反向代理
+---
+
+# Nginx 基础入门
+
+Nginx(发音为 "engine-x")是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。
+
+
+
+## 什么是 Nginx
+
+Nginx(发音为 "engine-x")是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。由俄罗斯程序员 Igor Sysoev 开发,于 2004 年首次公开发布。
+
+### 主要特点
+
+- **高性能**:采用异步事件驱动架构,能够处理大量并发连接
+- **低资源消耗**:相比 Apache,占用更少的内存和 CPU
+- **高可靠性**:稳定性极高,可以 7×24 小时不间断运行
+- **模块化设计**:功能通过模块实现,易于扩展
+
+### 主要应用场景
+
+1. **Web 服务器**:直接提供静态资源服务
+2. **反向代理**:代理后端应用服务器
+3. **负载均衡**:分发请求到多个后端服务器
+4. **HTTP 缓存**:缓存静态资源,减轻后端压力
+
+## 安装 Nginx
+
+### Ubuntu/Debian 系统
+
+```bash
+# 更新软件包列表
+sudo apt update
+
+# 安装 Nginx
+sudo apt install nginx -y
+
+# 启动 Nginx
+sudo systemctl start nginx
+
+# 设置开机自启
+sudo systemctl enable nginx
+
+# 查看运行状态
+sudo systemctl status nginx
+```
+
+### CentOS/RHEL 系统
+
+```bash
+# 安装 Nginx
+sudo yum install nginx -y
+
+# 或者使用 dnf(CentOS 8+)
+sudo dnf install nginx -y
+
+# 启动 Nginx
+sudo systemctl start nginx
+
+# 设置开机自启
+sudo systemctl enable nginx
+```
+
+### 验证安装
+
+安装完成后,在浏览器中访问服务器 IP 地址,如果看到 Nginx 欢迎页面,说明安装成功。
+
+```bash
+# 查看 Nginx 版本
+nginx -v
+
+# 查看详细版本和编译参数
+nginx -V
+```
+
+## Nginx 目录结构
+
+### 主要目录(Ubuntu/Debian)
+
+```
+/etc/nginx/ # Nginx 配置目录
+├── nginx.conf # 主配置文件
+├── sites-available/ # 可用站点配置
+├── sites-enabled/ # 已启用站点配置(软链接)
+├── conf.d/ # 其他配置文件
+├── snippets/ # 配置片段
+└── modules-enabled/ # 已启用的模块
+
+/var/log/nginx/ # 日志目录
+├── access.log # 访问日志
+└── error.log # 错误日志
+
+/var/www/html/ # 默认网站根目录
+/usr/share/nginx/html/ # Nginx 默认页面
+```
+
+### 主要目录(CentOS/RHEL)
+
+```
+/etc/nginx/ # Nginx 配置目录
+├── nginx.conf # 主配置文件
+└── conf.d/ # 配置文件目录
+
+/var/log/nginx/ # 日志目录
+/usr/share/nginx/html/ # 默认网站根目录
+```
+
+## 基本配置
+
+### nginx.conf 主配置文件结构
+
+```nginx
+# 全局块:影响整个 Nginx 服务器
+user nginx;
+worker_processes auto; # 工作进程数,auto 表示自动检测 CPU 核心数
+error_log /var/log/nginx/error.log;
+pid /run/nginx.pid;
+
+# events 块:影响 Nginx 服务器与用户的网络连接
+events {
+ worker_connections 1024; # 每个进程的最大连接数
+ use epoll; # 使用 epoll 事件驱动模型
+}
+
+# http 块:配置最频繁的部分
+http {
+ # 日志格式
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+
+ # 基本设置
+ sendfile on;
+ tcp_nopush on;
+ tcp_nodelay on;
+ keepalive_timeout 65;
+ types_hash_max_size 2048;
+
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ # 包含其他配置文件
+ include /etc/nginx/conf.d/*.conf;
+ include /etc/nginx/sites-enabled/*;
+}
+```
+
+### 虚拟主机配置示例
+
+创建配置文件 `/etc/nginx/conf.d/example.conf`:
+
+```nginx
+server {
+ listen 80; # 监听端口
+ server_name example.com; # 域名
+ root /var/www/example; # 网站根目录
+ index index.html index.htm; # 默认首页
+
+ # 访问日志
+ access_log /var/log/nginx/example.access.log;
+ error_log /var/log/nginx/example.error.log;
+
+ # 静态文件处理
+ location / {
+ try_files $uri $uri/ =404;
+ }
+
+ # 错误页面
+ error_page 404 /404.html;
+ error_page 500 502 503 504 /50x.html;
+}
+```
+
+## 常用配置场景
+
+### 1. 静态网站部署
+
+```nginx
+server {
+ listen 80;
+ server_name static.example.com;
+ root /var/www/static;
+ index index.html;
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # 静态资源缓存
+ location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
+ expires 30d;
+ add_header Cache-Control "public, immutable";
+ }
+}
+```
+
+### 2. 反向代理
+
+```nginx
+server {
+ listen 80;
+ server_name api.example.com;
+
+ location / {
+ proxy_pass http://localhost:8080; # 后端服务地址
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
+```
+
+### 3. 负载均衡
+
+```nginx
+upstream backend {
+ server 192.168.1.10:8080 weight=3; # 权重为 3
+ server 192.168.1.11:8080 weight=2; # 权重为 2
+ server 192.168.1.12:8080 backup; # 备用服务器
+}
+
+server {
+ listen 80;
+ server_name lb.example.com;
+
+ location / {
+ proxy_pass http://backend;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+}
+```
+
+### 4. HTTPS 配置
+
+```nginx
+server {
+ listen 443 ssl http2;
+ server_name secure.example.com;
+
+ ssl_certificate /etc/nginx/ssl/cert.pem;
+ ssl_certificate_key /etc/nginx/ssl/key.pem;
+
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers HIGH:!aNULL:!MD5;
+ ssl_prefer_server_ciphers on;
+
+ location / {
+ root /var/www/secure;
+ index index.html;
+ }
+}
+
+# HTTP 重定向到 HTTPS
+server {
+ listen 80;
+ server_name secure.example.com;
+ return 301 https://$server_name$request_uri;
+}
+```
+
+### 5. 限流配置
+
+```nginx
+http {
+ # 定义限流区域
+ limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
+
+ server {
+ listen 80;
+ server_name api.example.com;
+
+ location /api/ {
+ # 应用限流,允许 20 个突发请求
+ limit_req zone=api_limit burst=20 nodelay;
+ proxy_pass http://backend;
+ }
+ }
+}
+```
+
+## 常用命令
+
+### 服务管理
+
+```bash
+# 启动 Nginx
+sudo systemctl start nginx
+
+# 停止 Nginx
+sudo systemctl stop nginx
+
+# 重启 Nginx
+sudo systemctl restart nginx
+
+# 重新加载配置(不中断服务)
+sudo systemctl reload nginx
+
+# 查看状态
+sudo systemctl status nginx
+```
+
+### 配置管理
+
+```bash
+# 测试配置文件语法
+sudo nginx -t
+
+# 测试配置并显示配置内容
+sudo nginx -T
+
+# 重新加载配置
+sudo nginx -s reload
+
+# 停止 Nginx
+sudo nginx -s stop
+
+# 优雅停止(处理完当前请求后停止)
+sudo nginx -s quit
+```
+
+### 日志管理
+
+```bash
+# 查看访问日志
+sudo tail -f /var/log/nginx/access.log
+
+# 查看错误日志
+sudo tail -f /var/log/nginx/error.log
+
+# 重新打开日志文件(日志轮转后使用)
+sudo nginx -s reopen
+```
+
+## Location 匹配规则
+
+### 语法
+
+```nginx
+location [ = | ~ | ~* | ^~ ] uri {
+ ...
+}
+```
+
+### 优先级(从高到低)
+
+1. **`=`** 精确匹配
+2. **`^~`** 前缀匹配(匹配成功后不再检查正则)
+3. **`~`** 正则匹配(区分大小写)
+4. **`~*`** 正则匹配(不区分大小写)
+5. **无修饰符** 前缀匹配
+
+### 示例
+
+```nginx
+# 精确匹配
+location = / {
+ return 200 "exact match";
+}
+
+# 前缀匹配,优先级高于正则
+location ^~ /static/ {
+ root /var/www;
+}
+
+# 正则匹配(区分大小写)
+location ~ \.php$ {
+ fastcgi_pass 127.0.0.1:9000;
+}
+
+# 正则匹配(不区分大小写)
+location ~* \.(jpg|jpeg|png|gif)$ {
+ expires 30d;
+}
+
+# 普通前缀匹配
+location /api/ {
+ proxy_pass http://backend;
+}
+```
+
+## 常见问题排查
+
+### 1. 端口被占用
+
+```bash
+# 查看 80 端口占用
+sudo lsof -i :80
+# 或
+sudo netstat -tlnp | grep :80
+```
+
+### 2. 权限问题
+
+```bash
+# 检查文件权限
+ls -la /var/www/html/
+
+# 修改所有者
+sudo chown -R nginx:nginx /var/www/html/
+
+# 修改权限
+sudo chmod -R 755 /var/www/html/
+```
+
+### 3. SELinux 问题(CentOS/RHEL)
+
+```bash
+# 临时关闭 SELinux
+sudo setenforce 0
+
+# 永久关闭(需编辑配置文件)
+sudo vi /etc/selinux/config
+# 设置 SELINUX=disabled
+
+# 或者设置 SELinux 策略
+sudo setsebool -P httpd_can_network_connect 1
+```
+
+### 4. 防火墙设置
+
+```bash
+# Ubuntu/Debian (UFW)
+sudo ufw allow 'Nginx Full'
+sudo ufw allow 80/tcp
+sudo ufw allow 443/tcp
+
+# CentOS/RHEL (firewalld)
+sudo firewall-cmd --permanent --add-service=http
+sudo firewall-cmd --permanent --add-service=https
+sudo firewall-cmd --reload
+```
+
+## 性能优化建议
+
+### 1. 工作进程优化
+
+```nginx
+worker_processes auto; # 自动设置为 CPU 核心数
+worker_rlimit_nofile 65535; # 每个进程最大文件打开数
+
+events {
+ worker_connections 4096; # 每个进程最大连接数
+ use epoll; # 使用 epoll 模型
+ multi_accept on; # 一次接受多个连接
+}
+```
+
+### 2. 开启 Gzip 压缩
+
+```nginx
+http {
+ gzip on;
+ gzip_vary on;
+ gzip_min_length 1024;
+ gzip_comp_level 6;
+ gzip_types text/plain text/css text/xml text/javascript
+ application/json application/javascript application/xml+rss
+ application/rss+xml font/truetype font/opentype
+ application/vnd.ms-fontobject image/svg+xml;
+}
+```
+
+### 3. 缓存配置
+
+```nginx
+http {
+ # 文件缓存
+ open_file_cache max=10000 inactive=30s;
+ open_file_cache_valid 60s;
+ open_file_cache_min_uses 2;
+ open_file_cache_errors on;
+}
+```
+
+## 总结
+
+Nginx 是一个功能强大且灵活的 Web 服务器和反向代理服务器。掌握其基本配置和常用功能,能够帮助你:
+
+- ✅ 快速部署静态网站
+- ✅ 搭建反向代理和负载均衡
+- ✅ 配置 HTTPS 安全连接
+- ✅ 实现请求限流和访问控制
+- ✅ 优化网站性能
+
+建议在学习过程中多实践,通过实际项目来加深理解。
+
+## 参考资料
+
+- [Nginx 官方文档](https://nginx.org/en/docs/)
+- [Nginx 中文文档](https://www.nginx.cn/doc/)
+- [Nginx 配置生成器](https://www.digitalocean.com/community/tools/nginx)
\ No newline at end of file
diff --git a/src/tools/01gkd.md b/src/tools/01gkd.md
new file mode 100644
index 0000000..387ce0f
--- /dev/null
+++ b/src/tools/01gkd.md
@@ -0,0 +1,75 @@
+---
+title: GKD(搞快点)
+icon: lightbulb
+category:
+ - 软件推荐
+ - 跳过开屏广告
+tag:
+ - Android
+ - 广告拦截
+ - GKD
+---
+
+::: center
+
+:::
+
+## 为什么推荐 GKD?
+
+- **痛点解决**:自动跳过开屏广告、摇一摇跳转广告、应用内弹窗广告
+- **开源免费**:GitHub 25.7K⭐,GPL-3.0 协议,社区持续维护
+- **非侵入式**:模拟点击跳过广告,无需 Root,不修改应用代码
+
+## 核心功能
+
+| 功能 | 说明 |
+|---|---|
+| 开屏广告秒杀 | 自动点击“跳过”,成功率 >90% |
+| 应用内广告清除 | 支持知乎、贴吧、微信等主流 APP |
+| 扩展自动化 | 微信登录确认、红包领取等 |
+
+## 快速上手(3 步搞定)
+
+### 1. 下载安装
+
+::: tip 推荐下载
+- **官网**:[gkd.li/guide/](https://gkd.li/guide/)
+- **GitHub 稳定版**:[Releases](https://github.com/gkd-kit/gkd/releases)
+ :::
+
+### 2. 开启权限
+
+```bash
+设置 → 无障碍 → 授权 GKD
+```
+
+### 3. 添加规则
+
+1. 打开规则列表:[GitHub Topics](https://github.com/topics/gkd-subscription)
+2. 复制订阅链接 → GKD 内粘贴 → 启用规则
+
+## 用户口碑
+
+> “运行稳如老狗,无需反复开权限,比李跳跳还香!”
+> —— 知乎用户
+
+- **GitHub 25.7K⭐** 开发者认可
+- **零差评稳定**:不卡顿、不耗电、无隐私风险
+
+## 横向对比
+
+| 工具 | 优势 | 局限 |
+|---|---|---|
+| **GKD** | 开源、无需 Root、规则丰富 | 需无障碍权限 |
+| AdGuard | 网络层拦截,省流量 | 无法处理原生广告 |
+| Tasker | 高度自定义 | 配置复杂,学习成本高 |
+
+## 未来展望
+
+- ✅ 支持更多 APP 自定义规则
+- ✅ 优化低端机性能
+- ✅ 扩展自动化任务(签到、填表)
+
+## 立即体验
+- [GKD 官网下载](https://gkd.li/guide/)
+- [GitHub 仓库](https://github.com/gkd-kit/gkd)
\ No newline at end of file
diff --git a/src/tools/02WSL2.md b/src/tools/02WSL2.md
new file mode 100644
index 0000000..200761a
--- /dev/null
+++ b/src/tools/02WSL2.md
@@ -0,0 +1,559 @@
+---
+icon: mdi:microsoft-windows
+date: 2025-05-08
+category:
+ - win10
+tag:
+ - wsl
+ - http
+title: WSL2
+---
+
+
+# WSL2完全配置指南:从安装到实用工具
+Windows Subsystem for Linux (WSL2) 为Windows用户提供了无需双系统或虚拟机就能运行Linux环境的能力。本文将全面介绍WSL2的安装、配置和优化过程,包括网络设置、常用工具安装和问题排查等内容。
+
+## 安装WSL2
+
+1. **启用WSL功能**: 打开控制面板,启用"Windows Subsystem for Linux"和"虚拟机平台"功能,然后重启电脑。
+
+2. **检查WSL版本**:
+
+ ```powershell
+ wsl --status
+ wsl --update
+ ```
+
+3. **设置默认为WSL2**:
+
+ ```powershell
+ wsl --set-default-version 2
+ ```
+
+4. **查看可用的Linux发行版**:
+
+ ```powershell
+ wsl --list --online
+ ```
+
+5. **安装Linux发行版**:
+
+ ```powershell
+ # 安装默认版本(Ubuntu)
+ wsl --install
+
+ # 或安装指定版本
+ wsl --install -d Ubuntu-24.04
+ ```
+
+6. **设置用户名和密码**,安装完成后系统会提示设置。
+
+7. **更新系统**:
+
+ ```bash
+ sudo apt update && sudo apt upgrade
+ ```
+
+8. **验证安装**:
+
+ ```powershell
+ wsl -l -v
+ ```
+
+## 网络配置
+
+### 配置DNS
+
+WSL2使用的DNS服务器在`/etc/resolv.conf`文件中设置,为防止每次启动重置,需要进行以下配置:
+
+1. 创建`/etc/wsl.conf`文件:
+
+ ```bash
+ [network]
+ generateResolvConf = false
+ ```
+
+2. 删除原链接文件:
+
+ ```bash
+ rm /etc/resolv.conf
+ ```
+
+3. 创建新配置:
+
+ ```bash
+ vi /etc/resolv.conf
+ # 添加内容
+ nameserver 114.114.114.114
+ ```
+
+4. 重启WSL:
+
+ ```bash
+ exit
+ wsl --shutdown
+ wsl
+ ```
+
+5. 测试网络:
+
+ ```bash
+ ping www.baidu.com
+ ```
+
+### 配置桥接网络
+
+1. **开启Hyper-V**后执行以下命令:
+
+ ```powershell
+ Get-NetAdapter
+ New-VMSwitch -SwitchName "VETH" -NetAdapterName "以太网" -AllowManagementOS $True
+ ```
+
+2. 创建`.wslconfig`配置文件:
+
+ ```powershell
+ cd ~
+ New-Item .wslconfig
+ notepad .\.wslconfig
+ ```
+
+3. 添加以下内容:
+
+ ```
+ [wsl2]
+ networkingMode=bridged
+ vmSwitch=VETH
+ ipv6=true
+ ```
+
+### 配置网络代理
+
+1. 关闭自动更新DNS:
+
+ ```bash
+ #/etc/wsl.conf
+ [network]
+ generateResolvConf = false
+ ```
+
+2. 添加以下脚本至`.bashrc`或`.zshrc`:
+
+ ```bash
+ vi ~/.bashrc
+ # 添加代理配置
+ export hostip=10.6.212.22 # 替换为你的代理IP
+ export hostport=7890 # 替换为你的代理端口
+ alias proxy='
+ export HTTPS_PROXY="http://${hostip}:${hostport}";
+ export HTTP_PROXY="http://${hostip}:${hostport}";
+ export ALL_PROXY="http://${hostip}:${hostport}";
+ echo -e "Acquire::http::Proxy \"http://${hostip}:${hostport}\";" | sudo tee -a /etc/apt/apt.conf.d/proxy.conf > /dev/null;
+ echo -e "Acquire::https::Proxy \"http://${hostip}:${hostport}\";" | sudo tee -a /etc/apt/apt.conf.d/proxy.conf > /dev/null;
+ '
+ alias unproxy='
+ unset HTTPS_PROXY;
+ unset HTTP_PROXY;
+ unset ALL_PROXY;
+ sudo sed -i -e '/Acquire::http::Proxy/d' /etc/apt/apt.conf.d/proxy.conf;
+ sudo sed -i -e '/Acquire::https::Proxy/d' /etc/apt/apt.conf.d/proxy.conf;
+ '
+ ```
+
+3. 执行命令启用/禁用代理:
+
+ ```bash
+ # 启用代理
+ proxy
+
+ # 禁用代理
+ unproxy
+ ```
+
+4. 固定DNS配置:
+
+ ```bash
+ sudo rm /etc/resolv.conf
+ sudo bash -c 'echo "nameserver 8.8.8.8" > /etc/resolv.conf'
+ sudo bash -c 'echo "[network]" > /etc/wsl.conf'
+ sudo bash -c 'echo "generateResolvConf = false" >> /etc/wsl.conf'
+ sudo chattr +i /etc/resolv.conf
+ ```
+
+5. 测试代理:
+
+ ```bash
+ curl -vv google.com
+ ```
+
+## 基础功能配置
+
+### 开启SSH服务
+
+1. 安装SSH服务:
+
+ ```bash
+ sudo apt update && sudo apt upgrade
+ sudo apt-get install openssh-server
+ ```
+
+2. 修改SSH配置:
+
+ ```bash
+ sudo vi /etc/ssh/sshd_config
+ ```
+
+ 找到`PasswordAuthentication`行,确保设置为`yes`
+
+3. 启动并设置开机自启:
+
+ ```bash
+ sudo systemctl status ssh
+ sudo systemctl start ssh
+ sudo systemctl enable ssh
+ ```
+
+### 安装桌面环境
+
+```bash
+sudo apt update
+sudo apt install ubuntu-desktop
+# 安装远程桌面服务
+sudo apt-get install xrdp
+sudo systemctl start xrdp
+sudo systemctl enable xrdp
+```
+
+### 映射Windows目录至WSL
+
+```bash
+# 创建挂载点
+sudo mkdir /mnt/z
+# 挂载Windows目录
+sudo mount -t drvfs C:/User/xxx/Desktop/挂载文件 /mnt/z
+```
+
+## 系统管理
+
+### 查看端口
+
+查询端口占用有两种常用方法:
+
+1. 使用`netstat`:
+
+ ```bash
+ sudo apt-get install net-tools
+ sudo netstat -tunlp | grep 端口号
+ ```
+
+2. 使用`lsof`:
+
+ ```bash
+ sudo apt-get install lsof
+ sudo lsof -i:端口号
+ ```
+
+### 修改主机名
+
+1. 使用`hostnamectl`命令:
+
+ ```bash
+ sudo hostnamectl set-hostname 新主机名
+ ```
+
+2. 修改配置文件:
+
+ ```bash
+ sudo vi /etc/hostname
+ sudo vi /etc/hosts
+ ```
+
+ 在hosts文件中将`127.0.1.1`对应的旧主机名替换为新主机名
+
+3. 重启系统(可选):
+
+ ```bash
+ sudo reboot
+ ```
+
+## 开发环境配置
+
+### IDEA中文乱码修复
+
+1. 安装语言包:
+
+ ```bash
+ sudo apt install language-pack-zh-hans
+ ```
+
+2. 配置语言环境:
+
+ ```bash
+ sudo dpkg-reconfigure locales
+ # 选择en_US.UTF-8和zh_CN.UTF-8,并将zh_CN.UTF-8设为默认
+ ```
+
+3. 安装字体工具:
+
+ ```bash
+ sudo apt install fontconfig
+ ```
+
+4. 配置Windows字体:
+
+ ```bash
+ sudo vi /etc/fonts/local.conf
+ ```
+
+ 添加内容:
+
+ ```xml
+
+
+
+ /mnt/c/Windows/Fonts
+
+ ```
+
+5. 刷新字体缓存:
+
+ ```bash
+ fc-cache -f -v
+ ```
+
+6. 重启WSL:
+
+ ```bash
+ wsl --shutdown
+ ```
+
+### IDEA配置输入法
+
+1. 安装fcitx输入法:
+
+ ```bash
+ sudo apt install fcitx dbus-x11 im-config fcitx-sunpinyin
+ ```
+
+2. 编辑`/etc/locale.gen`:
+
+ ```bash
+ vi /etc/locale.gen
+ # 取消注释行:zh_CN.UTF-8
+ ```
+
+3. 配置环境变量:
+
+ ```bash
+ vi ~/.profile
+ # 添加内容
+ export GTK_IM_MODULE=fcitx
+ export QT_IM_MODULE=fcitx
+ export XMODIFIERS=@im=fcitx
+ export DefaultIMModule=fcitx
+ fcitx-autostart &>/dev/null
+ ```
+
+4. 更新配置:
+
+ ```bash
+ source ~/.profile
+ ```
+
+5. 配置快捷键:
+
+ ```bash
+ fcitx-config-gtk3
+ ```
+
+6. IDEA支持:编辑`idea.sh`启动脚本,添加:
+
+ ```properties
+ export XMODIFIERS=@im=fcitx
+ export QT_IM_MODULE=fcitx
+ ```
+
+### 安装Docker
+
+1. 更新系统包:
+
+ ```bash
+ sudo apt update
+ ```
+
+2. 安装依赖:
+
+ ```bash
+ sudo apt install ca-certificates curl gnupg lsb-release
+ ```
+
+3. 添加Docker官方GPG密钥:
+
+ ```bash
+ sudo mkdir -p /etc/apt/keyrings
+ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
+ ```
+
+4. 添加Docker APT源:
+
+ ```bash
+ echo \
+ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
+ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
+ ```
+
+5. 更新包列表:
+
+ ```bash
+ sudo apt update
+ ```
+
+6. 安装Docker引擎:
+
+ ```bash
+ sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
+ ```
+
+7. 启动并验证:
+
+ ```bash
+ sudo systemctl start docker
+ sudo systemctl enable docker
+ sudo docker --version
+ ```
+
+### 安装1Panel
+
+一键安装:
+
+```bash
+curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh && sudo bash quick_start.sh
+```
+
+查看管理员密码:
+
+```bash
+sudo 1pctl user-info
+```
+
+### 安装SVN
+
+```bash
+sudo apt update
+sudo apt install subversion
+svn --version # 验证安装
+```
+
+## 问题记录
+
+### SSH连接异常
+
+症状:SSH服务启动失败,出现以下错误:
+
+```
+error: Bind to port 22 on 0.0.0.0 failed: Address already in use.
+fatal: Missing privilege separation directory: /run/sshd
+```
+
+解决方法:
+
+1. 检查端口占用:
+
+ ```bash
+ sudo lsof -i:22
+ ```
+
+2. 终止占用进程:
+
+ ```bash
+ sudo kill
+ ```
+
+3. 创建缺失目录:
+
+ ```bash
+ sudo mkdir -p /run/sshd
+ sudo chmod 0755 /run/sshd
+ ```
+
+4. 重启SSH服务:
+
+ ```bash
+ sudo systemctl restart ssh
+ ```
+
+## 常用指令
+
+```powershell
+# 列出可用的Linux发行版
+wsl --list --online
+
+# 列出已安装的发行版
+wsl --list --verbose # 或 wsl -l -v
+
+# 设置WSL版本
+wsl --set-version <发行版名称> <版本号>
+
+# 设置默认WSL版本
+wsl --set-default-version <版本号>
+
+# 设置默认Linux发行版
+wsl --set-default <发行版名称>
+
+# 运行特定发行版
+wsl --distribution <发行版名称> --user <用户名>
+
+# 更新WSL
+wsl --update
+
+# 检查WSL状态
+wsl --status
+
+# 检查WSL版本
+wsl --version
+
+# 以特定用户身份运行
+wsl --user <用户名>
+
+# 卸载Linux发行版
+wsl --unregister <发行版名称>
+
+# 标识IP地址
+wsl hostname -I # 返回WSL2 IP地址
+ip route show | grep -i default | awk '{ print $3}' # 返回Windows主机IP
+
+# 更改默认用户
+<发行版名称> config --default-user <用户名>
+```
+
+## 实用软件安装
+
+### 安装适用于 Linux 的 Google Chrome
+
+```bash
+cd /tmp
+wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
+sudo apt install --fix-missing ./google-chrome-stable_current_amd64.deb
+```
+
+启动命令:`google-chrome`
+
+### 安装 VLC
+
+```bash
+sudo apt install vlc -y
+```
+
+启动命令:`vlc`
+
+### 安装 X11 应用
+
+```bash
+sudo apt install x11-apps -y
+```
+
+启动命令示例:`xcalc`、`xclock`、`xeyes`
+
+------
+
+通过本文的配置指南,你可以构建一个功能完善的WSL2环境,满足日常开发、学习和娱乐需求。WSL2的灵活性使得Windows用户无需切换操作系统就能享受Linux的强大功能,是开发人员的理想工具。
\ No newline at end of file
diff --git a/src/tools/03Scoop.md b/src/tools/03Scoop.md
new file mode 100644
index 0000000..cb5bd16
--- /dev/null
+++ b/src/tools/03Scoop.md
@@ -0,0 +1,289 @@
+---
+icon: mdi:download-circle
+date: 2025-05-09
+category:
+ - 实用工具
+tag:
+ - Scoop
+title: Scoop
+---
+
+
+# Scoop
+
+欢迎体验 **Scoop** —— Windows 的命令行包管理神器!本文将带你全面了解 Scoop 的功能、安装方法、使用技巧以及常见问题解决方法。如果你厌倦了繁琐的软件安装流程,Scoop 绝对是你的最佳选择!🚀
+
+------
+
+## 什么是 Scoop?
+
+[Scoop](https://github.com/ScoopInstaller/Scoop?tab=readme-ov-file#installatio) 是一个专为 Windows 设计的命令行包管理工具,旨在简化软件的安装与管理。无论是开发者还是普通用户,Scoop 都能让你通过几行命令快速安装和管理应用程序,省去繁琐的图形界面操作。
+
+> **Scoop 核心优势**:轻量、快速、无需管理员权限、环境整洁!
+
+------
+
+## Scoop 的核心功能
+
+Scoop 的设计理念是简化和自动化,以下是它的主要功能:
+
+- **消除 UAC 提示**:无需频繁点击“允许”弹窗。
+- **隐藏 GUI 安装向导**:告别繁琐的下一步、下一步。
+- **保持 PATH 环境整洁**:避免 PATH 变量被杂乱无章的路径污染。
+- **无副作用安装/卸载**:安装和删除软件时不会影响系统其他部分。
+- **自动解析依赖**:自动安装软件所需的依赖项。
+- **一键配置环境**:通过脚本快速配置开发环境,例如:
+
+------
+
+## 安装 Scoop
+
+### 1. 安装PowerShell并配置权限
+
+确保你的系统满足以下要求:
+
+- **PowerShell**:安装最新版本的 [PowerShell](https://aka.ms/powershell) 或 [Windows PowerShell 5.1](https://aka.ms/wmf5download)。然后执行以下指令:
+
+```powershell
+Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
+```
+
+这是一条用于配置 PowerShell 执行策略的命令。具体含义如下:
+
+- `Set-ExecutionPolicy`:这是 PowerShell 的一个 cmdlet,用于设置执行策略,控制脚本的运行方式。
+- `ExecutionPolicy RemoteSigned`:将执行策略设置为 `RemoteSigned` 模式。在这种模式下,允许本地创建的脚本无限制运行,但任何从互联网下载的脚本都必须具有受信任的签名才能执行。
+- `Scope CurrentUser`:指定此执行策略的更改仅适用于当前用户,而不会影响系统中的其他用户或其他范围(如整个计算机)。
+
+### 2. 安装
+
+以**非管理员身份**运行以下命令,Scoop 将默认安装到 `C:\Users\\scoop`:
+
+```powershell
+# 查看 PowerShell 版本
+Get-Host | Select-Object Version
+# 标准安装
+irm get.scoop.sh | iex
+# 使用代理安装(若无法访问 GitHub)
+irm https://get.scoop.sh -Proxy '127.0.0.1:7890' | iex
+```
+
+------
+
+## 加速下载:Aria2 配置
+
+Scoop 支持 [aria2](https://github.com/aria2/aria2) 来实现多连接下载,提升下载速度。只需安装 aria2 即可:
+
+```powershell
+scoop install aria2
+scoop config aria2-warning-enabled false
+```
+
+### Aria2 配置项
+
+通过 `scoop config` 调整 aria2 设置:
+
+1. **aria2-enabled**(默认:true):启用/禁用 aria2。
+2. **aria2-warning-enabled**(默认:true):显示/隐藏 aria2 警告。
+3. **aria2-retry-wait**(默认:2):重试等待时间(秒)。
+4. **aria2-split**(默认:5):文件分段数。
+5. **aria2-max-connection-per-server**(默认:5):单服务器最大连接数。
+6. **aria2-min-split-size**(默认:5M):最小分段大小。
+7. **aria2-options**:自定义 aria2 参数。
+
+示例:
+
+```powershell
+scoop config aria2-split 10
+scoop config aria2-max-connection-per-server 8
+```
+
+------
+
+## 软件仓库推荐
+
+Scoop 的软件来源于“桶(bucket)”,分为官方和第三方仓库。
+
+### 官方仓库
+
+- **[Main](https://github.com/ScoopInstaller/Main)**:常用非 GUI 应用。
+- **[Extras](https://github.com/ScoopInstaller/Extras)**:不符合 Main 标准的应用。
+- **[Games](https://github.com/Calinou/scoop-games)**:开源/免费游戏及工具。
+- **[Nerd-fonts](https://github.com/matthewjberger/scoop-nerd-fonts)**:Nerd 字体。
+- **[Nirsoft](https://github.com/ScoopInstaller/Nirsoft)**:Nirsoft 工具集。
+- **[Sysinternals](https://github.com/niheaven/scoop-sysinternals)**:Sysinternals 套件。
+- **[Java](https://github.com/ScoopInstaller/Java)**:JDK/JRE 相关。
+- **[Nonportable](https://github.com/ScoopInstaller/Nonportable)**:不可移植应用。
+- **[PHP](https://github.com/ScoopInstaller/PHP)**:PHP 安装包。
+- **[Versions](https://github.com/ScoopInstaller/Versions)**:应用的替代版本。
+
+### 第三方仓库
+
+- **[scoopcn](https://github.com/scoopcn/scoopcn)**:国内应用为主。
+- **[dorado](https://github.com/chawyehsu/dorado)**:优质应用集合。
+- **[Cluttered-bucket](https://github.com/Paxxs/Cluttered-bucket)**:绿色软件及独立开发者应用。
+- **[scoopet](https://github.com/ivaquero/scoopet)**:学术研究相关工具。
+- **[scoop-zapps](https://github.com/kkzzhizhou/scoop-zapps)**:自动化更新仓库。
+- **[aki-apps](https://github.com/akirco/aki-apps)**:个人维护的 bucket。
+- **[siku](https://github.com/amorphobia/siku)**:个性化应用集合。
+- **[scoop-bear](https://github.com/AStupidBear/scoop-bear)**:包含 Navicat 等。
+- **[echo-scoop](https://github.com/echoiron/echo-scoop)**:包含 IDM 等。
+- **[abgo_bucket](https://github.com/abgox/abgo_bucket)**:包含 123pan 等。
+- **[diklios-scoop-bucket](https://github.com/diklios5768/diklios-scoop-bucket)**:包含 CooDesker 等。
+
+添加第三方仓库示例:
+
+```powershell
+scoop bucket add scoopcn https://github.com/scoopcn/scoopcn
+```
+
+------
+
+## 常用命令速查
+
+### 1. 获取帮助
+
+```powershell
+scoop help
+scoop help install
+```
+
+### 2. 安装应用
+
+- **当前用户安装**:
+
+```powershell
+scoop install nano
+```
+
+- **全局安装(需管理员权限)**:
+
+```powershell
+scoop install nano -g
+```
+
+- **高级选项**:
+
+```powershell
+scoop install git -g --no-cache --skip
+```
+
+### 3. 卸载应用
+
+- **卸载程序**:
+
+```powershell
+scoop uninstall nano
+```
+
+- **卸载并移除配置文件**:
+
+```powershell
+scoop uninstall nano -p
+```
+
+- **卸载全局程序**:
+
+```powershell
+scoop uninstall nano -g
+```
+
+### 4. 更新操作
+
+- **更新 Scoop 和 bucket**:
+
+```powershell
+scoop update
+```
+
+- **更新指定应用**:
+
+```powershell
+scoop update nano
+```
+
+- **更新所有**:
+
+```powershell
+scoop update *
+```
+
+### 5. 其他实用命令
+
+- **查看已安装应用**:
+
+```powershell
+scoop list
+```
+
+- **检查可更新应用**:
+
+```powershell
+scoop status
+```
+
+- **打开应用主页**:
+
+```powershell
+scoop home nano
+```
+
+- **管理 bucket**:
+
+```powershell
+scoop bucket known
+scoop bucket add extras
+scoop bucket list
+scoop bucket rm extras
+```
+
+- **清理旧版本和缓存**:
+
+```powershell
+scoop cleanup *
+scoop cache rm *
+```
+
+- 配置代理
+
+```powershell
+# 配置代理
+scoop config proxy '127.0.0.1:7890'
+# 移除代理
+scoop config rm proxy
+```
+
+------
+
+## 常见问题及解决
+
+### 问题:Inno Setup 错误
+
+错误信息:
+
+```
+Signature detected: Inno Setup Setup Data (6.3.0)
+This is not directly supported, but i'll try to unpack it as version 5602; Version detected: 6300
+Critical error: The setup files are corrupted. Please obtain a new copy of the program.
+```
+
+**解决方法**:
+
+安装 `innounp-unicode` 版本:
+
+```powershell
+scoop uninstall innounp
+scoop bucket add versions
+scoop install versions/innounp-unicode
+```
+
+------
+
+## 进阶学习
+
+想深入了解 Scoop?查看 [官方文档](https://github.com/ScoopInstaller/Scoop/wiki) 获取更多高级用法和技巧!
+
+------
+
+## 总结
+
+Scoop 是一个强大而优雅的工具,让 Windows 软件管理变得轻松高效。无论是快速安装开发工具,还是管理日常应用,Scoop 都能让你事半功倍。快来试试吧,打造属于你的高效环境!💻
\ No newline at end of file
diff --git a/src/tools/04gitee-ssh.md b/src/tools/04gitee-ssh.md
new file mode 100644
index 0000000..28c5768
--- /dev/null
+++ b/src/tools/04gitee-ssh.md
@@ -0,0 +1,191 @@
+---
+icon: mdi:github
+date: 2025-05-08
+category:
+ - 实用工具
+tag:
+ - gitee
+ - 码云
+title: Gitee SSH
+---
+
+
+在Gitee上设置SSH公钥:完整指南
+
+
+# 在Gitee上设置SSH公钥:完整指南
+
+## 引言
+
+在使用Git进行代码管理时,SSH协议为开发者提供了一种安全且便捷的仓库访问方式。相比HTTPS协议,SSH协议不需要每次操作都输入用户名和密码,大大提高了开发效率。对于经常使用Gitee平台的Java开发者来说,正确配置SSH公钥是日常工作流程中的重要一环。本文将详细介绍如何在Gitee平台上生成并设置SSH公钥,帮助您实现更高效的代码管理。
+
+## SSH公钥的基本概念
+
+在深入了解操作步骤前,我们先来理解一下SSH公钥认证的基本原理:
+
+- SSH认证基于非对称加密,包含一对密钥:私钥(保存在本地)和公钥(上传至服务器)
+- 私钥必须安全保管,不可泄露;公钥则可以自由分享
+- 当您使用SSH协议访问仓库时,Gitee会验证您的身份,确认您是否拥有与已注册公钥匹配的私钥
+
+## 详细操作步骤
+
+### 1. 生成SSH密钥对
+
+首先,我们需要生成SSH密钥对。对于Windows用户,建议使用**Windows PowerShell**或**Git Bash**执行以下命令(注意:Windows的命令提示符中没有`cat`和`ls`命令)。
+
+```bash
+ssh-keygen -t ed25519 -C "Gitee SSH Key"
+```
+
+参数说明:
+- `-t`:指定密钥类型,这里使用更安全的ed25519算法
+- `-C`:添加注释,便于识别密钥的用途
+
+执行命令后,系统会提示您输入保存密钥的位置和密码短语。一般情况下,直接按三次回车键即可使用默认设置:
+
+```bash
+Generating public/private ed25519 key pair.
+Enter file in which to save the key (/home/git/.ssh/id_ed25519):
+Enter passphrase (empty for no passphrase):
+Enter same passphrase again:
+Your identification has been saved in /home/git/.ssh/id_ed25519
+Your public key has been saved in /home/git/.ssh/id_ed25519.pub
+```
+
+这样我们就成功生成了两个文件:
+- `id_ed25519`:私钥文件(保密,不要分享)
+- `id_ed25519.pub`:公钥文件(需要上传到Gitee)
+
+### 2. 获取公钥内容
+
+执行以下命令查看生成的SSH公钥内容:
+
+```bash
+cat ~/.ssh/id_ed25519.pub
+```
+
+命令会输出类似以下格式的公钥内容:
+
+```
+ssh-ed25519 AAAA***5B Gitee SSH Key
+```
+
+请完整复制这段输出的内容,包括开头的`ssh-ed25519`和末尾的注释`Gitee SSH Key`。
+
+### 3. 将公钥添加到Gitee账户
+
+获取到公钥内容后,我们需要将其添加到Gitee账户中:
+
+1. 登录Gitee账户,点击右上角头像
+2. 选择「个人设置」->「安全设置」->「SSH 公钥」
+3. 点击「添加公钥」按钮
+4. 在表单中填入公钥标题(自定义,方便识别)和公钥内容
+5. 输入Gitee账户密码进行验证
+6. 点击「添加」按钮完成操作
+
+
+
+### 4. 验证SSH连接
+
+添加完成后,可以通过以下命令测试SSH连接是否配置成功:
+
+```bash
+ssh -T git@gitee.com
+```
+
+如果配置正确,您将看到以下输出,其中USERNAME是您的Gitee用户名:
+
+```bash
+Hi USERNAME! You've successfully authenticated, but GITEE.COM does not provide shell access.
+```
+
+看到这个提示就说明SSH公钥已经成功配置,您可以开始使用SSH协议来操作Gitee上的仓库了。
+
+## SSH公钥的管理
+
+在Gitee平台上,您可以方便地管理已添加的SSH公钥:
+
+1. 浏览公钥列表:通过「个人设置」->「安全设置」->「SSH 公钥」页面查看所有已添加的SSH公钥
+2. 查看公钥详情:点击具体的公钥可以查看其详细信息
+3. 删除公钥:当某个SSH公钥不再需要时,可以直接删除它
+
+
+
+
+
+## 账户SSH公钥vs仓库SSH公钥
+
+在Gitee平台上,存在两种不同类型的SSH公钥,理解它们的区别对正确使用Git操作非常重要:
+
+### 账户SSH公钥
+
+- 与您的Gitee账户绑定
+- 当您的账户对仓库有推送/拉取权限时,可以使用这个公钥进行对应操作
+- 验证时会显示您的用户名:`Hi USERNAME! You've successfully authenticated...`
+- 适用于日常开发过程中的代码提交、拉取等操作
+
+### 仓库SSH公钥(部署公钥)
+
+- 仅针对特定仓库,与账户无关
+- 仅具有仓库的**拉取**权限,无法进行推送操作
+- 验证时显示为匿名用户:`Hi Anonymous! You've successfully authenticated...`
+- 主要用于部署环境(如生产服务器)拉取代码,避免在服务器上存储个人账户凭证
+
+> 注意:如果您需要设置仓库的部署公钥,请参考Gitee帮助文档中的"添加部署公钥"章节。
+
+## 实用技巧与常见问题
+
+### 使用不同的SSH密钥访问不同的Git服务
+
+如果您同时使用多个Git平台(如Gitee、GitHub、GitLab等),可以为每个平台生成不同的SSH密钥对,并在SSH配置文件中指定使用规则。
+
+1. 为不同平台生成不同的密钥,例如:
+ ```bash
+ ssh-keygen -t ed25519 -C "Gitee" -f ~/.ssh/gitee_id_ed25519
+ ssh-keygen -t ed25519 -C "GitHub" -f ~/.ssh/github_id_ed25519
+ ```
+
+2. 创建或编辑`~/.ssh/config`文件:
+ ```
+ # Gitee平台
+ Host gitee.com
+ HostName gitee.com
+ PreferredAuthentications publickey
+ IdentityFile ~/.ssh/gitee_id_ed25519
+
+ # GitHub平台
+ Host github.com
+ HostName github.com
+ PreferredAuthentications publickey
+ IdentityFile ~/.ssh/github_id_ed25519
+ ```
+
+### SSH连接故障排查
+
+如果在验证SSH连接时遇到问题,可以尝试以下步骤:
+
+1. 使用`-v`参数获取详细信息:
+ ```bash
+ ssh -vT git@gitee.com
+ ```
+
+2. 检查SSH密钥权限(Unix/Linux/macOS系统):
+ ```bash
+ chmod 700 ~/.ssh
+ chmod 600 ~/.ssh/id_ed25519
+ chmod 644 ~/.ssh/id_ed25519.pub
+ ```
+
+3. 确认SSH代理正在运行:
+ ```bash
+ eval $(ssh-agent -s)
+ ssh-add ~/.ssh/id_ed25519
+ ```
+
+## 总结
+
+正确设置和使用SSH公钥对于Java开发者在Gitee平台上进行高效的代码管理至关重要。通过本文介绍的步骤,您可以轻松生成SSH密钥对、将公钥添加到Gitee账户,并开始使用SSH协议访问您的代码仓库。这不仅提高了安全性,还简化了您的日常工作流程,避免了重复输入用户名和密码的繁琐过程。
+
+对于团队开发和持续集成/持续部署(CI/CD)环境,了解账户SSH公钥和仓库部署公钥的区别尤为重要,这有助于您根据不同场景选择最合适的认证方式,确保代码安全的同时提高开发和部署效率。
+
+希望本指南能帮助您在Gitee平台上更加顺畅地使用Git进行项目管理和协作开发。如果在实际操作中遇到任何问题,欢迎参考Gitee的官方帮助文档获取更多支持。
\ No newline at end of file
diff --git a/src/tools/05Google.md b/src/tools/05Google.md
new file mode 100644
index 0000000..0eed706
--- /dev/null
+++ b/src/tools/05Google.md
@@ -0,0 +1,17 @@
+---
+icon: mdi:google
+date: 2025-05-23
+category:
+ - google
+tag:
+ - 强制背景黑色
+title: Google
+---
+暗黑模式
+
+
+```text
+地址栏输入:chrome://flags/#enable-force-dark
+然后选择enable
+然后刷新,OK,成功
+```
\ No newline at end of file
diff --git a/src/tools/06MobaXterm.md b/src/tools/06MobaXterm.md
new file mode 100644
index 0000000..4e010ee
--- /dev/null
+++ b/src/tools/06MobaXterm.md
@@ -0,0 +1,19 @@
+---
+icon: mdi:terminal
+date: 2025-05-08
+category:
+ - 实用工具
+tag:
+ - MobaXterm
+title: MobaXterm
+---
+
+
+MobaXterm工具实用教程
+
+
+1. 去官网安装正版软件,比如23.6版本的:[MobaXterm free Xserver and tabbed SSH client for Windows](https://mobaxterm.mobatek.net/)
+2. 打开这个网站,输入信息:[MobaXterm Keygen](https://inused.github.io/pages/file/tool/MobaXtermKeygen.html)
+3. 将自动下载的Custom.mxtpro文件放入到[mobaxterm]的目录下
+4. [重启软件]即已完成注册
+
diff --git a/src/tools/README.md b/src/tools/README.md
new file mode 100644
index 0000000..4ee22eb
--- /dev/null
+++ b/src/tools/README.md
@@ -0,0 +1,45 @@
+---
+title: 导航页
+icon: toolbox
+index: true
+order: 1
+category:
+ - 实用工具
+tag:
+ - 工具
+ - 效率
+---
+
+# 🧰 工具箱
+
+这里收集了开发和工作中常用的工具配置、使用技巧和实用指南,涵盖远程工具、构建工具、开发环境、浏览器相关等。
+
+## 📚 工具文档列表
+
+- [Gitee SSH 配置](04gitee-ssh.md)
+- [Maven 安装与配置](../programming/backend/java/功能整理/02Maven.md)
+- [MobaXterm 远程终端](06MobaXterm.md)
+- [RustDesk 自建远程桌面服务器](../apps/07RustDesk自建远程桌面服务器.md)
+- [Scoop 包管理器](03Scoop.md)
+- [Spring Boot JAR 瘦身与加密](../programming/backend/java/功能整理/06Spring%20Boot%20JAR%20瘦身与加密.md)
+- [WebSocket 和 HTTP 关系](../programming/backend/java/功能整理/03WebSocket和HTTP关系.md)
+- [WSL2 使用指南](02WSL2.md)
+- [XJar 加密工具](../programming/backend/java/功能整理/01XJar.md)
+- [浏览器常用技巧](05Google.md)
+
+## 🧭 快速导航
+
+::: card
+title: 推荐工具
+icon: rocket
+desc: 常用开发运维工具的配置与技巧
+items:
+ - text: Gitee SSH
+ icon: link
+ link: ./gitee-ssh.md
+ - text: RustDesk 自建
+ icon: desktop
+ link: ./RustDesk自建远程桌面服务器.md
+ - text: Scoop 包管理器
+ icon: download
+ link: ./Scoop.md
diff --git a/src/tools/assets/20210721105228.png b/src/tools/assets/20210721105228.png
new file mode 100644
index 0000000..ed034a9
Binary files /dev/null and b/src/tools/assets/20210721105228.png differ
diff --git a/src/tools/assets/sshkeys_create-8409f453e6780ca1a8db3ce33c74240b.png b/src/tools/assets/sshkeys_create-8409f453e6780ca1a8db3ce33c74240b.png
new file mode 100644
index 0000000..c585b23
Binary files /dev/null and b/src/tools/assets/sshkeys_create-8409f453e6780ca1a8db3ce33c74240b.png differ
diff --git a/src/tools/assets/sshkeys_list-bff1a324894abbdc3ab8f61c49bb63d5.png b/src/tools/assets/sshkeys_list-bff1a324894abbdc3ab8f61c49bb63d5.png
new file mode 100644
index 0000000..8132a32
Binary files /dev/null and b/src/tools/assets/sshkeys_list-bff1a324894abbdc3ab8f61c49bb63d5.png differ
diff --git a/src/tools/assets/sshkeys_show-a14cdfb89475debed237bfded2bd9848.png b/src/tools/assets/sshkeys_show-a14cdfb89475debed237bfded2bd9848.png
new file mode 100644
index 0000000..b5e9db5
Binary files /dev/null and b/src/tools/assets/sshkeys_show-a14cdfb89475debed237bfded2bd9848.png differ
diff --git a/src/tools/assets/v2-751a78a5288f3230e3ba1ed114c97646_1440w.webp b/src/tools/assets/v2-751a78a5288f3230e3ba1ed114c97646_1440w.webp
new file mode 100644
index 0000000..a5308ad
Binary files /dev/null and b/src/tools/assets/v2-751a78a5288f3230e3ba1ed114c97646_1440w.webp differ
diff --git a/src/work/README.md b/src/work/README.md
new file mode 100644
index 0000000..341f28a
--- /dev/null
+++ b/src/work/README.md
@@ -0,0 +1,9 @@
+---
+title: 工作
+index: false
+icon: mdi:briefcase
+category:
+ - 工作
+---
+
+
diff --git a/src/work/assets/image-20240513143908211.png b/src/work/assets/image-20240513143908211.png
new file mode 100644
index 0000000..5c0b148
Binary files /dev/null and b/src/work/assets/image-20240513143908211.png differ
diff --git a/src/work/assets/image-20240716164500637.png b/src/work/assets/image-20240716164500637.png
new file mode 100644
index 0000000..194e638
Binary files /dev/null and b/src/work/assets/image-20240716164500637.png differ
diff --git a/src/work/log/2025-06.md b/src/work/log/2025-06.md
new file mode 100644
index 0000000..27a1a05
--- /dev/null
+++ b/src/work/log/2025-06.md
@@ -0,0 +1,124 @@
+---
+icon: mdi:note-text
+date: 2025-06-01
+category:
+ - 日志
+tag:
+ - 个人日志
+title: 202506日志
+---
+202506日志
+
+# 上周
+PR7050:
+
+1. 根据ue提供的模型文件重构了导入功能
+2. 新增构建状态管理及关联相关功能
+3. 图元服务适配了除了主设备-辅设备外的巡视设备相关图元信息查询接口
+4. 新增了动态路由的功能(根据辅设备自动生成菜单)
+
+巡视系统
+
+1. 智能巡视系统新增四遥信号和点位关联的业务逻辑及巡视任务(c++未开始)
+2. 新增巡视点位在线状态统计和最新巡视任务信息获取(100%)
+
+# 本周
+
+## PR7050
+
+1. 调整属性存储方式(100%)
+2. 图元控制相关功能联调(遥控和遥设)(80% 未测试)
+3. 三维告警初始化及告警推送相关功能(100%)
+4. 业务上的图片存储地址调整(80% 测试流程存在问题 c++目前无法抓图 还需要调整excel导入导出图片位置)
+5. PR7050系统依赖变更,需重新适配打包加密环境(0% 未开始)
+6. 三维页面联动相关功能开发。查询构件台账 查询构件告警 (70% 待联调)
+7. 完善三维启动程序(问题:三维提供的linux版本启动需要在linx80,需要ue去适配)
+8. 问题:分析主机如何重ftp取文件(分析主机默认获取数据是根据FTP)(已解决)
+
+巡视系统
+
+1. 智能巡视系统新增四遥信号和点位关联的业务逻辑及巡视任务(可以正常跑通 )(100%)
+
+## 主站
+
+1. 功能测试(测试了自定义模板并修复了异常)
+2. 处理了子系统上报数据异常问题
+
+# PR7050
+
+1. 完善了告警推送相关功能(100%)
+2. 新增四遥信号值变化消息发布功能(100%)
+3. 测试业务中调整为FTP文件处理的逻辑(50%,优先级最低。暂停中)
+4. 参考重庆三维实现了多用户多个模型的业务逻辑(100%)
+5. 图元服务新增 100(巡视设备类型)(100%)
+6. 模型需要根据meta过滤数据(已完成)
+7. 调整新版菜单结构(80%)
+8. 图元相关服务调整(70%)
+
+### 问题:
+
+1. 测试流程存在问题, c++目前无法抓图 还需要调整excel导入导出图片位置
+2. 抓图、手动录像接口。如果原数据没有在/temp目录下,需要删除
+3. 本地录像失败
+4. 手动下载录像失败
+
+## 巡视
+
+1. 现场主站新增是否上送告警文件字段 (已完成)
+2. 处理现场点位在线状态统计信息(新增机器人 无人机 声纹装置是否在线状态检测)(已完成)
+3. 现场问题一些配置问题处理(已完成)
+4. excel点位导入导出新增四遥信号
+5. 首页地图新增json格式(已完成)
+6. 适配多ip统计信息(未开始 优先级 1)先查询双ip 作为新的类型双光谱云台(枪击,球机)。再查询其它类型(已完成)
+7. 导入多个算法消失的问题(已完成)
+8. YUFAN人脸门禁操作优化(待定)
+9. 适配机器人直接上送分析结果的业务(已完成)
+
+
+
+## 主站
+
+1. 巡视主机上报至主站需要新增任务模型(已完成)
+2. 适配上报分析主机数据业务处理 (已完成)
+3. 需要排除变电站为主站那个变电站,只显示子站,ips默认变电站为-1(待适配 优先级 2)(已完成)
+4. 变电站新增公司字段信息(已完成)
+5. 主站首页变电站状态调整,右下角告警等级及确认信息统计(已完成)
+6. 适配主站的联动业务逻辑(已完成)
+7. 适配主站超期逻辑(已完成)
+8. 适配主站缺陷类型描述(已完成)
+9. 主站新增变电站图标字段(已完成)
+
+## 区域主机
+
+1. 下发任务时如果任务正在执行。计划不能删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/work/log/2025-07.md b/src/work/log/2025-07.md
new file mode 100644
index 0000000..2315118
--- /dev/null
+++ b/src/work/log/2025-07.md
@@ -0,0 +1,78 @@
+---
+icon: mdi:note-text
+date: 2025-07-01
+category:
+ - 日志
+tag:
+ - 个人日志
+title: 202507日志
+---
+202507日志
+
+## TODO
+1. 判断ue文件是否存在,调整目录
+1. 改掉平台代码redis *的匹配
+1. rtdbBasicService的keys方法删除
+
+
+
+## 2025-06-30 至 2025-07-06
+
+### **PRS-7950 巡视系统**
+
+1. **巡视任务调度与执行逻辑优化**:重构了巡视任务的调度、执行和清理机制。通过优化调度策略、调整任务优先级和引入异步清理僵尸任务,提升了系统执行效率和稳定性。新增了唯一时间戳以确保计划的精确执行,并在执行数据中加入了时间记录,便于追踪与分析。
+2. **数据处理与查询逻辑重构**:优化了多个核心模块的数据查询和处理逻辑。在巡视设备查询中,统一使用 `patrolDeviceId` 作为关键索引。重构了机器人视频位置(`videopos`)的解析逻辑,以兼容字符串格式并增强了代码的健壮性。同时,优化了设备点位导出和视频设备状态数据的处理,提高了数据处理的准确性和效率。
+3. **功能新增与业务适配**:为适应机器人上送带有分析数据的业务场景,增加了新的设备查询方式和分析结果处理逻辑,并统一了结果发送格式。新增了巡视任务点位识别类型的枚举,为不同设备(主站、机器人)提供了差异化的分析类型支持。
+4. **修复与健壮性提升**:修复了多个关键问题,包括巡视报告无图导出失败、FTPS 根目录字段名错误以及日志输出错误等。在代码层面,通过增加空值检查、优化异常捕获和引入重试机制,显著提升了系统的稳定性和容错能力。
+5. **代码与配置同步**:为保持不同部署环境下的代码一致性,对部分主站特定的逻辑代码(如采集失败信息上报)进行了注释处理,并统一了非区域主机模式下的进度计算方式。
+
+
+
+### **PRS-7050 场站智慧管控**
+
+1. **信号处理与操作控制模块发布**:新增 `cygbusiness-control` 模块,全面实现信号操作控制功能。支持对模拟量和状态量信号进行加解锁、延长有效期、人工置数/取反、告警抑制与恢复等精细化操作。该功能集成了审计日志,确保所有操作可追溯。
+2. **权限验证与安全强化**:为信号操作控制功能增加了严格的权限验证机制,实现了操作员与监护员的双重权限校验逻辑。同时,对用户权限菜单的获取逻辑进行了重构,简化了代码,提升了后端服务效率。权限跳过功能(`@EnableSkipAuth`)经历了临时开启用于测试和最终禁用的过程,以强化系统的安全配置。
+3. **构建、依赖及部署优化**:对 Maven 项目结构进行了系列重构,调整了模块的 `artifactId` 并更新了相关依赖引用。通过移除冗余依赖和插件、排除特定依赖冲突(`tomcat-embed-websocket`),优化了项目整体结构。同时,将静态资源路径和日志文件路径修改为相对路径,显著提高了项目的可移植性和部署灵活性。
+4. **后端服务与日志系统完善**:优化了 Actor 子节点的处理逻辑,通过增加过滤条件提高了数据处理的准确性。为多个模块(如 `platform`, `sunri-service-report-starter`)添加了详细的 `logback-config.xml` 配置文件,实现了日志的彩色输出、分级管理和按日期滚动,提升了系统的可维护性。
+5. **数字孪生(Twins)功能优化**:对三维模型的导入功能进行了持续优化,提升了模型处理的效率和稳定性。
+
+
+
+## 2025-07-07至2025-07-13
+
+### **PRS-7950 巡视系统**
+
+1. 新增多光谱局方测温功能,实现了从数据采集、告警生成到消息推送的全流程。
+2. 开发了联动立即抓图功能,并优化了高优先级任务的调度与执行逻辑。
+3. 增强了分析结果处理能力,能正确解析机器人返回的中文值和逗号分隔数字。
+4. 重构了巡视任务代码的获取方式,并强化了文件复制操作的权限检查与日志记录,提升了系统稳定性。
+
+
+
+### **PRS-7050 场站智慧管控**
+
+1. 对报告和巡视等核心服务接口进行了重命名与重构,优化了厂站名称的获取逻辑,使代码结构更清晰。
+2. 优化了三维(UE)应用的WebSocket连接管理,取消客户端断开时自动关闭进程的逻辑,增强了稳定性。
+3. 修复了巡视设备名称显示异常的问题,并为部分模块补充了日志记录与统一的编译配置。
+4. 在代码层面统一了注解使用规范,并为多个模块添加了更精细的日志配置。
+
+
+
+## 2025-07-14至2025-07-20
+
+### **PRS-7950 巡视系统 (2025-07-14至2025-07-20)**
+
+1. 为巡视历史任务、首页通知、数据分析等模块新增了任务类型、点位状态、电站ID等关键字段,丰富了数据维度。
+2. 新增了联动信号去重功能,通过同步控制和逻辑优化,避免5秒内重复任务的产生。
+3. 重构并优化了联动任务的执行逻辑与摄像头巡检数据的更新流程,提升了代码的可读性与执行效率。
+4. 修复了巡检数据解析时因数组索引错误导致的异常,并解决了红外测温规则更新不生效的问题。
+5. 优化了分析结果的描述逻辑,当存在多个检测结果时,能更准确地合并和展示描述信息。
+
+### **PRS-7050 场站智慧管控 (2025-07-14至2025-07-20)**
+
+1. 本周进行了大规模代码同步,全面集成了主站业务逻辑、告警与工业集控同步、四遥信号支持、国际化等新功能,并包含了对任务调度、数据处理、设备在线率计算、XML解析及大量B接口联调问题的深度优化与修复。
+
+
+
+
+
diff --git a/src/work/常用.md b/src/work/常用.md
new file mode 100644
index 0000000..9355c3a
--- /dev/null
+++ b/src/work/常用.md
@@ -0,0 +1,461 @@
+---
+icon: mdi:star-circle
+date: 2025-07-10
+category:
+ - 常用
+tag:
+ - 工作常用
+title: 工作常用
+---
+
+工作常用
+
+# 工作常用记录
+## 文件传输命令 (SCP)
+
+```bash
+# 传输 Java 相关文件
+scp -r -P 10022 sunri@10.6.221.209:/home/sunri/V1.00_2024/binary/java/jar/x64/dist /home/sunri/PRS7950/binary/java/jar/x64
+scp -r -P 10022 sunri@10.6.220.209:/home/sunri/V1.00_2024/resource/conf/dbmanager /home/sunri/PRS7950/resource/conf/
+scp -r -P 10022 sunri@10.6.220.223:/home/sunri/docker/mavenRepository/com/sunri /home/sunri/.m2/repository/com
+scp -r -P 10022 sunri@10.6.221.38:/home/sunri/V1.00_2024/packagemake/PRS-7950-IPS-V2.00-Beta-NewStart6-241212.tar.gz /home/sunri/Desktop/
+scp -r -P 10022 sunri@10.6.221.120:/home/sunri/PRS-7950/V1.00_2024/packagemake/PRS-7950-IPS-V2.00-Beta-NewStart6-241212.tar.gz /home/sunri/Desktop/
+scp -r -P 10022 sunri@10.6.213.128:/home/sunri/Desktop/sh/cygstart.xml /home/sunri/PRS7950/resource/conf
+scp -r -P 10022 sunri@10.6.220.107:/home/sunri/docker/mavenRepository/com/sunri /root/.m2/repository/com/
+```
+
+## SVN 合并命令
+
+### 2024 合并到 V1.00_2024
+
+```bash
+svn merge -c 239816 http://10.6.220.216:8080/svn/houtai/001_后台软件/PRS-7950在线巡视/01_开发库/V1.00_2024/src_java/platapp
+svn merge -c 239600 http://10.6.220.216:8080/svn/houtai/001_后台软件/PRS-7950在线巡视/01_开发库/V1.00_2024/resource/conf/dbmanager
+svn merge -c 241108 https://10.1.0.101/svn/houtai/001_后台软件/PRS-7950在线巡视/01_开发库/V1.00_2024/src_java/platapp
+```
+
+### V2.00 合并到 V1.00_2024
+
+```bash
+svn merge -c 240928 http://10.6.220.216:8080/svn/houtai/001_后台软件/PRS-7950在线巡视/01_开发库/V2.00/src_java/platapp
+svn merge -c 238665 https://10.1.0.101/svn/houtai/001_后台软件/PRS-7950在线巡视/01_开发库/V2.00/src_java/platapp
+svn merge -c 237503 http://10.6.220.216:8080/svn/houtai/001_后台软件/PRS-7950在线巡视/01_开发库/V2.00/src_java/platform
+```
+
+## SVN 仓库地址
+
+### PRS-7950 在线巡视
+
+- V1.00_2024
+ - http://10.6.220.216:8080/svn/houtai/001_后台软件/PRS-7950在线巡视/01_开发库/V1.00_2024/src_java
+ - https://10.1.0.101/svn/houtai/001_后台软件/PRS-7950在线巡视/01_开发库/V1.00_2024/src_java
+ - https://10.1.0.101/svn/houtai/001_后台软件/PRS-7950在线巡视/01_开发库/V1.00
+ - https://10.1.0.101/svn/houtai/001_后台软件/PRS-7950在线巡视/01_开发库/V1.00_2024/src_java/platapp
+- V2.00
+ - http://10.6.220.236:8080/svn/houtai/001_后台软件/PRS-7950在线巡视/01_开发库/V2.00/src_java/tool
+ - https://10.1.0.101/svn/houtai/001_后台软件/PRS-7950在线巡视/01_开发库/V2.00/src_java/tool
+
+### PRS-7050 场站智慧管控
+
+- V1.00
+ - http://10.6.220.216:8080/svn/houtai/001_后台软件/PRS-7050场站智慧管控/01_开发库/V1.00
+ - http://10.6.220.216:8080/svn/houtai/001_后台软件/PRS-7050场站智慧管控/01_开发库/V1.00/binary/java
+ - http://10.6.220.216:8080/svn/houtai/001_后台软件/PRS-7050场站智慧管控/01_开发库/V1.00/src_java/platapp
+ - http://10.6.220.216:8080/svn/houtai/001_后台软件/PRS-7050场站智慧管控/01_开发库/V1.00/src_web/prw
+ - https://10.1.0.101/svn/houtai/001_后台软件/PRS-7050场站智慧管控/01_开发库/V1.00
+ - https://10.1.0.101/svn/houtai/001_后台软件/PRS-7050场站智慧管控/01_开发库/V1.00/src_java/platapp
+ - https://10.1.0.101/svn/houtai/001_后台软件/PRS-7050场站智慧管控/01_开发库/V1.00/src_web/prw
+
+## Docker 命令
+
+```bash
+docker exec -it -e TERM=xterm jenkins-stand /bin/bash
+```
+
+## 常用命令与配置
+
+```bash
+# 禁用 IntelliJ IDEA 依赖跟踪
+-Djps.track.ap.dependencies=false
+
+# 启动 IntelliJ IDEA
+nohup /home/sunri/.jdks/idea/bin/idea.sh &
+nohup /home/sunri/.jdks/java/idea/bin/idea.sh &
+nohup /home/sunri/.mydata/idea/bin/idea.sh & # 10.6.221.46
+nohup /home/sunri/Desktop/idea/bin/idea.sh & # 10.6.213.128
+
+# 运行算法仿真并输出日志
+nohup ./AlgoSimulation > output.log 2>&1 &
+
+# 查看特定进程(避免 grep 自身出现在结果中)
+pgrep -fl tcptester
+
+# 杀死 Java 进程
+pkill -15 -f java
+pgrep java | grep -v -E '1001|2002' | xargs kill -15 # 排除指定 PID
+pkill -9 -f java
+
+# 杀死所有 ExternalJavacProcess 进程
+jps -l | grep ExternalJavacProcess | awk '{print $1}' | xargs kill -9
+
+# C++ 编译
+cd src_cxx
+CLEAN
+qmake-qt4 CONFIG+=release
+make -j8
+cd /home/sunri/CygLog/logspatrolcenter
+
+# 运行 JAR 文件
+./xjar java -jar -Dplainload.dir.path=../../lib/common cyggridb.jar
+```
+
+## 认证信息
+
+### 登录凭据
+
+- **10.6.220.50**
+ - 用户名: administrator@vsphere.local
+ - 密码: Prs7950.sunri
+- **10.6.223.50**
+ - 用户名: liujing2@vsphere.local
+ - 密码: Aaaa@1234
+- **其他凭据**
+ - 用户名: tgy
+ - 密码: Prs7950.sunri
+ - 用户名: prs7000.sunri
+ - 密码: Yzzx@220901
+ - 用户名: liujing2
+ - 密码: sunri@20230620*#& / sunri2015.. / sunri@2021*#&
+
+### 配置文件
+
+```ini
+[auth]
+password-stores =
+store-plaintext-passwords = yes
+```
+
+### 环境变量
+
+```bash
+export CYGWEBMODE=DEV
+export file:/home/sunri/PRS7950/binary/java/jar/x64/dist/
+```
+
+## 网络配置
+
+- **NVR**: 10.6.220.5
+- **主站**: 10.6.221.179
+- **子站**: 10.6.221.211
+- **Win10 SSH 异常修复**:
+ ```bash
+ ssh -o MACs=hmac-sha1 -p 10022 sunri@10.6.221.106
+ ```
+
+## 日志与监控
+
+```plaintext
+161550,OnlineMonitor#/udprecv/10.6.220.46/test.jpg,1;
+161739,OnlineMonitor#/udprecv/10.6.220.46/test.jpg,2;
+161950,OnlineMonitor#/udprecv/10.6.220.46/test.jpg,2
+```
+
+## 问题记录
+
+### 主机名与 IP 检查
+
+检查 cygrunset 的主机名和 IP,或修改 jkcfghostnode 的主机名和 IP 保持一致。
+
+```latex
+\documentclass{article}
+\begin{document}
+检查 cygrunset 的主机名和 IP,或修改 jkcfghostnode 的主机名和 IP 保持一致。
+\end{document}
+```
+
+## 杀死win10进程
+
+```powershell
+ Stop-Process -Id 5668 -Force
+```
+
+
+
+
+
+
+
+
+
+
+
+### 权限图
+
+
+
+### 问题截图
+
+
+
+## 脚本:杀死所有 Java 进程(排除 Main)
+
+```bash
+#!/bin/bash
+
+# 获取当前用户的所有 Java 进程的 PID,同时排除名为 Main 的进程
+pids=$(ps -u $USER -o pid,cmd | grep java | grep -v 'Main' | awk '{print $1}')
+
+# 检查是否获取到 PID
+if [ -z "$pids" ]; then
+ echo "没有找到需要杀掉的 Java 进程。"
+ exit 0
+fi
+
+# 杀掉所有非 Main 的 Java 进程
+for pid in $pids; do
+ kill -9 $pid
+ echo "杀掉进程 $pid"
+done
+
+echo "所有非 Main 的 Java 进程已被杀掉。"
+```
+
+
+
+多光谱测试代码:
+
+```java
+//测试用
+@Override
+public void run(String... args) throws Exception {
+//查询数据
+List cameraDeviceList = cameraDeviceMapper.queryByCameraUsage(3);
+List belongDeviceIDList = cameraDeviceList.stream().map(PatrolCameraDevice::getBelongDeviceId).distinct().collect(Collectors.toList());
+List collect = cameraDeviceList
+.stream()
+.filter(item -> DeviceProtocol.VideoProtocol.VIDEO_PROTOCOL_MOLE_WEI_WEI_SHI.equals(item.getDeviceProtocol())).collect(Collectors.toList());
+
+List alarmDtoList = Collections.emptyList();
+if (!collect.isEmpty()) {
+alarmDtoList = collect.stream().map(item -> {
+AlarmDto alarmDto = new AlarmDto();
+alarmDto.setBelongDeviceID(item.getBelongDeviceId());
+alarmDto.setDeviceID(item.getId());
+alarmDto.setDeviceName(item.getDeviceName());
+return alarmDto;
+}).collect(Collectors.toList());
+}
+
+scheduler = Executors.newScheduledThreadPool(1);
+deviceIDList = belongDeviceIDList;
+alarmDto = alarmDtoList;
+
+startScheduledTask();
+}
+
+private ScheduledExecutorService scheduler;
+private List deviceIDList;
+private List alarmDto;
+
+/**
+* 启动定时任务
+*/
+public void startScheduledTask() {
+System.out.println("定时任务已启动,每5秒随机执行一个方法...");
+
+scheduler.scheduleAtFixedRate(() -> {
+try {
+// 随机选择执行哪个方法 (0或1)
+int randomChoice = (int) (Math.random() * 2);
+
+if (randomChoice == 0) {
+// 执行 generateRandomDeviceData
+String deviceData = generateRandomDeviceData(deviceIDList);
+System.out.println(" 结果: " + JSONUtil.toJsonStr(deviceData));
+mqBusService.sendCygCommonMessage(
+JSONUtil.toJsonStr(deviceData),
+StateTopicEnum.RUN.getIndex(),
+"/patrolsys/webfront/notify",
+MqBusClientInitConfig.appName,
+"multispectral");
+} else {
+// 执行 generateRandomAlarm
+AlarmDto alarm = AlarmGenerator.generateRandomAlarm(alarmDto);
+System.out.println(" 结果: " + JSONUtil.toJsonStr(alarm));
+mqBusService.sendCygCommonMessage(
+JSONUtil.toJsonStr(alarm),
+StateTopicEnum.RUN.getIndex(),
+"/patrolsys/webfront/notify",
+MqBusClientInitConfig.appName,
+"multispectral");
+}
+
+System.out.println("----------------------------------------");
+
+} catch (Exception e) {
+System.err.println("定时任务执行出错: " + e.getMessage());
+}
+}, 0, 5, TimeUnit.SECONDS); // 立即开始,每5秒执行一次
+}
+
+/**
+* 从设备ID列表中随机选择一个设备ID,并生成随机曲线数据
+*
+* @param belongDeviceIDList 设备ID列表
+* @return JSON格式的设备数据对象
+*/
+public static String generateRandomDeviceData(List belongDeviceIDList) {
+if (belongDeviceIDList == null || belongDeviceIDList.isEmpty()) {
+throw new IllegalArgumentException("设备ID列表不能为空");
+}
+
+// 使用Math.random()随机选择一个设备ID
+int randomIndex = (int) (Math.random() * belongDeviceIDList.size());
+int belongDeviceID = belongDeviceIDList.get(randomIndex);
+
+// 生成1-1000范围内的随机曲线数据
+int curveData = (int) (Math.random() * 1000) + 1;
+
+// 使用JSONUtil生成更安全的JSON(自动处理特殊字符)
+JSONObject json = new JSONObject();
+json.set("belongDeviceID", belongDeviceID);
+json.set("curveData", curveData);
+json.set("msgType", "real-time-curve");
+return json.toString();
+}
+
+
+// 使用示例
+public static void main(String[] args) {
+List deviceIds = Arrays.asList(101, 102, 103, 104, 105);
+String result = generateRandomDeviceData(deviceIds);
+System.out.println(result);
+// 输出示例: {"belongDeviceID":103,"curveData":756}
+}
+
+
+/**
+* 告警数据传输对象
+*/
+@Data
+static class AlarmDto {
+private int belongDeviceID; // 所属设备id
+private int deviceID; // 紫外设备id
+private String deviceName; // 紫外设备名称
+private int alarmTimestamp; // 告警时间戳(s)
+private String alarmMessage; // 告警信息
+private String msgType;
+
+// 构造函数
+public AlarmDto() {
+}
+
+public AlarmDto(int belongDeviceID, int deviceID, String deviceName) {
+this.belongDeviceID = belongDeviceID;
+this.deviceID = deviceID;
+this.deviceName = deviceName;
+}
+
+public AlarmDto(int belongDeviceID, int deviceID, String deviceName,
+int alarmTimestamp, String alarmMessage) {
+this.belongDeviceID = belongDeviceID;
+this.deviceID = deviceID;
+this.deviceName = deviceName;
+this.alarmTimestamp = alarmTimestamp;
+this.alarmMessage = alarmMessage;
+}
+
+
+@Override
+public String toString() {
+return String.format("AlarmDto{belongDeviceID=%d, deviceID=%d, deviceName='%s', " +
+"alarmTimestamp=%d, alarmMessage='%s'}",
+belongDeviceID, deviceID, deviceName, alarmTimestamp, alarmMessage);
+}
+}
+
+/**
+* 告警数据生成器
+*/
+static class AlarmGenerator {
+
+// 预定义的中文告警信息
+private static final String[] ALARM_MESSAGES = {
+"设备温度过高,请检查散热系统",
+"紫外灯管老化,建议更换",
+"设备电压异常,请检查供电线路",
+"传感器读数异常,需要校准",
+"设备运行时间过长,建议停机检修",
+"紫外强度不足,检查灯管状态",
+"设备过载保护触发",
+"通信连接中断,请检查网络",
+"设备内部湿度过高",
+"风扇转速异常,请检查风扇",
+"电流过大,触发保护机制",
+"设备门未关闭,请检查安全门",
+"滤网堵塞,需要清洁维护",
+"设备震动异常,请检查固定",
+"控制板故障,需要技术支持"
+};
+
+/**
+* 从AlarmDto列表中随机选择一个,设置当前时间戳和随机告警信息
+*
+* @param alarmDtoList 包含基础信息的AlarmDto列表
+* @return 完整的AlarmDto对象
+*/
+public static AlarmDto generateRandomAlarm(List alarmDtoList) {
+if (alarmDtoList == null || alarmDtoList.isEmpty()) {
+throw new IllegalArgumentException("AlarmDto列表不能为空");
+}
+
+// 随机选择一个AlarmDto
+int randomIndex = (int) (Math.random() * alarmDtoList.size());
+AlarmDto selectedAlarm = alarmDtoList.get(randomIndex);
+
+// 创建新的AlarmDto对象,复制基础信息
+AlarmDto result = new AlarmDto(
+selectedAlarm.getBelongDeviceID(),
+selectedAlarm.getDeviceID(),
+selectedAlarm.getDeviceName()
+);
+result.setMsgType("alarm-message");
+// 设置当前时间戳(秒)
+result.setAlarmTimestamp((int) (System.currentTimeMillis() / 1000));
+
+// 随机选择告警信息
+int messageIndex = (int) (Math.random() * ALARM_MESSAGES.length);
+result.setAlarmMessage(ALARM_MESSAGES[messageIndex]);
+
+return result;
+}
+
+// 使用示例
+public static void main(String[] args) {
+// 创建测试数据
+List alarmList = Arrays.asList(
+new AlarmDto(1001, 2001, "紫外消毒设备A"),
+new AlarmDto(1002, 2002, "紫外消毒设备B"),
+new AlarmDto(1003, 2003, "紫外消毒设备C"),
+new AlarmDto(1004, 2004, "紫外消毒设备D")
+);
+
+// 生成随机告警
+AlarmDto randomAlarm = generateRandomAlarm(alarmList);
+System.out.println("生成的随机告警:");
+System.out.println(randomAlarm);
+
+// 多次生成示例
+System.out.println("\n连续生成3个随机告警:");
+for (int i = 0; i < 3; i++) {
+AlarmDto alarm = generateRandomAlarm(alarmList);
+System.out.println((i + 1) + ". " + alarm);
+}
+}
+}
+```
+
+
+
+
+
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..e7496b0
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "target": "ES2022"
+ },
+ "include": [
+ "src/.vuepress/**/*.ts",
+ "src/.vuepress/**/*.vue"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}