Golang Gin 入门 (三) golang入门视频教程
ninehua 2024-12-30 06:03 31 浏览
接上,预期中的调用流程如下图
简单逻辑代码
web/user.go
func (c *UserHandler) RegisterRoutes(server *gin.Engine) {
// 分组注册
up := server.Group("/users")
up.POST("/signup", c.SignUp)
up.POST("/login", c.Login)
up.POST("/edit", c.Edit)
up.GET("/profile", c.Profile)
}
type UserHandler struct {
svc *service.UserService
emailRegexExp *regexp2.Regexp
PasswordRegexExp *regexp2.Regexp
}
const (
// 定义邮箱的正则表达式
emailRegex = `^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}Golang Gin 入门 (三) - 今日头条
// 定义密码的正则表达式 至少8个字符 //包含大写字母 //包含小写字母 //包含数字 //包含特殊字符
passwordRegex = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}Golang Gin 入门 (三) - 今日头条
)
func NewUserHandler(svc *service.UserService) *UserHandler {
return &UserHandler{
svc: svc,
emailRegexExp: regexp2.MustCompile(emailRegex, regexp2.None),
PasswordRegexExp: regexp2.MustCompile(passwordRegex, regexp2.None),
}
}
func (u *UserHandler) SignUp(context *gin.Context) {
type SingUpReq struct {
Email string `json:"email"`
ConfirmPassword string `json:"confirmPassword"`
Password string `json:"password"`
}
var req SingUpReq
// Bind 方法会根据 Content-Type 来解析你的数据req里面
// 解析错了,就直接回写一个 400(http.StatusBadRequest)的错误. c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)
if err := context.Bind(&req); err != nil {
return
}
isEmail, err := u.emailRegexExp.MatchString(req.Email)
if err != nil {
context.String(http.StatusOK, "系统错误 %v", err)
return
}
if !isEmail {
context.String(http.StatusOK, "邮箱校验不通过")
return
}
isPassword, err := u.PasswordRegexExp.MatchString(req.Password)
if err != nil {
context.String(http.StatusOK, "系统错误 %v", err)
return
}
if !isPassword {
context.String(http.StatusOK, "密码校验不通过")
return
}
if req.ConfirmPassword != req.Password {
context.String(http.StatusOK, "两次输入的密码不一致")
return
}
err = u.svc.Signup(context, domain.User{
Email: req.Email,
Password: req.Password,
})
if err != nil {
context.String(http.StatusOK, "系统错误 %v", err)
return
}
context.String(http.StatusOK, "注册成功")
//log.Printf("%v\n", req)
}
service/user.go
type UserService struct {
repo *repository.UserRepository
}
func NewUserService(repo *repository.UserRepository) *UserService {
return &UserService{repo: repo}
}
func (scv *UserService) Signup(ctx context.Context, u domain.User) error {
// 要考虑加密放在哪 然后存起来
return scv.repo.Create(ctx, u)
}
repository/user.go
type UserRepository struct {
dao *dao.UserDAO
}
func NewUserRepository(dao *dao.UserDAO) *UserRepository {
return &UserRepository{dao: dao}
}
func (r *UserRepository) Create(ctx context.Context, u domain.User) error {
return r.dao.Insert(ctx, dao.User{
Email: u.Email,
Password: u.Password,
})
}
func (r *UserRepository) FindById(int64) {
// 先从cache里找
// 再从dao里面找
// 找到了在回写cache
}
repository/dao/user.go
type UserDAO struct {
db *gorm.DB
}
func NewUserDAO(db *gorm.DB) *UserDAO {
return &UserDAO{db: db}
}
func (dao *UserDAO) Insert(ctx context.Context, u User) error {
now := time.Now().UnixMilli() // 存毫秒
u.Ctime = now
u.Utime = now
return dao.db.WithContext(ctx).Create(&u).Error
}
func InitTable(db *gorm.DB) error {
return db.AutoMigrate(&User{})
}
// User在DAO里直接对应数据库表
type User struct {
Id int64
Email string
Password string
// 创建时间和更新时间毫秒数,用time.time会涉及到时区,在数据库和应用层都会涉及到时区,可以选择用UTC时间戳,在前端转时区
Ctime int64
Utime int64
}
main.go组装好全部的东西,在抽取到不同的方法里面
func initWebServer() *gin.Engine {
engine := gin.Default()
engine.Use(cors.New(cors.Config{
//AllowOrigins: []string{"http://localhost:3000"},
AllowMethods: []string{"PUT", "PATCH", "GET", "POST", "OPTIONS"},
AllowHeaders: []string{"Content-Type", "authorization"},
//ExposeHeaders: []string{"x-jwt-token"},
AllowCredentials: true, //是否允许你带cookie之类的
AllowOriginFunc: func(origin string) bool {
if strings.HasPrefix(origin, "http://localhost") {
return true // 开发环境
}
return strings.Contains(origin, "your.company.com")
},
MaxAge: 12 * time.Hour,
}))
return engine
}
func initDB() *gorm.DB {
db, err := gorm.Open(mysql.Open("root:ssqj@easyviews.pw@tcp(10.1.125.32:3307)/webook"))
if err != nil {
panic(err)
}
err = dao.InitTable(db)
if err != nil {
panic(err)
}
return db
}
func initUser(server *gin.Engine, db *gorm.DB) {
ud := dao.NewUserDAO(db)
ur := repository.NewUserRepository(ud)
us := service.NewUserService(ur)
c := web.NewUserHandler(us)
c.RegisterRoutes(server)
}
func main() {
db := initDB()
server := initWebServer()
initUser(server, db)
if err := server.Run(":8080"); err != nil {
log.Printf("Listen err: %v\n", err)
}
}
调用注册接口,查看数据库是否写入
mysql> show tables;
+------------------+
| Tables_in_webook |
+------------------+
| users |
+------------------+
1 row in set (0.01 sec)
mysql> select * from users;
+----+-------------------+-------------+---------------+---------------+
| id | email | password | ctime | utime |
+----+-------------------+-------------+---------------+---------------+
| 1 | 2864048202@qq.com | Rrootroot1* | 1730300023836 | 1730300023836 |
+----+-------------------+-------------+---------------+---------------+
1 row in set (0.01 sec)
密码加密
密码怎么加密? 代码看上去没有问题,但是好像忘了一件事情:密码是敏感信息,需要加密存储。 问题来了:谁来加密?service 还是 repository 还是 dao? 怎么加密?怎么选择一个安全的加密算法? 敏感信息你要防两类人:研发,包括你和你的同事。攻击者。 PS:敏感信息应该是连日志都不能打。
关于密码加密的位置存在多种选择且各有理由:
- service 加密:加密被认为是业务概念,不是存储概念,这里选择 service 加密是因为认为加密是业务逻辑的一部分但不是 domain 应该管的。
- repository 加密:加密可被看作是存储概念,即 “加密存储”。
- dao 加密:加密是数据库概念,可以利用数据库本身的加密功能实现。
- domain 加密:加密是业务概念,认为用户(“User”)自己才知道怎么加密。这是编程中比较无正确答案的实践问题,不同的加密位置会影响到其他接口(如登录)的实现细节。
这种就是编程里面比较无聊的、没有正确答案的实践问题。这里我选择 service 加密,也就是认为加密是业务逻辑的一部分,但是它不是 domain 应该管的。如果你选择不同的加密位置,那么它会影响到你别的接口的实现细节,比如说登录。
加密算法的选择对系统安全性至关重要,攻击者拿到密码可能为所欲为。选择加密算法的标准是难破解,需考虑以下问题:
- 相同密码加密后结果应不同,防止很多用户使用简单密码(如 123456)时数据库存储值相同。
- 难以通过碰撞、彩虹表来破解。
常见加密算法安全性逐步提高的有:
- md5之类的哈希算法。
- 在哈希算法基础上引入盐值或进行多次哈希。
- PBKDF2、BCrypt这一类随机盐值的加密算法,同样的文本加密后的结果都不同。
bcrypt被称为号称最安全的加密算法,具有以下优点:
- 不需要自己生成盐值。
- 不需要额外存储盐值。
- 可以通过控制cost来控制加密性能。
- 同样的文本加密后的结果不同。 如果要使用它,需要使用golang.org/x/crypto。由于bcrypt限制密码长度不能超过 72 字节,所以在校验时要校验这个长度,只需要修改一下正则表达式即可。
使用bcrypt加密后无法解密,只能通过比较加密后的值来确定两个值是否相等。
func (scv *UserService) Signup(ctx context.Context, u domain.User) error {
// 要考虑加密放在哪 然后存起来
hash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hash)
return scv.repo.Create(ctx, u)
}
登录功能
大多数网站的资源需要登录才能访问,比如编辑和查看用户信息。
登录功能分为两件事:
- 实现登录功能
- 登录态校验
登录请求会被发送到/users/login。
在登录接口实现中,可以看出 service 和 repository 的分界线。service 会调用 repository 查找邮箱对应的用户,然后 service 会匹配输入的密码和数据库中保存的密码是否一致。如果用户没找到或者密码错误,都返回同一个 error。
// dao层
func (dao *UserDAO) FindByEmail(ctx context.Context, email string) (u User, err error) {
// err = dao.db.WithContext(ctx).First(&u,"email = ?",email).Error // 二选一
err = dao.db.WithContext(ctx).Where("email = ?", email).First(&u).Error
return u, err
}
// repository层
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (domain.User, error) {
u, err := r.dao.FindByEmail(ctx, email)
if err != nil {
return domain.User{}, err
}
return domain.User{
Email: u.Email,
Password: u.Password,
}, err
}
//service层
func (scv *UserService) Login(ctx context.Context, email, password string) error {
// 先看有没有这个用户
u, err := scv.repo.FindByEmail(ctx, email)
if err != nil {
return ErrInvalidUserOrPaswword
}
//
err = bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
if err != nil {
return ErrInvalidUserOrPaswword
}
return nil
}
//web
func (c *UserHandler) Login(context *gin.Context) {
type LoginReq struct {
Email string `json:"email"`
Password string `json:"password"`
}
var req LoginReq
if err := context.Bind(&req); err != nil {
context.String(http.StatusOK, "系统错误")
return
}
err := c.svc.Login(context, req.Email, req.Password)
if err != nil {
context.String(http.StatusOK, "账户或密码错误")
return
}
context.String(http.StatusOK, "登录成功")
return
}
Cookie 和 Session
在登录成功后,当访问/users/profile时,面临如何确定用户是否已登录的问题。
HTTP 协议是无状态的,即连续发送两次请求时,HTTP 无法知道这两个请求都是同一个用户发出的,无法将上一次请求和这一次请求关联起来。
因此需要一种机制来记录状态,于是就有了 Cookie 和 Session。
Cookie 是浏览器存储在本地的一些数据,简单理解为存储在电脑上的键值对。由于 Cookie 存放在浏览器本地,所以很不安全。
在使用 Cookie 时要注意安全配置:
- Domain:设定 Cookie 可用的域名,按照最小化原则设置。
- Path:设定 Cookie 可用的路径,同样按照最小化原则设置。
- Max-Age 和 Expires:设置 Cookie 的过期时间,只保留必要时间。
- Http-Only:设置为 true 时,浏览器上的 JS 代码无法使用该 Cookie,应永远设置为 true。
- Secure:仅用于 HTTPS 协议,在生产环境永远设置为 true。
- SameSite:决定是否允许跨站发送 Cookie,尽量避免。
在面试初级工程师岗位时,能详细解释这些参数含义会赢得微小竞争优势。
由于 Cookie 具有不安全的特性,所以大部分时候只在 Cookie 中放一些不太关键的数据。关键数据希望放在后端存储,这个存储的东西叫做 Session。在登录场景中,可以通过 Session 来记录登录状态。
在使用 Session 进行登录时,关键在于服务器要给浏览器一个 sess_id(即 Session 的 ID)。后续每次请求都带上这个 Session ID,服务端就能通过这个 ID 知道发出请求的用户是谁。
Session 机制中后端服务器认 ID 不认人。这意味着如果攻击者拿到了用户的 Session ID,那么服务器就会把攻击者当成该用户。例如在图中,攻击者窃取到了 sess_id 后,就可以冒充用户。
由于 sess_id 是标识用户身份的东西,需要在每一次访问系统时携带。有以下几种方式:
- 最佳方式:使用 Cookie,将 sess_id 放到 Cookie 里面。因为 sess_id 自身没有任何敏感信息,所以放在 Cookie 中是可行的。
- 另一种方式:放在 Header 中,例如在 Header 里面带一个 sess_id,但这需要前端研发人员记得在 Header 中带上。
- 还可以:放在查询参数中,即 ?sess_id=xxx。
- 理论上也可以放在请求体(body)中,但基本没人这么做。
- 在一些禁用了 Cookie 功能的浏览器上,只能考虑后两种方式(放在 Header 中或查询参数中)。
在浏览器上,可以通过插件 cookieEditor 查看某个网站的 Cookie 信息。
可以使用 Gin 的 Session 插件来实现登录功能。一般遇到问题可以找插件,热门功能 Gin 通常有插件可用。这里提到的 Gin 的 Session 插件使用分为两部分:
- 在 middleware(中间件)里面接入,它会从 Cookie 中找到 sess_id,再根据 sess_id 找到对应的 Session。
- 拿到 Session 之后可以进行各种操作,例如在这里用来校验是否登录。其插件地址为 https://github.com/gin-contrib/sessions。
func initWebServer() *gin.Engine {
engine := gin.Default()
engine.Use(cors.New(cors.Config{
//AllowOrigins: []string{"http://localhost:3000"},
AllowMethods: []string{"PUT", "PATCH", "GET", "POST", "OPTIONS"},
AllowHeaders: []string{"Content-Type", "authorization"},
//ExposeHeaders: []string{"x-jwt-token"},
AllowCredentials: true, //是否允许你带cookie之类的
AllowOriginFunc: func(origin string) bool {
if strings.HasPrefix(origin, "http://localhost") {
return true // 开发环境
}
return strings.Contains(origin, "your.company.com")
},
MaxAge: 12 * time.Hour,
}))
store := cookie.NewStore([]byte("secret"))
// cookie 的名字叫ssid
engine.Use(sessions.Sessions("ssid", store))
engine.Use(middleware.NewLoginMiddlewareBuilder().Build())
return engine
}
// middleware.go
type LoginMiddlewareBuilder struct {
}
func NewLoginMiddlewareBuilder() *LoginMiddlewareBuilder {
return &LoginMiddlewareBuilder{}
}
func (l *LoginMiddlewareBuilder) Build() gin.HandlerFunc {
return func(ctx *gin.Context) {
if ctx.Request.URL.Path == "/users/login" || ctx.Request.URL.Path == "/users/signup" {
return // 登录和注册不需要校验
}
sess := sessions.Default(ctx)
if sess.Get("userId") == nil {
// 没有登录
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
}
}
// web
func (c *UserHandler) Login(context *gin.Context) {
type LoginReq struct {
Email string `json:"email"`
Password string `json:"password"`
}
var req LoginReq
if err := context.Bind(&req); err != nil {
context.String(http.StatusOK, "系统错误")
return
}
user, err := c.svc.Login(context, req.Email, req.Password)
if err != nil {
context.String(http.StatusOK, "账户或密码错误")
return
}
sess := sessions.Default(context)
sess.Set("userId", user.Id)
sess.Save()
context.String(http.StatusOK, "登录成功")
return
}
面试题
一、什么是 Cookie,什么是 Session?
- Cookie:
- Cookie 是浏览器存储在本地的一些数据,简单理解为存储在电脑上的键值对。例如,一些网站会将用户的偏好设置、登录状态等信息存储在 Cookie 中。
- Cookie 可以在不同的请求中被发送到服务器,服务器可以根据 Cookie 中的信息识别用户。但是,由于 Cookie 存储在客户端,所以安全性较低,容易被篡改或窃取。
- 在使用 Cookie 时,要注意安全配置,如按照最小化原则设置 Domain 和 Path、合理设置 Max-Age 和 Expires、将 Http-Only 设置为 true(防止浏览器上的 JS 代码使用 Cookie)、在生产环境将 Secure 设置为 true(仅用于 HTTPS 协议)、尽量避免使用 SameSite 允许跨站发送 Cookie。
- Session:
- 由于 Cookie 本身不安全的特性,大部分时候我们只在 Cookie 里面放一些不太关键的数据。关键数据我们希望放在后端,这个存储关键数据的东西就叫做 Session。
- 在登录场景中,可以通过 Session 来记录登录状态。服务器给浏览器一个 sess_id(Session 的 ID),后续每一次请求都带上这个 Session ID,服务端就能通过这个 ID 识别用户身份。
二、Cookie 和 Session 比起来有什么缺点?
- 安全性低:Cookie 存储在客户端,容易被篡改或窃取。而 Session 数据存储在服务器端,相对安全。
- 存储容量有限:Cookie 的存储容量通常较小,一般为 4KB 左右。而 Session 可以存储更多的数据。
- 性能影响:每次请求都需要发送 Cookie 数据,会增加网络传输量,对性能有一定影响。而 Session ID 通常较小,对网络传输的影响较小。
三、Session ID 可以放在哪里?这个问题,你要记得提起 Cookie 禁用的问题。
Session ID 可以放在以下几个地方:
- Cookie 中:这是最常见的方式。将 Session ID 放在 Cookie 中,每次请求时浏览器会自动发送 Cookie,服务器可以根据 Session ID 找到对应的 Session。但是,如果用户禁用了 Cookie,这种方式就不可行了。
- Header 中:可以在请求的 Header 中添加一个自定义的字段,用来携带 Session ID。例如,可以在 Header 里面带一个 sess_id。这需要前端的研发人员记得在每次请求时在 Header 中带上 Session ID。
- 查询参数中:可以将 Session ID 放在查询参数中,即 ?sess_id=xxx。但是这种方式不太安全,容易被用户看到和篡改。
- 理论上也可以放在请求体(body)中,但基本没人这么做。
在一些禁用了 Cookie 功能的浏览器上,可以考虑使用 Header 或查询参数的方式来携带 Session ID。
四、用户密码加密算法选取有什么注意事项?你用的是什么?
用户密码加密算法选取的注意事项:
- 难破解:选择加密算法的标准是难破解。要考虑相同的密码加密后的结果应该不同,防止很多用户使用简单密码(如 123456)时数据库存储值相同。同时,难以通过碰撞、彩虹表来破解。
- 安全性高:常见加密算法安全性逐步提高的有 md5 之类的哈希算法,但 md5 相对不安全;在哈希算法基础上引入盐值或进行多次哈希可以提高安全性;PBKDF2、BCrypt 这一类随机盐值的加密算法安全性更高,同样的文本加密后的结果都不同。
这里可以选择使用 BCrypt 加密算法,它有以下优点:
- 不需要自己生成盐值。
- 不需要额外存储盐值。
- 可以通过控制 cost 来控制加密性能。
- 同样的文本,加密后的结果不同。
如果要使用 BCrypt,需要使用 golang.org/x/crypto。由于 BCrypt 限制密码长度不能超过 72 字节,所以在校验时要校验这个长度,只需要修改一下正则表达式即可。
五、怎么做登录校验?核心是利用 Gin 的 middleware。
登录校验可以通过以下步骤实现:
登录接口实现:
- 在登录请求被发到 /users/login 上时,service 会调用 repository 查找邮箱所对应的用户。
- 然后 service 会匹配输入的密码和数据库中保存的是否一致。如果用户没找到或者密码错误,都返回同一个 error。
- 利用 Gin 的 middleware 进行登录校验:
Gin 的 Session 插件用起来分成两部分:
- 一个是在 middleware(中间件)里面接入,它会帮你从 Cookie 里面找到 sess_id,再根据 sess_id 找到对应的 Session。
- 另外一部分就是你拿到这个 Session 之后,就可以进行登录校验等操作。例如,可以在 Session 中存储用户的登录状态信息,当用户访问需要登录才能访问的资源(如 /users/profile)时,通过 middleware 检查 Session 中的登录状态信息,如果用户已登录,则允许访问,否则返回错误信息。
相关推荐
- Origin将普通双Y轴柱状图升级为高级的3D倒影柱状图
-
双Y轴柱状图可以在同一张图表中展示两组具有不同量纲或数量级的数据。3D倒影柱状图是一种通过三维立体及倒影形式展示多个数据的图表,它通过柱子的高度和颜色来表示不同的数据类别和数值大小,以及通过不同平面展...
- Origin 2021 安装教程(附安装包下载)
-
Origin2021软件介绍广泛用于科研、工程和数据分析领域。它支持多种数据格式,包括Excel、ASCII、CSV等,可以方便地导入和分析各种类型的数据。Origin提供了丰富的数据分析工具,包括...
- VIVO origin os系统关闭广告教程(IQOO同理)
-
以下是我使用期间发现的,如有其他未发现的,欢迎大家评论补充。1设置-安全-更多安全设置-广告与隐私-个性化广告推荐关闭2设置-安全-更多安全设置-应用安装-应用推荐关闭3设置-系统管理-全局搜索-首页...
- Origin 2019 安装教程(附安装包下载)
-
Origin2019软件介绍广泛用于科研、工程和数据分析领域。它支持多种数据格式,包括Excel、ASCII、CSV等,可以方便地导入和分析各种类型的数据。Origin提供了丰富的数据分析工具,包括...
- vivo开启Origin OS3系统公测,并且公布首批至第四批可升级名单
-
vivo在本月初发布了OriginOS3.0操作系统,11月25日开启了首批机型的公测招募活动,首批公测机型包括vivo和iQOO,共计14款机型。首批公测招募的机型有:vivoxfold+、v...
- Origin将普通热图升级为高级的3D堆叠热图
-
3D堆叠热图是一种将多个热图组合在一起展示的图表可视化形式。每个热图代表一个数据集,通过比较不同数据集在不同条件(分组)下的颜色深浅变化来挖掘数据的大小差异或者相关性,能够直观地比较多组数据的相对强度...
- 最新最详细的Origin2021安装教程(保姆级)
-
直入正题,下面是Origin2021的安装过程安装包(复制到浏览器打开):%74%2E%63%6E/%41%36%4E%67%6D%54%74%4A安装步骤1、下载得到安装包后,请先解压!一定要先解压...
- vivo和iQOO抓紧更新 OriginOS5末班车 12月最后一波升级
-
进入12月底了,很多老旧机型也已经开始更新各家的新系统了,所以大家一定要注意查看系统版本更新。那么就拿今年国内销量第一的vivo和iQOO来说,OriginOS5都已经推出两个月了,根据之前公布的适...
- Origin将普通点线图转换为高级的3D堆叠点线图
-
点线图用于显示数据随时间或其他连续变量的变化趋势;点线图主要功能是展示数据随时间或连续变量的变化趋势,强调变化幅度,比较多个变量,描述周期性变化,识别异常值,并帮助分析趋势和预测未来趋势。当具有多组数...
- 蓝厂Origin OS 2.0首批适配机型曝光!有没有你的手机型号?
-
2020年11月18日蓝厂新系统OriginOS正式发布,并由后来的X60系列首发,新系统发布后网上就有言论:蓝厂补足了最后一块短板!发布至今大概已经有11月的时间了,网上也是褒贬不一,Origin...
- Origin2018软件安装包以及安装教程
-
安装步骤:1.鼠标右击软件压缩包,选择“解压到Origin2018”2.打开“Origin2018\Origin2018”文件夹,鼠标右击“setup”,选择“以管理员身份运行”3.软件正在准备...
- Origin这样操作,轻松绘制漂亮的雷达图
-
背景介绍雷达图是以从同一点开始的轴上表示的三个或更多个定量变量的二维图表的形式显示多变量数据的图形方法。轴的相对位置和角度通常没有信息。雷达图也称为网络图、蜘蛛图、星图、星图、蜘蛛网、不规则多边形...
- 曝OriginOS新版本在“玩命打磨底层”流畅性比肩鸿蒙
-
日前,vivo正式官宣了2022年vivo开发者大会的举办时间:11月8日-11月9日。OriginOS全新版本届时将正式登场。时隔一年,这次全新OriginOS又将给我们带来什么样的全新体验? ...
- Dify工具使用全场景:1.0.1发布了,升级(功能篇·第8期)
-
上一篇:Dify工具使用全场景:API扩展(功能篇·第7期)一、Dify1.0.1版本升级特性Dify1.0.1版本在稳定性、性能和用户体验方面进行了全面升级,修复了之前版本中的一些已知问题,带来...
- vivo 新系统originOS 升级公测后增加手机内存
-
vivo新系统originOS,通过内存融合、进程优化、应用预载三种技术,一方面调用部分闲置的闪存空间,另一方面低系统对内存的占用,从而可以让原硬件内存增加2-3GB内存使用。vivo新系统ori...