跳到主要内容

91 篇博文 含有标签「后端」

后端开发技术文章

查看所有标签

Go 项目使用 Alpine 构建镜像

· 阅读需 2 分钟
素��明诚
Full stack development

示例 Dockerfile

Go 应用能够在 Alpine Linux 上运行,你需要确保它完全静态链接,即不依赖于任何外部的动态链接库

FROM golang:1.22.2 AS build2

WORKDIR /gin-blog

# 设置环境变量,确保 Go 模块被启用,CGO 被禁用,并指定操作系统为 Linux
ENV GO111MODULE=on \
CGO_ENABLED=0 \
GOOS=linux

# 设置 Go 代理,以便能够下载依赖
ENV GOPROXY=https://goproxy.io,direct

# 下载所有依赖
RUN go mod download

# 使用特定的编译标志来构建应用
# -tags netgo:确保使用 Go 的纯 Go 网络栈
# -ldflags '-w -s -extldflags "-static"':减小生成的二进制文件的大小,并确保所有的链接都是静态的
RUN go build -tags netgo -ldflags '-w -s -extldflags "-static"' -o gin-blog

设置说明

CGO_ENABLED=0

CGO 允许 Go 程序调用 C 代码。默认情况下,CGO 是启用的,这可能导致生成的可执行文件依赖于本地的 C 库,如 glibc 或 musl libc。在多数 Linux 发行版上,默认的 libc 是 glibc,而 Alpine 使用的是 musl libc,两者在某些实现和行为上有差异。禁用 CGO 并启用纯 Go 编译,可以生成完全静态链接的二进制文件,不依赖于操作系统的 C 库,从而提高在不同 Linux 发行版(特别是在使用 musl libc 的 Alpine Linux)上的兼容性。

GOOS=linux: 这告诉 Go 编译器,目标操作系统是 Linux

-tags netgo: 这个编译标志确保 Go 使用其自身的网络库,而非系统级的网络库这是为了避免依赖系统的动态网络库,特别是在静态编译的情况下

-ldflags '-w -s -extldflags "-static"'

  • -w -s 用来减少编译后的二进制文件的大小,通过去除调试信息
  • -extldflags "-static"' 确保链接过程中不链接任何动态库,即使是系统的 libc

JWTpayload字段解释

· 阅读需 1 分钟
素明诚
Full stack development

注册的 Claims

字段名描述类型
iss发行人字符串
sub主题字符串
aud接收方字符串
exp过期时间时间戳
nbf在此之前不可用时间戳
iat发行时间时间戳
jtiJWT ID字符串

公共的 Claims

公共的 claims 可以被定义用于公共或私有的信息交换。为了避免冲突,它们应该在 IANA JSON Web Token Registry 中注册,或者包含一个命名空间以防止命名冲突。

私有的 Claims

私有的 claims 是发送者和接收者之间共同定义的 claims,并不是注册或公共的 claims。这些 claims 通常用于携带业务逻辑相关的信息。

什么是倒排索引Inverted Index

· 阅读需 4 分钟
素明诚
Full stack development

假设淘宝上有以下三个商品描述,我们将建立每个词对应的文档列表,如下表所示

商品 1:苹果手机新款发布,支持 5G 网络

商品 2:小米最新 5G 智能手机,性能卓越

商品 3:华为 5G 网络手机,实际 4G 功耗所以续航时间长

构建倒排索引

对每个商品的描述进行分词和处理后,构建如下的倒排索引

词汇商品 ID 列表
苹果1
手机1, 2, 3
新款1
发布1
支持1
5G1, 2, 3
网络1, 3
小米2
最新2
智能2
性能2
卓越2
华为3
实际3
4G3
功耗3
续航3
时间3
3

用户实际搜索

当用户在淘宝搜索“5G 手机”时,搜索系统会进行以下操作:

分词:将“5G 手机”分解为“5G”和“手机”

查找索引:查询倒排索引,找到含有“5G”和“手机”的商品 ID

  1. “5G”对应商品 1, 2
  2. “手机”对应商品 1, 2, 3

合并结果:由于两个词的搜索结果相同,返回商品 1, 2

排序和展示:基于其他因素(如商品评分、销量等)对搜索结果进行排序,并展示给用户

存储和查找

存储:对于需要极高读取性能的应用,倒排索引可以存储在内存中的数据结构,如Redis或者是 map 这种数据结构也可以存储在分布式文件系统Hadoop,或者是搜索引擎专用的Elasticsearch

查找:内存数据库提供极快的数据访问速度,适用于实时搜索和高频访问场景

倒排索引的构建过程

1.文档预处理

去噪声:移除文档中的 HTML 标签、特殊符号、数字等无关内容

标准化:统一文档的格式,如将所有文字转换为小写,统一使用的日期和数字格式等

2.分词(Tokenization)

分词:中文文本的分词比英文复杂,因为中文没有明显的词与词之间的空格分隔因此,需要使用专门的中文分词工具来识别中文句子中的词汇,如结巴分词(jieba)、汉语言处理包(HanLP)等

词干提取(Stemming):将词汇还原为基本形式(例如,“running”还原为“run”)

去停用词:中文的停用词(如“的”、“了”、“在”等)需要特别处理,因为这些词在中文中非常常见,但通常不携带重要信息

编码:确保处理和存储过程中使用统一的字符编码(如 UTF-8),这对于处理中文等多字节语言尤为重要,以防出现乱码问题

查询处理:在处理查询时,同样需要对查询文本进行分词处理,确保查询词与索引中的词能够正确匹配

3.构建倒排记录

创建倒排记录:对于每一个分词,创建一个记录,记录中包含该词出现的所有文档 ID 以及该词在每个文档中的位置

记录更新:随着新文档的添加,不断更新每个词的倒排记录

4.索引存储

索引格式化:将倒排索引以某种格式(如哈希表、B 树)存储在文件或数据库中

压缩:为了节省存储空间和提高查询效率,倒排索引通常会被压缩常用的压缩技术包括编码文档 ID 差值、使用位运算等

5.优化和维护

索引优化:定期优化索引结构,合并小索引文件,删除无用索引,以提高搜索效率

动态更新:支持索引的实时更新,以反映内容的变化

6.索引使用

查询处理:接收用户查询,解析查询词,通过倒排索引快速找到包含这些词的文档

排名算法:基于文档中词的频率、位置等因素对搜索结果进行排名

Go 实现简单倒排索引

package algorithm

type Doc struct {
Id int
Keywords []string
}

func BuildInvertedIndex(docs []Doc) map[string][]int {
index := make(map[string][]int)
for _, doc := range docs {
for _, keyword := range doc.Keywords {
index[keyword] = append(index[keyword], doc.Id)
}
}
return index
}

常见的访问控制模型和表结构

· 阅读需 7 分钟
素明诚
Full stack development

RBAC(Role-Based Access Control)

在一所大学的信息系统中,根据用户的角色(如学生、教授、行政人员)分配不同的访问权限。教授可以访问和修改课程成绩,学生只能查看成绩和课程资料,行政人员可以访问学生记录和财务信息。

表结构

表名字段说明
UsersUserID, UserName用户表
RolesRoleID, RoleName角色表
PermissionsPermissionID, PermissionDescription权限表
UserRoleUserID, RoleID用户角色关系表
RolePermissionRoleID, PermissionID角色权限关系表

关系说明

  • 用户与角色是多对多关系,通过 UserRole 表联系
  • 角色与权限是多对多关系,通过 RolePermission 表联系

查询语句: 查询一个用户的所有权限

SELECT PermissionDescription FROM Permissions
JOIN RolePermission ON Permissions.PermissionID = RolePermission.PermissionID
JOIN UserRole ON RolePermission.RoleID = UserRole.RoleID
WHERE UserRole.UserID = [用户ID];

ABAC(Attribute-Based Access Control)

在一个智能家居系统中,系统根据时间、地点以及设备状态控制家居设备的访问例如,只有家庭成员在家时,才能通过手机应用控制家中的安全系统

表结构

表名字段说明
DevicesDeviceID, DeviceType设备表
PoliciesPolicyID, PolicyRule, Effect策略表,定义访问条件和效果
AttributesAttributeID, DeviceID, AttributeName, AttributeValue设备属性表

关系说明

  • 设备具有多个属性,属性表通过 DeviceID 与设备表关联
  • 策略定义了基于属性的访问规则

查询语句: 检查是否允许访问设备

SELECT Effect FROM Policies
WHERE PolicyRule = 'DeviceType = "SecuritySystem" AND AttributeName = "Status" AND AttributeValue = "AtHome"'
AND DeviceID = [设备ID];

ACL(Access Control List)

在公司的文件共享服务器上,管理员为不同的文件夹设置 ACL,规定哪些部门或个人有读取或编辑文件的权限,以此来保护敏感数据不被未授权访问

表结构

表名字段说明
FilesFileID, FileName文件表
UsersUserID, UserName用户表
FilePermissionsFileID, UserID, PermissionType文件权限表

关系说明

  • 文件与用户之间的权限关系通过 FilePermissions 表定义

查询语句: 查询某个用户对文件的权限

SELECT FileName, PermissionType FROM Files
JOIN FilePermissions ON Files.FileID = FilePermissions.FileID
WHERE FilePermissions.UserID = [用户ID];

DAC(Discretionary Access Control)

在操作系统中,用户可以设置个人文件的权限,决定哪些其他用户可以访问或修改这些文件,如在家庭电脑上父母可以访问孩子的学习资料文件夹

表结构

表名字段说明
UsersUserID, UserName用户表
FilesFileID, FileName, OwnerID文件表,包含所有者 ID
FilePermissionsPermissionID, FileID, UserID, PermissionType文件权限表,定义用户对文件的权限

关系说明

  • 文件有一个所有者,所有者通过 OwnerID 与用户表关联
  • 用户对文件的访问权限通过 FilePermissions 表定义,权限类型包括读取、写入等

查询语句: 查询用户对文件的权限

SELECT FileName, PermissionType FROM Files
JOIN FilePermissions ON Files.FileID = FilePermissions.FileID
WHERE FilePermissions.UserID = [用户ID];

MAC(Mandatory Access Control)

在军事或政府通信系统中,根据信息的安全级别(如机密、秘密、公开)决定谁可以访问这些信息,确保只有具有相应安全级别的人员才能访问

表结构

表名字段说明
UsersUserID, UserName, ClearanceLevel用户表,包含安全级别
DocumentsDocumentID, SecurityLevel文档表,包含文档安全级别
AccessRulesRuleID, MinClearanceLevel, DocumentID访问规则表

关系说明

  • 文档具有特定的安全级别,用户访问需满足最低安全级别要求
  • 访问规则定义了文档的最低清除级别

查询语句: 查询用户可以访问的所有文档

SELECT DocumentID FROM Documents
JOIN AccessRules ON Documents.DocumentID = AccessRules.DocumentID
WHERE AccessRules.MinClearanceLevel <= (SELECT ClearanceLevel FROM Users WHERE UserID = [用户ID]);

PBAC(Policy-Based Access Control)

在健康管理应用中,可以设置策略允许医生访问他们患者的医疗记录,但禁止访问其他患者的信息,除非在紧急情况下得到授权

表结构

表名字段说明
UsersUserID, UserName用户表
RecordsRecordID, PatientID医疗记录表
AccessPoliciesPolicyID, UserID, RecordID, Conditions, Effect访问策略表

关系说明

  • 访问策略表定义了在特定条件下用户可以访问的记录
  • 策略可能包含条件,如“只有紧急情况”或“只有是主治医生”

查询语句: 查询医生可以访问的医疗记录

SELECT RecordID FROM Records
JOIN AccessPolicies ON Records.RecordID = AccessPolicies.RecordID
WHERE AccessPolicies.UserID = [用户ID] AND Conditions = 'Emergency';

RB-RBAC (Rule-Based Role-Based Access Control)

在金融机构中,高级管理人员可以查看所有财务报告,而普通员工只能在完成相关培训并被授权后,才能访问特定的财务数据

表结构

表名字段说明
UsersUserID, UserName用户表
RolesRoleID, RoleName角色表
RulesRuleID, RoleID, RuleCondition规则表,包含条件
RoleRulesRoleID, RuleID角色规则关联表
PermissionsPermissionID, Description权限表
RolePermissionsRoleID, PermissionID角色权限关联表

关系说明

  • 角色通过 RoleRules 与规则关联
  • 角色与权限通过 RolePermissions 表关联

查询语句: 查询用户角色的权限

SELECT Description FROM Permissions
JOIN RolePermissions ON Permissions.PermissionID = RolePermissions.PermissionID
JOIN Roles ON RolePermissions.RoleID = Roles.RoleID
JOIN Users ON Roles.RoleID = Users.UserID
WHERE Users.UserID = [用户ID];

SCAC(Social Context-Aware Access Control)

在社交媒体平台上,用户可以设置隐私控制,允许只有特定的好友列表(如亲密好友)可以查看其个人照片或动态

表结构

表名字段说明
UsersUserID, UserName用户表
PostsPostID, Content, PrivacySetting帖子表,包含隐私设置
FriendListsListID, UserID, FriendID好友列表表
ListPermissionsListID, PostID, PermissionType列表权限表

关系说明

  • 帖子的可见性由 PrivacySetting 定义,与 FriendLists 表中的列表匹配
  • 用户通过 FriendLists 定义自己的社交圈

查询语句: 查询用户可以看到的帖子

SELECT PostID FROM Posts
WHERE PrivacySetting = 'FriendsOnly' AND PostID IN (
SELECT PostID FROM ListPermissions
JOIN FriendLists ON ListPermissions.ListID = FriendLists.ListID
WHERE FriendLists.FriendID = [用户ID]
);

结合 RBAC 和 ABAC

在许多企业级应用中,RBAC(角色基于访问控制)通常与ABAC(基于属性的访问控制)结合使用来提供更灵活的访问控制策略例如,一个系统可能基于用户的角色(如经理或员工)授予权限,同时根据其他属性(如地理位置、访问时间或设备安全状态)对权限进行细粒度的调整这种组合允许系统在维持角色管理的简便性的同时,增加基于环境或上下文的安全策略

结合 DAC 和 MAC

在需要极高安全级别的环境中,DAC(自主访问控制)和MAC(强制访问控制)可以同时使用例如,在政府或军事系统中,文件的创建者可以设置自己的访问控制列表(DAC 模式),同时系统还会强制执行根据文件分类和用户安全级别的访问控制(MAC 模式)这样,即使文件的创建者给予了访问权限,只有符合安全级别要求的用户才能实际访问文件

结合 PBAC 和 RBAC

PBAC(基于策略的访问控制)经常与RBAC结合使用,尤其是在复杂的业务逻辑需要映射到访问控制策略时在这种情况下,角色定义基本的访问权限,而策略则可以根据业务规则进一步调整这些权限例如,在医疗信息系统中,所有医生可能基于他们的角色拥有访问病人信息的权限,但基于政策,只有主治医生才能访问敏感的治疗历史,且仅在病人住院期间

算法中的大O表示法Big O notation

· 阅读需 3 分钟
素明诚
Full stack development

大 O 表示法(Big O notation)源自数学中的“渐进符号”,用来描述函数当输入值趋向无穷大时的增长率 在算法分析中,我们通常关注最坏情况的时间复杂度,即输入数据量极大时,算法执行时间的上界

这种表示法的重点是帮助我们抽象化算法的性能,忽略具体实现的细节和低阶增长,只关注主导行为 例如,一个算法的时间复杂度可能是 3n^2 + 5n + 7,但我们会用 O(n^2) 来描述它,因为当 n 足够大时,n^2 项是决定性因素

通过这种方式,大 O 表示法为算法设计和分析提供了一个清晰、一致的框架,使得不同算法间的比较和选择更加基于理论和数学基础,而不仅仅是经验 这有助于在实际应用中优化性能和资源使用

不考虑的因素

具体执行时间:大 O 不关心算法执行的具体秒数,因为这受到太多外部因素的影响,如处理器速度、系统负载、编译器优化等

系数和低阶项:在大 O 表示法中,忽略系数和低阶项是因为它们对于大规模数据的影响较小

常数时间:即使某些操作(如数据复制、移动等)在实际执行中需要时间,大 O 分析中常常假设这些操作的时间是常数,并聚焦于变化最显著的部分

b05eef6228a03d66e8ceaecd60db87db## Go 语言示例

O(1) - 常数时间复杂度

操作的执行时间不依赖于输入数据的大小 例如,访问数组中的特定元素

func getFirstElement(elements []int) int {
return elements[0] // 总是返回数组的第一个元素
}

O(log n) - 对数时间复杂度

通常见于“分而治之”的策略,如二分搜索

func binarySearch(slice []int, target int) int {
low := 0
high := len(slice) - 1

for low <= high {
mid := low + (high-low)/2
if slice[mid] < target {
low = mid + 1
} else if slice[mid] > target {
high = mid - 1
} else {
return mid
}
}
return -1
}

O(n) - 线性时间复杂度

操作的执行时间与输入数据的大小成正比 例如,计算所有元素的总和

func sumElements(elements []int) int {
sum := 0
for _, v := range elements {
sum += v
}
return sum
}

O(n log n) - 线性对数时间复杂度

常见于某些高效的排序算法,如归并排序

func merge(left, right []int) []int {
result := make([]int, 0, len(left)+len(right))
for len(left) > 0 && len(right) > 0 {
if left[0] < right[0] {
result = append(result, left[0])
left = left[1:]
} else {
result = append(result, right[0])
right = right[1:]
}
}
result = append(result, left...)
result = append(result, right...)
return result
}

func mergeSort(elements []int) []int {
if len(elements) < 2 {
return elements
}
mid := len(elements) / 2
return merge(mergeSort(elements[:mid]), mergeSort(elements[mid:]))
}

静态链接库Static Libraries和动态链接库Dynamic Libraries

· 阅读需 3 分钟
素明诚
Full stack development

静态链接库

在编译阶段,静态库的内容直接复制到最终的可执行文件中 一旦程序被编译,它就包含了所有它需要的代码和资源,无需外部库文件

优点

自包含 可执行文件包含所有必需的库代码,便于分发

启动性能 无需在运行时加载外部库,可能提高加载速度

缺点

文件大小 可执行文件较大,因包含了所有库代码

更新困难 库更新后,所有使用该库的应用都需重新编译和分发

动态链接库

动态库在程序运行时被加载 可执行文件包含库的引用,实际的库代码在运行时才载入内存 多个程序可以共享同一份库副本

优点

节省空间 多个程序可共享单一库副本

更新容易 更新库文件后,所有使用该库的程序自动受益

缺点

依赖性 必须确保运行时库可用,否则程序无法启动

性能开销 加载库可能增加程序启动时间,运行时解析也可能轻微影响性能

举例说明

