Commit 1f699944 authored by 李科's avatar 李科

init project

parent e425989f
workspace:
path: /go/src/gitlab.wodcloud.com/smart-operation/so-operation-api
volumes:
- name: docker-sock
host:
path: /var/run/docker.sock
kind: pipeline
name: dev
trigger:
branch:
- dev
clone:
disable: true
steps:
- name: fetch
image: registry.cn-qingdao.aliyuncs.com/wod/devops-git:1.0
network_mode: host
- name: s3-cache
image: registry.cn-qingdao.aliyuncs.com/wod/devops-s3-cache:1.0
network_mode: host
settings:
restore: true
mount:
- ./vendor
endpoint: http://cache.wodcloud.com
access_key:
from_secret: ACCESS_KEY_MINIO
secret_key:
from_secret: SECRET_KEY_MINIO
- name: build
image: registry.cn-qingdao.aliyuncs.com/wod/devops-go-arch:1.19-alpine
environment:
GOPROXY: https://goproxy.cn
settings:
binary: so-operation-api
main: src
goarchs: amd64,arm64
- name: docker-amd64
image: registry.cn-qingdao.aliyuncs.com/wod/devops-docker:1.0
volumes:
- name: docker-sock
path: /var/run/docker.sock
settings:
base: registry.cn-qingdao.aliyuncs.com/wod/alpine:3.16-amd64
dockerfile: .beagle/dockerfile
repo: smart-operation/so-operation-api
Version: "v1.0.0"
channel: amd64
args: "TARGETOS=linux,TARGETARCH=amd64"
registry: hub.wodcloud.com
registry_user:
from_secret: REGISTRY_USER
registry_password:
from_secret: REGISTRY_PASSWORD
- name: deploy-cloud
image: registry.cn-qingdao.aliyuncs.com/wod/devops-kubernetes:1.0
settings:
namespace: smart-operation
deployment: so-operation-api
container: so-operation-api
image: hub.wodcloud.com/smart-operation/so-operation-api:v1.0.0-amd64
environment:
KUBERNETES_SERVER:
from_secret: KUBERNETES_SERVER
KUBERNETES_TOKEN:
from_secret: KUBERNETES_TOKEN
- name: docker-arm64
image: registry.cn-qingdao.aliyuncs.com/wod/devops-docker:1.0
volumes:
- name: docker-sock
path: /var/run/docker.sock
settings:
base: registry.cn-hangzhou.aliyuncs.com/xmod/alpine:3-arm64
dockerfile: .beagle/dockerfile
repo: smart-operation/so-operation-api
Version: "v1.0.0"
channel: arm64
args: "TARGETOS=linux,TARGETARCH=arm64"
registry: hub.wodcloud.com
registry_user:
from_secret: REGISTRY_USER
registry_password:
from_secret: REGISTRY_PASSWORD
- name: docker-arch
image: registry.cn-qingdao.aliyuncs.com/wod/devops-docker-manifest:1.0
volumes:
- name: docker
path: /var/run/docker.sock
settings:
insecure: true
platforms: linux/amd64,linux/arm64
template: hub.wodcloud.com/smart-operation/so-operation-api:v1.0.0-ARCH
target: hub.wodcloud.com/smart-operation/so-operation-api:v1.0.0
username:
from_secret: REGISTRY_USER
password:
from_secret: REGISTRY_PASSWORD
---
clone:
disable: true
trigger:
branch:
- master
volumes:
- name: docker-sock
host:
path: /var/run/docker.sock
kind: pipeline
name: master
steps:
- name: harbor-amd64
image: registry.cn-qingdao.aliyuncs.com/wod/devops-docker-tag:1.0
volumes:
- name: docker-sock
path: /var/run/docker.sock
pull: always
environment:
REGISTRY_USER:
from_secret: REGISTRY_USER
REGISTRY_PASSWORD:
from_secret: REGISTRY_PASSWORD
settings:
source: hub.wodcloud.com/smart-operation/so-operation-api:v1.0.0-amd64
target: hub.wodcloud.com/smart-operation/so-operation-api:v1.0-amd64
registry: hub.wodcloud.com
- name: harbor-arm64
image: registry.cn-qingdao.aliyuncs.com/wod/devops-docker-tag:1.0
volumes:
- name: docker-sock
path: /var/run/docker.sock
pull: always
environment:
REGISTRY_USER:
from_secret: REGISTRY_USER
REGISTRY_PASSWORD:
from_secret: REGISTRY_PASSWORD
settings:
source: hub.wodcloud.com/smart-operation/so-operation-api:v1.0.0-arm64
target: hub.wodcloud.com/smart-operation/so-operation-api:v1.0-arm64
registry: hub.wodcloud.com
---
kind: secret
name: REGISTRY_USER
get:
name: REGISTRY_USER
path: devops-secrets
---
kind: secret
name: REGISTRY_PASSWORD
get:
name: REGISTRY_PASSWORD
path: devops-secrets
---
kind: secret
name: REGISTRY_USER_ALIYUN
get:
name: USERNAME
path: devops-registry-aliyun
---
kind: secret
name: REGISTRY_PASSWORD_ALIYUN
get:
name: PASSWORD
path: devops-registry-aliyun
---
kind: secret
name: KUBERNETES_SERVER
get:
name: KUBERNETES_SERVER
path: devops-deploy
---
kind: secret
name: KUBERNETES_TOKEN
get:
name: KUBERNETES_TOKEN
path: devops-deploy
---
kind: secret
name: ACCESS_KEY_MINIO
get:
name: ACCESS_KEY
path: devops-minio
---
kind: secret
name: SECRET_KEY_MINIO
get:
name: SECRET_KEY
path: devops-minio
ARG BASE
FROM $BASE
ENV TZ Asia/Shanghai
ARG TARGETOS
ARG TARGETARCH
ARG TARGETBINARY
WORKDIR /app
COPY ./dist/$TARGETBINARY-$TARGETOS-$TARGETARCH /app/$TARGETBINARY
EXPOSE 80
ENTRYPOINT ["/app/so-operation-api", "--port=80", "--prefix=/v1/api"]
\ No newline at end of file
### VisualStudioCode template
.vscode/*
*.code-workspace
# Local History for Visual Studio Code
.history/
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
# File-based project format
*.iws
# IntelliJ
out/
.idea
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
### Go template
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
vendor/
### Vim template
# Swap
[._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
Sessionx.vim
# Temporary
.netrwhist
*~
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
### Archives template
# It's better to unpack these files and commit the raw source because
# git has its own built in compression methods.
*.7z
*.jar
*.rar
*.zip
*.gz
*.gzip
*.tgz
*.bzip
*.bzip2
*.bz2
*.xz
*.lzma
*.cab
*.xar
# Packing-only formats
*.iso
*.tar
# Package management formats
*.dmg
*.xpi
*.gem
*.egg
*.deb
*.rpm
*.msi
*.msm
*.msp
*.txz
\ No newline at end of file
module gitlab.wodcloud.com/smart-operation/so-operation-api
go 1.19
require (
github.com/gin-gonic/gin v1.9.0
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.13.0
github.com/go-redis/redis v6.15.9+incompatible
github.com/google/uuid v1.3.0
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/lib/pq v1.10.9
github.com/spf13/cast v1.5.0
github.com/spf13/pflag v1.0.5
github.com/thoas/go-funk v0.9.3
github.com/valyala/fasthttp v1.47.0
go.uber.org/zap v1.24.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
xorm.io/xorm v1.3.2
)
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/bytedance/sonic v1.8.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.3 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/leodido/go-urn v1.2.3 // indirect
github.com/lestrrat-go/strftime v1.0.6 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 // indirect
)
This diff is collapsed.
package entity
type User struct {
Id string `json:"id" xorm:"id"` //uuid
UserId string `json:"user_id" xorm:"user_id"` //用户id
UserName string `json:"user_name" xorm:"user_name"` //用户名称
UserType int `json:"user_type" xorm:"user_type"` //用户类型
Phone string `json:"phone" xorm:"phone"` //联系电话
Email string `json:"email" xorm:"email"` //邮箱
UnitName string `json:"unit_name" xorm:"unit_name"` //迁移单位名称
CreatedBy string `json:"created_by" xorm:"created_by"` //创建人
CreatedAt string `json:"created_at" xorm:"created_at"` //创建时间
UpdatedBy string `json:"updated_by" xorm:"updated_by"` //更新人
UpdatedAt string `json:"updated_at" xorm:"updated_at"` //更新时间
Status int `json:"status" xorm:"status"` //状态 1 正常 2 禁用 3 删除
}
package request
// Pagination 分页
type Pagination struct {
Page int `json:"page" form:"page" binding:"omitempty"`
PageSize int `json:"page_size" form:"page_size" binding:"omitempty"`
}
func (p Pagination) GetPage() int {
if p.Page == 0 {
p.Page = 1
}
return p.Page
}
func (p Pagination) GetPageSize() int {
if p.PageSize == 0 {
p.PageSize = 15
}
return p.PageSize
}
package request
type UserAddReq struct {
UserType int `json:"user_type" form:"user_type" binding:"required,oneof=2 3 4"` //用户类型
UserId string `json:"user_id" form:"user_id" binding:"required"` //账号
UserName string `json:"user_name" form:"user_name" binding:"required"` //负责人
Email string `json:"email" form:"email" binding:"email"` //邮箱
Password string `json:"password" form:"password" binding:"required,gte=6"` //密码
PassWordRepeat string `json:"password_repeat" form:"password_repeat" binding:"required,gte=6,eqfield=password"` //重复密码
//普通用户、租户 独有字段
UnitName string `json:"unit_name" form:"unit_name" binding:"omitempty"` //单位名称
Phone string `json:"phone" form:"phone" binding:"omitempty"` //联系方式
Id string `json:"id" form:"id" binding:"omitempty"` //用户唯一标识
}
type UserListReq struct {
Keyword string `json:"keyword" form:"keyword" binding:"omitempty"` //搜索关键词
UserType int `json:"user_type" form:"user_type" binding:"omitempty,oneof=1 2 3 4"` //用户类型
UserId string `json:"user_id" form:"user_id" binding:"omitempty"` //账号
Id string `json:"id" form:"id" binding:"omitempty"` //用户唯一标识
OrderBy string `json:"order_by" form:"order_by" binding:"omitempty"` //排序
Pagination
}
type UserDelReq struct {
UserIds []string `json:"user_ids" form:"user_ids[]" binding:"required"` //用户账号集合
}
type UserUpdateReq struct {
OptType string `json:"opt_type" form:"opt_type" binding:"required,oneof=change_pass reset_pass change_info"`
UserId string `json:"user_id" form:"user_id" binding:"required"` //账号
//基础信息
UserName string `json:"user_name" form:"user_name" binding:"omitempty,required_if=opt_type change_info"` //负责人
Email string `json:"email" form:"email" binding:"omitempty,required_if=opt_type change_info,email"` //邮箱
//迁移人员 独有字段
UnitName string `json:"unit_name" form:"unit_name" binding:"omitempty,required_if=opt_type change_info"` //迁移单位名称
Phone string `json:"phone" form:"phone" binding:"omitempty,required_if=opt_type change_info"` //联系方式
//修改密码、重置密码
OriginalPassword string `json:"original_password" form:"original_password" binding:"required_if=opt_type change_pass,omitempty,gte=6"` //原始密码
Password string `json:"password" form:"password" binding:"required_if=opt_type change_pass,required_if=opt_type reset_pass,omitempty,gte=6"` //新密码
PassWordRepeat string `json:"password_repeat" form:"password_repeat" binding:"required_if=opt_type change_pass,required_if=opt_type reset_pass,omitempty,gte=6,eqfield=password"` //重复密码
}
package echarts
// 饼状图
type PieChart struct {
Name string `json:"name"`
Value string `json:"value"`
}
// 饼状图
type PieChartInt struct {
Name string `json:"name"`
Value int `json:"value"`
}
// 折线图
type LineChart struct {
XAxis `json:"xAxis"`
YAxis `json:"yAxis"`
Series []Series `json:"series"`
}
// 数据
type Series struct {
Name string `json:"name"`
Data []interface{} `json:"data"`
}
// X 轴
type XAxis struct {
Data []string `json:"data"`
}
// Y 轴
type YAxis struct {
Data []string `json:"data"`
}
// 柱状图
type Histogram struct {
XAxis `json:"xAxis"`
YAxis `json:"yAxis"`
Series []Series `json:"series"`
}
package response
import (
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/bean/entity"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/pkg/beagle/jsontime"
)
type UserListItem struct {
entity.User
CreatedAt jsontime.Time `column:"created_at" json:"created_at"` //创建时间
UpdatedAt jsontime.Time `column:"updated_at" json:"updated_at"` //更新时间
}
type UserList struct {
TotalCount int `json:"total_count"`
List []UserListItem `json:"list"`
}
type UserMigListItem struct {
Id string `json:"id"` //用户唯一标识
UnitName string `json:"unit_name"` //迁移单位名称
Phone string `json:"phone"` //联系电话
UserName string `json:"username"` //用户名称
}
/**
* @Author: gaoshiyao
* @Description: db
* @File: db
* @Date: 2021/05/08 14:33
*/
package client
import (
"time"
_ "github.com/lib/pq"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/common/conf"
"go.uber.org/zap"
"xorm.io/xorm"
"xorm.io/xorm/names"
)
var dbCli *xorm.Engine
func GetDbClient() (xormDB *xorm.Engine, err error) {
if dbCli != nil {
return dbCli, nil
}
xormDB, err = xorm.NewEngine(conf.Options.DbDriver, conf.Options.DbURL)
if err != nil {
conf.Logger.Error("创建xorm引擎", zap.Error(err), zap.String("dbURL", conf.Options.DbURL))
return nil, err
}
if err = xormDB.Ping(); err != nil {
conf.Logger.Error("ping db", zap.Error(err), zap.String("dbURL", conf.Options.DbURL))
return nil, err
}
xormDB.SetMapper(names.SnakeMapper{})
if conf.RunMode == "debug" {
xormDB.ShowSQL(true)
}
xormDB.TZLocation, _ = time.LoadLocation("Asia/Shanghai")
xormDB.SetMaxOpenConns(50) // 数据库连接数
xormDB.SetMaxIdleConns(10) // 空闲数
xormDB.DB().SetConnMaxIdleTime(5 * time.Minute) // 最大闲置时间
dbCli = xormDB
conf.Logger.Info("connect db", zap.String("dbURL", conf.Options.DbURL))
return dbCli, err
}
/**
* @Author: gaoshiyao
* @Description: redis
* @File: redis
* @Date: 2021/05/08 14:45
*/
package client
import (
"fmt"
"strings"
"time"
"github.com/go-redis/redis"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/common/conf"
"go.uber.org/zap"
)
type Redis struct {
Conn *redis.Client
Prefix string
}
var rs Redis
func GetRedisClient() (rcon Redis, e error) {
if rs.Conn != nil {
return rs, nil
}
addr := conf.Options.RedisURL
if strings.HasPrefix(addr, "redis://") {
addr = strings.TrimPrefix(addr, "redis://")
}
rcon.Conn = redis.NewClient(&redis.Options{
Addr: addr,
Password: "", // no password set
DB: conf.Options.RedisDB, // use default DB
})
if _, e := rcon.Conn.Ping().Result(); e != nil {
conf.Logger.Error("connect redis err", zap.Error(e), zap.String("redis url", addr))
return rcon, e
}
rcon.Prefix = conf.Options.RedisTag
rs = rcon
conf.Logger.Info("connect redis", zap.String("redis url", addr))
return
}
func (r Redis) HGet(key string, tag string) (str string, err error) {
key = fmt.Sprintf("%s-%s", strings.ToUpper(r.Prefix), strings.ToUpper(key))
str, err = r.Conn.HGet(strings.ToUpper(key), strings.ToUpper(tag)).Result()
return str, err
}
func (r Redis) HSet(key string, tag string, value interface{}) error {
key = fmt.Sprintf("%s-%s", strings.ToUpper(r.Prefix), strings.ToUpper(key))
bmd := r.Conn.HSet(strings.ToUpper(key), strings.ToUpper(tag), value)
return bmd.Err()
}
func (r Redis) HDel(key string, tag string) error {
key = fmt.Sprintf("%s-%s", strings.ToUpper(r.Prefix), strings.ToUpper(key))
bmd := r.Conn.HDel(strings.ToUpper(key), strings.ToUpper(tag))
return bmd.Err()
}
func (r Redis) Get(key string) (str string, err error) {
key = fmt.Sprintf("%s-%s", strings.ToUpper(r.Prefix), strings.ToUpper(key))
str, err = r.Conn.Get(strings.ToUpper(key)).Result()
return str, err
}
func (r Redis) Set(key string, value interface{}, time time.Duration) error {
key = fmt.Sprintf("%s-%s", strings.ToUpper(r.Prefix), strings.ToUpper(key))
bmd := r.Conn.Set(strings.ToUpper(key), value, time)
return bmd.Err()
}
func (r Redis) Del(key string) error {
key = fmt.Sprintf("%s-%s", strings.ToUpper(r.Prefix), strings.ToUpper(key))
bmd := r.Conn.Del(strings.ToUpper(key))
return bmd.Err()
}
package conf
import (
"go.uber.org/zap"
)
var (
Options *Config
Logger *zap.Logger
LoggerLevel string
RunMode string
)
// 公共配置
type Config struct {
DbURL string
DbDriver string
RedisURL string
RedisDB int
RedisTag string
Prefix string
LogDirPrefix string
LogDirName string
LogSaveDays int
LogMode int
ArgBool bool
ArgInt int
}
package controller
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/pkg/beagle/resp"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/util"
)
// 健康检查
func Health(c *gin.Context) {
SendJsonResponse(c, resp.OK, gin.H{
"host": c.Request.Host,
"header": c.Request.Header,
"serverTime": time.Now(),
"ip": util.RemoteIp(c.Request),
})
}
// 发送json响应信息
func SendJsonResponse(c *gin.Context, err error, data interface{}) {
code, message, data := resp.DecodeErr(err, data)
c.JSON(http.StatusOK, resp.Resp{
Code: code,
Msg: message,
Data: data,
})
return
}
/**
* @Author: gaoshiyao
* @Description: example
* @File: example
* @Date: 2021/05/08 13:54
*/
package controller
import (
"github.com/gin-gonic/gin"
"github.com/spf13/cast"
"github.com/thoas/go-funk"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/bean/vo/request"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/common/conf"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/pkg/beagle/resp"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/service"
"go.uber.org/zap"
)
// 示例
func Example(c *gin.Context) {
name := c.Query("name")
var req struct {
Name string `json:"name" form:"name" binding:"required"`
request.Pagination
}
if err := c.ShouldBind(&req); err != nil {
SendJsonResponse(c, resp.InvalidParam.TranslateError(err), "")
return
}
// 日志示例
conf.Logger.Sugar().Infof("cast类型转换器:%d", cast.ToInt("123"))
conf.Logger.Sugar().Infof("funk工具,数组最大值:%d,数组去重:%d", funk.MaxInt([]int{1, 2, 3, 4, 5}), funk.UniqInt([]int{3, 1, 2, 2, 5}))
conf.Logger.Info("示例信息", zap.String(name, "调用成功"))
SendJsonResponse(c, resp.OK, name)
}
// 查询数据
func GetExampleList(c *gin.Context) {
svc := service.Example{}
ls, err := svc.List()
if err != nil {
SendJsonResponse(c, err, "")
return
}
SendJsonResponse(c, resp.OK, ls)
}
package main
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/spf13/pflag"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/common/client"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/common/conf"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/pkg/beagle/log"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/router"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/util"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// run args
var (
port = pflag.Int("port", 80, "server port")
prefix = pflag.String("prefix", "/v1/api", "url prefix")
)
// main start
func main() {
initTimeZone()
pflag.Parse() // init start args
initConfig()
// init log config
cfg := initLogConfig()
conf.Logger = log.NewLogger(cfg, conf.LoggerLevel)
// init validator
util.RegValid()
// db client
go client.GetDbClient()
// redis client
go client.GetRedisClient()
// server start...
conf.Logger.Error("server start err", zap.Error(newServer().ListenAndServe()))
}
// init commonutil config value
func initConfig() {
conf.LoggerLevel = util.SetEnvStr("LOG_LEVEL", "info")
conf.RunMode = util.SetEnvStr("GIN_MODE", "debug") // project run mode, available parameters: debug、release
conf.Options = &conf.Config{
Prefix: *prefix,
DbURL: util.SetEnvStr("DB_URL", "host=localhost port=1131 user=postgres password=spaceIN511 dbname=cloud sslmode=disable"),
DbDriver: util.SetEnvStr("DB_DRIVER", "postgres"),
RedisURL: util.SetEnvStr("REDIS_URL", "localhost:6379"),
RedisDB: 0,
RedisTag: "bg",
LogDirPrefix: util.SetEnvStr("LOG_DIR_PREFIX", "/app/log"), // 日志目录
LogDirName: util.SetEnvStr("LOG_NAME", "syslog"), // 日志名称
LogSaveDays: util.SetEnvInt("LOG_SAVE_DAYS", 7), // 日志最大存储天数
LogMode: util.SetEnvInt("LOG_MODE", 1), // 1.标准打印 2.输出文件
ArgBool: util.SetEnvBool("ARG_BOOL", false), // 示例参数
ArgInt: util.SetEnvInt("ARG_INT", 10), // 示例参数
}
}
// init log config
func initLogConfig() zapcore.EncoderConfig {
return zapcore.EncoderConfig{
LevelKey: "level",
TimeKey: "time",
MessageKey: "msg",
NameKey: "logger",
CallerKey: "caller",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05"),
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
}
func newServer() *http.Server {
// set run mod
gin.SetMode(conf.RunMode)
// load gin router
r := gin.New()
router.Load(r, log.GinLogger(), log.GinRecovery(true))
conf.Logger.Info("server is starting...", zap.Int("port", *port))
return &http.Server{
Addr: fmt.Sprintf(":%d", *port),
Handler: r,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
MaxHeaderBytes: 1 << 20,
}
}
func initTimeZone() {
//var cstZone, _ = time.LoadLocation("Asia/Shanghai")
var cstZone = time.FixedZone("CST", 8*3600) // 固定东八区(CST: China Standard Time)
time.Local = cstZone
}
package jsontime
import (
"bytes"
"database/sql/driver"
"fmt"
"strings"
"time"
)
// 时间序列化和反序列化
var CSTZone = time.FixedZone("CST", 8*3600) // 固定东八区(CST: China Standard Time)
// Time 自定义时间
// 设置当前时间: jsonDate := Time(time.Now())
type Time time.Time
// MarshalJSON JsonDate序列化
func (t Time) MarshalJSON() ([]byte, error) {
b := make([]byte, 0, len("2006-01-02 15:04:05")+2)
b = append(b, '"')
b = time.Time(t).AppendFormat(b, "2006-01-02 15:04:05")
b = append(b, '"')
//b = bytes.Trim(b, "\"")
if bytes.Contains(b, []byte("0001-01-01 00:00:00")) {
b = []byte("\"\"")
}
return b, nil
}
// UnmarshalJSON JsonDate反序列化
func (t *Time) UnmarshalJSON(data []byte) error {
s := string(data)
s = strings.Trim(s, "\"")
if s == "" {
*t = Time(time.Time{})
return nil
}
layouts := []string{
"2006-01-02 15:04:05",
"2006-01-02 15:04:05.000",
"2006-01-02 15:04:05.000000",
"2006-01-02T15:04:05Z",
"2006-01-02T15:04:05.000Z",
"2006-01-02T15:04:05.000000Z",
}
var err error
var jDate time.Time
for _, layout := range layouts {
jDate, err = time.ParseInLocation(layout, s, time.Local)
if err == nil {
break
}
}
if err != nil {
return err
}
*t = Time(jDate)
return nil
}
func (t *Time) String() string {
stamp := time.Time(*t).Format("2006-01-02 15:04:05")
if stamp == "0001-01-01 00:00:00" {
return ""
}
return stamp
}
// Value 实现 driver.Valuer 接口
func (t Time) Value() (driver.Value, error) {
return time.Time(t), nil
}
// Scan 实现 sql.Scanner 接口
func (t *Time) Scan(value interface{}) error {
switch v := value.(type) {
case time.Time:
*t = Time(v)
case []byte:
jTime, err := time.ParseInLocation("2006-01-02 15:04:05", string(v), time.Local)
if err != nil {
return err
}
*t = Time(jTime)
case string:
jTime, err := time.ParseInLocation("2006-01-02 15:04:05", v, time.Local)
if err != nil {
return err
}
*t = Time(jTime)
case nil:
*t = Time(time.Time{})
default:
return fmt.Errorf("Time.Scan: unsupported type %T", value)
}
return nil
}
package log
import (
"fmt"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/common/conf"
"io"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
func NewSugaredLogger(config zapcore.EncoderConfig, logLevel string) *zap.SugaredLogger {
return initLog(config, logLevel).Sugar()
}
func NewLogger(config zapcore.EncoderConfig, logLevel string) *zap.Logger {
return initLog(config, logLevel)
}
func initLog(config zapcore.EncoderConfig, logLevel string) *zap.Logger {
atom := zap.NewAtomicLevel()
// zapcore.Lock(os.Stdout), // 1.标准输出
// zapcore.Lock(zapcore.AddSync(getWriter("syslog"))), // 2.写入日志文件(使用file-rotatelogs)
// zapcore.AddSync(WriteSyncer(conf.Options.LogDirName)) // 3.写入日志文件(使用lumberjack)
var writeSyncer zapcore.WriteSyncer
switch conf.Options.LogMode {
case 1:
writeSyncer = zapcore.Lock(os.Stdout)
case 2:
writeSyncer = zapcore.AddSync(WriteSyncer(conf.Options.LogDirName))
}
lg := zap.New(zapcore.NewCore(
zapcore.NewJSONEncoder(config),
writeSyncer,
atom,
),
zap.AddCaller(), // 打印日志调用位置和行号
//zap.AddCallerSkip(1), // 上级调用
) // 根据上面的配置创建logger
zap.ReplaceGlobals(lg) // 替换zap库里全局的logger
defer lg.Sync()
atom.UnmarshalText([]byte(logLevel))
return lg
}
// WriteSyncer lumberjack 日志分割工具
func WriteSyncer(filename string) (writeSyncer zapcore.WriteSyncer) {
// 使用 lumberjack 实现 log rotate
fileName := fmt.Sprintf("%s/%s-%s.log", conf.Options.LogDirPrefix, filename, time.Now().Format("2006-01-02"))
lumberJackLogger := &lumberjack.Logger{
Filename: fileName, // 日志文件的位置
MaxSize: 100, // 单个文件最大100MB
MaxBackups: 5, // 多于 5 个日志文件后,清理较旧的日志
MaxAge: 1, // 一天一切割
Compress: false, // 是否压缩/归档旧文件
}
return zapcore.AddSync(lumberJackLogger)
}
// WriteSyncer2 file-rotatelogs 日志分割工具
func WriteSyncer2(filename string) io.Writer {
// 生成rotatelogs的Logger 实际生成的文件名 demo.log.YYmmddHH
// demo.log是指向最新日志的链接
// 保存7天内的日志,每1小时(整点)分割一次日志
hook, err := rotatelogs.New(
conf.Options.LogDirPrefix+"/"+filename+"-%Y-%m-%d.log", // 没有使用go风格反人类的format格式
//rotatelogs.WithLinkName(filename),
rotatelogs.WithMaxAge(time.Duration(conf.Options.LogSaveDays)*24*time.Hour),
rotatelogs.WithRotationTime(time.Hour*24),
)
if err != nil {
panic(err)
}
return hook
}
// GinLogger 接收gin框架默认的日志
func GinLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
cost := time.Since(start)
zap.L().Info("visit",
zap.Int("status", c.Writer.Status()),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", c.ClientIP()),
zap.String("user-agent", c.Request.UserAgent()),
zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
zap.Duration("cost", cost),
)
}
}
// GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志
func GinRecovery(stack bool) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
zap.L().Error(c.Request.URL.Path,
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) // nolint: errcheck
c.Abort()
return
}
if stack {
zap.L().Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
zap.String("stack", string(debug.Stack())),
)
} else {
zap.L().Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
}
c.AbortWithStatus(http.StatusInternalServerError)
}
}()
c.Next()
}
}
package resp
// xxx 前三位采用谷歌http错误码 xx 模块 xx 错误类型
// 400 客户端错误
// 401 权限错误
// 500 服务器错误
// 501 数据库错误
var (
OK = Resp{Code: 200, Msg: "OK"}
FAIL = Resp{Code: 10001, Msg: "操作失败"}
InvalidParam = Resp{Code: 10003, Msg: "参数错误"}
Unauthorized = Resp{Code: 10004, Msg: "认证失败"}
ForBidden = Resp{Code: 10005, Msg: "没有权限"}
NotFound = Resp{Code: 10006, Msg: "未找到资源"}
LoginFail = Resp{Code: 10007, Msg: "登录失败"}
)
package resp
import (
"fmt"
"github.com/go-playground/validator/v10"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/util"
"strings"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/common/conf"
"go.uber.org/zap"
)
type Resp struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"`
}
func (r Resp) WithMsg(msg string) Resp {
r.Msg = msg
return r
}
func (r Resp) WithMsgF(msg string, a ...interface{}) Resp {
r.Msg = fmt.Sprintf(msg, a)
return r
}
func (r Resp) WithData(data interface{}) Resp {
r.Data = data
return r
}
func (r Resp) WithError(err error) Resp {
if err != nil {
r.Data = err.Error()
}
return r
}
// TranslateError 翻译validate验证错误
func (r Resp) TranslateError(err error) Resp {
translatedErrors := make([]string, 0)
if validationErrors, ok := err.(validator.ValidationErrors); ok {
for _, validationError := range validationErrors {
translatedErrors = append(translatedErrors, validationError.Translate(util.Trans))
}
} else {
translatedErrors = append(translatedErrors, err.Error())
}
r.Msg = strings.Join(translatedErrors, ",")
return r
}
func (r Resp) Error() string {
return fmt.Sprintf("code:%d Message:%s", r.Code, r.Msg)
}
func DecodeErr(err error, data interface{}) (int, string, interface{}) {
if err == nil {
return OK.Code, OK.Msg, data
}
switch typed := err.(type) {
case Resp:
if typed.Code == 200 {
return OK.Code, OK.Msg, data
}
return typed.Code, typed.Msg, typed.Data
case *Resp:
return typed.Code, typed.Msg, typed.Data
}
conf.Logger.Error("响应错误信息被拦截", zap.Error(err))
return Unauthorized.Code, Unauthorized.Msg, err.Error()
}
package header
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// NoCache is a middleware function that appends headers
// to prevent the client from caching the HTTP response.
func NoCache(c *gin.Context) {
c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
c.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
c.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
c.Next()
}
// Options is a middleware function that appends headers
// for options requests and aborts then exits the middleware
// chain and ends the request.
func Options(c *gin.Context) {
if c.Request.Method != "OPTIONS" {
c.Next()
} else {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept")
c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Header("Content-Type", "application/json")
c.AbortWithStatus(200)
}
}
// Secure is a middleware function that appends security
// and resource access headers.
func Secure(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("X-Frame-Options", "DENY")
c.Header("X-Content-Type-Options", "nosniff")
c.Header("X-XSS-Protection", "1; mode=block")
if c.Request.TLS != nil {
c.Header("Strict-Transport-Security", "max-age=31536000")
}
// Also consider adding Content-Security-Policy headers
// c.Header("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com")
}
package header
import (
"bytes"
"github.com/gin-gonic/gin"
)
type responseBodyWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (r responseBodyWriter) Write(b []byte) (int, error) {
r.body.Write(b)
return r.ResponseWriter.Write(b)
}
func CopyResponseBody(c *gin.Context) {
w := &responseBodyWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer}
c.Writer = w
c.Next()
}
package router
import (
"github.com/gin-gonic/gin"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/common/conf"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/controller"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/router/middleware/header"
)
// 加载路由
func Load(r *gin.Engine, middleware ...gin.HandlerFunc) {
r.Use()
r.Use(gin.Recovery())
r.Use(header.NoCache)
r.Use(header.Options)
r.Use(header.Secure)
r.Use(header.CopyResponseBody)
r.Use(middleware...)
/*gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
conf.Logger.Info("routers", zap.String("endpoint", fmt.Sprintf("%v %v %v %v", httpMethod, absolutePath, handlerName, nuHandlers)))
}*/
base := r.Group(conf.Options.Prefix)
{
base.GET("/health", controller.Health) // 健康检查
base.GET("/example", controller.Example) // 示例
base.GET("/example/list", controller.GetExampleList) // 示例获取列表
}
}
package service
import (
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/bean/vo/response"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/common/client"
"gitlab.wodcloud.com/smart-operation/so-operation-api/src/pkg/beagle/resp"
)
type Example struct {
}
func (e Example) List() (result response.UserList, err error) {
db, err := client.GetDbClient()
if err != nil {
err = resp.FAIL.WithError(err)
return
}
if err = db.Find(&result); err != nil {
err = resp.FAIL.WithError(err)
return
}
return
}
package util
import (
"os"
"strconv"
)
func SetEnvStr(env string, value string) string {
if e := os.Getenv(env); e != "" {
return e
}
return value
}
func SetEnvInt(env string, value int) int {
if e := os.Getenv(env); e != "" {
if res, err := strconv.Atoi(e); err != nil {
panic("环境变量参数格式错误,无法注入!")
} else {
return res
}
}
return value
}
func SetEnvBool(env string, value bool) bool {
if e := os.Getenv(env); e != "" {
if res, err := strconv.ParseBool(e); err != nil {
panic("环境变量参数格式错误,无法注入!")
} else {
return res
}
}
return value
}
package util
import (
"crypto/tls"
"github.com/valyala/fasthttp"
"net"
"time"
)
const (
MediaTypeJSON = "application/json"
MediaTypeXML = "application/xml"
MediaTypeForm = "application/x-www-form-urlencoded"
MediaTypeMultipartForm = "multipart/form-data"
MediaTypeOctetStream = "application/octet-stream"
MediaTypeHTML = "text/html"
MediaTypePlain = "text/plain"
MediaTypeJavascript = "application/javascript"
MediaTypeCSS = "text/css"
MediaTypeGIF = "image/gif"
MediaTypeJPEG = "image/jpeg"
MediaTypePNG = "image/png"
MediaTypeBMP = "image/bmp"
MediaTypeIcon = "image/x-icon"
MediaTypePDF = "application/pdf"
MediaTypeZip = "application/zip"
MediaTypeTAR = "application/x-tar"
MediaTypeGZ = "application/x-gzip"
MediaTypeProtobuf = "application/protobuf"
MediaTypeGRPC = "application/grpc"
)
// Request 包装fasthttp的请求工具
/*
请求示例
1.GET请求
Request("https://httpbin.org/get", http.MethodGet, nil, map[string]string{"Cookie": "token=e69f5609-378f-4461-89d0-7508f2a06022"})
2.POST请求
Request("https://httpbin.org/post",
http.MethodPost,
body,
map[string]string{"Content-Type": MediaTypeJSON})
3.PUT请求
Request("https://httpbin.org/put",
http.MethodPut,
body,
map[string]string{
"Content-Type": MediaTypeJSON,
"Cookie": "aweToken=3ab9f63f-b0b3-4935-80ec-405d76ac111d",
})
*/
func Request(url string, method string, body []byte, headers map[string]string) ([]byte, error) {
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
req.SetRequestURI(url)
req.Header.SetMethod(method)
// 设置默认请求头部
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36")
// 设置请求头部
for k, v := range headers {
req.Header.Set(k, v)
}
// 设置请求体
if body != nil {
req.SetBody(body)
}
// 创建一个忽略证书问题的客户端
client := &fasthttp.Client{
Dial: func(addr string) (net.Conn, error) {
return net.DialTimeout("tcp", addr, 30*time.Second)
},
TLSConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
// 发送请求
if err := client.Do(req, resp); err != nil {
return nil, err
}
// 返回响应体和错误信息
return resp.Body(), nil
}
/**
* @Author: gaoshiyao
* @Description: iputil
* @File: iputil
* @Date: 2021/05/08 13:55
*/
package util
import (
"net"
"net/http"
)
// RemoteIp 获取访问Ip
func RemoteIp(req *http.Request) string {
remoteAddr := req.RemoteAddr
if ip := req.Header.Get("XRealIP"); ip != "" {
remoteAddr = ip
} else if ip = req.Header.Get("XForwardedFor"); ip != "" {
remoteAddr = ip
} else {
remoteAddr, _, _ = net.SplitHostPort(remoteAddr)
}
if remoteAddr == "::1" {
remoteAddr = "127.0.0.1"
}
return remoteAddr
}
package util
import (
"github.com/google/uuid"
"strings"
)
func NewUUID() string {
return uuid.New().String()
}
func NewUUIDNoHyphens() string {
id := uuid.New().String()
return strings.Replace(id, "-", "", -1)
}
package util
import (
"fmt"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
translations "github.com/go-playground/validator/v10/translations/zh"
"reflect"
)
var (
validate *validator.Validate
Trans ut.Translator
)
func RegValid() {
// 注册自定义的校验器错误消息
Trans, _ = ut.New(zh.New()).GetTranslator("zh")
validate = binding.Validator.Engine().(*validator.Validate)
if err := translations.RegisterDefaultTranslations(validate, Trans); err != nil {
fmt.Println("RegisterDefaultTranslations failed", err)
return
}
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
if jsonTag := fld.Tag.Get("json"); jsonTag != "" {
return jsonTag
}
if formTag := fld.Tag.Get("form"); formTag != "" {
return formTag
}
return fld.Name
})
//validate.RegisterValidation("required_if", requiredIf)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment