小红娘

185 阅读7分钟

项目介绍

该小程序设计初衷是建立一个考研社交平台,以社交形式为考研学子寻找到目标院校合适的学长学姐或是志同道合的学习伙伴,设计并实现了用户、学院、消息、钱包等模块,内置积分设置、会员、红钱、支付等功能。本项目遵循 RESTful API 设计规范,基于 GIN WEB API 框架,提供了丰富的中间件支持(用户认证、访问日志、追踪ID等),基于Casbin的 RBAC 访问控制模型,通过JWT 认证实现跨域登录校验,支持 Swagger 文档(基于swag-go),基于 GORM 的数据库存储,可扩展多种类型数据库。
特性
遵循 RESTful API 设计规范 基于 GIN WEB API 框架,提供了丰富的中间件支持(用户认证、访问日志、追踪ID等) 基于Casbin的 RBAC 访问控制模型 JWT 认证 跨域 支持 Swagger 文档(基于swag-go) 基于 GORM 的数据库存储,可扩展多种类型数据库 配置文件简单的模型映射,快速能够得到想要的配置
内置
用户管理 学院管理 用户反馈 登录模块 积分、会员、红钱功能 消息管理 用户收藏 钱包管理 用户支付

封装http

type Http struct {
    Request *Request
    Response *Response
}

func BindHttpContext(Ctx *gin.Context)  *Http{
    var http Http
    http.Request = &Request{Ctx:Ctx}
    http.Response = &Response{Ctx:Ctx}
    return &http
}
type Response struct { 
    Ctx    *gin.Context
    Data   interface{}
    Status int
    Error  error
}

type Message struct {
    Success bool        `json:"success"`
    Errors  string      `json:"errors"`
    Result  interface{} `json:"result"`
}

func (r *Response) Success() {

if r.Status == 0 {
	r.Status = http.StatusOK
}

if r.Error != nil {
	r.Ctx.JSON(r.Status, Message{Success: false, Errors: r.Error.Error(), Result: r.Data})
} else {
	r.Ctx.JSON(r.Status, Message{Success: true, Errors: "", Result: r.Data})
}
} 

报错:Headers were already written Wanted to override status code 400 with 200 Gin框架编写PUT接口使用BindJSON绑定参数报错,当结构体绑定参数有误时, 状态码为400,强制返回200,结果code还是400. 原因 : BindXXX方法都return c.MustBindWith(obj, binding.XXX)。如果绑定错误,则c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)。这将响应状态码设置为400,并将Content-Type标题设置为text/plain; charset=utf-8。如果强制修改成200,就会警告[WARNING] Headers were already written. Wanted to override status code 400 with 200。

解决方法 : 绑定结构体的方法把BindJSON改为ShouldBind。

打卡

在打卡功能部分,需要显示的是周一到周日打卡的哪几天,以weekArr数组来记录打卡的天数,GetUserPunchOfWeek(uid int) [ ]interface{} 返回参数punchArr。以offset = time.monday - now.weekday()来作为偏移量。由于Sunday 定为0,当offset>0的时候,说明是周日,则设置offset为-6,本周开始日期通过time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local).AddData(0,0,offset)获取。再以Format("2006-01-02")转换日期格式。通过time.Parse 把时间字符串转换为Time,时区是UTC时区,以mondayDate字段存储。 time.Weekday类型可以做运算,强制转int,会得到偏差数。 默认是 Sunday 开始到 Saturday 算 0,1,2,3,4,5,6

	 // A Weekday specifies a day of the week (Sunday = 0, ...)
	type Weekday int
	const (
	Sunday Weekday = iota
	Monday
	Tuesday
	Wednesday
	Thursday
	Friday
	Saturday
    ) 

所以只有Monday减去Sunday的时候是正数,特殊处理下就可以了。

for i := 0; i < 7;  i++{
	//Unix转化mondayDate再加上到某天的秒数
	dateUnix := mondayDate.Unix() + int64(i * 24 * 3600 )
	//秒数转化为"2006-01-02"格式
	date := time.Unix(dateUnix,0).Format("2006-01-02")
	record := model.FindUserPunchedRecord(uid,date)
	var data map[string]interface{}
	data = map[string]interface{}{
		"date":date,
		"week":i+1,
	}
	if record.ID != 0{
		data["credit"] = record.Credit
		data["punched"] = true//record存在,则已打卡
	}else {
		punchConfig := model.FindPunchConfig()
		data["credit"] = punchConfig.Credit
		data["punched"] = false
	}
	punchArr = append(punchArr,data)
}
  1. Date.AddData() : 返回将给定的年、月、日添加到t的时间。例如,应用于2011年1月1日的AddDate(- 1,2,3)返回2010年3月4日。

  2. time包中Parse和Format:Format表示将时间转化为字符串,parse表示将字符串转化为时间

  3. time.Unix(sed, nsed):Unix()返回一个Time类型, 然后这个时间是UTC时间加上参数时间

连续打卡天数统计

	if membership.LastDate == "" {
		membership.Duration = 1
		membership.LastDate = today
		membership.Update()
	}else {
	lastDate,_ := time.Parse("2006-01-02",membership.LastDate)
	todayDate,_ := time.Parse("2006-01-02",today)
	if (todayDate.Unix() - lastDate.Unix())/3600 < 24 {
		membership.Duration += 1
		if membership.Duration >= 7{//连续打卡天数大于等于七天,积分增加
			membership.Credit += record.Credit
		}
	}else {
		membership.Duration = 1
	}
	membership.LastDate = today
	membership.Update()
	}

不定条件查询

在gorm可以分别任选上述两种中的任意一种:

1.orm操作; 2.拼接原生sql语句;

采用orm操作

func FindUserByMultiConditions(isNewest bool, kyYear string, kyCollege string, kyMajor string, kyTutor int, kyTiaoJi int, name string) []*User {
users := make([]*User, 0)
db := database.DB.Model(&User{})

if name != "" {
	name = "%" + name + "%"
	db = db.Where("nick_name like ?", name)
}
if kyYear != "" {
	db = db.Where("ky_year = ?", kyYear)
}
if kyCollege != "" {
	db = db.Where("ky_college = ?", kyCollege)
}
if kyMajor != "" {
	db = db.Where("ky_major = ?", kyMajor)
}

if kyTutor == 0 {
	db = db.Where("ky_tutor = 0 or ky_tutor = 1")
} else if kyTutor == 1 {
	db = db.Where("ky_tutor = 0")
} else {
	db = db.Where("ky_tutor = 1")
}
if kyTiaoJi == 0 {
	db = db.Where("ky_tiao_ji = 0 or ky_tiao_ji = 1")
} else if kyTiaoJi == 1 {
	db = db.Where("ky_tiao_ji = 0")
} else {
	db = db.Where("ky_tiao_ji = 1")
}

db = db.Where("status = ?", status.UserOnline)
if isNewest {
	db = db.Order("id desc")
}
db.Find(&users)
return users
}

这里需要注意一个细节,首先将全局的db变量赋值给了Db,如果用db直接进行操作,那一系列的赋值语句将会影响db的地址,影响后续的数据库操作.

go中如果直接使用db.update()无法更改字段为空
需要将字段定义为指针类型或者对字段单独进行判断赋值。

跨域问题

常见跨域场景 当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。常见跨域场景如下图所示:

image.png

通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。

微信小程序码

mpQRCodeURL 获取二维码 MPGETAccessTokenURL = "api.weixin.qq.com/cgi-bin/tok…"

mpQRCodeURL 获取二维码 mpQRCodeURL = "api.weixin.qq.com/wxa/getwxac…"

1、获取小程序授权的accesstoken
拼接url := fmt.Sprintf(MPGETAccessTokenURL, setting.WeixinSetting.AppId, setting.WeixinSetting.SecretKey)

resp, err := net.GetIgnoreHttpsClient().Get(url)

请求的时候需要忽略证书,因为 wx.request 发起的是 https 请求,微信小程序读取大部分是请求API接口,必须要有https的协议才可以使用,否则会提示域名不合法。这也就是说微信小程序是强制使用SSL证书的,否则无法运行。在开发环境测试时由于在微信开发者设置跳过证书,安全等认证忽略了这个问题,到线上测试的时候二维码无法生成

2、RequestQRCode 获取小程序二维码
apiURL = fmt.Sprintf(mpQRCodeURL, token.AccessToken) resp, err := net.GetIgnoreHttpsClient().Post(apiURL, "application/json;charset=utf-8", postData)

跨域解决方案

Jsonp
(1) JSONP原理 利用<script>标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。
(2) JSONP和AJAX对比 JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略, JSONP属于非同源策略(跨域请求)
(3) JSONP优缺点
JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。
(4)JSONP的实现流程
声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。 创建一个<script>标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是show('我不爱你')。 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。

在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP函数。

Nginx反向代理

实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。
使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

server {
    listen       81;
    server_name  www.domain1.com;
    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}     

最后通过命令行nginx -s reload启动nginx

// index.html
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();