如果你的程序需要进行数学计算,如求平方根,你可能会使用数学库(比如 C 语言的 math.h

静态链接 在编译时,math.h 提供的函数直接被复制到你的可执行文件中,使得程序不需要在系统中查找数学库

动态链接 程序保留对数学库函数的引用,不直接包含这些代码 当程序运行时,操作系统加载这些函数的动态库(如 libm.so) 如果其他程序也使用这些函数,它们可以共享同一份 libm.so

go 创建静态链接的可执行文件

下面是创建一个静态链接的 Go 程序的一般步骤和指令

# 设置 CGO_ENABLED=0 禁用 CGO
CGO_ENABLED=0

# 使用 go build 命令并加入适当的标志
go build -tags netgo -ldflags '-extldflags "-static"'

参数说明

CGO_ENABLED=0

禁用 CGO,确保不调用任何 C 代码库,这是实现纯静态链接的关键

-tags netgo

确保 Go 使用其自己的网络库(net package)实现,而不是系统的网络库。这有助于在网络操作中也保持静态链接,尤其是 DNS 解析相关功能

-ldflags '-extldflags "-static"'

静态链接。-extldflags "-static" 是传递给外部链接器的,确保没有动态库被链接

Go 创建 goroutine 的四种方式

· 阅读需 1 分钟
素明诚
Full stack development

直接启动匿名函数

这是创建 goroutine 最常见的方式使用 go 关键字后跟一个匿名函数调用,立即在新的 goroutine 上执行该函数

go func() {
fmt.Println("Running in a goroutine")
}()

启动一个已有函数

如果你已经有一个函数,你可以创建一个 goroutine 来运行这个函数这仍然使用 go 关键字,后面跟随函数名和参数

func printMessage(message string) {
fmt.Println(message)
}

go printMessage("Hello, goroutine!")

启动一个方法

如果你有一个对象的方法需要在 goroutine 中运行,也可以使用 go 关键字直接调用

type Greeter struct {
greeting string
}

func (g Greeter) greet() {
fmt.Println(g.greeting)
}

var greeter = Greeter{"Hello from a goroutine"}
go greeter.greet()

使用闭包调用

如果需要在 goroutine 中使用当前作用域内的变量,可以通过闭包来捕获这些变量

name := "Gopher"
go func(who string) {
fmt.Println("Hello,", who)
}(name)

Go 搭建并使用 goproxy 作为代理服务器

· 阅读需 1 分钟
素明诚
Full stack development

安装 goproxy

仓库地址:https://github.com/goproxyio/goproxy

services:
goproxy:
image: goproxy/goproxy:latest
command: "-listen=0.0.0.0:8081 -cacheDir=/ext"
ports:
- "8081:8081"
restart: always
environment:
- HTTP_PROXY=http://172.22.220.64:7890 # 代理地址
- HTTPS_PROXY=http://172.22.220.64:7890 # 这个得有,否则白玩
volumes:
- ./cacheDir:/ext

配置代理

如果要测试的话先清除缓存

go clean -modcache

设置为自己搭建的代理,我这里的测试环境的 windows。注意使用export是替换该环境变量之前的值,而不是添加新值到现有的列表中

export GOPROXY=http://172.22.220.64:8081

查看配置

go env GOPROXY

下载包

go get -v github.com/sirupsen/logrus

测试

36dd244a94607dce1a9e004e60e89a2e76da0346edcede665a737c847c6473a20cb74ada0bc74a2d10bd4d9941276636

成功~

使用场景

企业内部:企业可以搭建内部的 goproxy 服务器,用来提供企业内部使用的私有模块,同时也可以缓存公共模块,减少对外部网络的依赖。

开源社区:开源项目可以设置使用公共的 goproxy 服务,如 goproxy.cngoproxy.io,这些都是为了提供更快速的模块下载服务而设立的。

个人开发者:个人开发者也可以使用公共的或私有的 goproxy 实例来优化自己的开发流程,尤其是在网络连接不佳的情况下。

embed 包在 Go 使用场景

· 阅读需 1 分钟
素明诚
Full stack development

单页应用(SPA)

单页应用通常由 HTML、CSS 和 JavaScript 构成,这些静态文件可以被嵌入到 Go 服务器中,实现完全的后端前端集成。

import (
"embed"
"net/http"
)

//go:embed dist/*
var staticFiles embed.FS

func main() {
http.Handle("/", http.FileServer(http.FS(staticFiles)))
http.ListenAndServe(":8080", nil)
}

命令行工具的配置文件和资源

对于命令行工具,配置文件、模板或其他资源可以被嵌入到二进制文件中,简化用户的配置过程,用户无需手动设置或修改配置文件。

import (
"embed"
"log"
)

//go:embed config/default.yaml
var defaultConfig embed.FS

func loadConfig() {
data, err := defaultConfig.ReadFile("config/default.yaml")
if err != nil {
log.Fatalf("Error loading config: %v", err)
}
// 解析配置文件
parseConfig(data)
}

桌面应用的资源和多媒体内容

在桌面应用中,如图标、图像、音频和视频文件可以被嵌入到应用中,确保应用运行时所有资源都可用。

import "embed"

//go:embed assets/icons/*
var icons embed.FS

//go:embed assets/sounds/notification.wav
var soundNotification embed.FS

如何编写版本号语义化版本控制规范

· 阅读需 2 分钟
素明诚
Full stack development

版本号的构成

语义化版本号由三部分组成:主版本号、次版本号和补丁版本号,格式为 X.Y.Z。

主版本号(X):当你做出不兼容的 API 更改时递增这个版本号。不兼容的更改指的是,任何现有用户必须修改其代码才能继续运行的更改,例如重大的重构或功能移除。

次版本号(Y):当你添加了向后兼容的新功能时递增这个版本号。向后兼容意味着旧版本的用户可以安全升级,不会影响现有功能。

补丁版本号(Z):当你进行了向后兼容的问题修正时递增这个版本号。这通常包括对 bug 的修复或安全问题的修复,不改变软件的功能和 API。

版本号的递增规则

版本号需要按顺序递增。如果主版本号增加,次版本号和补丁版本号应重置为 0;如果次版本号增加,补丁版本号应重置为 0。

不必每个版本都发布,可以跳过某些数字,例如从 1.0.0 直接升级到 2.0.0,或者从 1.5.0-beta 直接升级到 1.5.0。

版本控制注意事项

正常版本号如v1.1.11.1.1更推荐使用前者

如果主版本号没有为 1,那说这个应用可能还没有发布稳定版本,需要注意

每次发布主版本的时候,一般都会提前发布一个beta版本,等beta版本稳定了再发布新版本

迭代版本的时候最好使用 CI/CD 工具来代替人为的设置,这样有记录而且更清晰