From 59de4f24a641f217261eac5400a474141e609b35 Mon Sep 17 00:00:00 2001 From: zhangsan <646228430@qq.com> Date: Fri, 18 Jul 2025 21:09:59 +0800 Subject: [PATCH] =?UTF-8?q?Commit=20on=202025/07/18=20=E5=91=A8=E4=BA=94?= =?UTF-8?q?=2021:09:59.19?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 后端学习/Java笔记本.md | 9 +- 后端学习/Maven.md | 26 +++++- 后端学习/力扣Hot 100题.md | 125 +++++++++++++++++-------- 杂项/Docker指南.md | 19 ++-- 杂项/JavaWeb——前端.md | 186 +++++++++++++++++++++++++++++++++++++- 项目/拼团交易系统.md | 130 +++++++++++++++++++++++++- 项目/苍穹外卖.md | 21 +++++ 7 files changed, 462 insertions(+), 54 deletions(-) diff --git a/后端学习/Java笔记本.md b/后端学习/Java笔记本.md index 6ec609b..b4dec53 100644 --- a/后端学习/Java笔记本.md +++ b/后端学习/Java笔记本.md @@ -412,9 +412,9 @@ MyInterface obj = () -> { 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。 -可选的参数圆括号:**一个参数无需定义圆括号**,但无参数或多个参数需要定义圆括号。 +可选的参数圆括号():**一个参数无需定义圆括号**,但无参数或多个参数需要定义圆括号。 -可选的大括号:如果主体**只有一个语句,可以不使用大括号**。 +可选的大括号{}:如果主体**只有一个语句,可以不使用大括号**。 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,使用大括号需显示retrun;如果函数是void则不需要返回值。 @@ -480,15 +480,12 @@ public interface Comparator { ``` ```java -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - public class Main { public static void main(String[] args) { List names = Arrays.asList("John", "Jane", "Adam", "Dana"); // 使用Lambda表达式排序 + //public static void sort(List list, Comparator c) Collections.sort(names, (a, b) -> a.compareTo(b)); // 输出排序结果 diff --git a/后端学习/Maven.md b/后端学习/Maven.md index 3303448..da9ae6e 100644 --- a/后端学习/Maven.md +++ b/后端学习/Maven.md @@ -395,4 +395,28 @@ target/ 打包失败的把test步骤去掉! -![image-20250514125309214](https://pic.bitday.top/i/2025/05/14/kq2fpz-0.png) \ No newline at end of file +![image-20250514125309214](https://pic.bitday.top/i/2025/05/14/kq2fpz-0.png) + + + +### Maven镜像加速 + +maven构建依赖可能比较慢,需创建 `.mvn/settings.xml` (这个提速非常多!务必添加) + +```xml + + + + aliyun + aliyun maven + https://maven.aliyun.com/repository/public + central,apache.snapshots + + + +``` + +使用阿里云镜像加速,`.mvn` 放在项目根目录下 \ No newline at end of file diff --git a/后端学习/力扣Hot 100题.md b/后端学习/力扣Hot 100题.md index 5417ae1..6631f30 100644 --- a/后端学习/力扣Hot 100题.md +++ b/后端学习/力扣Hot 100题.md @@ -498,7 +498,7 @@ Set set1 = new HashSet<>(Arrays.asList(wordList)); //构造器直接初 `remove()` / `poll()`: -- 功能:移除并返回队首元素。 +- 功能:移除并返回**队首**元素。 - 时间复杂度:`O(log n)` - 区别 - `remove`:队列为空时抛出异常。 @@ -506,7 +506,7 @@ Set set1 = new HashSet<>(Arrays.asList(wordList)); //构造器直接初 `element()` / `peek()`: -- 功能:查看队首元素,但不移除。 +- 功能:查看**队首**元素,但不移除。 - 时间复杂度:`O(1)` - 区别 - `element`:队列为空时抛出异常。 @@ -1130,57 +1130,110 @@ public class Main { 快速排序是一种基于“分治”思想的排序算法,通过选定一个“枢轴元素(pivot)”,将数组划分为左右两个子区间:左边都小于等于 pivot,右边都大于等于 pivot;然后对这两个子区间递归排序,最终使整个数组有序。 ```java -public class QuickSortWithSwap { - - // 交换数组中两个元素的位置 - private static void swap(int[] arr, int i, int j) { - int temp = arr[i]; - arr[i] = arr[j]; - arr[j] = temp; - } +public class QuickSort { - private static void quickSort(int[] arr, int low, int high) { + /** + * 对数组 arr 在下标 low 到 high 范围内进行快速排序 + * 使用“挖坑填坑”方式,不再调用 swap 方法 + */ + public static void quickSort(int[] arr, int low, int high) { if (low < high) { int pivotPos = partition(arr, low, high); // 划分 - quickSort(arr, low, pivotPos - 1); // 递归排序左子表 - quickSort(arr, pivotPos + 1, high); // 递归排序右子表 + quickSort(arr, low, pivotPos - 1); // 递归排序左子表 + quickSort(arr, pivotPos + 1, high); // 递归排序右子表 } } + /** + * 划分函数:以 arr[low] 作为枢轴,通过“挖坑—填坑”将小于枢轴的元素移到左边, + * 大于枢轴的元素移到右边,最后返回枢轴的最终下标 + */ private static int partition(int[] arr, int low, int high) { - int pivot = arr[low]; // 选取第一个元素作为枢轴 - int left = low; // 左指针 - int right = high; // 右指针 - + int pivot = arr[low]; // 先保存枢轴 + int left = low, right = high; + while (left < right) { - // 从右向左找第一个小于枢轴的元素 + // 从右向左找第一个小于 pivot 的元素,填到 left 这个“坑”里 while (left < right && arr[right] >= pivot) { right--; } - // 从左向右找第一个大于枢轴的元素 + arr[left] = arr[right]; + + // 从左向右找第一个大于 pivot 的元素,填到 right 这个“坑”里 while (left < right && arr[left] <= pivot) { left++; } - // 交换这两个元素 - if (left < right) { - swap(arr, left, right); - } + arr[right] = arr[left]; + } + // 最后把枢轴填回中心位置 + arr[left] = pivot; + return left; + } +} + +``` + + + +### 快速选择 + +时间复杂度: O(n) + +```java +public class QuickSelect { + + /** + * 在 nums[low..high] 区间内,寻找排序后下标为 k 的元素(第 k 小) + */ + public int quickselect(int[] nums, int low, int high, int k) { + // 区间内只有一个元素,直接返回 + if (low == high) { + return nums[low]; + } + + // 选取区间第一个元素作为枢轴 + int pivot = nums[low]; + int left = low, right = high; + + // “挖坑填坑”分区:左边填小于 pivot 的值,右边填大于 pivot 的值 + while (left < right) { + // 从右向左找第一个 < pivot 的 + while (left < right && nums[right] >= pivot) { + right--; + } + nums[left] = nums[right]; // 填到左“坑” + + // 从左向右找第一个 > pivot 的 + while (left < right && nums[left] <= pivot) { + left++; + } + nums[right] = nums[left]; // 填到右“坑” + } + + // 把 pivot 放回最终位置 + nums[left] = pivot; + + // 根据 pivot 位置与 k 比较,决定去哪一边继续 + if (left == k) { + return nums[left]; + } else if (k < left) { + return quickselect(nums, low, left - 1, k); + } else { + return quickselect(nums, left + 1, high, k); } - // 将枢轴放到最终位置 - swap(arr, low, left); - return left; // 返回枢轴的位置 } - public static void main(String[] args) { - int[] arr = {49, 38, 65, 97, 76, 13, 27, 49}; - - quickSort(arr, 0, arr.length - 1); - - System.out.println("\n排序后:"); - for (int num : arr) { - System.out.print(num + " "); - } + /** + * 返回数组中第 k 大的元素 + * @param nums 输入数组 + * @param k 1-based,第 k 大 + */ + public int findKthLargest(int[] nums, int k) { + int n = nums.length; + // 第 k 大对应第 (n - k) 小 + return quickselect(nums, 0, n - 1, n - k); } + } ``` @@ -1379,8 +1432,8 @@ public class DescendingSortExample { // 使用Comparator进行降序排序(使用lambda表达式) Arrays.sort(arr, (a, b) -> Integer.compare(b, a)); - // 或者使用Collections.reverseOrder()也可以: + // 或者使用Collections.reverseOrder()也可以: // 对下标 [1, 4) 的区间,也就是 {2,9,1},按降序排序 Arrays.sort(arr, 1, 4, Collections.reverseOrder()); diff --git a/杂项/Docker指南.md b/杂项/Docker指南.md index 7574417..13a27c3 100644 --- a/杂项/Docker指南.md +++ b/杂项/Docker指南.md @@ -693,9 +693,20 @@ docker images -### **docker可能遇到的问题:** +## Docker部署可能遇到的问题 -- **linux中构建镜像问题:** +- **镜像拉取失败** + +排除镜像站的问题,如果拉取不到镜像,很有可能是你使用的TAG不正确!! + +直接去[maven Tags | Docker Hub](https://hub.docker.com/)上看看你输入的TAG是否存在,比如: +[Docker Hub](https://hub.docker.com/_/maven/tags) + +如果你输入的都是个错误的TAG,那肯定拉不下来。 + + + +- **构建镜像失败** ```text RuntimeError: can‘t start new thread。 @@ -706,8 +717,6 @@ RuntimeError: can‘t start new thread。 解决方法:在dockerfile文件中增加一行关闭pip进度条展示 RUN pip config set global.progress_bar off - - ```text => [flask_app internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 982B 0.0s @@ -726,8 +735,6 @@ exit status 1 - - - **docker运行权限问题** ```text diff --git a/杂项/JavaWeb——前端.md b/杂项/JavaWeb——前端.md index 1a172bc..cdc4115 100644 --- a/杂项/JavaWeb——前端.md +++ b/杂项/JavaWeb——前端.md @@ -1107,4 +1107,188 @@ axios.post("http://yapi.smart-xwork.cn/mock/169327/emp/deleteById","id=1").then( ``` -Vue中先定义emps空数组,再axios将数据取到里面 this.emps=xxxx \ No newline at end of file +Vue中先定义emps空数组,再axios将数据取到里面 this.emps=xxxx + + + +## Nginx + +**docker-compose.yml** + +```yml +version: '3.8' + +networks: + group-buy-network: + external: true + +services: + # 前端(保持不变) + group-buy-market-front: + image: nginx:alpine + ports: ['86:80'] + volumes: + - ./nginx/html:/usr/share/nginx/html + - ./nginx/conf/nginx.conf:/etc/nginx/nginx.conf:ro + networks: + - group-buy-network + + # 后端实例1 + group-buying-sys-1: + image: smile/group-buying-sys:latest + ports: ['8091:8091'] + networks: + - group-buy-network + + # 后端实例2 + group-buying-sys-2: + image: smile/group-buying-sys:latest # 使用相同镜像 + ports: ['8092:8091'] # 宿主机端口不同,容器内部端口相同 + networks: + - group-buy-network + +``` + +**nginx.conf** + +```nginx +# 全局配置块 +user nginx; # 以nginx用户身份运行worker进程(安全性考虑) +worker_processes auto; # 自动根据CPU核心数设置工作进程数量(优化性能) +error_log /var/log/nginx/error.log warn; # 错误日志路径,只记录警告及以上级别的错误 +pid /var/run/nginx.pid; # 存储Nginx主进程ID的文件位置(用于进程管理) + +# 事件处理模块配置 +events { + worker_connections 1024; # 每个worker进程能处理的最大并发连接数 + # 总并发量 = worker_processes * worker_connections +} + +# HTTP服务器配置 +http { + # 基础文件类型支持 + include /etc/nginx/mime.types; # 包含MIME类型定义文件(扩展名与Content-Type映射) + default_type application/octet-stream; # 默认MIME类型(当无法识别文件类型时使用) + + # 性能优化参数 + sendfile on; # 启用高效文件传输模式(零拷贝技术) + keepalive_timeout 65; # 客户端保持连接的超时时间(秒),减少TCP握手开销 + + # 上游服务器组定义(负载均衡) + upstream gbm_backend { + # 负载均衡配置(两个后端实例) + server group-buying-sys-1:8091 weight=3; # 后端实例1,权重为3(获得60%流量) + server group-buying-sys-2:8091 weight=2; # 后端实例2,权重为2(获得40%流量) + keepalive 32; # 保持到后端服务器的长连接数(提升性能) + } + + # 支付服务上游(单实例) + upstream pay_backend { + server pay-mall:8092; # 支付服务地址(Docker服务名) + } + + # 虚拟主机配置(一个server代表一个网站) + server { + # 监听配置 + listen 80 default_server; # 监听IPv4的80端口,作为默认服务器 + listen [::]:80 default_server; # 监听IPv6的80端口 + server_name groupbuy.bitday.top; # 绑定的域名(支持多个,用空格分隔) + + # 静态文件服务配置 + root /usr/share/nginx/html; # 前端静态文件根目录(Docker需挂载此目录) + index index.html; # 默认访问的索引文件 + + # 前端路由处理(适配Vue/React等单页应用) + location / { + try_files $uri $uri/ /index.html; # 尝试顺序:匹配文件→匹配目录→回退到index.html + } + + # 组购系统API代理配置 + location /api/v1/gbm/ { + proxy_pass http://gbm_backend; # 请求转发到负载均衡组 + proxy_set_header Host $host; # 传递原始域名 + proxy_set_header X-Real-IP $remote_addr; # 传递客户端真实IP + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 代理链IP信息 + + } + + # 支付/登录API代理(正则匹配) + location ~ ^/api/v1/(alipay|login)/ { + proxy_pass http://pay_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + } +} +``` + +**注意事项:** + +1.由于是docker部署,要区别宿主机端口与容器内端口!比如 `server group-buying-sys-1:8091 weight=3;` 这里的8091就是 `group-buying-sys-1` 这个容器内端口。类似地,nginx也是docker部署,通过宿主机端口86映射到`group-buy-market-front`这个容器内的80端口。`nginx.conf`配置中的端口一定是和容器内挂钩的,而不是和宿主机!!! + +2.如果不是docker部署,比如在本地windows运行`nginx.exe` ,那么 就没有 `ports: ['8091:8091']` 这种映射关系,你的java项目中写的端口是多少,那么`nginx.conf`就配置多少 + +3.docker部署,如果 `nginx` 与 `group-buying-sys` 位于同一网络中,可以`服务名:容器内端口` 等价于 `宿主机ip:端口`。比如 `group-buying-sys-2` 中 `ports: ['8092:8091']` ,假设服务器ip地址为`124.71.159.xxx` ,那么 + +```nginx +upstream gbm_backend { + server group-buying-sys-2:8091 weight=2; #推荐! nginx->后端容器 + } +``` + +```nginx +upstream gbm_backend { + server 124.71.159.xxx:8092 weight=2; #非常不推荐!相关nginx->宿主机->后端容器 + } +``` + +两种效果一样,但推荐第一种!!! + +4.Nginx 反向代理的核心配置 + +```nginx +location /api/v1/gbm/ { + proxy_pass http://gbm_backend; # 请求转发到负载均衡组 + proxy_set_header Host $host; # 传递原始域名 + proxy_set_header X-Real-IP $remote_addr; # 传递客户端真实IP + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 代理链IP信息 + +} +``` + +将所有匹配 `/api/v1/gbm/` 路径的请求转发到 `gbm_backend` 这个后端容器(可负载均衡为多台)。 + +请求 `http://nginx-server/api/v1/gbm/order` → 转发到 `http://gbm_backend/api/v1/gbm/order` + +**注意,**如果 + +```nginx +location /api/v1/gbm/ { + proxy_pass http://gbm_backend/; # 注意末尾的斜杠 +} +``` + +请求 `http://nginx-server/api/v1/gbm/order` → 转发到 `http://gbm_backend/order`(`/api/v1/gbm/` 被移除) + +5.如果配置了反向代理(第4点内容),那么前端调用后端接口时,HTML、JS中无需写具体的后端服务器所在地址(ip:端口),而且如果做了负载均衡,有多台,这里也不好指定哪一台。 + +**为什么?** + +比如前端发送请求fetch(`/api/v1/gbm/index/query_group_buy_market_config`) + +发现匹配 `/api/v1/gbm` ,nginx根据配置就发给http://gbm_backend负载均衡组,组里有`group-buying-sys-1`和 `group-buying-sys-2`,根据负载均衡规则转发到其中一台后端服务器。 + +6.静态文件挂载 + +```nginx +server { + ... + # 静态文件服务配置 + root /usr/share/nginx/html; # 前端静态文件根目录(Docker需挂载此目录) + index index.html; + ... +} +``` + +root 要指定静态文件根目录,如果是docker部署,一定要写**容器内**的位置;`index.html` 这个文件是`/usr/share/nginx/html`目录下的。 diff --git a/项目/拼团交易系统.md b/项目/拼团交易系统.md index 4dd88f6..c81a7d7 100644 --- a/项目/拼团交易系统.md +++ b/项目/拼团交易系统.md @@ -291,6 +291,26 @@ EndNode.apply() → 组装结果并返回 TrialBalanceEntity +## 拼团结算 + +d56fcfb9708eb0d99600c751808bb843 + + + +## 对接商城和拼团系统 + +![80e1ef00f3e6eca3f1e4f66ad1823aac](https://pic.bitday.top/i/2025/07/15/rdkird-0.jpg) + +上面左侧,小型支付商城,用户下单过程。增加一个营销锁单,从营销锁单中获取拼团营销拿到的优惠价格。之后小型商城继续往下走,创建支付订单。右侧,拼团交易平台,提供营销锁单流程,锁单目标、优惠试算,规则过滤,最终落库和返回结果(订单ID、原始价格、折扣金额、支付金额、订单状态)。 + +下面小型支付商城在用户完成支付后,调用拼团**组队结算**,更新当前xx拼团完成人数,当拼团完成后**接收**回调消息执行后续交易结算发货(暂时模拟的)。 + + + +**注意**两个回调函数不要搞混:1:alipay用户支付成功后的回调,告诉pay-mall当前用户支付完毕,可以调用拼团**组队结算**。2:group_buy_market的某teamId拼团达到目标人数,拼团成功的回调,告诉pay-mall可以进行下一步动作,比如给该teamId下的所有人发货。 + + + ## 收获 ### 实体对象 @@ -1139,7 +1159,9 @@ Retrofit 在运行时会生成这个接口的实现类,帮你完成: -### 微信扫码登录流程 +### 公众号扫码登录流程 + +场景:用微信的能力来替你的网站做“扫码登录”或“社交登录”,代替自己写一整套帐号/密码体系。后台只需要基于 `openid` 做一次性关联(比如把某个微信号和你系统的用户记录挂钩),后续再次扫码就当作同一用户; ![image-20250711192110034](https://pic.bitday.top/i/2025/07/11/vrgj6u-0.png) @@ -1160,7 +1182,7 @@ Retrofit 在运行时会生成这个接口的实现类,帮你完成: 微信返回 `{ "access_token":"ACCESS_TOKEN_VALUE", "expires_in":7200 }`,后端缓存这个值(有效期约 2 小时)。 -- 后端利用 `access_token` 创建二维码 ticket,返给前端。 +- 后端利用 `access_token` 创建二维码 ticket,返给前端。(**每次调用微信会返回不同的ticket**) **2.前端展示二维码** @@ -1168,13 +1190,113 @@ Retrofit 在运行时会生成这个接口的实现类,帮你完成: **3.微信回调后端** -- 用户确认扫描后,微信服务器向你预先配置的**回调 URL**(如 `POST /api/v1/weixin/portal/receive`)推送包含 `ticket` 和 `openid` 的消息。 +- 用户确认**扫描后**,微信服务器向你预先配置的**回调 URL**(如 `POST /api/v1/weixin/portal/receive`)推送包含 `ticket` 和 `openid` 的消息。 - 后端:将 `ticket → openid` **存入缓存**(`openidToken.put(ticket, openid)`);调用 `sendLoginTemplate(openid)` 给用户推送“登录成功”模板消息(手机公众号上推送,非网页) **4.前端获知登录结果** -- **轮询方式**:生成二维码后,前端每隔几秒向后端发送 `ticket`来验证登录状态,后端**查缓存**来判断 `ticket` 对应用户是否成功登录。 +- **轮询方式**:生成二维码后,前端每隔几秒向后端 `check_login` 接口发送 `ticket`来验证登录状态,后端**查缓存**来判断 `ticket` 对应用户是否成功登录。 - **推送方式**:前端通过 WebSocket/SSE 建立长连接,后端回调处理完成后直接往该连接推送登录成功及 JWT。 +### 浏览器指纹获取登录ticket + +在扫码登录流程的基础上改进!!! + +**目的:**把「这张二维码/ticket」严格绑在发起请求的那台浏览器上,防止别的设备或会话**拿到同一个 ticket** 就能登录。 + +**1.生成指纹** +前端在用户打开「扫码登录页」时,先用 JS/浏览器 API(比如 User-Agent、屏幕分辨率、插件列表、Canvas 指纹等)算出一个唯一的浏览器指纹 `fp`。 + +**2.获取 ticket 时携带指纹** + +前端发起请求: + +```bash +GET /api/v1/login/weixin_qrcode_ticket_scene?sceneStr= +``` + +后端执行: + +```java +String ticket = loginPort.createQrCodeTicket(sceneStr); +sceneTicketCache.put(sceneStr, ticket); // 把 fp→ticket 映射进缓存 +``` + +**3.扫码后轮询校验** + +前端轮询:传入 `ticket` 和 `sceneStr` 指纹 + +```bash +GET /api/v1/login/check_login_scene?ticket=&sceneStr= +``` + +后端逻辑(简化): + +```java +// 1) 验证拿到的 sceneStr(fp) 对应的 ticket 是否一致 + String cachedTicket = sceneTicketCache.getIfPresent(sceneStr); +if (!ticket.equals(cachedTicket)) { + // fp 不匹配,拒绝 + return NO_LOGIN; +} + +// 2) 再看 ticket→openid 有没有被写入(扫码并回调后,saveLoginState 会写入) +String openid = ticketOpenidCache.getIfPresent(ticket); +if (openid != null) { + // 同一浏览器,且已扫码确认,返回 openid(或 JWT) + return SUCCESS(openid); +} +return NO_LOGIN; +``` + +**4.回调时保存登录状态** + +当用户扫描二维码,微信会回调你预定的接口地址,拿到 `ticket`、`openid` 后,调用: + +```java +ticketOpenidCache.put(ticket, openid); // 保存 ticket→openid +``` + +注意 `ticketOpenidCache `和 `sceneTicketCache` 一般是一个Cache Bean,这里只是为了更清晰。 + + + +**安全性提升** + +- **防止“票据劫持”**:别人就算截获了这个 ticket,想拿去自己那台机器上轮询也不行,因为指纹对不上。 +- **防止多人共用**:多个人在不同设备上**同时扫同一个码**,只有最先发起获取 ticket 的那台浏览器能完成登录。 + + + +### 无痕登录 + +“无痕登录”(又称“免扫码登录”或“静默登录”)的核心思想,是在用户首次通过二维码/授权完成登录后,给这台设备发放一份**长期信任凭证**,以后再访问就能悄无声息地登录,不再需要人为地再扫码或输入密码。 + +#### 典型流程 + +**1.初次登录(扫码授权)** + +即前面**"浏览器指纹获取登录ticket"**的流程 + +**2.后续“无痕”自动登录** + +1)前端再次打开页面,重新生成指纹 + +2)前端调用“免扫码”接口,仅传递指纹 + +3)后端校验 fingerprint → openid + +```java +String openid = sceneLoginCache.getIfPresent(sceneStr); +if (openid != null) { + // 直接返回登录态(Session / JWT) + return SUCCESS(openid); +} else { + // 指纹过期或未绑定,返回未登录,前端再走扫码流程 + return NO_LOGIN; +} +``` + +4)**成功后**,前端拿到 openid/JWT,直接进入应用,无需用户任何操作。 diff --git a/项目/苍穹外卖.md b/项目/苍穹外卖.md index 9d4bff2..6eebf2f 100644 --- a/项目/苍穹外卖.md +++ b/项目/苍穹外卖.md @@ -809,6 +809,27 @@ Maven 默认会用 `artifactId-version`来命名,eg: +部署时占用问题端口: + +```bash +Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:13306 -> 0.0.0.0:0: listen tcp 0.0.0.0:13306: bind: An attempt was made to access a socket in a way forbidden by its access permissions. +``` + +先查看是否端口被占用: + +```shell +netstat -aon | findstr 5672 +``` + +如果没有被占用,那么就是windows的bug,在CMD使用管理员权限重启NAT网络服务即可 + +```shell +net stop winnat +net start winnat +``` + + + ## 前端部署 直接部署开发完毕的前端代码,准备: