首页IT科技商业项目战略合作协议书范本(结合商业项目深入理解Go知识点)

商业项目战略合作协议书范本(结合商业项目深入理解Go知识点)

时间2025-07-19 21:14:04分类IT科技浏览4357
导读:这篇文章比较硬核,爆肝5千字,把之前整理的知识点都串起来了。建议先收藏,慢慢看。...

这篇文章比较硬核            ,爆肝5千字                   ,把之前整理的知识点都串起来了            。建议先收藏      ,慢慢看                   。

前言

上一篇文章 #【Go WEB进阶实战】开源的电商前后台API系统 很受大家欢迎      ,有好多小伙伴私信我问题:“gtoken真不错                   ,能不能再多讲讲?             ”             、“接口怎么设计Cache好?                  ”                  、“代码规范讲一下吧      ”      、“这个系统有没有前端页面?             ”             ,等等....

那我就再写一篇作为补充喽      ,小伙伴们还有什么问题欢迎在评论区留言      。

之前整理过一篇可能是全网最用心的「Go学习建议和资料汇总」                  ,也深受大家好评             ,大家可以先收藏慢慢学      。

这篇文章更进一步,会结合电商前后台API系统                  ,把Go语言的知识点应用到商业项目中                   ,让大家结合实际的场景去理解,这样应该对大家更有帮助!

小提示:这篇文章的重点不是把各个知识点讲透            ,而是为了让大家理解各个知识点在商业项目中的应用                   。如果你的基础比较薄弱                   ,每个知识点的最后也都附上了详解链接      ,方便大家去查漏补缺             。

下面就开始和我进阶实战吧:

登录鉴权

我们在上一篇文章中有介绍            ,系统的登录鉴权是通过gtoken实现的                   ,有的小伙伴没有搞清楚登录信息存储在哪里?我们是如何获得当前登录用户的信息?

首先gtoken的数据默认使用内存缓存gcache      ,这种缓存会随着服务的终止而销毁      ,当重启服务时                   ,之前缓存的数据就丢失了;gtoken也支持使用redis             ,比如我们的项目中就是使用了gredis      ,将登录信息存储在redis中进行管理      。

更多关于gtoken的知识点可以看这篇专题文章:# 通过阅读源码解决项目难题:GToken替换JWT实现SSO单点登录

如果你基础比较弱的话                  ,我还录制了视频教程:# 【视频】登录鉴权的三种方式:token             、jwt                   、session实战分享

下面聊聊如何获得登录用户信息的问题:

我们使用Go语言无论开发http项目还是rpc项目             ,上下文都是很重要的概念,用于共享变量和链路跟踪                  。

我们通过Context上下文对象在一次请求中设置用户信息                  ,共享变量                   ,进而实现在后续链路中都能获得当前登录用户的信息:

Context上下文

以修改密码举例:

我们通过ghttp.Request的实例r,调用GetCtxVar() 方法             。

比如:r.GetCtxVar(middleware.CtxAccountId)            ,通过这种方式我们就可以获得登录用户信息了

小提示:为了行文清晰                   ,让大家更直观的看到和知识点相关的代码      ,不重要的代码会用三个竖着的.省略。完整的代码可以fork文末的GitHub            ,已把这个项目开源                  。

调用示例代码

func (s *rotationService) UpdateMyPassword(r *ghttp.Request, req *UpdateMyPasswordReq) (res sql.Result, err error) { . . . //获得当前登录用户 req.Id = gconv.Int(r.GetCtxVar(middleware.CtxAccountId)) ctx := r.GetCtx() res, err = dao.AdminInfo.Ctx(ctx).WherePri(req.Id).Update(req) if err != nil { return nil, err } return }

赋值示例代码

赋值的核心代码也很简单                   ,就是通过 r.SetCtxVar(key, value) 方法      ,就能把变量赋值到context中了

package middleware import ( "github.com/goflyfox/gtoken/gtoken" "github.com/gogf/gf/net/ghttp" "github.com/gogf/gf/util/gconv" "malu/library/response" ) const ( CtxAccountId = "account_id" //token获取 . . . ) type TokenInfo struct { Id int . . . } var GToken *gtoken.GfToken var MiddlewareGToken = tokenMiddleware{} type tokenMiddleware struct{} func (s *tokenMiddleware) GetToken(r *ghttp.Request) { var tokenInfo TokenInfo token := GToken.GetTokenData(r) err := gconv.Struct(token.GetString("data"), &tokenInfo) if err != nil { response.Auth(r) return } r.SetCtxVar(CtxAccountId, tokenInfo.Id) . . . r.Middleware.Next() }

小技巧

在架构设计中      ,在哪个场景下设置Context是非常重要的:上下文的变量必须在请求一开始便注入到请求流程中                   ,以便于其他方法调用             ,所以我们在中间件中来实现是比较优雅的选择                   。 结合实际场景      ,我们设置到Context中的变量可以是指针类型                  ,因为任何地方获取到这个指针             ,不仅可以获取到里面的数据,而且能够直接修改里面的数据。 建议养成好习惯:在service层的方法中                  ,第一个参数必传context.Context对象或者*ghttp.Request对象            。这样有利于我们后续扩展                   ,能够方便的通过context共享数据,而且还能进行链路追踪

更详细的介绍看这里:# GoFrame 如何优雅的共享变量 | Context的使用

接口缓存

关于接口缓存            ,有小伙伴提出这样的疑问?

当然要设计接口数据缓存了                   ,而且在GoFrame中还有比较优雅的实践方式:链式操作设置缓存                   。

我们给查询接口添加缓存的思路是这样的:

常规操作

定义缓存key 根据缓存key查询是否有值 有值返回缓存中的值      ,不查询DB 无值            ,查询DB                   ,写入缓存 返回数据 func (s *rotationService) Detail(r *ghttp.Request, req *DetailReq) (res model.ArticleInfo, err error) { cacheKey := ArticleDetailCacheKey + gconv.String(req.Id) res := Cache::get(cacheKey) if(!res){ err = dao.ArticleInfo.Ctx(r.GetCtx()).WherePri(req.Id).Scan(&res) if err != nil { return res, err } Cache::set(cacheKey,res,time.Hour) } return }

GoFrame为我们提供了非常优雅的链接操作:

链式操作

我们只需要在链式查询中使用Cache()方法      ,设置缓存时间和缓存key就可以了      ,GoFrame为我们实现了上述常规操作中的繁琐操作:

链式操作:取值

func (s *rotationService) Detail(r *ghttp.Request, req *DetailReq) (res model.ArticleInfo, err error) { //查询时优先查询缓存 cacheKey := ArticleDetailCacheKey + gconv.String(req.Id) err = dao.ArticleInfo.Ctx(r.GetCtx()).Cache(time.Hour, cacheKey).WherePri(req.Id).Scan(&res) if err != nil { return res, err } return }

链式操作:更新值

更新操作只需要将Cache()方法的第一个参数过期时间设置为负数                   ,就会清空缓存

func (s *rotationService) Update(r *ghttp.Request, req *UpdateArticleReq) (res sql.Result, err error) { ctx := r.GetCtx() . . . //更新缓存 cacheKey := ArticleDetailCacheKey + gconv.String(req.Id) res, err = dao.ArticleInfo.Ctx(ctx).Cache(-1, cacheKey).WherePri(req.Id).Update(req) if err != nil { return nil, err } return }

除了这个典型的场景             ,我们项目的热门商品是通过LRU缓存淘汰策略实现的      ,小伙伴们可以看这篇详解一探究竟:# GoFrame gcache使用实践 | 缓存控制 淘汰策略

接口兼容处理

需求场景

我们电商系统的文章和商品都支持收藏和取消收藏

取消收藏有2种情况:一种是根据收藏id

删除;另一种是根据收藏类型和文章id(或者商品id)删除

思考题

我们根据上述的需求是设计两个接口分别实现呢?还是只设计一个接口兼容实现呢?

我倾向于只使用一种接口                  ,兼容实现:这样不仅减少代码量             ,而且后期有逻辑调整时,只修改一处代码就可以了      。

看下我们是如何实现的:

结构体

首先定义我们的请求结构体                  ,允许通过收藏id删除;

或者根据类型和对象id删除(收藏类型:1商品 2文章) type DeleteReq struct { Id int `json:"id"` Type int `json:"type"` ObjectId int `json:"object_id"` }

api层

然后我们编写api层                   ,这部分代码很简单,所有的api层代码都是这种规范

定义请求参数结构体 解析请求参数            ,做数据校验                   ,有问题直接返回错误;正常则继续向下执行 调用service层对应的方法      ,传入上下文context和请求体 根据service层的返回结果决定是返回错误码            ,还是返回数据            。

小技巧:所有的api层都是这样的思路                   ,我们的逻辑处理一般写在service中

func (*collectionApi) Delete(r *ghttp.Request) { var req *DeleteReq if err := r.Parse(&req); err != nil { response.ParamErr(r, err) } if res, err := service.Delete(r.Context(), req); err != nil { response.Code(r, err) } else { response.SuccessWithData(r, res) } }

service层

最后我们编写service层代码      ,实现取消收藏接口兼容的重点也在这里了

我们根据传入的id做判断      ,如果id不为0                   ,根据收藏id删除;否则的话就根据传入的type类型区别是文章还是商品             ,根据ObjectId确定要删除对象的id                   。

func (s *collectionService) Delete(ctx context.Context, req *DeleteReq) (res sql.Result, err error) { if req.Id != 0 { //根据收藏id删除 res, err = dao.CollectionInfo.Ctx(ctx).WherePri(req.Id).Delete() } else { //根据类型和对象id删除 res, err = dao.CollectionInfo.Ctx(ctx). Where(dao.CollectionInfo.Columns.Type, req.Type). Where(dao.CollectionInfo.Columns.ObjectId, req.ObjectId). Delete() } if err != nil { return nil, err } return }

小技巧:我们查询条件的字段都是通过这种方式取值的:dao.CollectionInfo.Columns.Type      ,而不会写死字符串type                  ,原因是如果我们的字段有修改             ,前者这种写法可以一改全改;而后者写死字符串的方式很难找全要修改的地方,维护成本比较高      。

统计查询

咱们想一个复杂点的场景                  ,进阶实战一下GoFrame ORM的使用:

我们需要查询最近7天每天的订单量                   ,如果当天没有订单就返回0      。期望的数据结构是这样的:

"order_total": [10, 0, 10, 20, 10, 0, 7],

我们如何实现呢?

service层

重点看这段查询语句

err := dao.OrderInfo.Ctx(ctx).Where(dao.OrderInfo.Columns.CreatedAt+" >= ", shared.GetBefore7Date()).Fields("count(id) total,date_format(created_at, %Y-%m-%d) today").Group("today").Scan(&TodayTotals)

在GoFrame中 where的第二个参数如果传数组,默认就是where in查询;

我们在Fields()方法中除了可以指定查询字段            ,还可以使用查询函数                   ,也可以指定别名:

func OrderTotal(ctx context.Context) (counts []int) { counts = []int{0, 0, 0, 0, 0, 0, 0} recent7Dates := shared.GetRecent7Date() TodayTotals := []TodayTotal{} //只取最近7天 err := dao.OrderInfo.Ctx(ctx).Where(dao.OrderInfo.Columns.CreatedAt+" >= ", shared.GetBefore7Date()).Fields("count(*) total,date_format(created_at, %Y-%m-%d) today").Group("today").Scan(&TodayTotals) fmt.Printf("result:%v", TodayTotals) for i, date := range recent7Dates { for _, todayTotal := range TodayTotals { if date == todayTotal.Today { counts[i] = todayTotal.Total } } } if err != nil { return counts } return }

工具类

受某位知乎大神的启发      ,生成最近一周的日期我是这么实现的:

从性能角度考虑可能不是最优写法            ,但是理解成本肯定非常低:

//生成最近一周的日期 func GetRecent7Date() (dates []string) { gt := gtime.New(time.Now()) dates = []string{ gt.Format("Y-m-d"), //今天 gt.Add(-gtime.D * 1).Format("Y-m-d"), //1天前 gt.Add(-gtime.D * 2).Format("Y-m-d"), gt.Add(-gtime.D * 3).Format("Y-m-d"), gt.Add(-gtime.D * 4).Format("Y-m-d"), gt.Add(-gtime.D * 5).Format("Y-m-d"), gt.Add(-gtime.D * 6).Format("Y-m-d"), //6天前 } return }

事务处理

事务的应用场景很清晰:当我们提供的某个服务                   ,需要操作多次DB      ,并且这些操作要具有原子性      ,要么都成功                   ,要么都失败                   。这种情况就需要事务处理             。

事务处理的特点是:只要其中有一个环节失败了             ,之前成功的DB操作也会回滚到之前的状态      。

事务处理实战

比如我们创建订单时就需要做事务处理      ,我们一个订单可以添加多个商品                  ,创建订单时除了添加主订单表             ,也会添加商品订单表                  。

GoFrame的事务处理非常简单:

只需要我们通过g.DB().Begin()开启事务 在链式操作中通过.TX(tx)方法添加事务 在最后判断是否有错误发生,有错误则通过Rollback()回滚事务                  ,没错误则通过Commit()方法提交事务             。 func (s *orderService) Add(r *ghttp.Request, req *AddOrderReq) (res sql.Result, err error) { req.OrderInfo.UserId = gconv.Int(r.GetCtxVar(middleware.CtxAccountId)) req.OrderInfo.Number = shared.GetOrderNum() tx, err := g.DB().Begin() if err != nil { return nil, errors.New("启动事务失败") } //defer方法最后执行 如果有报错则回滚 如果没有报错                   ,则提交事务 defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }() //生成主订单 lastInsertId, err := dao.OrderInfo.Ctx(r.GetCtx()).TX(tx).InsertAndGetId(req.OrderInfo) if err != nil { return nil, err } //生成商品订单 for _, info := range req.OrderGoodsInfos { info.OrderId = gconv.Int(lastInsertId) _, err := dao.OrderGoodsInfo.Ctx(r.GetCtx()).TX(tx).Insert(info) if err != nil { return nil, err } } return }

更多关于事务的知识点可以阅读这篇文章:

# Go语言中比较优雅的写法

灵活应用

需求

我们需要根据多个查询条件去查询商品,比如根据商品名称和商品分类去查询。

需求分析

我们来分析一下            ,客户端会有几种查询场景?

根据商品名称和商品分类2个条件查询 只根据商品名称查询 只根据商品分类查询 都没有传值                   ,不命中查询条件      ,返回全部商品

常规实现

做了需求分析之后            ,正常的思路就是写if...else...判断了:

whereCondition := gmap.New() if req.Keyword != "" && req.CategoryId != 0 { whereCondition = g.Map{ "name like": "%" + req.Keyword + "%", "level1_category_id =? OR level2_category_id =? OR level3_category_id =? ": g.Slice{req.CategoryId, req.CategoryId, req.CategoryId}, } } else if req.Keyword != "" { whereCondition = g.Map{ "name like": "%" + req.Keyword + "%", } } else if req.CategoryId != 0 { whereCondition = g.Map{ "level1_category_id =? OR level2_category_id =? OR level3_category_id =? ": g.Slice{req.CategoryId, req.CategoryId, req.CategoryId}, } } else { whereCondition = g.Map{} }

但是这种写法太乱了                   ,而且不容易扩展:如果我们再加一个查询条件      ,不仅要新增一个else      ,就要改已经存在的if...else判断                   ,后面维护起来简直是噩梦啊                  。

优化之后

在经过思考之后             ,使用map的set方法灵活赋值是个很好的选择      ,优化后的代码如下:

whereCondition := gmap.New() if req.Keyword != "" { whereCondition.Set(dao.GoodsInfo.Columns.Name+" like ", "%"+req.Keyword+"%") } if req.CategoryId != 0 { whereCondition.Set("level1_category_id =? OR level2_category_id =? OR level3_category_id =? ", g.Slice{req.CategoryId, req.CategoryId, req.CategoryId}) }

优化后的代码异常清晰                  ,如果我们再加新的查询条件             ,只需要在代码中再加一个if判断就可以了                   。

我的感悟

我在学习map基础用法的时候,并不能想到这种应用场景                  ,这是很正常的。只有当真正开发商业项目                   ,在具体需求的驱动之下,督促我们做优化            ,这时候会刺激我们回顾之前学到的知识点            。结合实际需求帮助大家将之前学到的Go知识灵活运用                   ,这是我开源这个项目的目的      ,也是我写这篇文章的目的                   。

了解更多set知识

# GoFrame gset使用技巧总结 | 天然支持排序和有序遍历      、出栈      、子集判断                   、序列化            、遍历修改

好了            ,扩展的知识点就聊到这里                   ,下面是对你学Go有帮助的学习资料      ,欢迎和我一起学习Go      ,实践Go      。

GitHub

本项目的GitHub地址                   ,欢迎star      、fork                   、复刻:

电商实战项目V1版本

电商实战项目V2版本

抱团取暖

公众号:程序员升职加薪之旅

微信:wangzhongyang1993

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
调频法测距(调频连续波(FMCW)原理)