feat: user & article: dev complete

This commit is contained in:
fallen-angle
2022-02-27 16:36:33 +08:00
parent 4f3b16ab9d
commit 80ca1cd46e
33 changed files with 2373 additions and 185 deletions

View File

@@ -10,6 +10,7 @@ type ServerConfig struct {
MySQL MySQLConfig `yaml:"mysql"` MySQL MySQLConfig `yaml:"mysql"`
Redis RedisConfig `yaml:"redis"` Redis RedisConfig `yaml:"redis"`
Jwt JwtConfig `yaml:"jwt"` Jwt JwtConfig `yaml:"jwt"`
Email EmailConfig `yaml:"email"`
} }
type MySQLConfig struct { type MySQLConfig struct {
@@ -31,3 +32,10 @@ type JwtConfig struct {
RenewExpireDays uint `yaml:"renewExpireDays"` RenewExpireDays uint `yaml:"renewExpireDays"`
RenewAheadDays uint `yaml:"renewAheadDays"` RenewAheadDays uint `yaml:"renewAheadDays"`
} }
type EmailConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Account string `yaml:"account"`
Password string `yaml:"password"`
}

View File

@@ -16,6 +16,228 @@ const docTemplate_swagger = `{
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "paths": {
"/article": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Article"
],
"summary": "save article",
"parameters": [
{
"description": "article",
"name": "Article",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.BackArticle"
}
},
{
"type": "string",
"description": "token",
"name": "Token",
"in": "header",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/utils.GinResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.BackArticle"
}
}
}
]
}
}
}
}
},
"/article/list": {
"get": {
"description": "Admin can get not published article",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Article"
],
"summary": "get all articles",
"parameters": [
{
"type": "string",
"description": "token",
"name": "Token",
"in": "header"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/utils.GinResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.BackArticle"
}
}
}
}
]
}
}
}
}
},
"/article/{id}": {
"get": {
"description": "Admin can get not published article",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Article"
],
"summary": "get all articles",
"parameters": [
{
"type": "string",
"description": "token",
"name": "Token",
"in": "header"
},
{
"type": "string",
"description": "id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/utils.GinResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.BackArticle"
}
}
}
]
}
}
}
},
"delete": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Article"
],
"summary": "delete an article",
"parameters": [
{
"type": "string",
"description": "token",
"name": "Token",
"in": "header",
"required": true
},
{
"type": "string",
"description": "id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
},
"/article/{id}/publish": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Article"
],
"summary": "get all articles",
"parameters": [
{
"type": "string",
"description": "token",
"name": "Token",
"in": "header",
"required": true
},
{
"type": "string",
"description": "id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
},
"/statistics/china": { "/statistics/china": {
"get": { "get": {
"produces": [ "produces": [
@@ -31,7 +253,7 @@ const docTemplate_swagger = `{
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/models.GinResponse" "$ref": "#/definitions/utils.GinResponse"
}, },
{ {
"type": "object", "type": "object",
@@ -76,7 +298,7 @@ const docTemplate_swagger = `{
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/models.GinResponse" "$ref": "#/definitions/utils.GinResponse"
}, },
{ {
"type": "object", "type": "object",
@@ -110,7 +332,7 @@ const docTemplate_swagger = `{
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/models.GinResponse" "$ref": "#/definitions/utils.GinResponse"
}, },
{ {
"type": "object", "type": "object",
@@ -144,7 +366,7 @@ const docTemplate_swagger = `{
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/models.GinResponse" "$ref": "#/definitions/utils.GinResponse"
}, },
{ {
"type": "object", "type": "object",
@@ -192,7 +414,7 @@ const docTemplate_swagger = `{
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/models.GinResponse" "$ref": "#/definitions/utils.GinResponse"
}, },
{ {
"type": "object", "type": "object",
@@ -210,6 +432,236 @@ const docTemplate_swagger = `{
} }
} }
} }
},
"/user/approve": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "admin approve account, user can use account after approved",
"parameters": [
{
"type": "string",
"description": "token",
"name": "Token",
"in": "header",
"required": true
},
{
"description": "json",
"name": "json",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.UserApprove"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
},
"/user/chpwd": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "change user's password",
"parameters": [
{
"description": "json",
"name": "json",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.UserChangePwd"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
},
"/user/code/{email}/{code}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "send verify code",
"parameters": [
{
"type": "string",
"description": "email",
"name": "email",
"in": "path",
"required": true
},
{
"type": "string",
"description": "code",
"name": "code",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
},
"/user/login": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "user login",
"parameters": [
{
"description": "json",
"name": "json",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.UserLogin"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
},
"/user/register": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "user register account",
"parameters": [
{
"description": "json",
"name": "json",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.UserRegister"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
},
"/user/registers": {
"get": {
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "list register infos, which is to be approved",
"parameters": [
{
"type": "string",
"description": "token",
"name": "Token",
"in": "header",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
},
"/user/{code}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "send verify code",
"parameters": [
{
"type": "string",
"description": "email",
"name": "email",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
@@ -273,6 +725,55 @@ const docTemplate_swagger = `{
} }
} }
}, },
"models.BackArticle": {
"type": "object",
"properties": {
"content": {
"description": "文章内容(如有需要可迁移至对象存储)",
"type": "string"
},
"cover": {
"description": "文章封面",
"type": "string"
},
"createTime": {
"description": "文章新建时间",
"type": "string"
},
"createUser": {
"description": "文章创建者id",
"type": "string"
},
"isDelete": {
"description": "删除标志",
"type": "integer"
},
"isPublish": {
"description": "发布状态(0:未发布, 1: 发布)",
"type": "integer"
},
"modifyTime": {
"description": "文章最后更新时间",
"type": "string"
},
"modifyUser": {
"description": "文章最后更新者id",
"type": "string"
},
"resume": {
"description": "文章简述",
"type": "string"
},
"tags": {
"description": "文章Tag",
"type": "string"
},
"title": {
"description": "文章标题",
"type": "string"
}
}
},
"models.ChinaAdd": { "models.ChinaAdd": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -369,7 +870,63 @@ const docTemplate_swagger = `{
} }
} }
}, },
"models.GinResponse": { "models.UserApprove": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"pass": {
"type": "boolean"
}
}
},
"models.UserChangePwd": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"email": {
"type": "string"
},
"newPassword": {
"type": "string"
}
}
},
"models.UserLogin": {
"type": "object",
"properties": {
"account": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"models.UserRegister": {
"type": "object",
"properties": {
"aptitude": {
"type": "string"
},
"email": {
"type": "string"
},
"password": {
"type": "string"
},
"phone": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"utils.GinResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"code": { "code": {

View File

@@ -6,6 +6,228 @@
"version": "1.0" "version": "1.0"
}, },
"paths": { "paths": {
"/article": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Article"
],
"summary": "save article",
"parameters": [
{
"description": "article",
"name": "Article",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.BackArticle"
}
},
{
"type": "string",
"description": "token",
"name": "Token",
"in": "header",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/utils.GinResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.BackArticle"
}
}
}
]
}
}
}
}
},
"/article/list": {
"get": {
"description": "Admin can get not published article",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Article"
],
"summary": "get all articles",
"parameters": [
{
"type": "string",
"description": "token",
"name": "Token",
"in": "header"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/utils.GinResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.BackArticle"
}
}
}
}
]
}
}
}
}
},
"/article/{id}": {
"get": {
"description": "Admin can get not published article",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Article"
],
"summary": "get all articles",
"parameters": [
{
"type": "string",
"description": "token",
"name": "Token",
"in": "header"
},
{
"type": "string",
"description": "id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/utils.GinResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.BackArticle"
}
}
}
]
}
}
}
},
"delete": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Article"
],
"summary": "delete an article",
"parameters": [
{
"type": "string",
"description": "token",
"name": "Token",
"in": "header",
"required": true
},
{
"type": "string",
"description": "id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
},
"/article/{id}/publish": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Article"
],
"summary": "get all articles",
"parameters": [
{
"type": "string",
"description": "token",
"name": "Token",
"in": "header",
"required": true
},
{
"type": "string",
"description": "id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
},
"/statistics/china": { "/statistics/china": {
"get": { "get": {
"produces": [ "produces": [
@@ -21,7 +243,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/models.GinResponse" "$ref": "#/definitions/utils.GinResponse"
}, },
{ {
"type": "object", "type": "object",
@@ -66,7 +288,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/models.GinResponse" "$ref": "#/definitions/utils.GinResponse"
}, },
{ {
"type": "object", "type": "object",
@@ -100,7 +322,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/models.GinResponse" "$ref": "#/definitions/utils.GinResponse"
}, },
{ {
"type": "object", "type": "object",
@@ -134,7 +356,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/models.GinResponse" "$ref": "#/definitions/utils.GinResponse"
}, },
{ {
"type": "object", "type": "object",
@@ -182,7 +404,7 @@
"schema": { "schema": {
"allOf": [ "allOf": [
{ {
"$ref": "#/definitions/models.GinResponse" "$ref": "#/definitions/utils.GinResponse"
}, },
{ {
"type": "object", "type": "object",
@@ -200,6 +422,236 @@
} }
} }
} }
},
"/user/approve": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "admin approve account, user can use account after approved",
"parameters": [
{
"type": "string",
"description": "token",
"name": "Token",
"in": "header",
"required": true
},
{
"description": "json",
"name": "json",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.UserApprove"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
},
"/user/chpwd": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "change user's password",
"parameters": [
{
"description": "json",
"name": "json",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.UserChangePwd"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
},
"/user/code/{email}/{code}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "send verify code",
"parameters": [
{
"type": "string",
"description": "email",
"name": "email",
"in": "path",
"required": true
},
{
"type": "string",
"description": "code",
"name": "code",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
},
"/user/login": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "user login",
"parameters": [
{
"description": "json",
"name": "json",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.UserLogin"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
},
"/user/register": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "user register account",
"parameters": [
{
"description": "json",
"name": "json",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.UserRegister"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
},
"/user/registers": {
"get": {
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "list register infos, which is to be approved",
"parameters": [
{
"type": "string",
"description": "token",
"name": "Token",
"in": "header",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
},
"/user/{code}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "send verify code",
"parameters": [
{
"type": "string",
"description": "email",
"name": "email",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.GinResponse"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
@@ -263,6 +715,55 @@
} }
} }
}, },
"models.BackArticle": {
"type": "object",
"properties": {
"content": {
"description": "文章内容(如有需要可迁移至对象存储)",
"type": "string"
},
"cover": {
"description": "文章封面",
"type": "string"
},
"createTime": {
"description": "文章新建时间",
"type": "string"
},
"createUser": {
"description": "文章创建者id",
"type": "string"
},
"isDelete": {
"description": "删除标志",
"type": "integer"
},
"isPublish": {
"description": "发布状态(0:未发布, 1: 发布)",
"type": "integer"
},
"modifyTime": {
"description": "文章最后更新时间",
"type": "string"
},
"modifyUser": {
"description": "文章最后更新者id",
"type": "string"
},
"resume": {
"description": "文章简述",
"type": "string"
},
"tags": {
"description": "文章Tag",
"type": "string"
},
"title": {
"description": "文章标题",
"type": "string"
}
}
},
"models.ChinaAdd": { "models.ChinaAdd": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -359,7 +860,63 @@
} }
} }
}, },
"models.GinResponse": { "models.UserApprove": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"pass": {
"type": "boolean"
}
}
},
"models.UserChangePwd": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"email": {
"type": "string"
},
"newPassword": {
"type": "string"
}
}
},
"models.UserLogin": {
"type": "object",
"properties": {
"account": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"models.UserRegister": {
"type": "object",
"properties": {
"aptitude": {
"type": "string"
},
"email": {
"type": "string"
},
"password": {
"type": "string"
},
"phone": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"utils.GinResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"code": { "code": {

View File

@@ -38,6 +38,42 @@ definitions:
wzz: wzz:
type: integer type: integer
type: object type: object
models.BackArticle:
properties:
content:
description: 文章内容(如有需要可迁移至对象存储)
type: string
cover:
description: 文章封面
type: string
createTime:
description: 文章新建时间
type: string
createUser:
description: 文章创建者id
type: string
isDelete:
description: 删除标志
type: integer
isPublish:
description: '发布状态(0:未发布, 1: 发布)'
type: integer
modifyTime:
description: 文章最后更新时间
type: string
modifyUser:
description: 文章最后更新者id
type: string
resume:
description: 文章简述
type: string
tags:
description: 文章Tag
type: string
title:
description: 文章标题
type: string
type: object
models.ChinaAdd: models.ChinaAdd:
properties: properties:
confirm: confirm:
@@ -101,7 +137,43 @@ definitions:
suspect: suspect:
type: integer type: integer
type: object type: object
models.GinResponse: models.UserApprove:
properties:
email:
type: string
pass:
type: boolean
type: object
models.UserChangePwd:
properties:
code:
type: string
email:
type: string
newPassword:
type: string
type: object
models.UserLogin:
properties:
account:
type: string
password:
type: string
type: object
models.UserRegister:
properties:
aptitude:
type: string
email:
type: string
password:
type: string
phone:
type: string
username:
type: string
type: object
utils.GinResponse:
properties: properties:
code: code:
type: integer type: integer
@@ -113,6 +185,143 @@ info:
title: nCov Tracker title: nCov Tracker
version: "1.0" version: "1.0"
paths: paths:
/article:
post:
consumes:
- application/json
parameters:
- description: article
in: body
name: Article
required: true
schema:
$ref: '#/definitions/models.BackArticle'
- description: token
in: header
name: Token
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/utils.GinResponse'
- properties:
data:
$ref: '#/definitions/models.BackArticle'
type: object
summary: save article
tags:
- Article
/article/{id}:
delete:
consumes:
- application/json
parameters:
- description: token
in: header
name: Token
required: true
type: string
- description: id
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/utils.GinResponse'
summary: delete an article
tags:
- Article
get:
consumes:
- application/json
description: Admin can get not published article
parameters:
- description: token
in: header
name: Token
type: string
- description: id
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/utils.GinResponse'
- properties:
data:
$ref: '#/definitions/models.BackArticle'
type: object
summary: get all articles
tags:
- Article
/article/{id}/publish:
post:
consumes:
- application/json
parameters:
- description: token
in: header
name: Token
required: true
type: string
- description: id
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/utils.GinResponse'
summary: get all articles
tags:
- Article
/article/list:
get:
consumes:
- application/json
description: Admin can get not published article
parameters:
- description: token
in: header
name: Token
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/utils.GinResponse'
- properties:
data:
items:
$ref: '#/definitions/models.BackArticle'
type: array
type: object
summary: get all articles
tags:
- Article
/statistics/china: /statistics/china:
get: get:
produces: produces:
@@ -122,7 +331,7 @@ paths:
description: OK description: OK
schema: schema:
allOf: allOf:
- $ref: '#/definitions/models.GinResponse' - $ref: '#/definitions/utils.GinResponse'
- properties: - properties:
data: data:
$ref: '#/definitions/models.ChinaData' $ref: '#/definitions/models.ChinaData'
@@ -149,7 +358,7 @@ paths:
description: OK description: OK
schema: schema:
allOf: allOf:
- $ref: '#/definitions/models.GinResponse' - $ref: '#/definitions/utils.GinResponse'
- properties: - properties:
data: data:
items: items:
@@ -168,7 +377,7 @@ paths:
description: OK description: OK
schema: schema:
allOf: allOf:
- $ref: '#/definitions/models.GinResponse' - $ref: '#/definitions/utils.GinResponse'
- properties: - properties:
data: data:
items: items:
@@ -187,7 +396,7 @@ paths:
description: OK description: OK
schema: schema:
allOf: allOf:
- $ref: '#/definitions/models.GinResponse' - $ref: '#/definitions/utils.GinResponse'
- properties: - properties:
data: data:
items: items:
@@ -216,7 +425,7 @@ paths:
description: OK description: OK
schema: schema:
allOf: allOf:
- $ref: '#/definitions/models.GinResponse' - $ref: '#/definitions/utils.GinResponse'
- properties: - properties:
data: data:
items: items:
@@ -226,4 +435,152 @@ paths:
summary: province statistics summary: province statistics
tags: tags:
- Statistics - Statistics
/user/{code}:
get:
parameters:
- description: email
in: path
name: email
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/utils.GinResponse'
summary: send verify code
tags:
- User
/user/approve:
post:
consumes:
- application/json
parameters:
- description: token
in: header
name: Token
required: true
type: string
- description: json
in: body
name: json
required: true
schema:
$ref: '#/definitions/models.UserApprove'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/utils.GinResponse'
summary: admin approve account, user can use account after approved
tags:
- User
/user/chpwd:
post:
consumes:
- application/json
parameters:
- description: json
in: body
name: json
required: true
schema:
$ref: '#/definitions/models.UserChangePwd'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/utils.GinResponse'
summary: change user's password
tags:
- User
/user/code/{email}/{code}:
get:
parameters:
- description: email
in: path
name: email
required: true
type: string
- description: code
in: path
name: code
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/utils.GinResponse'
summary: send verify code
tags:
- User
/user/login:
post:
consumes:
- application/json
parameters:
- description: json
in: body
name: json
required: true
schema:
$ref: '#/definitions/models.UserLogin'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/utils.GinResponse'
summary: user login
tags:
- User
/user/register:
post:
consumes:
- application/json
parameters:
- description: json
in: body
name: json
required: true
schema:
$ref: '#/definitions/models.UserRegister'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/utils.GinResponse'
summary: user register account
tags:
- User
/user/registers:
get:
parameters:
- description: token
in: header
name: Token
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/utils.GinResponse'
summary: list register infos, which is to be approved
tags:
- User
swagger: "2.0" swagger: "2.0"

View File

@@ -36,4 +36,8 @@ func GetHttpClient(key string) (*http.Client, error) {
const ( const (
CHINA_NCOV_STATISTIC_URL = "https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5" CHINA_NCOV_STATISTIC_URL = "https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5"
ENV_NOLOG = "nolog" ENV_NOLOG = "nolog"
TOKEN_EXPIRE_DAYS = 15
REGISTER_REDIS_KEY = "register_key"
CHANGEPWD_REDIS_KEY = "changepwd_key"
) )

2
go.mod
View File

@@ -30,9 +30,11 @@ require (
github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect github.com/jinzhu/now v1.1.4 // indirect
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect github.com/leodido/go-urn v1.2.1 // indirect

4
go.sum
View File

@@ -172,6 +172,8 @@ github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
@@ -188,6 +190,8 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=

View File

@@ -8,53 +8,108 @@ import (
"strconv" "strconv"
) )
// SaveArticleHandler save an article
// @Tags Article
// @Accept json
// @Produce json
// @Summary save article
// @Success 200 {object} utils.GinResponse{data=models.BackArticle}
// @Router /article [post]
// @Param Article body models.BackArticle true "article"
// @Param Token header string true "token"
func SaveArticleHandler(c *gin.Context) { func SaveArticleHandler(c *gin.Context) {
var articleSave models.BackArticle jsonMap := bindJson(c)
err := c.ShouldBindJSON(&articleSave) if jsonMap == nil {
if err != nil {
var requestBody []byte
_, err := c.Request.Body.Read(requestBody)
if err != nil {
return
}
utils.RequestErr(c, requestBody)
return return
} }
if ok := article.SaveArticle(&articleSave); !ok { colMap := models.MapJ2c[models.BackArticle](jsonMap, true)
utils.ServerErr(c, "Save Failed") if ok := article.SaveArticle(colMap); !ok {
ServerErr(c, "Save Failed")
return return
} }
utils.Succ(c, articleSave) utils.Succ(c, jsonMap)
} }
// GetAllArticlesHandler get all article
// @Tags Article
// @Accept json
// @Produce json
// @Summary get all articles
// @Description Admin can get not published article
// @Success 200 {object} utils.GinResponse{data=[]models.BackArticle}
// @Router /article/list [get]
// @Param Token header string false "token"
func GetAllArticlesHandler(c *gin.Context) { func GetAllArticlesHandler(c *gin.Context) {
articles := article.GetArticleList() // TODO: admin need to show more articles
articles := article.ListAllArticles()
utils.Succ(c, articles) utils.Succ(c, articles)
} }
// DeleteArticleHandler delete article
// @Tags Article
// @Accept json
// @Produce json
// @Summary delete an article
// @Success 200 {object} utils.GinResponse{}
// @Router /article/{id} [delete]
// @Param Token header string true "token"
// @Param id path string true "id"
func DeleteArticleHandler(c *gin.Context) { func DeleteArticleHandler(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
utils.RequestErr(c, map[string]interface{}{"URI": c.Request.RequestURI}) RequestErr(c, map[string]interface{}{"URI": c.Request.RequestURI})
return return
} }
if ok := article.DeleteArticle(id); !ok { if ok := article.DeleteArticle(id); !ok {
utils.DataNotFound(c, "The article not found id = "+strconv.Itoa(id)) ServerErr(c, "Can't delete the article")
return return
} }
utils.Succ(c, nil) utils.Succ(c, nil)
} }
// GetArticleHandler get an article
// @Tags Article
// @Accept json
// @Produce json
// @Summary get all articles
// @Description Admin can get not published article
// @Success 200 {object} utils.GinResponse{data=models.BackArticle}
// @Router /article/{id} [get]
// @Param Token header string false "token"
// @Param id path string true "id"
func GetArticleHandler(c *gin.Context) { func GetArticleHandler(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
utils.RequestErr(c, map[string]interface{}{"URI": c.Request.RequestURI}) RequestErr(c, map[string]interface{}{"URI": c.Request.RequestURI})
return return
} }
res := article.GetArticleById(id) res := article.GetArticleById(id)
//TODO: if not admin, will not show not published article
if res == nil { if res == nil {
utils.DataNotFound(c, nil) DataNotFound(c, nil)
return return
} }
utils.Succ(c, res) utils.Succ(c, res)
} }
// PublishArticleHandler publish an article
// @Tags Article
// @Accept json
// @Produce json
// @Summary get all articles
// @Success 200 {object} utils.GinResponse{}
// @Router /article/{id}/publish [post]
// @Param Token header string true "token"
// @Param id path string true "id"
func PublishArticleHandler(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
RequestErr(c, map[string]interface{}{"URI": c.Request.RequestURI})
return
}
if ok := article.PublishArticle(id); !ok {
ServerErr(c, "Can't publish the article")
return
}
utils.Succ(c, nil)
}

33
handler/errors.go Normal file
View File

@@ -0,0 +1,33 @@
package handler
import (
"github.com/gin-gonic/gin"
"nCovTrack-Backend/utils"
"net/http"
)
// This file is define some business error
const (
BAD_REQUEST = "Bad Request"
DATA_NOT_FOUND = "Data not Found"
STATUS_DATA_NOT_FOUND = 210
)
func RequestError(c *gin.Context, code int, data interface{}) {
utils.Error(c, http.StatusBadRequest, code, BAD_REQUEST, data)
}
func RequestErr(c *gin.Context, data interface{}) {
RequestError(c, http.StatusBadRequest, data)
}
func ServerError(c *gin.Context, code int, msg interface{}) {
utils.Err(c, http.StatusInternalServerError, code, msg)
}
func ServerErr(c *gin.Context, msg interface{}) {
ServerError(c, http.StatusInternalServerError, msg)
}
func DataNotFound(c *gin.Context, data interface{}) {
utils.Success(c, http.StatusOK, STATUS_DATA_NOT_FOUND, DATA_NOT_FOUND, data)
}

View File

@@ -12,7 +12,7 @@ import (
// @Tags Statistics // @Tags Statistics
// @Produce json // @Produce json
// @Summary province statistics // @Summary province statistics
// @Success 200 {object} models.GinResponse{data=[]models.AreaInfo} // @Success 200 {object} utils.GinResponse{data=[]models.AreaInfo}
// @Router /statistics/province/{sort} [get] // @Router /statistics/province/{sort} [get]
// @Param sort path string false "data sorted by" Enums(today, total, now, default) // @Param sort path string false "data sorted by" Enums(today, total, now, default)
func ProvinceDataHandler(c *gin.Context) { func ProvinceDataHandler(c *gin.Context) {
@@ -25,7 +25,7 @@ func ProvinceDataHandler(c *gin.Context) {
// @Tags Statistics // @Tags Statistics
// @Produce json // @Produce json
// @Summary city statistics // @Summary city statistics
// @Success 200 {object} models.GinResponse{data=[]models.AreaInfo} // @Success 200 {object} utils.GinResponse{data=[]models.AreaInfo}
// @Router /statistics/city/{sort} [get] // @Router /statistics/city/{sort} [get]
// @Param sort path string false "data sorted by" Enums(today, total, now, default) // @Param sort path string false "data sorted by" Enums(today, total, now, default)
func CityDataHandler(c *gin.Context) { func CityDataHandler(c *gin.Context) {
@@ -38,7 +38,7 @@ func CityDataHandler(c *gin.Context) {
// @Tags Statistics // @Tags Statistics
// @Produce json // @Produce json
// @Summary country statistics // @Summary country statistics
// @Success 200 {object} models.GinResponse{data=[]models.AreaInfo} // @Success 200 {object} utils.GinResponse{data=[]models.AreaInfo}
// @Router /statistics/country/child [get] // @Router /statistics/country/child [get]
// @Router /statistics/country [get] // @Router /statistics/country [get]
func CountryDataHandler(c *gin.Context) { func CountryDataHandler(c *gin.Context) {
@@ -51,7 +51,7 @@ func CountryDataHandler(c *gin.Context) {
// @Tags Statistics // @Tags Statistics
// @Produce json // @Produce json
// @Summary china data // @Summary china data
// @Success 200 {object} models.GinResponse{data=models.ChinaData} // @Success 200 {object} utils.GinResponse{data=models.ChinaData}
// @Router /statistics/china [get] // @Router /statistics/china [get]
func ChinaDataHandler(c *gin.Context) { func ChinaDataHandler(c *gin.Context) {
data := service.GetChinaNCovStatistic() data := service.GetChinaNCovStatistic()

144
handler/user.go Normal file
View File

@@ -0,0 +1,144 @@
package handler
import (
"github.com/gin-gonic/gin"
"nCovTrack-Backend/models"
"nCovTrack-Backend/service/user"
"nCovTrack-Backend/utils"
"regexp"
)
//UserRegisterHandler user register
// @Tags User
// @Accept json
// @Produce json
// @Summary user register account
// @Success 200 {object} utils.GinResponse{}
// @Router /user/register [post]
// @Param json body models.UserRegister true "json"
func UserRegisterHandler(c *gin.Context) {
jsonMap := bindJsonStruct[models.UserRegister](c)
if jsonMap == nil {
return
}
registered := user.NoDuplicatePhoneOrEmail(jsonMap["phone"].(string), jsonMap["email"].(string))
if registered {
utils.Success(c, 200, 200, "Registered", nil)
}
colMap := models.MapJ2c[models.BackUser](jsonMap, true)
user.Register(colMap)
}
//UserApproveHandler admin approve account
// @Tags User
// @Accept json
// @Produce json
// @Summary admin approve account, user can use account after approved
// @Success 200 {object} utils.GinResponse{}
// @Router /user/approve [post]
// @Param Token header string true "token"
// @Param json body models.UserApprove true "json"
func UserApproveHandler(c *gin.Context) {
//TODO: auth user is admin or not
jsonMap := bindJsonStruct[models.UserApprove](c)
if jsonMap == nil {
return
}
if !user.ApproveRegister(jsonMap["email"].(string), jsonMap["pass"].(bool)) {
RequestErr(c, "approve failed")
return
}
utils.Succ(c, nil)
}
//UserLoginHandler admin approve account
// @Tags User
// @Accept json
// @Produce json
// @Summary user login
// @Success 200 {object} utils.GinResponse{}
// @Router /user/login [post]
// @Param json body models.UserLogin true "json"
func UserLoginHandler(c *gin.Context) {
jsonMap := bindJsonStruct[models.UserLogin](c)
if jsonMap == nil {
return
}
token := user.Login(jsonMap)
if token == "" {
// TODO: change to request error
utils.Succ(c, map[string]interface{}{"msg": "failed"})
return
}
c.Writer.Header().Set("X-Token", token)
utils.Succ(c, nil)
}
//ListRegisterUserHandler list register infos
// @Tags User
// @Produce json
// @Summary list register infos, which is to be approved
// @Success 200 {object} utils.GinResponse{}
// @Router /user/registers [get]
// @Param Token header string true "token"
func ListRegisterUserHandler(c *gin.Context) {
registers := user.ListRegister()
utils.Succ(c, registers)
}
//SendEmailCodeHandler send verify code
// @Tags User
// @Produce json
// @Summary send verify code
// @Success 200 {object} utils.GinResponse{}
// @Router /user/{code} [get]
// @Param email path string true "email"
func SendEmailCodeHandler(c *gin.Context) {
email := c.Param("email")
match, _ := regexp.Match("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$", []byte(email))
if !match {
RequestErr(c, map[string]interface{}{"email": email})
return
}
if ok := user.SendEmailCode(email); !ok {
ServerErr(c, "Send Email Failed")
return
}
utils.Succ(c, nil)
}
//VerifyEmailCodeHandler verify code
// @Tags User
// @Produce json
// @Summary send verify code
// @Success 200 {object} utils.GinResponse{}
// @Router /user/code/{email}/{code} [get]
// @Param email path string true "email"
// @Param code path string true "code"
func VerifyEmailCodeHandler(c *gin.Context) {
email := c.Param("email")
code := c.Param("code")
emailMatch, _ := regexp.Match("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$", []byte(email))
codeMatch, _ := regexp.Match("^[\\w]{6}$", []byte(code))
if !codeMatch || !emailMatch {
RequestErr(c, map[string]interface{}{"email": email, "code": code})
return
}
utils.Succ(c, user.VerifyEmailCode(email, code))
}
//ChangePasswordHandler change user's password
// @Tags User
// @Accept json
// @Produce json
// @Summary change user's password
// @Success 200 {object} utils.GinResponse{}
// @Router /user/chpwd [post]
// @Param json body models.UserChangePwd true "json"
func ChangePasswordHandler(c *gin.Context) {
jsonMap := bindJsonStruct[models.UserChangePwd](c)
if jsonMap == nil {
return
}
utils.Succ(c, map[string]interface{}{"success": user.ChangePassword(jsonMap)})
}

48
handler/utils.go Normal file
View File

@@ -0,0 +1,48 @@
package handler
import (
"encoding/json"
"github.com/gin-gonic/gin"
)
// bindJson bind body as a map
func bindJson(c *gin.Context) map[string]interface{} {
var jsonMap map[string]interface{}
err := c.ShouldBindJSON(&jsonMap)
if err != nil {
var requestBody []byte
_, err := c.Request.Body.Read(requestBody)
if err != nil {
panic(err)
}
RequestErr(c, requestBody)
return nil
}
if jsonMap == nil || len(jsonMap) == 0 {
RequestErr(c, map[string]interface{}{"Body": nil})
return nil
}
return jsonMap
}
// bindJsonStruct bind json as a struct, and convert to map
func bindJsonStruct[T any](c *gin.Context) map[string]interface{} {
var bindObj T
err := c.ShouldBind(&bindObj)
if err != nil {
var requestBody []byte
_, err := c.Request.Body.Read(requestBody)
if err != nil {
panic(err)
}
RequestErr(c, requestBody)
return nil
}
jsonStr, _ := json.Marshal(bindObj)
var jsonMap map[string]interface{}
_ = json.Unmarshal(jsonStr, &jsonMap)
if jsonMap == nil || len(jsonMap) == 0 {
return nil
}
return jsonMap
}

View File

@@ -12,7 +12,7 @@ import (
func initLogger() { func initLogger() {
zapConfig := zap.NewProductionConfig() zapConfig := zap.NewProductionConfig()
zapConfig.OutputPaths = []string{ zapConfig.OutputPaths = []string{
fmt.Sprintf("%slog_%s.log", global.ServerSettings.LogPath, utils.FormateDate(time.Now())), fmt.Sprintf("%slog_%s.log", global.ServerSettings.LogPath, utils.FormatDate(time.Now())),
"stdout", "stdout",
} }
logger, err := zapConfig.Build() logger, err := zapConfig.Build()

View File

@@ -1,9 +1,14 @@
package middleware package middleware
import ( import (
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
"nCovTrack-Backend/global"
"nCovTrack-Backend/utils" "nCovTrack-Backend/utils"
"net/http" "net/http"
"strconv"
"time"
) )
const UNAUTH_MSG = "unauthorized" const UNAUTH_MSG = "unauthorized"
@@ -17,8 +22,15 @@ func Auth() gin.HandlerFunc {
c.Abort() c.Abort()
return return
} }
// Write the field of token to request header
claims := utils.ParseClaims(oldToken[0])
c.Request.Header.Set("role", fmt.Sprint(claims["role"]))
c.Request.Header.Set("email", claims["email"].(string))
c.Request.Header.Set("id", fmt.Sprint(claims["id"]))
// renew token, and judge the token's iat is expired or not
renewToken := utils.RenewToken(oldToken[0]) renewToken := utils.RenewToken(oldToken[0])
if renewToken == "" { if renewToken == "" || !validAccountIssue(claims) {
utils.Err(c, http.StatusUnauthorized, http.StatusUnauthorized, UNAUTH_MSG) utils.Err(c, http.StatusUnauthorized, http.StatusUnauthorized, UNAUTH_MSG)
c.Abort() c.Abort()
return return
@@ -27,3 +39,23 @@ func Auth() gin.HandlerFunc {
c.Next() c.Next()
} }
} }
// validAccountIssue validate token is valid or not
// If user change password, or logoff on all password, we need to judge use's token is valid?
// Due to the token is no status, so we need to record something on the server-end.
// We use the "IssueAt" field of token, to judge token expired or not.
// TODO: Move this to jwt utils
func validAccountIssue(claims jwt.MapClaims) bool {
iafStr := global.Redis.HGet(global.CHANGEPWD_REDIS_KEY, claims["email"].(string)).Val()
if iafStr == "" {
return true
}
iaf, _ := strconv.Atoi(iafStr)
// Due to we allow token renew, although it was expired, so the token validity period will more than token's validity period
tokenMaxValidSeconds := (global.TOKEN_EXPIRE_DAYS + global.ServerSettings.Jwt.RenewExpireDays) * 24 * 60 * 60
if time.Now().Unix()-int64(iaf) > int64(tokenMaxValidSeconds) {
global.Redis.HDel(global.CHANGEPWD_REDIS_KEY, claims["email"].(string))
return true
}
return int(claims["iat"].(float64)) > iaf
}

View File

@@ -1,9 +1,12 @@
package models package models
import "time" import (
"time"
)
// BackArticle article struct
type BackArticle struct { type BackArticle struct {
ID int `gorm:"primaryKey;column:id" json:"id"` // 文章id ID int `gorm:"primaryKey;column:id" json:"-"` // 文章id
CreateTime time.Time `gorm:"column:create_time" json:"createTime"` // 文章新建时间 CreateTime time.Time `gorm:"column:create_time" json:"createTime"` // 文章新建时间
CreateUser string `gorm:"column:create_user" json:"createUser"` // 文章创建者id CreateUser string `gorm:"column:create_user" json:"createUser"` // 文章创建者id
ModifyTime time.Time `gorm:"column:modify_time" json:"modifyTime"` // 文章最后更新时间 ModifyTime time.Time `gorm:"column:modify_time" json:"modifyTime"` // 文章最后更新时间
@@ -13,4 +16,20 @@ type BackArticle struct {
Resume string `gorm:"column:resume" json:"resume"` // 文章简述 Resume string `gorm:"column:resume" json:"resume"` // 文章简述
Cover string `gorm:"column:cover" json:"cover"` // 文章封面 Cover string `gorm:"column:cover" json:"cover"` // 文章封面
Content string `gorm:"column:content" json:"content"` // 文章内容(如有需要可迁移至对象存储) Content string `gorm:"column:content" json:"content"` // 文章内容(如有需要可迁移至对象存储)
IsPublish int8 `gorm:"column:is_publish" json:"isPublish"` // 发布状态(0:未发布, 1: 发布)
IsDelete int8 `gorm:"column:is_delete" json:"isDelete"` // 删除标志
} }
func init() {
initJcMap[BackArticle]()
}
//func ArticleMapJ2c(jsonMap map[string]interface{}, ignoreNil bool) map[string]interface{} {
// colMap := make(map[string]interface{})
// for k, v := range jsonMap {
// if colKey := colKey != "" && (!ignoreNil && v == nil) {
// colMap[colKey] = v
// }
// }
// return colMap
//}

View File

@@ -3,13 +3,43 @@ package models
import "time" import "time"
type BackUser struct { type BackUser struct {
ID int `gorm:"primaryKey;column:id" json:"-"` // 用户ID ID int `gorm:"primaryKey;column:id" json:"-"` // 用户ID
Username string `gorm:"column:username" json:"username"` // 用户真实姓名 Username string `gorm:"column:username" json:"username"` // 用户真实姓名
Password string `gorm:"column:password" json:"password"` // 用户密码 Password string `gorm:"column:password" json:"password"` // 用户密码
Role int `gorm:"column:role" json:"role"` // 用户角色 Role int `gorm:"column:role" json:"role"` // 用户角色
Email string `gorm:"unique;column:email" json:"email"` // 用户邮箱 Email string `gorm:"column:email" json:"email"` // 用户邮箱
Phone string `gorm:"unique;column:phone" json:"phone"` // 用户手机号码 Phone string `gorm:"column:phone" json:"phone"` // 用户手机号码
Aptitude string `gorm:"column:aptitude" json:"aptitude"` // 用户资质证明(图片URL) Aptitude string `gorm:"column:aptitude" json:"aptitude"` // 用户资质证明(图片URL)
RegisterTime time.Time `gorm:"column:register_time" json:"registerTime"` // 用户注册时间 CreateTime time.Time `gorm:"column:create_time" json:"createTime"` // 用户注册时间
Approver int `gorm:"column:approver" json:"approver"` // 注册审核人ID Approver int `gorm:"column:approver" json:"approver"` // 注册审核人ID
ModifyTime time.Time `gorm:"column:modify_time" json:"modifyTime"`
IsDelete int8 `gorm:"column:is_delete" json:"isDelete"` // 删除标志
}
type UserLogin struct {
Account string `json:"account"`
Password string `json:"password"`
}
type UserRegister struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
Phone string `json:"phone"`
Aptitude string `json:"aptitude"`
}
type UserChangePwd struct {
Email string `json:"email"`
Code string `json:"code"`
NewPassword string `json:"newPassword"`
}
type UserApprove struct {
Email string `json:"email"`
Pass bool `json:"pass"`
}
func init() {
initJcMap[BackUser]()
} }

View File

@@ -1,14 +1,181 @@
package models package models
type UtilRequestInfo struct { import (
Url string "fmt"
Header string "gorm.io/gorm"
Body string "nCovTrack-Backend/global"
Timeout int "reflect"
"regexp"
"time"
)
var colNameReg, _ = regexp.Compile(".*column:(.*);?")
var j2cMap = make(map[string]map[string]string)
var c2jMap = make(map[string]map[string]string)
const IS_DELETE = "is_delete"
// initJcMap the gorm models need to call this function in init function
func initJcMap[T any]() {
t := reflect.TypeOf(new(T)).Elem()
tJ2cMap, tC2jMap := make(map[string]string), make(map[string]string)
for i := 0; i < t.NumField(); i++ {
colName := columnName(t.Field(i).Tag.Get("gorm"))
// TODO: Deal with (-)
jsonName := t.Field(i).Tag.Get("json")
if colName == "" || jsonName == "" {
continue
}
tJ2cMap[jsonName] = colName
tC2jMap[colName] = jsonName
}
j2cMap[t.Name()] = tJ2cMap
c2jMap[t.Name()] = tC2jMap
} }
type GinResponse struct { // columnName get the mysql column name of the tag
Code int `json:"code"` func columnName(gormTag string) string {
Msg interface{} `json:"msg"` colNames := colNameReg.FindSubmatch([]byte(gormTag))
Data interface{} `json:"data"` if len(colNames) != 2 {
panic("Model tag regex error")
}
return string(colNames[1])
}
// MapJ2c convert jsonMap to colMap, which will used by gorm
func MapJ2c[T any](jsonMap map[string]interface{}, ignoreNil bool) (colMap map[string]interface{}) {
tName := reflect.TypeOf(new(T)).Elem().Name()
tJ2cMap := j2cMap[tName]
if tJ2cMap == nil {
panic(tName + " is not init registered int j2cMap")
}
colMap = make(map[string]interface{})
for k, v := range jsonMap {
//TODO 无法转换
if colName := tJ2cMap[k]; colName != "" && (!ignoreNil || v != nil) {
colMap[colName] = v
}
}
return colMap
}
// BeforeSave need to set some field while insert or update
func BeforeSave(colMap map[string]interface{}, user int) {
if colMap["id"] == nil {
colMap["create_time"] = time.Now()
if user != -1 {
colMap["create_user"] = user
}
}
colMap["modify_time"] = time.Now()
if user != -1 {
colMap["modify_user"] = user
}
}
/*-----------------------------------------------<Gorm functions>-----------------------------------------------------------*/
// Due to gorm can't deal with the zero value, so we use gorm with map.
// The generic will make the function is generally used to gorm models
// TODO: add uniqueKey map, which can be used when Upsert
func Upsert[T any](colMap map[string]interface{}) (ok bool, rowsAffected int64) {
var tx *gorm.DB
if colMap["id"] == nil {
tx = global.Db.Model(new(T)).Create(colMap)
} else {
tx = global.Db.Model(new(T)).Where("id = ?", colMap["id"]).Updates(colMap)
}
if tx.Error != nil {
fmt.Println(tx.Error)
return false, 0
}
return true, tx.RowsAffected
}
// DeleteById will delete by id, not delete the record from database, only set the field "is_delete" as 1
func DeleteById[T any](id int) (ok bool, rowsAffected int64) {
tx := global.Db.Model(new(T)).Where("id = ?", id).Update("is_delete", 1)
if tx.Error != nil {
return false, 0
}
return true, rowsAffected
}
func List[T any](queryMap []map[string]interface{}) *[]map[string]interface{} {
tx := global.Db.Model(new(T))
for _, e := range queryMap {
e[IS_DELETE] = 0
tx = tx.Or(e)
}
return ListByOrm(tx)
}
func ListField[T any](queryMap []map[string]interface{}, isOmit bool, queryField ...string) *[]map[string]interface{} {
tx := global.Db.Model(new(T))
for _, e := range queryMap {
e[IS_DELETE] = 0
tx = tx.Or(e)
}
if len(queryMap) == 0 {
return ListByOrm(tx)
}
if isOmit {
tx = tx.Omit(queryField...)
} else {
tx = tx.Select(queryField[0], queryField[1:])
}
return ListByOrm(tx)
}
func ListByOrm(tx *gorm.DB) *[]map[string]interface{} {
var res []map[string]interface{}
tx.Find(&res)
return &res
}
func Get[T any](queryMap []map[string]interface{}) map[string]interface{} {
tx := global.Db.Model(new(T))
for _, e := range queryMap {
e[IS_DELETE] = 0
tx = tx.Or(e)
}
return GetByOrm(tx)
}
func GetField[T any](queryMap []map[string]interface{}, isOmit bool, queryField ...string) map[string]interface{} {
tx := global.Db.Model(new(T))
for _, e := range queryMap {
e[IS_DELETE] = 0
tx = tx.Or(e)
}
if len(queryMap) == 0 {
return GetByOrm(tx)
}
if isOmit {
tx = tx.Omit(queryField...)
} else {
tx = tx.Select(queryField[0], queryField[1:])
}
return GetByOrm(tx)
}
func GetByOrm(tx *gorm.DB) map[string]interface{} {
var res map[string]interface{}
tx.Limit(1).Find(&res)
return res
}
func Count[T any](queryMap []map[string]interface{}) int64 {
tx := global.Db.Model(new(T))
for _, e := range queryMap {
e[IS_DELETE] = 0
tx = tx.Or(e)
}
return CountByOrm(tx)
}
func CountByOrm(tx *gorm.DB) int64 {
var count int64
tx.Count(&count)
return count
} }

View File

@@ -8,15 +8,16 @@ import (
func articlePrivateRouter(router *gin.RouterGroup) { func articlePrivateRouter(router *gin.RouterGroup) {
articleRouter := router.Group("/article") articleRouter := router.Group("/article")
{ {
articleRouter.POST("/:id", handler.SaveArticleHandler) articleRouter.DELETE("/:id", handler.DeleteArticleHandler)
articleRouter.POST("/:id/publish", handler.PublishArticleHandler)
} }
} }
func articlePublicRouter(router *gin.RouterGroup) { func articlePublicRouter(router *gin.RouterGroup) {
articleRouter := router.Group("/article") articleRouter := router.Group("/article")
{ {
articleRouter.POST("", handler.SaveArticleHandler)
articleRouter.GET("/list", handler.GetAllArticlesHandler) articleRouter.GET("/list", handler.GetAllArticlesHandler)
articleRouter.DELETE("/:id", handler.DeleteArticleHandler)
articleRouter.GET("/:id", handler.GetArticleHandler) articleRouter.GET("/:id", handler.GetArticleHandler)
} }
} }

View File

@@ -14,10 +14,12 @@ func BusiRouter() {
{ {
statisticRouter(publicRouter) statisticRouter(publicRouter)
articlePublicRouter(publicRouter) articlePublicRouter(publicRouter)
userPublicRouter(publicRouter)
} }
// Private // Private
{ {
articlePrivateRouter(privateRouter) articlePrivateRouter(privateRouter)
userPrivateRouter(privateRouter)
} }
} }

24
router/user.go Normal file
View File

@@ -0,0 +1,24 @@
package router
import (
"github.com/gin-gonic/gin"
"nCovTrack-Backend/handler"
)
func userPublicRouter(router *gin.RouterGroup) {
userRouter := router.Group("/user")
{
userRouter.POST("/register", handler.UserRegisterHandler)
userRouter.POST("/login", handler.UserLoginHandler)
userRouter.GET("/code/:email", handler.SendEmailCodeHandler)
userRouter.GET("/code/:email/:code", handler.VerifyEmailCodeHandler)
userRouter.POST("/chpwd", handler.ChangePasswordHandler)
}
}
func userPrivateRouter(router *gin.RouterGroup) {
userRouter := router.Group("/user")
{
userRouter.POST("/approve", handler.UserApproveHandler)
userRouter.GET("/registers", handler.ListRegisterUserHandler)
}
}

View File

@@ -1,37 +1,49 @@
package article package article
import ( import (
"nCovTrack-Backend/global"
"nCovTrack-Backend/models" "nCovTrack-Backend/models"
"nCovTrack-Backend/utils"
) )
func GetArticleList() *[]models.BackArticle { //ListPublishedArticles list the articles published, use to show the articles to all people
var articles []models.BackArticle func ListPublishedArticles() *[]map[string]interface{} {
global.Db.Omit("content").Find(&articles) article := models.ListField[models.BackArticle]([]map[string]interface{}{{"is_publish": 0}}, true, "content")
return &articles if *article == nil {
article = &[]map[string]interface{}{}
}
return article
} }
func SaveArticle(article *models.BackArticle) (ok bool) { //ListAllArticles list all articles, will show the articles not published of the user
return utils.Upsert(article) // TODO: need only show the user's not published article
func ListAllArticles() *[]map[string]interface{} {
article := models.ListField[models.BackArticle]([]map[string]interface{}{{}}, true, "content")
if *article == nil {
article = &[]map[string]interface{}{}
}
return article
} }
//SaveArticle save the articles
func SaveArticle(article map[string]interface{}) (ok bool) {
models.BeforeSave(article, -1)
ok, rows := models.Upsert[models.BackArticle](article)
return ok && rows != 0
}
//DeleteArticle delete article by id
func DeleteArticle(id int) (ok bool) { func DeleteArticle(id int) (ok bool) {
tx := global.Db.Delete(&models.BackArticle{}, id) ok, rowsAffected := models.DeleteById[models.BackArticle](id)
if tx.Error != nil { return ok && rowsAffected != 0
panic(tx.Error)
}
if tx.RowsAffected == 0 {
return false
}
return false
} }
func GetArticleById(id int) *models.BackArticle { //GetArticleById get an article
var article models.BackArticle func GetArticleById(id int) map[string]interface{} {
tx := global.Db.Limit(1).Find(&article, id) return models.Get[models.BackArticle]([]map[string]interface{}{{"id": id}})
if tx.RowsAffected == 0 { }
return nil
} //PublishArticle publish an article
return &article func PublishArticle(id int) (ok bool) {
colMap := map[string]interface{}{"id": id, "is_publish": 1}
ok, rowsAffected := models.Upsert[models.BackArticle](colMap)
return ok && rowsAffected != 0
} }

140
service/user/user.go Normal file
View File

@@ -0,0 +1,140 @@
package user
import (
"encoding/json"
"fmt"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"nCovTrack-Backend/global"
"nCovTrack-Backend/models"
"nCovTrack-Backend/utils"
"strings"
"time"
)
const (
EMAIL_CODE_REDIS_KEY = "verify_code_"
)
// Login if login success, will return token
func Login(user map[string]interface{}) (token string) {
account := user["account"].(string)
var queryMap []map[string]interface{}
if strings.Contains(account, "@") {
queryMap = append(queryMap, map[string]interface{}{"email": account})
} else {
queryMap = append(queryMap, map[string]interface{}{"phone": account})
}
userInfo := models.Get[models.BackUser](queryMap)
if userInfo == nil {
return ""
}
if !utils.PasswordCompare(user["password"].(string), userInfo["password"].(string)) {
return ""
}
claims := jwt.MapClaims{
"id": userInfo["id"],
"username": userInfo["username"],
"role": userInfo["role"],
"email": userInfo["email"],
}
return utils.GenerateToken(claims)
}
// Register user register, user can use account after approved
func Register(user map[string]interface{}) {
user["password"] = utils.PasswordEncrypt(user["password"].(string))
userStr, _ := json.Marshal(user)
// insert into redis, wait for approve
cmd := global.Redis.HMSet(global.REGISTER_REDIS_KEY, map[string]interface{}{user["email"].(string): userStr})
if cmd.Err() != nil {
panic(cmd.Err())
}
}
// ListRegister list the registers in the redis to be approved
func ListRegister() *[]map[string]interface{} {
applyStrMap := global.Redis.HGetAll(global.REGISTER_REDIS_KEY).Val()
var applies []map[string]interface{}
for _, v := range applyStrMap {
var apply map[string]interface{}
_ = json.Unmarshal([]byte(v), &apply)
applies = append(applies, apply)
}
if applies == nil {
applies = []map[string]interface{}{}
}
return &applies
}
// ApproveRegister approve a register
func ApproveRegister(email string, pass bool) bool {
if !pass {
rowsAffected := global.Redis.HDel(global.REGISTER_REDIS_KEY, email).Val()
return rowsAffected != 0
}
// if pass, will get the register info from redis, and the insert into mysql, this mean user is register success
applyStr := global.Redis.HGet(global.REGISTER_REDIS_KEY, email).Val()
rowsAffected := global.Redis.HDel(global.REGISTER_REDIS_KEY, email).Val()
if rowsAffected == 0 {
return false
}
var apply map[string]interface{}
_ = json.Unmarshal([]byte(applyStr), &apply)
if !NoDuplicatePhoneOrEmail(apply["phone"].(string), apply["email"].(string)) {
return false
}
colMap := models.MapJ2c[models.BackUser](apply, true)
ok, rowsAffected := models.Upsert[models.BackUser](colMap)
return ok && rowsAffected != 0
}
// ChangePassword user change password, or user forgot password
func ChangePassword(changePwd map[string]interface{}) bool {
match := VerifyEmailCode(changePwd["email"].(string), changePwd["code"].(string))["match"].(bool)
if !match {
return false
}
newPassword := utils.PasswordEncrypt(changePwd["newPassword"].(string))
colMap := map[string]interface{}{
"id": 1,
"password": newPassword,
}
models.BeforeSave(colMap, -1)
delete(colMap, "id")
rowAffected := global.Db.Model(models.BackUser{}).Where("email = ?", changePwd["email"]).Updates(colMap).RowsAffected
if rowAffected == 0 {
return false
}
now := time.Now().Unix()
global.Redis.HSet(global.CHANGEPWD_REDIS_KEY, changePwd["email"].(string), now)
return true
}
// NoDuplicatePhoneOrEmail detect the phone or email is registered or not
func NoDuplicatePhoneOrEmail(phone string, email string) bool {
var queryMap []map[string]interface{}
if phone != "" {
queryMap = append(queryMap, map[string]interface{}{"phone": phone})
}
if email != "" {
queryMap = append(queryMap, map[string]interface{}{"email": email})
}
return len(queryMap) != 0 && models.Count[models.BackUser](queryMap) == 0
}
// SendEmailCode used to send email verify code
func SendEmailCode(email string) bool {
code := uuid.New().String()[0:6]
text := fmt.Sprintf("Your Verify Code is :%s, Will Expire After 10 Minutes", code)
subject := "nCovTrack Verify"
// only set expired, not limit the frequency of use
global.Redis.Set(EMAIL_CODE_REDIS_KEY+email, code, 10*time.Minute)
return utils.SendEmail(subject, text, email)
}
// VerifyEmailCode use to verify user's verify code is correct or not
func VerifyEmailCode(email string, code string) map[string]interface{} {
verifyCode := global.Redis.Get(EMAIL_CODE_REDIS_KEY + email).Val()
return map[string]interface{}{"match": verifyCode == code}
}

View File

@@ -21,3 +21,9 @@ jwt:
secret: bWF5YmVJYWxzb3NhbWV0b2JlZm9yZe secret: bWF5YmVJYWxzb3NhbWV0b2JlZm9yZe
renewExpireDays: 7 renewExpireDays: 7
renewAheadDays: 3 renewAheadDays: 3
email:
host: smtp.qq.com
port: 587
account: fallen-angle@foxmail.com
password: hxrisxltxsjvieec

26
utils/email.go Normal file
View File

@@ -0,0 +1,26 @@
package utils
import (
"fmt"
"github.com/jordan-wright/email"
"nCovTrack-Backend/global"
"net/smtp"
)
func SendEmail(subject string, text string, to ...string) bool {
//TODO: add logs
e := email.Email{
From: "nCovTrack Server<1853633282@qq.com>",
To: to,
Subject: subject,
Text: []byte(text),
}
err := e.Send(
fmt.Sprintf("%s:%d", global.ServerSettings.Email.Host, global.ServerSettings.Email.Port),
smtp.PlainAuth("", global.ServerSettings.Email.Account, global.ServerSettings.Email.Password, global.ServerSettings.Email.Host),
)
if err != nil {
return false
}
return true
}

23
utils/encrypt.go Normal file
View File

@@ -0,0 +1,23 @@
package utils
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/google/uuid"
)
func PasswordEncrypt(password string) string {
salt := uuid.New().String()[24:]
password = password + salt
shaRes := hex.EncodeToString(sha256.New().Sum([]byte(password)))
encryptPwd := fmt.Sprintf("ncov$%s$%s", shaRes[0:24], salt)
return encryptPwd
}
func PasswordCompare(plaintext string, ciphertext string) (ok bool) {
salt := ciphertext[30:]
password := plaintext + salt
shaRes := hex.EncodeToString(sha256.New().Sum([]byte(password)))
return shaRes[0:24] == ciphertext[5:29]
}

View File

@@ -58,7 +58,7 @@ func RenewToken(tokenStr string) string {
} }
fmt.Println(expireDuration) fmt.Println(expireDuration)
claims["exp"] = time.Now().Add(15 * 24 * time.Hour).Unix() claims["exp"] = time.Now().Add(global.TOKEN_EXPIRE_DAYS * 24 * time.Hour).Unix()
token = jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token = jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenStr, err = token.SignedString(JWT_KEY) tokenStr, err = token.SignedString(JWT_KEY)
if err != nil { if err != nil {
@@ -66,3 +66,18 @@ func RenewToken(tokenStr string) string {
} }
return tokenStr return tokenStr
} }
func ParseClaims(tokenStr string) jwt.MapClaims {
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
return JWT_KEY, nil
})
if err != nil {
switch err.(*jwt.ValidationError).Errors {
case jwt.ValidationErrorSignatureInvalid:
return nil
case jwt.ValidationErrorIssuedAt:
return nil
}
}
return token.Claims.(jwt.MapClaims)
}

14
utils/models.go Normal file
View File

@@ -0,0 +1,14 @@
package utils
type UtilRequestInfo struct {
Url string
Header string
Body string
Timeout int
}
type GinResponse struct {
Code int `json:"code"`
Msg interface{} `json:"msg"`
Data interface{} `json:"data"`
}

View File

@@ -1,67 +0,0 @@
package utils
import (
"gorm.io/gorm/clause"
"nCovTrack-Backend/global"
"reflect"
"regexp"
"time"
)
var colNameReg, _ = regexp.Compile(".*column:(.*);?")
var uniqueKeyReg, _ = regexp.Compile(".*(primaryKey|unique).*")
func getNotZeroFields[T any](model T) []string {
t := reflect.TypeOf(model)
v := reflect.ValueOf(model)
var notZeroFields []string
for i := 0; i < t.NumField(); i++ {
if !v.Field(i).IsZero() {
colName := colNameReg.FindSubmatch([]byte(t.Field(i).Tag.Get("gorm")))
if len(colName) != 2 {
panic("Model Tag regex error")
}
notZeroFields = append(notZeroFields, string(colName[1]))
}
}
return notZeroFields
}
func Upsert[T any](model *T, forceUpdateFiled ...string) (ok bool) {
t := reflect.TypeOf(model).Elem()
v := reflect.ValueOf(model).Elem()
var uniqueKeyField []clause.Column
notZeroField := NewSet(forceUpdateFiled...).Add("modify_time")
for i := 0; i < t.NumField(); i++ {
gormTag := t.Field(i).Tag.Get("gorm")
if uniqueKey(gormTag) {
uniqueKeyField = append(uniqueKeyField, clause.Column{Name: columnName(gormTag)})
continue
}
if !v.Field(i).IsZero() {
notZeroField.Add(columnName(gormTag))
} else if t.Field(i).Type.Name() == "Time" {
v.Field(i).Set(reflect.ValueOf(time.Now()))
}
}
tx := global.Db.Clauses(clause.OnConflict{
Columns: uniqueKeyField,
DoUpdates: clause.AssignmentColumns(notZeroField.ToSlice()),
}, clause.Returning{}).Create(model)
if tx.Error != nil {
return false
}
return true
}
func columnName(gormTag string) string {
colNames := colNameReg.FindSubmatch([]byte(gormTag))
if len(colNames) != 2 {
panic("Model tag regex error")
}
return string(colNames[1])
}
func uniqueKey(gormTag string) bool {
return uniqueKeyReg.Match([]byte(gormTag))
}

View File

@@ -1 +0,0 @@
package utils

View File

@@ -1,51 +1,26 @@
package utils package utils
import ( import (
"nCovTrack-Backend/models"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
const ( const (
SUCCESS = "Success" SUCCESS = "Success"
BAD_REQUEST = "Bad Request"
DATA_NOT_FOUND = "Data not Found"
STATUS_DATA_NOT_FOUND = 210
) )
func Success(c *gin.Context, status int, code int, msg interface{}, data interface{}) { func Success(c *gin.Context, status int, code int, msg interface{}, data interface{}) {
c.JSON(http.StatusOK, models.GinResponse{Code: code, Msg: msg, Data: data}) c.JSON(http.StatusOK, GinResponse{Code: code, Msg: msg, Data: data})
} }
func Error(c *gin.Context, status int, code int, msg interface{}, data interface{}) { func Error(c *gin.Context, status int, code int, msg interface{}, data interface{}) {
c.JSON(status, models.GinResponse{Code: code, Msg: msg, Data: data}) c.JSON(status, GinResponse{Code: code, Msg: msg, Data: data})
} }
func Succ(c *gin.Context, data interface{}) { func Succ(c *gin.Context, data interface{}) {
Success(c, http.StatusOK, http.StatusOK, SUCCESS, data) Success(c, http.StatusOK, http.StatusOK, SUCCESS, data)
} }
func DataNotFound(c *gin.Context, data interface{}) {
Success(c, http.StatusOK, STATUS_DATA_NOT_FOUND, DATA_NOT_FOUND, data)
}
func Err(c *gin.Context, status int, code int, msg interface{}) { func Err(c *gin.Context, status int, code int, msg interface{}) {
Error(c, status, code, msg, nil) Error(c, status, code, msg, nil)
} }
func ServerError(c *gin.Context, code int, msg interface{}) {
Err(c, http.StatusInternalServerError, code, msg)
}
func ServerErr(c *gin.Context, msg interface{}) {
ServerError(c, http.StatusInternalServerError, msg)
}
func RequestError(c *gin.Context, code int, data interface{}) {
Error(c, http.StatusBadRequest, code, BAD_REQUEST, data)
}
func RequestErr(c *gin.Context, data interface{}) {
RequestError(c, http.StatusBadRequest, data)
}

View File

@@ -2,6 +2,8 @@ package utils
type void struct{} type void struct{}
// This is set with generic
type Set[T comparable] struct { type Set[T comparable] struct {
setMap map[T]void setMap map[T]void
} }

View File

@@ -1 +0,0 @@
package utils

View File

@@ -4,6 +4,6 @@ import (
"time" "time"
) )
func FormateDate(date time.Time) string { func FormatDate(date time.Time) string {
return date.Format("06-01-02") return date.Format("06-01-02")
} }