init: import web_huangguo2 server project
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
package adcenter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/crypt"
|
||||
"videoweb/pub/common/godef"
|
||||
"videoweb/pub/common/httputil"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/redis"
|
||||
)
|
||||
|
||||
/*
|
||||
测试:
|
||||
部门CODE:1991129769626804224
|
||||
商户CODE:1991129808640122880
|
||||
应用CODE:jh_appid01
|
||||
客户CODE:1991129948159451136
|
||||
*/
|
||||
|
||||
type JtAdvertiseReq struct {
|
||||
MerchantCode string `json:"merchantCode"` // 商户 code,必填
|
||||
AppCode string `json:"appCode"` // 应用 code,必填
|
||||
AdStatus int `json:"adStatus"` // 广告状态 1-上架,0-下架,2-未开始,3-已过期,4-即将到期 默认查询全部
|
||||
//AdvertiseLocationCode string `json:"advertiseLocationCode,omitempty"` // 广告位唯一标识,选填
|
||||
//AdvertiseCode string `json:"advertiseCode,omitempty"` // 广告唯一标识,选填
|
||||
//PageNo int `json:"pageNo,omitempty"` // 页码,全量查询不需要传,选填
|
||||
//PageSize int `json:"pageSize,omitempty"` // 每页条数,全量查询不需要传,选填
|
||||
}
|
||||
|
||||
type JtAdvertiseRes struct {
|
||||
Code int `json:"code"` // 返回状态码
|
||||
Msg string `json:"msg"` // 返回消息
|
||||
Data string `json:"data"` // 广告信息 - 加密
|
||||
}
|
||||
|
||||
// AdSlot 广告位信息
|
||||
type AdSlot struct {
|
||||
DeptCode string `json:"deptCode"` // 部门 code
|
||||
MerchantCode string `json:"merchantCode"` // 商户 code
|
||||
AppCode string `json:"appCode"` // 应用 code
|
||||
LocationType int `json:"locationType"` // 广告位类型:0 → 开屏广告 1 → 弹窗广告 2 → 图标广告 3 → 九宫格广告 4 → 信息流广告 5 → 瀑布流广告 6 → 详情广告 7 → 片头广告
|
||||
AdvertiseLocationCode string `json:"advertiseLocationCode"` // 广告位标识
|
||||
AdvertiseLocationName string `json:"advertiseLocationName"` // 广告位名称
|
||||
DisplayMode int `json:"displayMode"` // 展示模式:0 → 多个轮播, 1 → 单体, 2 → 三分屏, 3 → 四分屏, 4 → 小图标, 5 → 大图标
|
||||
AdvertiseHeight int `json:"advertiseHeight"` // 高
|
||||
AdvertiseWidth int `json:"advertiseWidth"` // 宽
|
||||
MaterialType int `json:"materialType"` // 素材类型(视频/图片):1 → 视频, 2 → 图片, 3 → 视频、图片
|
||||
AdLimit int `json:"adLimit"` // 广告限制数量
|
||||
AdDetailInfoList []AdDetailInfo `json:"adDetailInfoList"` // 广告详情信息列表
|
||||
}
|
||||
|
||||
// AdDetailInfo 广告详情信息
|
||||
type AdDetailInfo struct {
|
||||
AdvertiseCode string `json:"advertiseCode"` // 广告业务code
|
||||
CustomerCode string `json:"customerCode"` // 客户code
|
||||
AdvertiseName string `json:"advertiseName"` // 广告名称
|
||||
AdvertiseUrl string `json:"advertiseUrl"` // 广告链接
|
||||
AdvertiseIcon string `json:"advertiseIcon"` // 广告图片地址
|
||||
AdvertiseType int `json:"advertiseType"` // 广告类型:1 → 播放器, 2 → 药台, 3 → 炮台, 4 → 黄游, 5 → 直播, 6 → BC
|
||||
AdMode string `json:"adMode"` // 广告模式
|
||||
AppCode string `json:"appCode"` // 应用 code
|
||||
LocationType int `json:"locationType"` // 广告位类型:0 → 开屏广告 1 → 弹窗广告 2 → 图标广告 3 → 九宫格广告 4 → 信息流广告 5 → 瀑布流广告 6 → 详情广告 7 → 片头广告
|
||||
AdvertiseDesc string `json:"advertiseDesc"` // 广告描述
|
||||
PageType string `json:"pageType"` // 内部路由页面类型:如 rechange_page → 会员充值页, gold_rechange_page → 金币充值页, invite_page → 邀请分享页
|
||||
StartTimeStamp int64 `json:"startTimeStamp"` // 广告开始时间 时间戳
|
||||
EndTimeStamp int64 `json:"endTimeStamp"` // 广告结束时间 时间戳
|
||||
Sort int `json:"sort"` // 排序
|
||||
AdExtData string `json:"adExtData"` // 扩展字段1 = CoverImgSize
|
||||
}
|
||||
type AdCacheInfo struct {
|
||||
AdvertiseCode string `json:"advertiseCode"` // 广告业务code
|
||||
CustomerCode string `json:"customerCode"` // 客户code
|
||||
AdvertiseName string `json:"advertiseName"` // 广告名称
|
||||
AdvertiseUrl string `json:"advertiseUrl"` // 广告链接
|
||||
AdvertiseIcon string `json:"advertiseIcon"` // 广告图片地址
|
||||
AdvertiseType int `json:"advertiseType"` // 广告类型:1 → 播放器, 2 → 药台, 3 → 炮台, 4 → 黄游, 5 → 直播, 6 → BC
|
||||
AdMode string `json:"adMode"` // 广告模式
|
||||
AppCode string `json:"appCode"` // 应用 code
|
||||
LocationType int `json:"locationType"` // 广告位类型:0 → 开屏广告 1 → 弹窗广告 2 → 图标广告 3 → 九宫格广告 4 → 信息流广告 5 → 瀑布流广告 6 → 详情广告 7 → 片头广告
|
||||
AdvertiseDesc string `json:"advertiseDesc"` // 广告描述
|
||||
PageType string `json:"pageType"` // 内部路由页面类型:如 rechange_page → 会员充值页, gold_rechange_page → 金币充值页, invite_page → 邀请分享页
|
||||
StartTimeStamp int64 `json:"startTimeStamp"` // 广告开始时间 时间戳
|
||||
EndTimeStamp int64 `json:"endTimeStamp"` // 广告结束时间 时间戳
|
||||
Sort int `json:"sort"` // 排序
|
||||
AdExtData string `json:"adExtData"` // 扩展字段1 = CoverImgSize
|
||||
DeptCode string `json:"deptCode"` // 部门 code
|
||||
MerchantCode string `json:"merchantCode"` // 商户 code
|
||||
AdvertiseLocationCode string `json:"advertiseLocationCode"` // 广告位标识
|
||||
AdvertiseLocationName string `json:"advertiseLocationName"` // 广告位名称
|
||||
DisplayMode int `json:"displayMode"` // 展示模式:0 → 多个轮播, 1 → 单体, 2 → 三分屏, 3 → 四分屏, 4 → 小图标, 5 → 大图标
|
||||
AdvertiseHeight int `json:"advertiseHeight"` // 高
|
||||
AdvertiseWidth int `json:"advertiseWidth"` // 宽
|
||||
MaterialType int `json:"materialType"` // 素材类型(视频/图片):1 → 视频, 2 → 图片, 3 → 视频、图片
|
||||
AdLimit int `json:"adLimit"` // 广告限制数量
|
||||
}
|
||||
|
||||
type Extra struct {
|
||||
ImgType int64 `json:"imgType"` // 图片展示类型 0:应用图标 1.小图标 2.大图标
|
||||
DownloadCount int64 `json:"downloadCount"`
|
||||
}
|
||||
|
||||
func (ad AdDetailInfo) GetExtraData() (res Extra) {
|
||||
_ = json.Unmarshal([]byte(ad.AdExtData), &res)
|
||||
return
|
||||
}
|
||||
|
||||
func (ad AdDetailInfo) GetRealLink() string {
|
||||
return strings.ReplaceAll(ad.AdvertiseUrl, "inner://yinseinner/", "yinseinner://")
|
||||
}
|
||||
|
||||
func (ad AdDetailInfo) GetCoverLsj() string {
|
||||
if ad.AdvertiseIcon == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return path.Join("laosiji", ad.AdvertiseIcon)
|
||||
}
|
||||
|
||||
func (ad AdDetailInfo) GetAdvertiseType() int {
|
||||
// 内部顺序:1 应用 2 游戏 3 炮台 4 棋牌 5 直播
|
||||
// 外部顺序:1:应用 2:药台、3:炮台、4:游戏、5:直播、6:棋牌
|
||||
if ad.AdvertiseType == 4 {
|
||||
return 2
|
||||
}
|
||||
if ad.AdvertiseType == 6 {
|
||||
return 4
|
||||
}
|
||||
|
||||
return ad.AdvertiseType
|
||||
}
|
||||
|
||||
// JtAdvertiseThreeServer 集团广告中心
|
||||
func JtAdvertiseThreeServer(apiDomain, merchantCode, appCode, aesKey string) ([]AdSlot, error) {
|
||||
var foreverCacheKey = fmt.Sprintf("jtAdForever-%s-%s", merchantCode, appCode)
|
||||
redisKey := fmt.Sprintf("jtAd-%s-%s", merchantCode, appCode)
|
||||
str, err := redis.Get(redisKey)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("JtAdvertiseThreeServer 缓存获取广告列表信息异常:%v", err))
|
||||
}
|
||||
var resp []AdSlot
|
||||
if str != nil {
|
||||
if err = json.Unmarshal([]byte(*str), &resp); err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
log.Error(fmt.Sprintf("JtAdvertiseThreeServer 解析缓存数据异常:%v", err))
|
||||
}
|
||||
req := JtAdvertiseReq{
|
||||
MerchantCode: merchantCode,
|
||||
AppCode: appCode,
|
||||
AdStatus: 1,
|
||||
}
|
||||
|
||||
bodyStr, _ := json.Marshal(req)
|
||||
headers := map[string]string{"Content-Type": "application/json"}
|
||||
serverResp, err := fetchJtAdvertiseWithFallback(apiDomain, headers, bodyStr)
|
||||
if err != nil {
|
||||
return getFromLastTime(foreverCacheKey, err)
|
||||
}
|
||||
|
||||
adsData, err := crypt.AdDecrypt(serverResp.Data, aesKey)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("JtAdvertiseThreeServer 解密广告数据异常:%v", err))
|
||||
return nil, errors.New("response data err")
|
||||
}
|
||||
err = json.Unmarshal([]byte(adsData), &resp)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("JtAdvertiseThreeServer 解析 JSON 失败:%v", err))
|
||||
return nil, errors.New("data format error")
|
||||
}
|
||||
|
||||
// 对广告进行升序排序
|
||||
for i := range resp {
|
||||
sort.Slice(resp[i].AdDetailInfoList, func(j, k int) bool {
|
||||
return resp[i].AdDetailInfoList[j].Sort < resp[i].AdDetailInfoList[k].Sort
|
||||
})
|
||||
}
|
||||
|
||||
// 将最后一次请求成功的数据更新到二级缓存
|
||||
godef.Go(func() {
|
||||
setAdsCache(foreverCacheKey, resp)
|
||||
})
|
||||
// 写入缓存
|
||||
godef.Go(func() {
|
||||
data, err := json.Marshal(&resp)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("JtAdvertiseThreeServer json序列化缓存数据异常:%v", err))
|
||||
return
|
||||
}
|
||||
if err = redis.Set(redisKey, data, time.Minute); err != nil {
|
||||
log.Error(fmt.Sprintf("JtAdvertiseThreeServer 保存缓存数据异常:%v", err))
|
||||
}
|
||||
})
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
var jtAdvertiseFallbackDomains = []string{
|
||||
"https://apiad01.cc",
|
||||
"https://apiad02.cc",
|
||||
"https://apiad03.cc",
|
||||
"https://awsapiad01.cc",
|
||||
"https://d3o2peztwy6jno.cloudfront.net",
|
||||
}
|
||||
|
||||
func jtAdvertiseRequestDomains(apiDomain string) []string {
|
||||
seen := make(map[string]struct{}, 1+len(jtAdvertiseFallbackDomains))
|
||||
domains := make([]string, 0, 1+len(jtAdvertiseFallbackDomains))
|
||||
add := func(domain string) {
|
||||
domain = strings.TrimRight(strings.TrimSpace(domain), "/")
|
||||
if domain == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[domain]; ok {
|
||||
return
|
||||
}
|
||||
seen[domain] = struct{}{}
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
add(apiDomain)
|
||||
for _, domain := range jtAdvertiseFallbackDomains {
|
||||
add(domain)
|
||||
}
|
||||
return domains
|
||||
}
|
||||
|
||||
func fetchJtAdvertiseWithFallback(apiDomain string, headers map[string]string, body []byte) (*JtAdvertiseRes, error) {
|
||||
var lastErr error
|
||||
for _, domain := range jtAdvertiseRequestDomains(apiDomain) {
|
||||
resp, err := fetchJtAdvertise(domain, headers, body)
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
lastErr = err
|
||||
log.Error("JtAdvertiseThreeServer 请求异常,尝试下一个域名", log.Any("domain", domain), log.E(err))
|
||||
}
|
||||
if lastErr == nil {
|
||||
lastErr = errors.New("adcenter api domain empty")
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func fetchJtAdvertise(apiDomain string, headers map[string]string, body []byte) (*JtAdvertiseRes, error) {
|
||||
url := apiDomain + "/openapi/getAdvertiseList"
|
||||
var serverResp *JtAdvertiseRes
|
||||
code, err := httputil.Client().POSTJsonWithJResp(&serverResp, url, headers, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
return nil, fmt.Errorf("response status %d", code)
|
||||
}
|
||||
if serverResp == nil {
|
||||
return nil, errors.New("empty response")
|
||||
}
|
||||
if serverResp.Code != 0 {
|
||||
return nil, fmt.Errorf("response code %d: %s", serverResp.Code, serverResp.Msg)
|
||||
}
|
||||
return serverResp, nil
|
||||
}
|
||||
|
||||
// getFromLastTime 返回最后一次请求的广告数据
|
||||
func getFromLastTime(foreverCacheKey string, e error) ([]AdSlot, error) {
|
||||
str, err := redis.Get(foreverCacheKey)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("getFromLastTime 缓存获取广告列表信息异常:%v", err))
|
||||
return nil, e
|
||||
}
|
||||
|
||||
var resp []AdSlot
|
||||
if str != nil {
|
||||
if err = json.Unmarshal([]byte(*str), &resp); err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
log.Error(fmt.Sprintf("getFromLastTime 解析缓存数据异常:%v", err))
|
||||
}
|
||||
|
||||
return nil, e
|
||||
}
|
||||
|
||||
// setAdsCache 更新最后一次请求的广告数据
|
||||
func setAdsCache(foreverCacheKey string, resp []AdSlot) {
|
||||
data, err := json.Marshal(&resp)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("setAdsCache json序列化缓存数据异常:%v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := redis.Set(foreverCacheKey, data, time.Hour*72); err != nil {
|
||||
log.Error(fmt.Sprintf("setAdsCache 保存缓存数据异常:%v", err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package comm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
"videoweb/pub/common/godef"
|
||||
"videoweb/pub/common/lock"
|
||||
"videoweb/pub/common/log"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type MessageType int64
|
||||
|
||||
const (
|
||||
MessageTypeError MessageType = iota // 心跳消息
|
||||
MessageTypeAlive
|
||||
MessageTypeMediaAdd // 添加一个媒体
|
||||
MessageTypeMediaDel // 删除一个媒体
|
||||
MessageTypePostAdd // 添加一个帖子
|
||||
MessageTypePostDel // 删除一个帖子
|
||||
)
|
||||
|
||||
type message struct {
|
||||
Type MessageType
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
type OnReceive func(data []byte)
|
||||
|
||||
type Communicate struct {
|
||||
sendChannel string // 发送消息的频道
|
||||
recvChannel string // 接收消息的频道
|
||||
client *redis.Client // Redis的客户端
|
||||
lock sync.RWMutex
|
||||
callbacks map[MessageType][]OnReceive
|
||||
publish *lock.Queue[*message]
|
||||
}
|
||||
|
||||
func NewCommunicate(client *redis.Client, sendChannel string, recvChannel string) *Communicate {
|
||||
return &Communicate{
|
||||
sendChannel: sendChannel,
|
||||
recvChannel: recvChannel,
|
||||
client: client,
|
||||
callbacks: make(map[MessageType][]OnReceive),
|
||||
publish: lock.NewQueue[*message](),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Communicate) RegisterReceiver(mType MessageType, receive OnReceive) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.callbacks[mType] = append(c.callbacks[mType], receive)
|
||||
}
|
||||
|
||||
func (c *Communicate) findEvent(mType MessageType) []OnReceive {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
return c.callbacks[mType]
|
||||
}
|
||||
|
||||
func (c *Communicate) dispatchMessage(mType MessageType, payload []byte) {
|
||||
events := c.findEvent(mType)
|
||||
for _, event := range events {
|
||||
event(payload)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Communicate) handleRecv(data *redis.Message) {
|
||||
if data.Channel == "error" {
|
||||
log.Error("recv from channel %s failed", zap.Any("error", data.Payload))
|
||||
return
|
||||
}
|
||||
if data.Channel == c.recvChannel {
|
||||
msg := &message{}
|
||||
err := json.Unmarshal([]byte(data.Payload), msg)
|
||||
if err != nil {
|
||||
log.Error("recv from channel %s failed", zap.Any("error", err))
|
||||
}
|
||||
c.dispatchMessage(msg.Type, msg.Payload)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Communicate) Send(mType MessageType, payload []byte) error {
|
||||
msg := &message{
|
||||
Type: mType,
|
||||
Payload: payload,
|
||||
}
|
||||
c.publish.Enqueue(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Communicate) Start() {
|
||||
// 启动接收线程
|
||||
godef.Go(func() {
|
||||
sub := c.client.Subscribe(c.recvChannel)
|
||||
defer sub.Close()
|
||||
// 循环接收消息
|
||||
for {
|
||||
msg, err := sub.ReceiveMessage()
|
||||
if err != nil {
|
||||
msg = &redis.Message{
|
||||
Channel: "error",
|
||||
Payload: err.Error(),
|
||||
}
|
||||
c.handleRecv(msg)
|
||||
time.Sleep(time.Second)
|
||||
} else {
|
||||
c.handleRecv(msg)
|
||||
}
|
||||
}
|
||||
})
|
||||
// 启动发送线程
|
||||
godef.Go(func() {
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
for !c.publish.IsEmpty() {
|
||||
msg, ok := c.publish.Dequeue()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
log.Error("Communicate msg marshal failed", zap.Any("error", err))
|
||||
break
|
||||
}
|
||||
err = c.client.Publish(c.sendChannel, data).Err()
|
||||
if err != nil {
|
||||
c.publish.Enqueue(msg) // 再写回队列
|
||||
log.Error("Communicate send msg failed", zap.Any("error", err))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/ipaddr"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/useragent"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var (
|
||||
cryptSecret string
|
||||
env consts.RunMod
|
||||
//routineCount int64
|
||||
)
|
||||
|
||||
// InitGinSecret InitResponseParam
|
||||
func InitGinSecret(secret string, env_ consts.RunMod) {
|
||||
cryptSecret = secret
|
||||
env = env_
|
||||
}
|
||||
|
||||
// JSONWriterCache 返回数据并标记需要写入缓存
|
||||
func JSONWriterCache(c *gin.Context, codeOrErr srvdef.ErrInfoer, data interface{}) {
|
||||
code := codeOrErr.GetCode()
|
||||
tip := codeOrErr.GetTip()
|
||||
msg := codeOrErr.GetMsg()
|
||||
var hash bool
|
||||
if code != srvdef.Success { //发生错误,记录日志
|
||||
var version, sysType, devType string
|
||||
id := GetUserId(c)
|
||||
ua, _ := GetUA(c)
|
||||
if ua.Ver != "" {
|
||||
version = ua.Ver
|
||||
}
|
||||
if ua.SysType != "" {
|
||||
sysType = ua.SysType
|
||||
}
|
||||
if devType != "" {
|
||||
devType = ua.DevType
|
||||
}
|
||||
log.Warn("Error:",
|
||||
log.Any("UID", strconv.FormatInt(id, 10)),
|
||||
log.Any("IP", ipaddr.ResolveClientIP(c)),
|
||||
log.Any("Version", version),
|
||||
log.Any(consts.CtxSysType, sysType),
|
||||
log.Any("DevType", devType),
|
||||
log.Any("Router", c.Request.RequestURI),
|
||||
log.Any("PID", os.Getpid()),
|
||||
log.Any("Error", data),
|
||||
log.Any("Code", code))
|
||||
}
|
||||
c.Header("writer-cache", "HIT")
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"hash": hash,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": data,
|
||||
"time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
|
||||
})
|
||||
}
|
||||
|
||||
// ServerJSON 返回数据并处理多语言
|
||||
func ServerJSON(c *gin.Context, codeOrErr srvdef.ErrInfoer, data interface{}) {
|
||||
code := codeOrErr.GetCode()
|
||||
tip := codeOrErr.GetTip()
|
||||
msg := codeOrErr.GetMsg()
|
||||
var hash bool
|
||||
if code != srvdef.Success { //发生错误,记录日志
|
||||
var version, sysType, devType string
|
||||
id := GetUserId(c)
|
||||
ua, _ := GetUA(c)
|
||||
if ua.Ver != "" {
|
||||
version = ua.Ver
|
||||
}
|
||||
if ua.SysType != "" {
|
||||
sysType = ua.SysType
|
||||
}
|
||||
if devType != "" {
|
||||
devType = ua.DevType
|
||||
}
|
||||
log.Warn("Error:",
|
||||
log.Any("UID", strconv.FormatInt(id, 10)),
|
||||
log.Any("IP", ipaddr.ResolveClientIP(c)),
|
||||
log.Any("Version", version),
|
||||
log.Any(consts.CtxSysType, sysType),
|
||||
log.Any("DevType", devType),
|
||||
log.Any("Router", c.Request.RequestURI),
|
||||
log.Any("PID", os.Getpid()),
|
||||
log.Any("Error", data),
|
||||
log.Any("Code", code))
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"hash": hash,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": data,
|
||||
"time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
|
||||
})
|
||||
}
|
||||
|
||||
// GetUID 获取用户uid
|
||||
// oauth.Auth执行后uid有效
|
||||
func GetUID(ctx *gin.Context) (uid uint64, err error) {
|
||||
val, exists := ctx.Get(consts.CtxUserID)
|
||||
if !exists {
|
||||
err = errors.New("userId not exist")
|
||||
return
|
||||
}
|
||||
uid, ok := val.(uint64)
|
||||
if !ok {
|
||||
return 0, errors.New("userId type error")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetUserId(ctx *gin.Context) int64 {
|
||||
return ctx.GetInt64(consts.CtxUserID)
|
||||
}
|
||||
|
||||
// GetUA
|
||||
func GetUA(ctx *gin.Context) (u useragent.UA, err error) {
|
||||
val, exists := ctx.Get(consts.CtxUA)
|
||||
if !exists {
|
||||
err = errors.New("user-agent not exists")
|
||||
return
|
||||
}
|
||||
u, ok := val.(useragent.UA)
|
||||
if !ok {
|
||||
err = errors.New("user-agent type error")
|
||||
return
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// GetIP 获取真实IP
|
||||
func GetIP(ctx *gin.Context) string {
|
||||
relIP, exists := ctx.Get(consts.CtxIP)
|
||||
if !exists {
|
||||
if relIP == "" {
|
||||
relIP = ipaddr.ResolveClientIP(ctx)
|
||||
}
|
||||
}
|
||||
v, ok := relIP.(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
log.Info(fmt.Sprintf("[IP-ROUTER] %s,PID %d", ctx.GetHeader("X-Forwarded-For"), os.Getpid()))
|
||||
return v
|
||||
}
|
||||
|
||||
func IsNilOrEmpty(in interface{}) bool {
|
||||
if in == nil || in == "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AlphabeticColl() []string {
|
||||
return []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
|
||||
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type ID = primitive.ObjectID
|
||||
|
||||
// ToJsonM 将struct转换为 Json Map
|
||||
func ToJsonM(obj interface{}) (map[string]interface{}, error) {
|
||||
j, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]interface{})
|
||||
if err := json.Unmarshal(j, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, err
|
||||
}
|
||||
|
||||
func ToBsonM(s interface{}) (bson.M, error) {
|
||||
data, err := bson.Marshal(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(bson.M)
|
||||
if err := bson.Unmarshal(data, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func MapToJsonString(m map[string]string) (string, error) {
|
||||
if data, err := json.Marshal(m); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return string(data), nil
|
||||
}
|
||||
}
|
||||
|
||||
// MapToStruct s is struct ptr
|
||||
func MapToStruct(s interface{}, m map[string]interface{}) error {
|
||||
bsonBytes, err := bson.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bson.Unmarshal(bsonBytes, s)
|
||||
}
|
||||
|
||||
// JSONStruct2Map 将struct转换为Map
|
||||
func JSONStruct2Map(obj interface{}) (map[string]interface{}, error) {
|
||||
m := make(map[string]interface{})
|
||||
j, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(j, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Map2JSONStruct 将map转换为Json struct
|
||||
func Map2JSONStruct(v interface{}, m map[string]interface{}) error {
|
||||
j, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(j, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func IDArray(ids []string) ([]ID, error) {
|
||||
var idArray []ID
|
||||
for _, id := range ids {
|
||||
_id, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idArray = append(idArray, _id)
|
||||
}
|
||||
|
||||
return idArray, nil
|
||||
}
|
||||
|
||||
func ObjectIDs2String(obj []primitive.ObjectID) []string {
|
||||
arr := []string{}
|
||||
if len(obj) == 0 {
|
||||
return arr
|
||||
}
|
||||
for _, o := range obj {
|
||||
arr = append(arr, o.Hex())
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
func String2ObjectID(obj []string) []primitive.ObjectID {
|
||||
arr := []primitive.ObjectID{}
|
||||
for _, o := range obj {
|
||||
oid, err := primitive.ObjectIDFromHex(o)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
arr = append(arr, oid)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// PercentOf - calculate what percent [number1] is of [number2].
|
||||
// ex. 300 is 12.5% of 2400
|
||||
func PercentOf(part int, total int) float64 {
|
||||
return (float64(part) * float64(100)) / float64(total)
|
||||
}
|
||||
|
||||
func MergeMap(src, dest map[string]interface{}) map[string]interface{} {
|
||||
if len(src) == 0 {
|
||||
return dest
|
||||
}
|
||||
if len(dest) == 0 {
|
||||
return src
|
||||
}
|
||||
if len(src) == 0 && len(dest) == 0 {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
for k, v := range dest {
|
||||
src[k] = v
|
||||
}
|
||||
return src
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package cronmgr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"videoweb/pub/common/log"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
var c *cron.Cron
|
||||
|
||||
// runIngs 正在运行中的任务
|
||||
var runIngs map[*context.Context]struct{}
|
||||
var r_lock sync.Mutex
|
||||
|
||||
// RegisterCron 注册一个定时任务
|
||||
func RegisterCron(spec string, cmd func(), errfunc func(err interface{})) (cron.EntryID, error) {
|
||||
if len(spec) == 0 || cmd == nil {
|
||||
return -1, errors.New("spec or cmd is nil")
|
||||
}
|
||||
return registerCron(spec, func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if errfunc != nil {
|
||||
errfunc(err)
|
||||
}
|
||||
log.Errorf("cron task error.%v", err)
|
||||
}
|
||||
}()
|
||||
cmd()
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterCronAsyncOne 注册一个协程执行的定时任务 限制了cmd同时只有一个协程在执行
|
||||
func RegisterCronAsyncOne(spec string, cmd func(), errfunc func(err interface{})) (cron.EntryID, error) {
|
||||
if len(spec) == 0 || cmd == nil {
|
||||
return -1, errors.New("spec or cmd is nil")
|
||||
}
|
||||
ctx := context.Background()
|
||||
return registerCron(spec, func() {
|
||||
go func(c *context.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if errfunc != nil {
|
||||
errfunc(err)
|
||||
}
|
||||
log.Errorf("cron task error.%v", err)
|
||||
}
|
||||
}()
|
||||
if taskRunIng(c) {
|
||||
cmd()
|
||||
taskRunEnd(c)
|
||||
} else {
|
||||
log.Warnf("Limits cmd to only one coroutine executing at the same time")
|
||||
}
|
||||
}(&ctx)
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterCronAsync 注册一个协程执行的定时任务
|
||||
func RegisterCronAsync(spec string, cmd func(), errfunc func(err interface{})) (cron.EntryID, error) {
|
||||
if len(spec) == 0 || cmd == nil {
|
||||
return -1, errors.New("spec or cmd is nil")
|
||||
}
|
||||
return registerCron(spec, func() {
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if errfunc != nil {
|
||||
errfunc(err)
|
||||
}
|
||||
log.Errorf("cron task error.%v", err)
|
||||
}
|
||||
}()
|
||||
cmd()
|
||||
}()
|
||||
})
|
||||
}
|
||||
func registerCron(spec string, cmd func()) (cron.EntryID, error) {
|
||||
if c == nil {
|
||||
c = cron.New(cron.WithSeconds())
|
||||
runIngs = make(map[*context.Context]struct{})
|
||||
}
|
||||
id, err := c.AddFunc(spec, cmd)
|
||||
if err != nil {
|
||||
log.Warnf("add cron [%s] fail", spec)
|
||||
return -1, err
|
||||
}
|
||||
c.Start()
|
||||
log.Infof("add cron [%d] spec:[%s].", id, spec)
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// UnregisterCron 注销一个定时任务
|
||||
func UnregisterCron(id cron.EntryID) bool {
|
||||
if id > 0 {
|
||||
c.Remove(id)
|
||||
log.Infof("remove cron id:[%d]", id)
|
||||
}
|
||||
log.Infof("remove cron [%d] not found", id)
|
||||
return false
|
||||
}
|
||||
|
||||
func taskRunIng(ctx *context.Context) bool {
|
||||
r_lock.Lock()
|
||||
defer r_lock.Unlock()
|
||||
if _, ok := runIngs[ctx]; ok {
|
||||
return false
|
||||
}
|
||||
runIngs[ctx] = struct{}{}
|
||||
return true
|
||||
}
|
||||
func taskRunEnd(ctx *context.Context) {
|
||||
r_lock.Lock()
|
||||
defer r_lock.Unlock()
|
||||
delete(runIngs, ctx)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package cronx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type CronJob interface {
|
||||
Run() error
|
||||
Desc() string
|
||||
Name() string
|
||||
}
|
||||
|
||||
type CronJobAndSpec struct {
|
||||
CronJob CronJob
|
||||
Spec string
|
||||
}
|
||||
|
||||
type BaseJob struct {
|
||||
ctx context.Context
|
||||
spec string
|
||||
name string
|
||||
desc string
|
||||
l *zap.SugaredLogger
|
||||
}
|
||||
|
||||
type cronFunc struct {
|
||||
*BaseJob
|
||||
fn func(ctx context.Context, l *zap.SugaredLogger) error
|
||||
}
|
||||
|
||||
func addError(fn func(ctx context.Context, l *zap.SugaredLogger)) func(context.Context, *zap.SugaredLogger) error {
|
||||
return func(ctx context.Context, l *zap.SugaredLogger) error {
|
||||
l.Info("start")
|
||||
defer l.Info("end")
|
||||
fn(ctx, l)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f cronFunc) Run() error { return f.fn(f.ctx, f.l) }
|
||||
func NewCronFunc(ctx context.Context, name, desc string, fn func(ctx context.Context, l *zap.SugaredLogger)) cronFunc {
|
||||
return cronFunc{
|
||||
BaseJob: NewBaseJob(ctx, name, desc),
|
||||
fn: addError(fn),
|
||||
}
|
||||
}
|
||||
|
||||
func NewCronFuncWithError(ctx context.Context, name, desc string, fn func(ctx context.Context, l *zap.SugaredLogger) error) cronFunc {
|
||||
return cronFunc{
|
||||
BaseJob: NewBaseJob(ctx, name, desc),
|
||||
fn: fn,
|
||||
}
|
||||
}
|
||||
|
||||
func NewBaseJob(ctx context.Context, name, desc string) *BaseJob {
|
||||
return &BaseJob{
|
||||
ctx: ctx,
|
||||
name: name,
|
||||
desc: desc,
|
||||
l: log.ZapLogS.Named(name),
|
||||
}
|
||||
}
|
||||
|
||||
func (j *BaseJob) Name() string { return j.name }
|
||||
func (j *BaseJob) Desc() string { return j.desc }
|
||||
func (j *BaseJob) L() *zap.SugaredLogger { return j.l }
|
||||
func (j *BaseJob) Context() context.Context { return j.ctx }
|
||||
|
||||
type CronLogger struct {
|
||||
l *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func NewCronLogger() *CronLogger {
|
||||
return &CronLogger{
|
||||
l: log.ZapLogS.Named("cron"),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *CronLogger) Info(msg string, kvs ...interface{}) {
|
||||
l.l.Infow(msg, kvs)
|
||||
}
|
||||
func (l *CronLogger) Error(err error, msg string, kvs ...interface{}) {
|
||||
l.l.Errorw(err.Error()+msg, kvs)
|
||||
}
|
||||
|
||||
func StartJobs(jobs ...CronJobAndSpec) (closer func()) {
|
||||
var once sync.Once
|
||||
l := NewCronLogger()
|
||||
c := cron.New(cron.WithSeconds(), cron.WithLocation(time.Local), cron.WithChain(cron.Recover(l)))
|
||||
for i := range jobs {
|
||||
j := jobs[i].CronJob
|
||||
spec := jobs[i].Spec
|
||||
name := j.Name()
|
||||
_, err := c.AddFunc(spec, func() {
|
||||
defer log.Infof("%s end", name)
|
||||
log.Infof("%s start", name)
|
||||
if err := j.Run(); err != nil {
|
||||
log.Errorf("%s run failed: %s", name, err.Error())
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("%s add failed: %s", err)
|
||||
}
|
||||
}
|
||||
c.Start()
|
||||
return func() {
|
||||
once.Do(func() {
|
||||
c.Stop()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type CronFuncType func(ctx context.Context, l *zap.SugaredLogger)
|
||||
|
||||
func TraceTime(fn CronFuncType) CronFuncType {
|
||||
return func(ctx context.Context, l *zap.SugaredLogger) {
|
||||
now := time.Now()
|
||||
defer func() {
|
||||
elapse := time.Since(now)
|
||||
if elapse > time.Second {
|
||||
l.Infof("takes %s", elapse.Truncate(time.Millisecond))
|
||||
} else {
|
||||
l.Infof("takes %d ms", elapse/time.Millisecond)
|
||||
}
|
||||
}()
|
||||
l.Info("start run")
|
||||
fn(ctx, l)
|
||||
l.Info("end run")
|
||||
}
|
||||
}
|
||||
|
||||
// MustSecondSpec is a helper method to validate spec at compile time
|
||||
func MustSecondSpec(spec string) string {
|
||||
parser := cron.NewParser(
|
||||
cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor,
|
||||
)
|
||||
_, err := parser.Parse(spec)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return spec
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package crypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
)
|
||||
|
||||
func PKCS7PaddingEx(ciphertext []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(ciphertext)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(ciphertext, padtext...)
|
||||
}
|
||||
|
||||
func PKCS7UnPaddingEx(origData []byte) []byte {
|
||||
length := len(origData)
|
||||
unpadding := int(origData[length-1])
|
||||
return origData[:(length - unpadding)]
|
||||
}
|
||||
|
||||
func EcbDecrypt(data, key []byte) []byte {
|
||||
block, _ := aes.NewCipher(key)
|
||||
decrypted := make([]byte, len(data))
|
||||
size := block.BlockSize()
|
||||
|
||||
for bs, be := 0, size; bs < len(data); bs, be = bs+size, be+size {
|
||||
block.Decrypt(decrypted[bs:be], data[bs:be])
|
||||
}
|
||||
|
||||
return PKCS7UnPaddingEx(decrypted)
|
||||
}
|
||||
|
||||
func EcbEncrypt(data, key []byte) []byte {
|
||||
block, _ := aes.NewCipher(key)
|
||||
data = PKCS7PaddingEx(data, block.BlockSize())
|
||||
decrypted := make([]byte, len(data))
|
||||
size := block.BlockSize()
|
||||
|
||||
for bs, be := 0, size; bs < len(data); bs, be = bs+size, be+size {
|
||||
block.Encrypt(decrypted[bs:be], data[bs:be])
|
||||
}
|
||||
|
||||
return decrypted
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
package crypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
// CreateToken 生成Token算法
|
||||
func CreateToken(secret string, tokenClaims map[string]interface{}) (tokeness string, err error) {
|
||||
if secret == "" {
|
||||
return "", errors.New("secret is empty")
|
||||
}
|
||||
claims := jwt.MapClaims(tokenClaims)
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokeness, err = token.SignedString([]byte(secret))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tokeness, nil
|
||||
}
|
||||
|
||||
func keyFunc(secret string) jwt.Keyfunc {
|
||||
return func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(secret), nil
|
||||
}
|
||||
}
|
||||
|
||||
// ParseToken 解析Token
|
||||
func ParseToken(secret string, tokeness string) (map[string]interface{}, error) {
|
||||
if secret == "" || tokeness == "" {
|
||||
return nil, errors.New("secret or tokeness is empty")
|
||||
}
|
||||
token, err := jwt.Parse(tokeness, keyFunc(secret))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//验证token,如果token被修改过则为false
|
||||
if !token.Valid {
|
||||
return nil, errors.New("token is invalid")
|
||||
}
|
||||
claimsToken, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.New("cannot convert claim to MapClaim")
|
||||
}
|
||||
return claimsToken, nil
|
||||
}
|
||||
|
||||
// StructToStr 结构体转json 先对结构体按照字典顺序排序 并返回字符串
|
||||
func StructToStr(obj interface{}) (string, error) {
|
||||
if obj == nil {
|
||||
return "", errors.New("obj is not nil")
|
||||
}
|
||||
data := make(map[string]interface{})
|
||||
bytes, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = json.Unmarshal(bytes, &data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
str, err := MapToStr(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return str, nil
|
||||
}
|
||||
|
||||
// MapToStr 对map按照字典顺序排序 并返回字符串
|
||||
func MapToStr(data map[string]interface{}) (string, error) {
|
||||
if data == nil || len(data) == 0 {
|
||||
return "", errors.New("data is nil or empty")
|
||||
}
|
||||
newData := make(map[string]interface{})
|
||||
keys := make([]string, 0, len(data))
|
||||
for k := range data {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
newData[k] = data[k]
|
||||
}
|
||||
b, err := json.Marshal(newData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
str := string(b)
|
||||
return str, nil
|
||||
}
|
||||
|
||||
// JSON2Struct json转struct
|
||||
func JSON2Struct(str string, obj interface{}) {
|
||||
if err := json.Unmarshal([]byte(str), &obj); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// JSONArray2Struct json转struct
|
||||
func JSONArray2Struct(str []string, obj interface{}) []interface{} {
|
||||
var data []interface{}
|
||||
for _, v := range str {
|
||||
JSON2Struct(v, &obj)
|
||||
data = append(data, obj)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// JSON2Map json转map
|
||||
func JSON2Map(str string) (map[string]interface{}, error) {
|
||||
var mapResult map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(str), &mapResult); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mapResult, nil
|
||||
}
|
||||
|
||||
// StructToStrNormal 结构体转str
|
||||
func StructToStrNormal(obj interface{}) (string, error) {
|
||||
str, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(str), nil
|
||||
}
|
||||
|
||||
func UrlValueToStr(values url.Values, exclude string) (string, string) {
|
||||
if values == nil {
|
||||
return "", ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
var sign string
|
||||
keys := make([]string, 0, len(values))
|
||||
for k, _ := range values {
|
||||
if k == exclude {
|
||||
sign = strings.Join(values[k], "")
|
||||
continue
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for i, k := range keys {
|
||||
if i != 0 {
|
||||
buf.WriteString("&")
|
||||
}
|
||||
buf.WriteString(k)
|
||||
buf.WriteString("=")
|
||||
buf.WriteString(strings.Join(values[k], ""))
|
||||
}
|
||||
return buf.String(), sign
|
||||
}
|
||||
|
||||
// 对参数进行排序后 拼接成url字符串 只对一级字符串类型的参数做处理
|
||||
func MapToUrlOnlyStr(data map[string]interface{}) string {
|
||||
if data == nil || len(data) == 0 {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
keys := make([]string, 0, len(data))
|
||||
for k := range data {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for i, k := range keys {
|
||||
if i != 0 {
|
||||
buf.WriteString("&")
|
||||
}
|
||||
buf.WriteString(k)
|
||||
buf.WriteString("=")
|
||||
if v, ok := data[k].(string); ok {
|
||||
buf.WriteString(v)
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// MapToStr 对map按照字典顺序排序 并返回字符串url 拼接的字段
|
||||
func MapToUrlStr(data map[string]interface{}) string {
|
||||
if data == nil || len(data) == 0 {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
keys := make([]string, 0, len(data))
|
||||
for k := range data {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for i, k := range keys {
|
||||
if i != 0 {
|
||||
buf.WriteString("&")
|
||||
}
|
||||
buf.WriteString(k)
|
||||
buf.WriteString("=")
|
||||
if v, ok := data[k].(string); ok {
|
||||
buf.WriteString(v)
|
||||
} else {
|
||||
t := reflect.TypeOf(data[k])
|
||||
if t != nil && t.Kind() == reflect.Map {
|
||||
if v, ok := data[k].(map[string]interface{}); ok {
|
||||
buf.WriteString("{")
|
||||
str := MapToUrlStr(v)
|
||||
buf.WriteString(str)
|
||||
buf.WriteString("}")
|
||||
}
|
||||
}
|
||||
if t != nil && (t.Kind() == reflect.Slice || t.Kind() == reflect.Array) {
|
||||
arraybyte, _ := json.Marshal(data[k])
|
||||
buf.WriteString(string(arraybyte))
|
||||
}
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// MapToURL 集合转URL
|
||||
func MapToURL(m map[string]string) string {
|
||||
if m == nil || len(m) == 0 {
|
||||
return ""
|
||||
}
|
||||
v := url.Values{}
|
||||
for k := range m {
|
||||
v.Set(k, string(m[k]))
|
||||
}
|
||||
return v.Encode()
|
||||
}
|
||||
|
||||
func Strcut2UrlValue(obj interface{}) (string, error) {
|
||||
t := reflect.TypeOf(obj)
|
||||
v := reflect.ValueOf(obj)
|
||||
if t.Kind() != reflect.Struct {
|
||||
return "", errors.New("obj must be struct type")
|
||||
}
|
||||
u := url.Values{}
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
var value string
|
||||
tag := t.Field(i).Tag.Get("json")
|
||||
switch t.Field(i).Type.Kind() {
|
||||
case reflect.String:
|
||||
value = v.Field(i).String()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
value = strconv.Itoa(int(v.Field(i).Int()))
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
value = strconv.FormatUint(v.Field(i).Uint(), 10)
|
||||
case reflect.Slice, reflect.Array:
|
||||
buf := bytes.Buffer{}
|
||||
b, _ := json.Marshal(v.Field(i).Interface())
|
||||
t := strings.TrimRight(strings.TrimLeft(string(b), "["), "]")
|
||||
array := strings.Split(t, ",")
|
||||
if len(array) == 0 {
|
||||
value = ""
|
||||
}
|
||||
if len(array) == 1 {
|
||||
value = array[0]
|
||||
} else {
|
||||
for i, v := range array {
|
||||
if i == 0 {
|
||||
buf.WriteString(v)
|
||||
continue
|
||||
}
|
||||
buf.WriteString("&")
|
||||
buf.WriteString(tag)
|
||||
buf.WriteString("=")
|
||||
buf.WriteString(v)
|
||||
value = buf.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
u.Add(tag, value)
|
||||
}
|
||||
dcodeurl, err := url.QueryUnescape(u.Encode())
|
||||
return dcodeurl, err
|
||||
}
|
||||
|
||||
// StrToMd5 字符串转md5
|
||||
func StrToMd5(str string) string {
|
||||
data := []byte(str)
|
||||
return ByteToMd5(data)
|
||||
}
|
||||
|
||||
// ByteToMd5 数组转md5
|
||||
func ByteToMd5(data []byte) string {
|
||||
md5Ctx := md5.New()
|
||||
md5Ctx.Write(data)
|
||||
cipherStr := md5Ctx.Sum(nil)
|
||||
return hex.EncodeToString(cipherStr)
|
||||
}
|
||||
|
||||
// FileToMd5 文件转MD5
|
||||
func FileToMd5(fileName string) string {
|
||||
file, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer file.Close()
|
||||
md5Ctx := md5.New()
|
||||
if _, err := io.Copy(md5Ctx, file); err != nil {
|
||||
return ""
|
||||
}
|
||||
cipherStr := md5Ctx.Sum(nil)
|
||||
return hex.EncodeToString(cipherStr)
|
||||
}
|
||||
|
||||
// HashCode 对一个字符串生成唯一的hasHcode 码
|
||||
func HashCode(src string) int {
|
||||
v := int(crc32.ChecksumIEEE([]byte(src)))
|
||||
if v > 0 {
|
||||
return v
|
||||
}
|
||||
if -v > 0 {
|
||||
return -v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// HexToString converts
|
||||
func HexToString(data []byte) string {
|
||||
return hex.EncodeToString(data)
|
||||
}
|
||||
|
||||
// StrToSha256 字符串转sha256
|
||||
func StrToSha256(str string) string {
|
||||
shaCtx := sha256.New()
|
||||
shaCtx.Write([]byte(str))
|
||||
shaBytes := shaCtx.Sum(nil)
|
||||
cipherStr := hex.EncodeToString(shaBytes[:])
|
||||
return cipherStr
|
||||
}
|
||||
|
||||
// StrToSha1 字符串转sha1
|
||||
func StrToSha1(str string, secret string) []byte {
|
||||
h := hmac.New(sha1.New, []byte(secret))
|
||||
h.Write([]byte(str))
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// StrToSha1 字符串转sha1 转16进制后
|
||||
func StrToHmacSha1(str, secret string) string {
|
||||
return hex.EncodeToString(StrToSha1(str, secret))
|
||||
}
|
||||
|
||||
// StrToHmacSha256 加密 字符串转sha256加密字符串
|
||||
func StrToHmacSha256(str string, secret string) string {
|
||||
h := hmac.New(sha256.New, []byte(secret))
|
||||
h.Write([]byte(str))
|
||||
cipherStr := hex.EncodeToString(h.Sum(nil))
|
||||
return cipherStr
|
||||
}
|
||||
|
||||
func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(ciphertext)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(ciphertext, padtext...)
|
||||
}
|
||||
|
||||
func PKCS7UnPadding(origData []byte) []byte {
|
||||
length := len(origData)
|
||||
unpadding := int(origData[length-1])
|
||||
return origData[:(length - unpadding)]
|
||||
}
|
||||
|
||||
// AesEncrypt AES加密
|
||||
func AesEncrypt(origData, key string) ([]byte, error) {
|
||||
origDataByte := []byte(origData)
|
||||
return CoreAesEncrypt(origDataByte, key)
|
||||
}
|
||||
|
||||
// AesDecrypt AES解密
|
||||
func AesDecrypt(crypted, key string) (string, error) {
|
||||
cryptedByte := []byte(crypted)
|
||||
return CoreAesDecrypt(cryptedByte, key)
|
||||
}
|
||||
|
||||
// AesDecrypt AES解密
|
||||
func AesDecryptByte(cryptedByte []byte, key string) (string, error) {
|
||||
return CoreAesDecrypt(cryptedByte, key)
|
||||
}
|
||||
|
||||
// CoreAesEncrypt AES加密
|
||||
func CoreAesEncrypt(origData []byte, key string) ([]byte, error) {
|
||||
keybyte := []byte(key)
|
||||
block, err := aes.NewCipher(keybyte)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockSize := block.BlockSize()
|
||||
origData = PKCS7Padding(origData, blockSize)
|
||||
blockMode := cipher.NewCBCEncrypter(block, keybyte[:blockSize])
|
||||
crypted := make([]byte, len(origData))
|
||||
blockMode.CryptBlocks(crypted, origData)
|
||||
return crypted, nil
|
||||
}
|
||||
|
||||
// CoreAesDecrypt AES解密
|
||||
func CoreAesDecrypt(crypted []byte, key string) (string, error) {
|
||||
keybyte := []byte(key)
|
||||
block, err := aes.NewCipher(keybyte)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
blockSize := block.BlockSize()
|
||||
blockMode := cipher.NewCBCDecrypter(block, keybyte[:blockSize])
|
||||
origData := make([]byte, len(crypted))
|
||||
blockMode.CryptBlocks(origData, crypted)
|
||||
origData = PKCS7UnPadding(origData)
|
||||
return string(origData), nil
|
||||
}
|
||||
|
||||
// XorEnc
|
||||
func XorEnc(src string, xorKey string) string {
|
||||
var result string
|
||||
j := 0
|
||||
bt := []rune(src)
|
||||
xor := []rune(xorKey)
|
||||
for i := 0; i < len(bt); i++ {
|
||||
s := strconv.FormatInt(int64(bt[i]^xor[j]), 10)
|
||||
result = result + s
|
||||
j = 1 % len(xor)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// XorDec
|
||||
func XorDec(src string, xorKey string) string {
|
||||
var result string
|
||||
j := 0
|
||||
bt := []rune(src)
|
||||
xor := []rune(xorKey)
|
||||
for i := 0; i < len(bt); i++ {
|
||||
s := strconv.FormatInt(int64(bt[i]^xor[j]), 10)
|
||||
result = result + s
|
||||
j = i % len(xor)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func XorDecode(src, key string) string {
|
||||
srcByte, _ := base64.StdEncoding.DecodeString(src)
|
||||
keyByte, _ := base64.StdEncoding.DecodeString(key)
|
||||
srcByte = xor(srcByte, keyByte)
|
||||
return base64.StdEncoding.EncodeToString(srcByte)
|
||||
}
|
||||
|
||||
func XorEncode(src, key string) string {
|
||||
return XorDecode(src, key)
|
||||
}
|
||||
|
||||
func xor(src []byte, key []byte) []byte {
|
||||
for i := 0; i < len(src); i++ {
|
||||
src[i] ^= key[i%len(key)]
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
func XORLength(src []byte, key []byte, length int) []byte {
|
||||
for i := 0; i < length; i++ {
|
||||
src[i] ^= key[i%len(key)]
|
||||
}
|
||||
return src[:length]
|
||||
}
|
||||
|
||||
// https://core.telegram.org/api/end-to-end#sending-and-receiving-messages-in-a-secret-chat
|
||||
func CoreAesEncryptEx(plain []byte, nonceLen int, key string) ([]byte, error) {
|
||||
nonce := make([]byte, nonceLen)
|
||||
_, err := io.ReadFull(rand.Reader, nonce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var largeShaRaw []byte
|
||||
largeShaRaw = append(largeShaRaw, []byte(key)...)
|
||||
largeShaRaw = append(largeShaRaw, nonce...)
|
||||
largeShaRawMid := len(largeShaRaw) / 2
|
||||
msgKeyLarge := sha256.Sum256(largeShaRaw)
|
||||
msgKey := msgKeyLarge[8:24] //16 bytes
|
||||
|
||||
var shaRawA []byte
|
||||
shaRawA = append(shaRawA, msgKey...)
|
||||
shaRawA = append(shaRawA, largeShaRaw[:largeShaRawMid]...)
|
||||
sha256a := sha256.Sum256(shaRawA) //32 bytes
|
||||
|
||||
var shaRawB []byte
|
||||
shaRawB = append(shaRawB, largeShaRaw[largeShaRawMid:]...)
|
||||
shaRawB = append(shaRawB, msgKey...)
|
||||
sha256b := sha256.Sum256(shaRawB) //32 bytes
|
||||
|
||||
var aesKey []byte //32 bytes AES-256
|
||||
aesKey = append(aesKey, sha256a[:8]...) //a: 8 bytes
|
||||
aesKey = append(aesKey, sha256b[8:24]...) //b: 16 bytes
|
||||
aesKey = append(aesKey, sha256a[24:32]...) //a: 8 bytes
|
||||
|
||||
var aesIV []byte // 16 bytes
|
||||
aesIV = append(aesIV, sha256b[:4]...) //b: 4 bytes
|
||||
aesIV = append(aesIV, sha256a[12:20]...) //a: 8 bytes
|
||||
aesIV = append(aesIV, sha256b[28:]...) //b: 4 bytes
|
||||
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockSize := block.BlockSize()
|
||||
ciphertext := PKCS7Padding(plain, blockSize)
|
||||
mode := cipher.NewCBCEncrypter(block, aesIV)
|
||||
cipher := make([]byte, nonceLen+len(ciphertext))
|
||||
copy(cipher, nonce)
|
||||
mode.CryptBlocks(cipher[nonceLen:], ciphertext)
|
||||
return cipher, nil
|
||||
}
|
||||
|
||||
func CoreAesDecryptEx(crypt []byte, nonceLen int, key string) ([]byte, error) {
|
||||
nonce := make([]byte, nonceLen)
|
||||
copy(nonce, crypt)
|
||||
var largeShaRaw []byte
|
||||
largeShaRaw = append(largeShaRaw, []byte(key)...)
|
||||
largeShaRaw = append(largeShaRaw, nonce...)
|
||||
largeShaRawMid := len(largeShaRaw) / 2
|
||||
msgKeyLarge := sha256.Sum256(largeShaRaw)
|
||||
msgKey := msgKeyLarge[8:24] //16 bytes
|
||||
|
||||
var shaRawA []byte
|
||||
shaRawA = append(shaRawA, msgKey...)
|
||||
shaRawA = append(shaRawA, largeShaRaw[:largeShaRawMid]...)
|
||||
sha256a := sha256.Sum256(shaRawA) //32 bytes
|
||||
|
||||
var shaRawB []byte
|
||||
shaRawB = append(shaRawB, largeShaRaw[largeShaRawMid:]...)
|
||||
shaRawB = append(shaRawB, msgKey...)
|
||||
sha256b := sha256.Sum256(shaRawB) //32 bytes
|
||||
|
||||
var aesKey []byte //32 bytes AES-256
|
||||
aesKey = append(aesKey, sha256a[:8]...) //a: 8 bytes
|
||||
aesKey = append(aesKey, sha256b[8:24]...) //b: 16 bytes
|
||||
aesKey = append(aesKey, sha256a[24:32]...) //a: 8 bytes
|
||||
|
||||
var aesIV []byte // 16 bytes
|
||||
aesIV = append(aesIV, sha256b[:4]...) //b: 4 bytes
|
||||
aesIV = append(aesIV, sha256a[12:20]...) //a: 8 bytes
|
||||
aesIV = append(aesIV, sha256b[28:]...) //b: 4 bytes
|
||||
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
realData := crypt[nonceLen:]
|
||||
blockMode := cipher.NewCBCDecrypter(block, aesIV)
|
||||
origData := make([]byte, len(realData))
|
||||
blockMode.CryptBlocks(origData, realData)
|
||||
return PKCS7UnPadding(origData), nil
|
||||
}
|
||||
|
||||
// base64补全
|
||||
func padding(origin string) string {
|
||||
missing := len(origin) % 4
|
||||
if missing != 0 {
|
||||
origin += strings.Repeat("=", 4-missing)
|
||||
}
|
||||
|
||||
return origin
|
||||
}
|
||||
|
||||
// AdDecrypt 集团广告中心-广告数据解密专用
|
||||
func AdDecrypt(encryptedData, keyBase64 string) (res string, err error) {
|
||||
// 1. Base64 解码获取密钥
|
||||
keyBytes, err := base64.StdEncoding.DecodeString(padding(keyBase64))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("密钥 Base64 解码失败: %v", err)
|
||||
}
|
||||
// 2. Base64 解码获取加密数据
|
||||
data, err := base64.StdEncoding.DecodeString(padding(encryptedData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("数据 Base64 解码失败: %v", err)
|
||||
}
|
||||
|
||||
// 3. 拆分 IV 与密文(前 12 字节为 IV)
|
||||
if len(data) < 12 {
|
||||
return "", errors.New("数据长度不足,无法提取 IV")
|
||||
}
|
||||
iv := data[:12]
|
||||
ciphertext := data[12:]
|
||||
// 4. 初始化 AES 密码块
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建 AES 密码块失败: %v", err)
|
||||
}
|
||||
// 5. 采用 GCM 模式
|
||||
aesGCM, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建 GCM 失败: %v", err)
|
||||
}
|
||||
// 6. 解密数据
|
||||
plainText, err := aesGCM.Open(nil, iv, ciphertext, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解密失败: %v", err)
|
||||
}
|
||||
|
||||
return string(plainText), nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package crypt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"videoweb/pub/common/log"
|
||||
)
|
||||
|
||||
var (
|
||||
PlatPublicKey = `
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCWtWnWmTgLemO+GBMWIX9N5yx2
|
||||
YMc+4MczBogNdTFjF4rrd/+Y1G9qJ8ZDHR7qjBQgJjzcZrGYRUf2wiY6T9ble9+h
|
||||
s14pYPpXqkSoD2VOQI4+idb6nRgVQ6rXyMQps7ORcshmEtIS/Ra3aG3XaAFAUCfO
|
||||
E3nHGyldtNIGvuBTAQIDAQAB
|
||||
-----END PUBLIC KEY-----`
|
||||
)
|
||||
|
||||
func TestRSA(t *testing.T) {
|
||||
//data := make([]byte, 0)
|
||||
testData := "是对的发送打赏打赏打赏打赏的"
|
||||
rawContext, err := RSAEncryptByPublicKey(PlatPublicKey, []byte(testData))
|
||||
if err != nil {
|
||||
log.Error("wnth RSAEncryptByPublicKey err", log.Any("err", err))
|
||||
return
|
||||
}
|
||||
log.Info(string(rawContext))
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package crypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"videoweb/pub/common/log"
|
||||
)
|
||||
|
||||
func splitBytesSlice(data []byte, n int) [][]byte {
|
||||
var chunk []byte
|
||||
chunks := make([][]byte, 0, len(data)/n+1)
|
||||
for len(data) >= n {
|
||||
chunk, data = data[:n], data[n:]
|
||||
chunks = append(chunks, chunk)
|
||||
}
|
||||
if len(data) > 0 {
|
||||
chunks = append(chunks, data[:])
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
func rsaBytesToPublicKey(pub []byte) (*rsa.PublicKey, error) {
|
||||
// fmt.Printf("pubKey is %s", string(pub))
|
||||
block, _ := pem.Decode(pub)
|
||||
enc := x509.IsEncryptedPEMBlock(block)
|
||||
b := block.Bytes
|
||||
var err error
|
||||
if enc {
|
||||
b, err = x509.DecryptPEMBlock(block, nil)
|
||||
if err != nil {
|
||||
log.Error("RSABytesToPublicKey DecryptPEMBlock err", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
pk, err := x509.ParsePKIXPublicKey(b)
|
||||
if err != nil {
|
||||
log.Error("RSABytesToPublicKey ParsePKIXPublicKey err", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
if pk_, ok := pk.(*rsa.PublicKey); ok {
|
||||
return pk_, nil
|
||||
}
|
||||
log.Error("RSABytesToPublicKey error input not PublicKey")
|
||||
return nil, errors.New("Not RSA PublicKey format")
|
||||
}
|
||||
|
||||
func rsaBytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) {
|
||||
block, _ := pem.Decode(priv)
|
||||
enc := x509.IsEncryptedPEMBlock(block)
|
||||
b := block.Bytes
|
||||
var err error
|
||||
if enc {
|
||||
b, err = x509.DecryptPEMBlock(block, nil)
|
||||
if err != nil {
|
||||
log.Error("RSABytesToPrivateKey DecryptPEMBlock err", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
key, err := x509.ParsePKCS8PrivateKey(b)
|
||||
if err != nil {
|
||||
log.Error("RSABytesToPrivateKey ParsePKCS8PrivateKey err", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
if pk_, ok := key.(*rsa.PrivateKey); ok {
|
||||
return pk_, nil
|
||||
}
|
||||
log.Error("RSABytesToPrivateKey error input not PrivateKey")
|
||||
return nil, errors.New("Not RSA PrivateKey format")
|
||||
}
|
||||
|
||||
// 公钥加密
|
||||
func RSAEncryptByPublicKey(pemPubKey string, data []byte) ([]byte, error) {
|
||||
pubKey, err := rsaBytesToPublicKey([]byte(pemPubKey))
|
||||
if err != nil {
|
||||
log.Error("RSAEncryptByPublicKey rsaBytesToPublicKey err", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
partLen := pubKey.N.BitLen()/8 - 11
|
||||
chunks := splitBytesSlice(data, partLen)
|
||||
result := bytes.Buffer{}
|
||||
for _, chunk := range chunks {
|
||||
cipherSilce, err := rsa.EncryptPKCS1v15(rand.Reader, pubKey, chunk)
|
||||
if err != nil {
|
||||
log.Error("RSAEncryptByPublicKey EncryptPKCS1v15 err", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
result.Write(cipherSilce)
|
||||
}
|
||||
return result.Bytes(), nil
|
||||
}
|
||||
|
||||
// 私玥签名
|
||||
func RSASignByPrivateKey(pemPrivateKey string, data []byte, hash crypto.Hash) ([]byte, error) {
|
||||
privateKey, err := rsaBytesToPrivateKey([]byte(pemPrivateKey))
|
||||
if err != nil {
|
||||
log.Error("RSASignByPrivateKey rsaBytesToPrivateKey err", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
h := hash.New()
|
||||
h.Write(data)
|
||||
hashed := make([]byte, 0)
|
||||
hashed = h.Sum(hashed)
|
||||
|
||||
return rsa.SignPKCS1v15(rand.Reader, privateKey, hash, hashed)
|
||||
|
||||
}
|
||||
func RSAVerifyByPublicKey(publicKey string, data, sign []byte, hash crypto.Hash) (err error) {
|
||||
pubKey, err := rsaBytesToPublicKey([]byte(publicKey))
|
||||
if err != nil {
|
||||
log.Error("RSAEncryptByPublicKey rsaBytesToPublicKey err", log.E(err))
|
||||
return err
|
||||
}
|
||||
h := hash.New()
|
||||
h.Write(data)
|
||||
hashed := make([]byte, 0)
|
||||
hashed = h.Sum(hashed)
|
||||
return rsa.VerifyPKCS1v15(pubKey, hash, hashed, sign)
|
||||
}
|
||||
|
||||
// 公钥解密
|
||||
func RSADecryptByPrivateKey(pemPrivateKey string, cipherData []byte) ([]byte, error) {
|
||||
privateKey, err := rsaBytesToPrivateKey([]byte(pemPrivateKey))
|
||||
if err != nil {
|
||||
log.Error("RSADecryptByPrivateKey rsaBytesToPrivateKey err", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
blockLen := privateKey.N.BitLen() / 8
|
||||
chunks := splitBytesSlice(cipherData, blockLen)
|
||||
result := bytes.Buffer{}
|
||||
for _, chunk := range chunks {
|
||||
plain, err := rsa.DecryptPKCS1v15(rand.Reader, privateKey, chunk)
|
||||
if err != nil {
|
||||
log.Error("RSADecryptByPrivateKey DecryptPKCS1v15 err", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
result.Write(plain)
|
||||
}
|
||||
return result.Bytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package consts
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RunMod string
|
||||
|
||||
const (
|
||||
RunModeDev RunMod = "dev"
|
||||
RunModeTest RunMod = "test"
|
||||
RunModeProd RunMod = "prod"
|
||||
RunModRelease RunMod = "release"
|
||||
)
|
||||
|
||||
// gin中ctx使用的key
|
||||
const (
|
||||
CtxUserID = "USER_ID"
|
||||
CtxUpUserID = "UP_USER_ID"
|
||||
CtxUpInfo = "UP_INFO"
|
||||
CtxUA = "UA"
|
||||
CtxUAH5 = "UAH5"
|
||||
CtxIP = "IP"
|
||||
CtxDevId = "DEVID"
|
||||
CtxDevType = "DEVTYPE"
|
||||
CtxSysType = "SYSTYPE"
|
||||
CtxVersion = "VERSION"
|
||||
CtxBuildId = "BUILDID"
|
||||
CtxSystemName = "SYSTEM_NAME"
|
||||
CtxSystemVersion = "SYSTEM_VERSION"
|
||||
CtxToken = "TOKEN"
|
||||
CtxOnlineTime = "ONLINE_TIME"
|
||||
)
|
||||
|
||||
const (
|
||||
AppSysType_And = ""
|
||||
AppSysType_Ios = ""
|
||||
)
|
||||
|
||||
// Etcd 的Key
|
||||
const (
|
||||
EtcdWarmupDB = "/micro/config/com.sz.api.server/warmupdb"
|
||||
EtcdSzDB = "/micro/config/com.sz.api.server/db"
|
||||
EtcdRds = "/micro/config/com.sz.api.server/redis"
|
||||
)
|
||||
|
||||
const (
|
||||
AdminJwtTokenExpire = 30 * 24 * time.Hour
|
||||
UserJwtTokenExpire = 30 * 24 * time.Hour
|
||||
UserSignInExpire = 60 * 24 * time.Hour
|
||||
)
|
||||
|
||||
const (
|
||||
UAHeader = "User-Agent"
|
||||
)
|
||||
|
||||
const (
|
||||
IOS = "ios"
|
||||
Android = "android"
|
||||
H5 = "h5"
|
||||
PC = "pc"
|
||||
H5IOS = "h5_ios"
|
||||
H5Android = "h5_android"
|
||||
H5PC = "h5_pc"
|
||||
Light = "light"
|
||||
)
|
||||
|
||||
const (
|
||||
CaptchaLen = 6 //验证码长度
|
||||
)
|
||||
|
||||
const (
|
||||
TypeIOS = "ios"
|
||||
TypeAndroid = "and"
|
||||
TypeWeb = "h5"
|
||||
)
|
||||
|
||||
// 机型
|
||||
const (
|
||||
WebTypeIOS = "ios"
|
||||
WebTypeAndroid = "android"
|
||||
WebSysTypeH5 = "h5"
|
||||
)
|
||||
|
||||
// func HandleSysType(sysType string) (sys string) {
|
||||
// if strings.Contains(strings.ToLower(sysType), TypeIOS) {
|
||||
// sys = IOS
|
||||
// }
|
||||
//
|
||||
// if strings.Contains(strings.ToLower(sysType), TypeAndroid) {
|
||||
// sys = Android
|
||||
// }
|
||||
//
|
||||
// if strings.Contains(strings.ToLower(sysType), TypeWeb) {
|
||||
// sys = H5
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
|
||||
func ToWebSysType(sysType string) (sys string) {
|
||||
if strings.Contains(strings.ToLower(sysType), TypeIOS) {
|
||||
sys = WebTypeIOS
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(sysType), TypeAndroid) {
|
||||
sys = WebTypeAndroid
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(sysType), TypeWeb) {
|
||||
sys = WebSysTypeH5
|
||||
}
|
||||
return
|
||||
}
|
||||
func ToNewSysType(sysType string) (sys string) {
|
||||
if strings.Contains(strings.ToLower(sysType), TypeIOS) {
|
||||
sys = WebTypeIOS
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(sysType), TypeAndroid) {
|
||||
sys = WebTypeAndroid
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(sysType), TypeWeb) {
|
||||
sys = WebSysTypeH5
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package redisconst
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/pubdef"
|
||||
)
|
||||
|
||||
const (
|
||||
visitLogFmt = "bd_visitlog:%s:%d"
|
||||
FiveMVisit = "bd_visitFmt:%s"
|
||||
BloomUser = "bd_bloom_user:%d"
|
||||
AppUserToken = "bd_app_user_token:%d"
|
||||
captchaKey = "bd_captcha_mobile_%s"
|
||||
CaptchaExpire = 5 * time.Minute
|
||||
replayNonceKey = "bd_replay:nonce:_%s"
|
||||
mediaM3U8Fmt = "m3u8cache:%s"
|
||||
MediaM3U8CacheExpire = time.Hour * 24 * 7
|
||||
appPlayletHomeCacheFmt = "app:playlet:home:%d:%s"
|
||||
appPlayletHomeCacheKeyFmt = "app:playlet:home:%d"
|
||||
appPlayletHomeCacheVersionFmt = "app:playlet:home:version:%d"
|
||||
AppPlayletHomeCacheExpire = 10 * time.Minute
|
||||
AppPlayletForwardHeatExpire = 3 * time.Second
|
||||
|
||||
//禁止登陆设置
|
||||
userForbit = "userForbit:%d"
|
||||
ipForbit = "ipForbit:%s"
|
||||
|
||||
//每分钟Ip注册次数
|
||||
registerLimist = "registerLimit:%s"
|
||||
RegisterLimistExpire = time.Minute * 30
|
||||
|
||||
//用户关注限制
|
||||
careLimitKey = "carelimit:%d"
|
||||
CareLimitKeyExpire = 15 * time.Second
|
||||
|
||||
//用户评论限制
|
||||
commentLimitKey = "commentlimit:%s"
|
||||
CommentLimitKeyExpire = 5 * time.Second
|
||||
CommentDayLimitKeyExpire = 10 * time.Hour
|
||||
|
||||
ContactUsLimitKey = "contactuslimit:%s"
|
||||
ContactusLimitKeyExpire = 1 * time.Minute
|
||||
ContactusLimitKeyDayExpire = 10
|
||||
//每用户访问次数限制
|
||||
userCallLimit = "userCall:%d"
|
||||
ipCallLimit = "ipCall:%s"
|
||||
CallLimitExpire = time.Second * 10
|
||||
|
||||
userSidKey = "userSid:%d"
|
||||
UserSidKeyExpire = time.Minute * 30
|
||||
|
||||
// 访问 IP 去重统计:每个 appId 每天一个 SET
|
||||
ipStatKey = "ipstat:%d:%s"
|
||||
IpStatExpire = 49 * time.Hour
|
||||
|
||||
// m3u8 / enkey 防盗链
|
||||
m3u8DayRateLimitIp = "m3u8dayrate:ip:%s:%s"
|
||||
m3u8DayRateLimitUser = "m3u8dayrate:user:%d:%s"
|
||||
m3u8DailyMediaIpFmt = "m3u8:daily:media:ip:%s:%d:%s"
|
||||
m3u8DailyMediaUserFmt = "m3u8:daily:media:user:%d:%d:%s"
|
||||
m3u8UserVipExpireFmt = "m3u8:user:vipExpire:%d"
|
||||
M3U8DayRateLimitMax = int64(500) // 非有效会员/未登录:按 IP 日限
|
||||
M3U8DayRateLimitVipMax = int64(1000) // 有效会员:按用户日限
|
||||
M3U8UserVipExpireCache = 5 * time.Minute
|
||||
)
|
||||
|
||||
func BloomUserKey(userId int64) string {
|
||||
return fmt.Sprintf(BloomUser, userId)
|
||||
}
|
||||
|
||||
func FiveMVisitKey(time string) string {
|
||||
return fmt.Sprintf(FiveMVisit, time)
|
||||
}
|
||||
|
||||
func UserVisitLogKey(uid int64, t time.Time) string {
|
||||
return fmt.Sprintf(visitLogFmt, t.Format("20060102"), uid)
|
||||
}
|
||||
|
||||
func MobileCaptchaKey(mobile string) string {
|
||||
return fmt.Sprintf(captchaKey, mobile)
|
||||
}
|
||||
|
||||
func AppUserTokenKey(userId int64) string {
|
||||
return fmt.Sprintf(AppUserToken, userId)
|
||||
}
|
||||
|
||||
func MediaM3U8Key(name string) string {
|
||||
return fmt.Sprintf(mediaM3U8Fmt, name)
|
||||
}
|
||||
|
||||
func AppPlayletHomeCacheKey() string {
|
||||
return fmt.Sprintf(appPlayletHomeCacheKeyFmt, pubdef.AppId)
|
||||
}
|
||||
|
||||
func AppPlayletHomeCacheVersionKey() string {
|
||||
return fmt.Sprintf(appPlayletHomeCacheVersionFmt, pubdef.AppId)
|
||||
}
|
||||
|
||||
func AppPlayletHomeCacheDataKey(version string) string {
|
||||
return fmt.Sprintf(appPlayletHomeCacheFmt, pubdef.AppId, version)
|
||||
}
|
||||
|
||||
func AppPlayletForwardHeatKey(operator string, playletId int64) string {
|
||||
operator = strings.ReplaceAll(operator, ":", "-")
|
||||
return fmt.Sprintf("app:playlet:forward:heat:%s:%d", operator, playletId)
|
||||
}
|
||||
|
||||
func UserForbitKey(userId int64) string {
|
||||
return fmt.Sprintf(userForbit, userId)
|
||||
}
|
||||
|
||||
func RegisterLimitKey(ip string) string {
|
||||
ip = strings.ReplaceAll(ip, ":", "-")
|
||||
return fmt.Sprintf(registerLimist, ip)
|
||||
}
|
||||
|
||||
func CareLimitKey(userId int64) string {
|
||||
return fmt.Sprintf(careLimitKey, userId)
|
||||
}
|
||||
func ReplayNonceKey(nonce string) string {
|
||||
return fmt.Sprintf(replayNonceKey, nonce)
|
||||
}
|
||||
|
||||
func CommentLimitKey(ip string) string {
|
||||
return fmt.Sprintf(commentLimitKey, ip)
|
||||
}
|
||||
func CommentOneDayLimitKey(ip string) string {
|
||||
return fmt.Sprintf("commentdaylimit:%s", ip)
|
||||
}
|
||||
|
||||
func UserCallLimitKey(userId int64) string {
|
||||
return fmt.Sprintf(userCallLimit, userId)
|
||||
}
|
||||
|
||||
func IpCallLimitKey(ip string) string {
|
||||
return fmt.Sprintf(ipCallLimit, ip)
|
||||
}
|
||||
|
||||
func IpForbitKey(ip string) string {
|
||||
ip = strings.ReplaceAll(ip, ":", "-")
|
||||
return fmt.Sprintf(ipForbit, ip)
|
||||
}
|
||||
|
||||
func UserSidKey(userId int64) string {
|
||||
return fmt.Sprintf(userSidKey, userId)
|
||||
}
|
||||
|
||||
func IpStatKey(appId int64, date string) string {
|
||||
return fmt.Sprintf(ipStatKey, appId, date)
|
||||
}
|
||||
|
||||
func M3U8DayRateLimitIpKey(ip string) string {
|
||||
ip = normalizeM3U8Ip(ip)
|
||||
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
loc = time.Local
|
||||
}
|
||||
day := time.Now().In(loc).Format("20060102")
|
||||
return fmt.Sprintf(m3u8DayRateLimitIp, ip, day)
|
||||
}
|
||||
|
||||
func M3U8DayRateLimitUserKey(userId int64) string {
|
||||
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
loc = time.Local
|
||||
}
|
||||
day := time.Now().In(loc).Format("20060102")
|
||||
return fmt.Sprintf(m3u8DayRateLimitUser, userId, day)
|
||||
}
|
||||
|
||||
func M3U8DayRateLimitExpire() time.Duration {
|
||||
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
loc = time.Local
|
||||
}
|
||||
now := time.Now().In(loc)
|
||||
next := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, loc)
|
||||
d := next.Sub(now)
|
||||
if d < time.Minute {
|
||||
return time.Minute
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func normalizeM3U8Ip(ip string) string {
|
||||
return strings.ReplaceAll(ip, ":", "-")
|
||||
}
|
||||
|
||||
func M3U8DailyMediaIpKey(ip string, mediaId int64) string {
|
||||
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
loc = time.Local
|
||||
}
|
||||
day := time.Now().In(loc).Format("20060102")
|
||||
return fmt.Sprintf(m3u8DailyMediaIpFmt, normalizeM3U8Ip(ip), mediaId, day)
|
||||
}
|
||||
|
||||
func M3U8DailyMediaUserKey(userId int64, mediaId int64) string {
|
||||
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
loc = time.Local
|
||||
}
|
||||
day := time.Now().In(loc).Format("20060102")
|
||||
return fmt.Sprintf(m3u8DailyMediaUserFmt, userId, mediaId, day)
|
||||
}
|
||||
|
||||
func M3U8UserVipExpireKey(userId int64) string {
|
||||
return fmt.Sprintf(m3u8UserVipExpireFmt, userId)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package video
|
||||
|
||||
type VideoType int64
|
||||
type videoStatus int64
|
||||
|
||||
// 0-上架、1-未上架
|
||||
const (
|
||||
Enable videoStatus = iota
|
||||
Disable
|
||||
)
|
||||
|
||||
// 1是AV、2是SP
|
||||
const (
|
||||
_ VideoType = iota
|
||||
AV
|
||||
SP
|
||||
)
|
||||
|
||||
const AVLimit = 1
|
||||
|
||||
type recordType int64
|
||||
|
||||
const (
|
||||
_ recordType = iota
|
||||
Watch
|
||||
Downlaoad
|
||||
)
|
||||
@@ -0,0 +1,169 @@
|
||||
package errors
|
||||
|
||||
type CodeType int64
|
||||
|
||||
const NotFind = "mongo: no documents in result"
|
||||
|
||||
// 错误级别
|
||||
const (
|
||||
ParamLevel = 1000 // 参数级
|
||||
StoreLevel = 3000 // 存储级
|
||||
ServerLevel = 4000 // 服务级
|
||||
BusinessLevel = 6000 // 业务逻辑级
|
||||
)
|
||||
|
||||
// app 端用到了
|
||||
// 1000, 重新登陆
|
||||
// 4002 短信发送失败
|
||||
// 6017 手机号码已绑定
|
||||
// 6023 user_video操作错误
|
||||
// 这里作出兼容
|
||||
|
||||
func (c CodeType) Error() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
const (
|
||||
InvalidateToken CodeType = ParamLevel + iota // 不可用的token,客户端需要重新登陆
|
||||
MethodNotFound // 方法为找到,指gin方法未找到
|
||||
ApiNotFound // api未找到
|
||||
Limiting // 限流
|
||||
NoToken // 无token 1004
|
||||
)
|
||||
|
||||
const (
|
||||
DBQuery CodeType = StoreLevel + iota
|
||||
DBInsert
|
||||
DBUpdate
|
||||
DBDelete
|
||||
RedisSet
|
||||
RedisGet
|
||||
RedisDel
|
||||
ProxyPutObject
|
||||
EtcdGet
|
||||
EtcdPut
|
||||
RedisRPush
|
||||
RedisLPop
|
||||
)
|
||||
|
||||
const (
|
||||
HttpClient CodeType = ServerLevel + iota // httpclient
|
||||
TypePointer // 指针类型
|
||||
SmsLimit // 短信限制
|
||||
SmsSend // 短信发送失败
|
||||
SmsTmp // 短信模版
|
||||
SmsTmpParse // 短信模版解析
|
||||
UserId // 用户ID
|
||||
OpenFile // 打开文件
|
||||
FsErr // seaweedfs
|
||||
CtxTimeout // context超时
|
||||
UnmarshalJson // json
|
||||
FSFiler // fs filer
|
||||
HttpGet //
|
||||
ReadData // read data
|
||||
IOCopy // io copy
|
||||
Pass // pass
|
||||
PassConfID // pass config id
|
||||
Tips // 提示错误
|
||||
)
|
||||
|
||||
const (
|
||||
// 用户权限
|
||||
UserPermission CodeType = BusinessLevel + iota
|
||||
Unknown // 未知错误
|
||||
InvalidKeyword // 无效的关键字
|
||||
UserName // 用户名称
|
||||
Mobile // 手机号码
|
||||
Captcha // 验证码
|
||||
CaptchaErr // 验证码错误
|
||||
Psw // 密码
|
||||
PayCode // 用户支付密码
|
||||
Number // 数字类型
|
||||
String // 字符串类型
|
||||
VideoId // 视频ID
|
||||
Progress // 进度
|
||||
Ids // id数组
|
||||
CatagoryExist // 视频种类已存在
|
||||
MobileBinded // 手机号码已绑定
|
||||
Rate // 返佣金比例
|
||||
Brokerage // 返佣
|
||||
Logo // 用户logo
|
||||
UploadFile //
|
||||
EmptyFile //
|
||||
Operate // user_video操作错误
|
||||
UserDisable //
|
||||
NickName //
|
||||
Gender //
|
||||
InviteCode //
|
||||
Coat //
|
||||
ReLogin //
|
||||
PopType //
|
||||
AgentBinded //
|
||||
M3u8FileName //
|
||||
M3u8Read //
|
||||
VideoName //
|
||||
ExchangeCodeErr // 兑换码错误
|
||||
ChanAcount //
|
||||
Recharge //
|
||||
Withdraw //
|
||||
ExportErr // 导出参数错误
|
||||
DomainErr // 主域名错误
|
||||
ForbiddenWord // 禁言
|
||||
ForbiddenLogin // 禁止登陆
|
||||
DuplicateLike // 重复点赞
|
||||
AliPayUnBind // 支付宝未绑定
|
||||
BankCardUnBind // 银行卡未绑定
|
||||
VideoUploadExist // 视频重复上传
|
||||
Task //
|
||||
PostsNewArgsInvalid // 帖子参数错误
|
||||
PostDupi // 帖子重复发送
|
||||
PostNotExisted // 帖子不存在
|
||||
PostLikeOnLiked // 重复点赞帖子
|
||||
PostUndoLikeOnUnliked // 尚未点赞,不可取消点赞
|
||||
PostFavOnFaved // 重复 收藏
|
||||
PostUndoFavOnUnfaved // 尚未收藏,不可取消收藏
|
||||
PostDisLikeOnDisLiked // 重复屏蔽帖子
|
||||
PostUndoDisLikeOnUnliked // 尚未屏蔽,不可取消屏蔽
|
||||
UserDoNotForSelf // 用户不能对自己进行此项操作
|
||||
UserWatchDupi // 不可重复关注同一用户
|
||||
UserNoWatch // 没有关注该用户,不可取消关注
|
||||
PostRepleNewArgsInvalid // 帖子 回复 参数错误
|
||||
PostReplyNotExisted // 评论不存在
|
||||
PostReplyAlready // 评论不存在
|
||||
NoWatchableUsers // 没有可以关注的用户池
|
||||
PostReplyLikeOnLiked // 重复点赞帖子
|
||||
PostReplyUndoLikeOnUnliked // 尚未点赞,不可取消点赞
|
||||
PostPriceInvalid // 帖子定价异常,不可购买
|
||||
PostIsBuyed // 帖子已购买,不可重复购买
|
||||
NoBalance // 余额不足
|
||||
BalanceErr // 编辑余额错误
|
||||
NoAccount // 没账户
|
||||
AccountDisabled // 账户已被封禁
|
||||
BuySelf // 自己帖子,不用购买
|
||||
RedisError // 服务器繁忙
|
||||
Button // 落地页按钮
|
||||
ReplyTimeShort // 回复间隔时间太短
|
||||
OperateLog // 日志出错
|
||||
RoleErr // 角色权限不够
|
||||
DocExists // 查找的文档已存在
|
||||
PostUnknownError // 未知错误
|
||||
SeekScoreConfirm // 商人追分确认
|
||||
GetOrderInfo // 商人充值订单报错
|
||||
PromSeq // 商区错误
|
||||
SignErr // 签名错误
|
||||
BadRequest // 传参错误
|
||||
ReplyPoolExisted // 评论已存在
|
||||
LimitIPErr // 限制IP登陆
|
||||
)
|
||||
|
||||
// 代充专用
|
||||
const (
|
||||
TakeChatFail CodeType = iota + 18000 //发起代充聊天失败 18000
|
||||
GetOrderInfoFail //获取代理充值订单信息失败 18001
|
||||
AppIDNotExist //代充AppID 不存在 18002
|
||||
TraderNotExist //此商人不存在 18003
|
||||
TraderBalanceNotEnough //商人余额不足 18004
|
||||
TraderHasLocked //商人被禁止登陆 18005
|
||||
TraderSeekScoreFail //商人追分失败 18006
|
||||
TraderSeekScoreFailDueNotEnough //商人追分失败 18007
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SrvErr 错误定义
|
||||
type SrvErr struct {
|
||||
Code CodeType `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Tips string `json:"tip,omitempty"`
|
||||
Cause string `json:"-"`
|
||||
}
|
||||
|
||||
// New create an error
|
||||
func New(code CodeType, msg string, cause string) *SrvErr {
|
||||
return &SrvErr{
|
||||
Code: code,
|
||||
Msg: msg,
|
||||
Cause: cause,
|
||||
}
|
||||
}
|
||||
|
||||
// New create an error
|
||||
func NewTips(code CodeType, tips string, cause string) *SrvErr {
|
||||
return &SrvErr{
|
||||
Code: code,
|
||||
Msg: tips,
|
||||
Tips: tips,
|
||||
Cause: cause,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SrvErr) Error() string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("code = %d, msg = %s ,case = %v", s.Code, s.Msg, s.Cause)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func MakeWithErr(code CodeType, err error) error {
|
||||
pc, fn, line, _ := runtime.Caller(1)
|
||||
cause := fmt.Sprintf("[error] in %s[%s:%d] %v", runtime.FuncForPC(pc).Name(), fn, line, err.Error())
|
||||
return New(code, err.Error(), cause)
|
||||
}
|
||||
|
||||
func MakeWithMsg(code CodeType, msg string) error {
|
||||
pc, fn, line, _ := runtime.Caller(1)
|
||||
cause := fmt.Sprintf("[error] in %s[%s:%d] %v", runtime.FuncForPC(pc).Name(), fn, line, msg)
|
||||
return New(code, msg, cause)
|
||||
}
|
||||
|
||||
func MakeWithTips(code CodeType, tips string) error {
|
||||
pc, fn, line, _ := runtime.Caller(1)
|
||||
cause := fmt.Sprintf("[error] in %s[%s:%d] %v", runtime.FuncForPC(pc).Name(), fn, line, tips)
|
||||
return NewTips(code, tips, cause)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package errors
|
||||
|
||||
// error tips
|
||||
const (
|
||||
|
||||
//基础错误提示
|
||||
ET_Store = "存储错误"
|
||||
ET_DBOp = "DB操作错误"
|
||||
ET_RdsOp = "Rds操作错误"
|
||||
ET_Param = "参数错误"
|
||||
ET_DBQury = "DB查询错误"
|
||||
ET_DBNoResult = "DB查询数据不存在"
|
||||
ET_TransactionsErr = "事务运行错误"
|
||||
//----------------------------------------------
|
||||
ET_UserNoExist = "用户不存在"
|
||||
ET_BloggerApply = "博主申请条件不满足"
|
||||
ET_BloggerAlreadyIs = "用户已经是博主"
|
||||
ET_BalanceNotEnough = "余额不足"
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package fconst
|
||||
|
||||
const (
|
||||
FileDir = "file-tmp"
|
||||
FsDir = "/fs"
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
package godef
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/tgbot"
|
||||
)
|
||||
|
||||
func Go(f func()) {
|
||||
//atomic.AddInt64(&routineCount, 1)
|
||||
go func() {
|
||||
defer func() {
|
||||
//atomic.AddInt64(&routineCount, -1)
|
||||
if r := recover(); r != nil {
|
||||
buf := make([]byte, 2048)
|
||||
length := runtime.Stack(buf, false)
|
||||
info := string(buf[:length])
|
||||
log.Error("cat a unkown excetion", log.Any("excetion :", info), log.Any("e", r))
|
||||
//发送tg消息
|
||||
msg := "goroutine crash,the stack:\n" + info
|
||||
tgbot.SendToProgram("程序异常", msg)
|
||||
}
|
||||
}()
|
||||
f()
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package googauth
|
||||
|
||||
import (
|
||||
"github.com/dgryski/dgoogauth"
|
||||
)
|
||||
|
||||
const windowSize = 30
|
||||
const hotpWindowSize = 5
|
||||
|
||||
var HotpCfg = dgoogauth.OTPConfig{
|
||||
Secret: "MRSHM6LTMFYHAMJSGM2DKNQ=",
|
||||
HotpCounter: 1,
|
||||
WindowSize: hotpWindowSize,
|
||||
}
|
||||
|
||||
func New(secret string, name string) string {
|
||||
cfg := &dgoogauth.OTPConfig{
|
||||
Secret: secret,
|
||||
WindowSize: windowSize,
|
||||
}
|
||||
return cfg.ProvisionURI(name)
|
||||
}
|
||||
|
||||
func Verify(secret string, pwd string) (bool, error) {
|
||||
cfg := &dgoogauth.OTPConfig{
|
||||
Secret: secret,
|
||||
WindowSize: windowSize,
|
||||
}
|
||||
return cfg.Authenticate(pwd)
|
||||
}
|
||||
|
||||
func VerifyHotp(cfg *dgoogauth.OTPConfig, authCode string, counter int) (bool, error) {
|
||||
return cfg.Authenticate(authCode)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package googauth
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/binary"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// HOTP implementation
|
||||
type HOTP struct {
|
||||
secret []byte
|
||||
digits int
|
||||
}
|
||||
|
||||
// At generate code
|
||||
func (h HOTP) At(counter uint64) string {
|
||||
counterBytes := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(counterBytes, counter)
|
||||
hash := hmac.New(sha1.New, h.secret)
|
||||
hash.Write(counterBytes)
|
||||
hs := hash.Sum(nil)
|
||||
offset := hs[19] & 0x0f
|
||||
binCodeBytes := make([]byte, 4)
|
||||
binCodeBytes[0] = hs[offset] & 0x7f
|
||||
binCodeBytes[1] = hs[offset+1] & 0xff
|
||||
binCodeBytes[2] = hs[offset+2] & 0xff
|
||||
binCodeBytes[3] = hs[offset+3] & 0xff
|
||||
binCode := binary.BigEndian.Uint32(binCodeBytes)
|
||||
mod := uint32(1)
|
||||
for i := 0; i < h.digits; i++ {
|
||||
mod *= 10
|
||||
}
|
||||
code := binCode % mod
|
||||
codeString := strconv.FormatUint(uint64(code), 10)
|
||||
if len(codeString) < h.digits {
|
||||
paddingByteLength := h.digits - len(codeString)
|
||||
paddingBytes := make([]byte, paddingByteLength)
|
||||
for i := 0; i < paddingByteLength; i++ {
|
||||
paddingBytes[i] = '0'
|
||||
}
|
||||
codeString = string(paddingBytes) + codeString
|
||||
}
|
||||
return codeString
|
||||
}
|
||||
|
||||
// Verify verify OTP code
|
||||
func (h HOTP) VerifyCode(code string, counter uint64) bool {
|
||||
realCode := h.At(counter)
|
||||
if realCode == code {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify verify OTP code
|
||||
func (h HOTP) GenerateCode(counter uint64) string {
|
||||
return h.At(counter)
|
||||
}
|
||||
|
||||
// NewHOTP generate new HOTP instance
|
||||
func NewHOTP(secret []byte, digits int) (h *HOTP) {
|
||||
h = new(HOTP)
|
||||
h.secret = secret
|
||||
h.digits = digits
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
client = http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConnsPerHost: 5,
|
||||
MaxConnsPerHost: 100,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type ByteSize int64
|
||||
|
||||
const (
|
||||
_ = iota // ignore first value by assigning to blank identifier
|
||||
KB ByteSize = 1 << (10 * iota)
|
||||
MB
|
||||
)
|
||||
|
||||
var (
|
||||
outOfBodySizeErr = errors.New("out of body size limit")
|
||||
)
|
||||
|
||||
type ResponseBody struct {
|
||||
StatusCode int
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type RequestFormData map[string]string
|
||||
|
||||
// Post
|
||||
// Note: 只会读取小于1M的数据
|
||||
func Post(url string, header map[string]string, body io.Reader) (fr *ResponseBody, err error) {
|
||||
fr, err = do(http.MethodPost, url, header, body, int64(1024*MB))
|
||||
return
|
||||
}
|
||||
|
||||
func PostJson(url string, body io.Reader) (fr *ResponseBody, err error) {
|
||||
header := map[string]string{}
|
||||
header["Content-Type"] = "application/json"
|
||||
fr, err = do(http.MethodPost, url, header, body, int64(1024*KB))
|
||||
return
|
||||
}
|
||||
|
||||
// PostFormData 提交标准的form表单
|
||||
func PostFormData(url string, fromdata RequestFormData) (fr *ResponseBody, err error) {
|
||||
var b bytes.Buffer
|
||||
writer := multipart.NewWriter(&b)
|
||||
|
||||
for k, v := range fromdata {
|
||||
if err = writer.WriteField(k, v); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = writer.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
header := map[string]string{"Content-Type": writer.FormDataContentType()}
|
||||
fr, err = do(http.MethodPost, url, header, &b, 2*int64(KB))
|
||||
return
|
||||
}
|
||||
|
||||
// Get
|
||||
// Note: 只会读取小于1M的数据
|
||||
func Get(url string, header map[string]string) (fr *ResponseBody, err error) {
|
||||
fr, err = do(http.MethodGet, url, header, nil, int64(KB))
|
||||
return
|
||||
}
|
||||
|
||||
// GetLargeFile 获取大文件。2M
|
||||
func GetLargeFile(url string, header map[string]string) (fr *ResponseBody, err error) {
|
||||
fr, err = do(http.MethodGet, url, header, nil, int64(20*MB))
|
||||
return
|
||||
}
|
||||
|
||||
func do(method string, url string, header map[string]string, body io.Reader, size int64) (fr *ResponseBody, err error) {
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for k, v := range header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.ContentLength > size {
|
||||
err = outOfBodySizeErr
|
||||
return
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(io.LimitReader(resp.Body, size))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fr = &ResponseBody{
|
||||
StatusCode: resp.StatusCode,
|
||||
Data: data,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 推送对象到文件服务器
|
||||
func PostObject(ctx context.Context, url string, header map[string]string, body []byte, objName string) (string, error) {
|
||||
base64data := base64.StdEncoding.EncodeToString(body)
|
||||
|
||||
req := struct {
|
||||
Name string `json:"fileName"`
|
||||
Data string `json:"fileData"`
|
||||
}{
|
||||
Name: objName,
|
||||
Data: base64data,
|
||||
}
|
||||
|
||||
jsonBytes, _ := json.Marshal(req)
|
||||
|
||||
resp, err := Post(url, header, bytes.NewReader(jsonBytes))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
type Obj struct {
|
||||
Domain string `json:"domain"`
|
||||
Name string `json:"fileName"`
|
||||
}
|
||||
|
||||
fsResp := struct {
|
||||
Code int `json:"code"`
|
||||
Object Obj `json:"data"`
|
||||
Msg string `json:"msg"`
|
||||
}{}
|
||||
|
||||
if err = json.Unmarshal(resp.Data, &fsResp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if fsResp.Code != 200 || fsResp.Object.Name == "" {
|
||||
return "", fmt.Errorf("上传fs失败")
|
||||
}
|
||||
|
||||
return fsResp.Object.Name, nil
|
||||
}
|
||||
|
||||
func PostWithBind(ctx context.Context, bind interface{}, url string, body io.Reader) error {
|
||||
if err := verifyBind(bind); err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("response status code:%d", resp.StatusCode)
|
||||
}
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, bind)
|
||||
}
|
||||
|
||||
func verifyBind(bind interface{}) error {
|
||||
bType := reflect.TypeOf(bind)
|
||||
if bType.Kind() != reflect.Ptr {
|
||||
return errors.New("bind must be a Ptr")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetWithBody(url string, header map[string]string, body io.Reader) (fr *ResponseBody, err error) {
|
||||
fr, err = do(http.MethodGet, url, header, body, int64(20*MB))
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package http
|
||||
|
||||
import "fmt"
|
||||
|
||||
func ExampleGet() {
|
||||
urlstr := "https://palce.hzbeisheng.com/api/sms"
|
||||
data, err := Get(urlstr, nil)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(string(data.Data))
|
||||
|
||||
// Output:
|
||||
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"videoweb/pub/common/log"
|
||||
|
||||
"github.com/ddliu/go-httpclient"
|
||||
)
|
||||
|
||||
// HTTPClient htttp客户端
|
||||
type HTTPClient struct {
|
||||
client *httpclient.HttpClient
|
||||
}
|
||||
|
||||
type ProxyCfg struct {
|
||||
Host string
|
||||
Timeout int //秒
|
||||
Source string
|
||||
}
|
||||
|
||||
func (p *ProxyCfg) Build(rawurl string, rawHeaders map[string]string) (string, map[string]string, error) {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
target := url.URL{Scheme: u.Scheme, Host: u.Host}
|
||||
u.Host = p.Host
|
||||
u.Scheme = "http"
|
||||
m := make(map[string]string)
|
||||
m["X-Proxy-Target-Host"] = target.String()
|
||||
m["X-Proxy-Source"] = "ys"
|
||||
if p.Timeout > 0 {
|
||||
m["X-Proxy-Timeout"] = strconv.Itoa(p.Timeout)
|
||||
}
|
||||
if len(rawHeaders) > 0 {
|
||||
for k, v := range rawHeaders {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
return u.String(), m, nil
|
||||
}
|
||||
|
||||
type httpClientPool struct {
|
||||
cliPool map[string]*httpclient.HttpClient
|
||||
poolSize int
|
||||
offset int
|
||||
}
|
||||
|
||||
// HTTPResponse http响应
|
||||
type HTTPResponse struct {
|
||||
StatusCode int `json:"statusCode"`
|
||||
Body *httpclient.Response `json:"boby"`
|
||||
}
|
||||
|
||||
var httpCliPool = httpClientPool{cliPool: map[string]*httpclient.HttpClient{}, poolSize: 50}
|
||||
|
||||
func New() *HTTPClient {
|
||||
h := httpclient.NewHttpClient().Defaults(httpclient.Map{
|
||||
httpclient.OPT_CONNECTTIMEOUT: 10,
|
||||
httpclient.OPT_TIMEOUT: 120,
|
||||
})
|
||||
return &HTTPClient{client: h}
|
||||
}
|
||||
|
||||
func NewCtx(ctx context.Context) *HTTPClient {
|
||||
h := httpclient.NewHttpClient().WithOptions(httpclient.Map{
|
||||
httpclient.OPT_CONNECTTIMEOUT: 10,
|
||||
httpclient.OPT_TIMEOUT: 10,
|
||||
httpclient.OPT_CONTEXT: ctx,
|
||||
})
|
||||
return &HTTPClient{client: h.Begin()}
|
||||
}
|
||||
|
||||
func Begin() *HTTPClient {
|
||||
h := httpclient.NewHttpClient().Defaults(httpclient.Map{
|
||||
httpclient.OPT_CONNECTTIMEOUT: 10,
|
||||
httpclient.OPT_TIMEOUT: 60,
|
||||
})
|
||||
return &HTTPClient{client: h.Begin()}
|
||||
}
|
||||
|
||||
// GetHTTPClient 获取httpclient
|
||||
func Client() *HTTPClient {
|
||||
cliID := "CLIENT-ID-"
|
||||
if len(httpCliPool.cliPool) < httpCliPool.poolSize {
|
||||
h := httpclient.NewHttpClient().Defaults(httpclient.Map{
|
||||
httpclient.OPT_CONNECTTIMEOUT: 10,
|
||||
httpclient.OPT_TIMEOUT: 120,
|
||||
})
|
||||
cliID = cliID + strconv.FormatInt(int64(len(httpCliPool.cliPool)+1), 10)
|
||||
httpCliPool.cliPool[cliID] = h
|
||||
httpCliPool.offset = httpCliPool.poolSize
|
||||
return &HTTPClient{client: h}
|
||||
}
|
||||
if httpCliPool.offset == httpCliPool.poolSize+1 {
|
||||
httpCliPool.offset = 1
|
||||
}
|
||||
cliID = cliID + strconv.FormatInt(int64(httpCliPool.offset), 10)
|
||||
httpCliPool.offset = httpCliPool.offset + 1
|
||||
h, ok := httpCliPool.cliPool[cliID]
|
||||
if !ok || h == nil {
|
||||
h = httpclient.NewHttpClient().WithOption(httpclient.OPT_TIMEOUT, 60)
|
||||
httpCliPool.cliPool[cliID] = h
|
||||
return &HTTPClient{client: h}
|
||||
}
|
||||
return &HTTPClient{client: h}
|
||||
}
|
||||
|
||||
func jsonUnmarshalResp(bind interface{}, resp *HTTPResponse) error {
|
||||
defer func() { _ = resp.Body.Body.Close() }()
|
||||
if bind == nil { //传入空表示只执行 defer
|
||||
return nil
|
||||
}
|
||||
b, err := resp.Body.ReadAll()
|
||||
if err != nil {
|
||||
log.Error("httputil jsonUnmarshalResp readall err", log.E(err))
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(b, bind)
|
||||
if err != nil {
|
||||
log.Error("httputil jsonUnmarshalResp Unmarshal err", log.E(err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Get Get方法
|
||||
func (h *HTTPClient) Get(url string, headers map[string]string, params ...interface{}) (*HTTPResponse, error) {
|
||||
h.client.WithHeaders(headers)
|
||||
response, err := h.client.Get(url, params...)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("http client Get method failed %+v:", err))
|
||||
return nil, err
|
||||
}
|
||||
return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
|
||||
}
|
||||
|
||||
func (h *HTTPClient) PGet(p *ProxyCfg, url string, headers map[string]string, params ...interface{}) (*HTTPResponse, error) {
|
||||
purl, pheaders, err := p.Build(url, headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.Get(purl, pheaders, params)
|
||||
}
|
||||
|
||||
// Get GetBytes
|
||||
func (h *HTTPClient) GetBytes(url string, headers map[string]string, params ...interface{}) ([]byte, error) {
|
||||
h.client.WithHeaders(headers)
|
||||
response, err := h.client.Get(url, params...)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("http client Get method failed %+v:", err))
|
||||
return nil, err
|
||||
}
|
||||
data, err := response.ReadAll()
|
||||
if err != nil {
|
||||
log.ZapLog.Error("resp readAll errror", log.Any("Error", err))
|
||||
return data, err
|
||||
}
|
||||
defer func() {
|
||||
_ = response.Body.Close()
|
||||
}()
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// GetWithJResp 结果json.Unmarshal到bind中
|
||||
func (h *HTTPClient) GetWithJResp(bind interface{}, url string, headers map[string]string, params ...interface{}) (int, error) {
|
||||
resp, err := h.Get(url, headers, params...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
err = jsonUnmarshalResp(bind, resp)
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
|
||||
func (h *HTTPClient) PGetWithJResp(p *ProxyCfg, bind interface{}, url string, headers map[string]string, params ...interface{}) (int, error) {
|
||||
purl, pheaders, err := p.Build(url, headers)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return h.GetWithJResp(bind, purl, pheaders, params...)
|
||||
}
|
||||
|
||||
// Get Get方法
|
||||
func (h *HTTPClient) Delete(url string, headers map[string]string, params ...interface{}) (*HTTPResponse, error) {
|
||||
h.client.WithHeaders(headers)
|
||||
response, err := h.client.Delete(url, params...)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("http client Delete method failed %+v:", err))
|
||||
return nil, err
|
||||
}
|
||||
return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
|
||||
}
|
||||
|
||||
// DeleteWithJResp 结果json.Unmarshal到bind中
|
||||
func (h *HTTPClient) DeleteWithJResp(bind interface{}, url string, headers map[string]string, params ...interface{}) (int, error) {
|
||||
resp, err := h.Delete(url, headers, params...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
err = jsonUnmarshalResp(bind, resp)
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
|
||||
// Post Post方法
|
||||
func (h *HTTPClient) Post(url string, headers map[string]string, params interface{}) (*HTTPResponse, error) {
|
||||
h.client.WithHeaders(headers)
|
||||
response, err := h.client.Post(url, params)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("http client Post method failed %+v:", err))
|
||||
return nil, err
|
||||
}
|
||||
return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
|
||||
}
|
||||
|
||||
func (h *HTTPClient) PPost(p *ProxyCfg, bind interface{}, url string, headers map[string]string, params interface{}) (*HTTPResponse, error) {
|
||||
purl, pheaders, err := p.Build(url, headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.Post(purl, pheaders, params)
|
||||
}
|
||||
|
||||
// POSTJson PostJson方法
|
||||
func (h *HTTPClient) POSTJson(url string, headers map[string]string, data interface{}) (*HTTPResponse, error) {
|
||||
h.client.WithHeaders(headers)
|
||||
response, err := h.client.PostJson(url, data)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("http client POSTJson method failed %+v:", err))
|
||||
return nil, err
|
||||
}
|
||||
return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
|
||||
}
|
||||
|
||||
// POSTJson PostJson方法, bind json.Unmarshal
|
||||
func (h *HTTPClient) POSTJsonWithJResp(bind interface{}, url string, headers map[string]string, data interface{}) (int, error) {
|
||||
resp, err := h.POSTJson(url, headers, data)
|
||||
if err != nil {
|
||||
log.Error("POSTJsonWithResp err", log.E(err))
|
||||
return 0, err
|
||||
}
|
||||
err = jsonUnmarshalResp(bind, resp)
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
|
||||
func (h *HTTPClient) PPOSTJsonWithJResp(p *ProxyCfg, bind interface{}, url string, headers map[string]string, data interface{}) (int, error) {
|
||||
if p == nil {
|
||||
return 0, fmt.Errorf("ProxyCfg is nil")
|
||||
}
|
||||
purl, pheaders, err := p.Build(url, headers)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return h.POSTJsonWithJResp(bind, purl, pheaders, data)
|
||||
}
|
||||
|
||||
// PostMultipart PostMultipart 上传文件
|
||||
func (h *HTTPClient) PostMultipart(url string, headers map[string]string, params interface{}) (*HTTPResponse, error) {
|
||||
h.client.WithHeaders(headers)
|
||||
response, err := h.client.PostMultipart(url, params)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("http client PostMultipart method failed %+v:", err))
|
||||
return nil, err
|
||||
}
|
||||
return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
|
||||
}
|
||||
|
||||
// Put 流式传输 可用于上传文件
|
||||
func (h *HTTPClient) Put(url string, headers map[string]string, body io.Reader) (*HTTPResponse, error) {
|
||||
h.client.WithHeaders(headers)
|
||||
response, err := h.client.Put(url, body)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("http client PostMultipart method failed %+v:", err))
|
||||
return nil, err
|
||||
}
|
||||
return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
|
||||
}
|
||||
|
||||
// PutJSON PutJSON方法
|
||||
func (h *HTTPClient) PutJSON(url string, headers map[string]string, data interface{}) (*HTTPResponse, error) {
|
||||
h.client.WithHeaders(headers)
|
||||
response, err := h.client.PutJson(url, data)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("http client PostMultipart method failed %+v:", err))
|
||||
return nil, err
|
||||
}
|
||||
return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
|
||||
}
|
||||
|
||||
// Header Header
|
||||
func (h *HTTPClient) Header(url string) (*HTTPResponse, error) {
|
||||
response, err := h.client.Head(url)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("http client PostMultipart method failed %+v:", err))
|
||||
return nil, err
|
||||
}
|
||||
return &HTTPResponse{StatusCode: response.StatusCode, Body: response}, nil
|
||||
}
|
||||
|
||||
// POSTWithJResp 结果json.Unmarshal到bind中 用于上传文件, content-type :"application/x-www-form-urlencoded" 如参数以@开头 则为上传文件
|
||||
func (h *HTTPClient) POSTWithJResp(bind interface{}, url string, headers map[string]string, params interface{}) (int, error) {
|
||||
resp, err := h.Post(url, headers, params)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
err = jsonUnmarshalResp(bind, resp)
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
|
||||
// POSTWithJResp 结果json.Unmarshal到bind中 用于上传文件, content-type :"application/x-www-form-urlencoded" 如参数以@开头 则为上传文件
|
||||
func (h *HTTPClient) PPOSTWithJResp(p *ProxyCfg, bind interface{}, url string, headers map[string]string, params interface{}) (int, error) {
|
||||
resp, err := h.PPost(p, bind, url, headers, params)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
err = jsonUnmarshalResp(bind, resp)
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type ResponseInfo struct {
|
||||
Gzipped bool `json:"gzipped"`
|
||||
Method string `json:"method"`
|
||||
Origin string `json:"origin"`
|
||||
Useragent string `json:"user-agent"` // http://httpbin.org/user-agent
|
||||
Form map[string]string `json:"form"`
|
||||
Files map[string]string `json:"files"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Cookies map[string]string `json:"cookies"`
|
||||
}
|
||||
|
||||
var info ResponseInfo
|
||||
|
||||
type Resport struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func Test_Get_Httpcli(t *testing.T) {
|
||||
client := Client()
|
||||
var headers = map[string]string{"auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjZTJiNzNmZTI3Y2YyMmE4ZjQyMTVkMCIsInVzZXJOYW1lIjoidHVib2xpbmcxNjgiLCJpc0xvZ2luIjp0cnVlLCJsb2dpblRpbWUiOjE1NjQxNDYwNzI5MDUsImlhdCI6MTU2NDE0NjA3Mn0.VQICBk71KDITus83pZEq3C1y_WbKPZc0HUfmIm_eby8"}
|
||||
respo, err := client.Get("https://no5.xiexiaojia.com/appjmall/api/showCore/appShowList", headers)
|
||||
defer respo.Body.Body.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("%+v", err)
|
||||
}
|
||||
resport := Resport{}
|
||||
body, err := respo.Body.ReadAll()
|
||||
//反序列化
|
||||
err = json.Unmarshal(body, &resport)
|
||||
fmt.Println("response", resport)
|
||||
//案例获取数据
|
||||
fmt.Println("response comName:", resport.Data.([]interface{})[0].(map[string]interface{})["comName"])
|
||||
}
|
||||
|
||||
func Test_Post_Httpcli(t *testing.T) {
|
||||
client := Client()
|
||||
fmt.Println("http 客户端", client)
|
||||
var headers = map[string]string{"auth_token": "xxxxx"}
|
||||
var params = map[string]string{"userId": "5ce2b73fe27cf22a8f4215d0", "price": "5000", "count": "1", "commId": "5ce40e03e27cf22a8f4215d8",
|
||||
"comName": "日本ceruru.b 脐带血面膜保湿补水修复5片",
|
||||
"comImageCar": "http://oam.szbslhb.com/4,01fb06b4adf1.jpg",
|
||||
"attributeCar": "",
|
||||
}
|
||||
respo, err := client.Post("https://no5.xiexiaojia.com/appjmall/api/cart/addCommToCart", headers, params)
|
||||
defer respo.Body.Body.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("%+v", err)
|
||||
}
|
||||
resport := Resport{}
|
||||
body, err := respo.Body.ReadAll()
|
||||
if err != nil {
|
||||
res, _ := respo.Body.ToString()
|
||||
fmt.Println("response", res)
|
||||
return
|
||||
}
|
||||
//反序列化
|
||||
err = json.Unmarshal(body, &resport)
|
||||
fmt.Println("response", resport)
|
||||
}
|
||||
|
||||
func Test_PostJson_Httpcli(t *testing.T) {
|
||||
client := Client()
|
||||
var headers = map[string]string{"Content-Type": "application/json"}
|
||||
fmt.Println("http 客户端", client)
|
||||
respo, err := client.Get("https://www.kancloud.cn/cattong/go_command_tutorial/261347", headers)
|
||||
defer respo.Body.Body.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("%+v", err)
|
||||
}
|
||||
body, err := respo.Body.ToString()
|
||||
fmt.Println("response", body)
|
||||
}
|
||||
|
||||
func Test_PutFile_Httpcli(t *testing.T) {
|
||||
client := Client()
|
||||
file, err := os.Open("/Users/mac/Downloads/1567069549ABBQCTFC.mp4")
|
||||
info, _ := file.Stat()
|
||||
|
||||
var headers = map[string]string{
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Authorization": "bucketName=bucket-01;accessKey=FSd58ed59fccc16812903c37068de8969b;ttl=1h",
|
||||
"Content-Disposition": "fileName=test02.mp4;size=" + strconv.FormatInt(info.Size(), 10),
|
||||
"Signature": "9cea21bf95f235e7d9c5a70888cf742cfb9c946ed25aef8b70ce77c953ccbff0",
|
||||
}
|
||||
fmt.Println("http 客户端", client)
|
||||
respo, err := client.Put("http://localhost:8888/fs/api/uploadStream", headers, file)
|
||||
defer respo.Body.Body.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("%+v", err)
|
||||
}
|
||||
body, err := respo.Body.ToString()
|
||||
fmt.Println("response", body)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package imgutil
|
||||
|
||||
import "bytes"
|
||||
|
||||
// 与管理后台 ShowImg / imgCode.js 保持一致。
|
||||
var (
|
||||
encryptMagicNumber = []byte{0x88, 0xA8, 0x30, 0xCB, 0x10, 0x76}
|
||||
decryptKey = []byte{50, 48, 49, 57, 121, 115, 97, 112, 112, 55, 53, 50, 55} // 2019ysapp7527
|
||||
plainImageHeaders = [][]byte{
|
||||
{0xFF, 0xD8, 0xFF}, // jpeg
|
||||
{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}, // png
|
||||
{0x47, 0x49, 0x46}, // gif
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
encryptedPrefixLen = 100
|
||||
encryptXORAllKey = 0xA3
|
||||
)
|
||||
|
||||
// DecryptImage 按后台 handleVerAutoImg 的规则解密图床图片。
|
||||
// 已是 jpg/png/gif 则原样返回。
|
||||
func DecryptImage(data []byte) []byte {
|
||||
if len(data) == 0 {
|
||||
return data
|
||||
}
|
||||
out := append([]byte(nil), data...)
|
||||
if hasEncryptMagic(out) {
|
||||
return xorAllLength(out)
|
||||
}
|
||||
if !isPlainImage(out) {
|
||||
xorPrefix(out, decryptKey, encryptedPrefixLen)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func hasEncryptMagic(data []byte) bool {
|
||||
return len(data) >= len(encryptMagicNumber) && bytes.Equal(data[:len(encryptMagicNumber)], encryptMagicNumber)
|
||||
}
|
||||
|
||||
func isPlainImage(data []byte) bool {
|
||||
for _, header := range plainImageHeaders {
|
||||
if len(data) >= len(header) && bytes.Equal(data[:len(header)], header) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func xorPrefix(src, key []byte, length int) {
|
||||
if len(key) == 0 || len(src) == 0 {
|
||||
return
|
||||
}
|
||||
if length <= 0 || length > len(src) {
|
||||
length = len(src)
|
||||
}
|
||||
for i := 0; i < length; {
|
||||
for j := 0; j < len(key) && i < length; j++ {
|
||||
src[i] ^= key[j]
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func xorAllLength(src []byte) []byte {
|
||||
out := make([]byte, 0, len(src))
|
||||
for i, b := range src {
|
||||
if i < len(encryptMagicNumber) && b == encryptMagicNumber[i] {
|
||||
continue
|
||||
}
|
||||
out = append(out, b^encryptXORAllKey)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package imgutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
"image/jpeg"
|
||||
_ "image/png"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
// CoverResizeLimitKB 与 media utils.Resize 一致:超过该大小才缩放
|
||||
CoverResizeLimitKB = 60
|
||||
// CoverMaxWidth 与 media Thumbnail 一致
|
||||
CoverMaxWidth = 750
|
||||
// CoverMaxHeight 与 media Thumbnail 一致
|
||||
CoverMaxHeight = 1560
|
||||
// CoverJPEGQuality 压缩后统一输出 JPEG
|
||||
CoverJPEGQuality = 75
|
||||
)
|
||||
|
||||
// IsGIF 按文件头判断是否为 GIF,不依赖扩展名。
|
||||
func IsGIF(data []byte) bool {
|
||||
return len(data) >= 3 && bytes.Equal(data[:3], []byte{0x47, 0x49, 0x46})
|
||||
}
|
||||
|
||||
// NeedCompress 原图是否超过 media Resize 阈值,未超过则无需压缩重传。
|
||||
// GIF 保持原格式和动效,不参与压缩。
|
||||
func NeedCompress(data []byte) bool {
|
||||
if IsGIF(data) {
|
||||
return false
|
||||
}
|
||||
return int64(len(data))/1024 > CoverResizeLimitKB
|
||||
}
|
||||
|
||||
// CompressCover 按 media Resize(60KB, 750×1560) 压缩封面。
|
||||
// GIF 原样返回;其它格式未超过阈值时原样返回,超过则缩放后统一输出 JPEG。
|
||||
func CompressCover(data []byte, fileName string) ([]byte, string, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, "", fmt.Errorf("图片内容为空")
|
||||
}
|
||||
outName := compressFileName(fileName)
|
||||
if IsGIF(data) {
|
||||
return data, withExt(outName, ".gif"), nil
|
||||
}
|
||||
if !NeedCompress(data) {
|
||||
return data, outName, nil
|
||||
}
|
||||
img, _, err := image.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("解码图片失败: %w", err)
|
||||
}
|
||||
thumb := thumbnail(img, CoverMaxWidth, CoverMaxHeight)
|
||||
var buf bytes.Buffer
|
||||
if err = jpeg.Encode(&buf, thumb, &jpeg.Options{Quality: CoverJPEGQuality}); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return buf.Bytes(), withExt(outName, ".jpg"), nil
|
||||
}
|
||||
|
||||
// SafeCoverFileName 生成仅含 ASCII 的上传文件名。
|
||||
// 原封面路径含中文时,直接用 path.Base 会把中文带进上传名,图床返回地址会异常。
|
||||
func SafeCoverFileName(src string) string {
|
||||
src = strings.TrimSpace(src)
|
||||
ext := asciiImageExt(src)
|
||||
if src == "" {
|
||||
return "playlet_cover" + ext
|
||||
}
|
||||
sum := sha1.Sum([]byte(src))
|
||||
return fmt.Sprintf("playlet_cover_%x%s", sum[:8], ext)
|
||||
}
|
||||
|
||||
func thumbnail(img image.Image, maxW, maxH int) image.Image {
|
||||
b := img.Bounds()
|
||||
w, h := b.Dx(), b.Dy()
|
||||
if w <= maxW && h <= maxH {
|
||||
return img
|
||||
}
|
||||
wr := float64(maxW) / float64(w)
|
||||
hr := float64(maxH) / float64(h)
|
||||
scale := wr
|
||||
if hr < wr {
|
||||
scale = hr
|
||||
}
|
||||
nw := int(float64(w) * scale)
|
||||
nh := int(float64(h) * scale)
|
||||
if nw < 1 {
|
||||
nw = 1
|
||||
}
|
||||
if nh < 1 {
|
||||
nh = 1
|
||||
}
|
||||
dst := image.NewRGBA(image.Rect(0, 0, nw, nh))
|
||||
for y := 0; y < nh; y++ {
|
||||
sy := b.Min.Y + y*h/nh
|
||||
for x := 0; x < nw; x++ {
|
||||
sx := b.Min.X + x*w/nw
|
||||
dst.Set(x, y, img.At(sx, sy))
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func compressFileName(fileName string) string {
|
||||
fileName = filepath.Base(strings.TrimSpace(fileName))
|
||||
if fileName == "" || fileName == "." || fileName == string(filepath.Separator) || !isASCIIFileName(fileName) {
|
||||
return "playlet_cover.jpg"
|
||||
}
|
||||
return fileName
|
||||
}
|
||||
|
||||
func asciiImageExt(src string) string {
|
||||
name := src
|
||||
if u, err := url.Parse(src); err == nil && u.Path != "" {
|
||||
name = u.Path
|
||||
}
|
||||
name = strings.Split(path.Base(name), "?")[0]
|
||||
if dec, err := url.PathUnescape(name); err == nil {
|
||||
name = dec
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg", ".png", ".gif", ".webp":
|
||||
return ext
|
||||
default:
|
||||
return ".jpg"
|
||||
}
|
||||
}
|
||||
|
||||
func isASCIIFileName(name string) bool {
|
||||
if !strings.Contains(name, ".") {
|
||||
return false
|
||||
}
|
||||
for _, r := range name {
|
||||
if r > unicode.MaxASCII || r < 32 || r == '"' || r == '/' || r == '\\' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func withExt(fileName, ext string) string {
|
||||
return strings.TrimSuffix(fileName, filepath.Ext(fileName)) + ext
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
package ipaddr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
||||
)
|
||||
|
||||
// XDBDetector 基于 xdb 的海外IP检测器
|
||||
type XDBDetector struct {
|
||||
searcher *xdb.Searcher
|
||||
cache sync.Map
|
||||
cacheTTL time.Duration
|
||||
}
|
||||
|
||||
// RegionInfo 区域信息
|
||||
type RegionInfo struct {
|
||||
Country string `json:"country"`
|
||||
Region string `json:"region"`
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
ISP string `json:"isp"`
|
||||
IsOverseas bool `json:"is_overseas"`
|
||||
IsChinaMainland bool `json:"is_china_mainland"`
|
||||
}
|
||||
|
||||
type IPVersion string
|
||||
|
||||
const (
|
||||
IPv4 IPVersion = "IPv4"
|
||||
IPv6 IPVersion = "IPv6"
|
||||
InvalidIP IPVersion = "Invalid"
|
||||
)
|
||||
|
||||
// GetIPVersion 获取IP版本
|
||||
func GetIPVersion(ip string) IPVersion {
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
return InvalidIP
|
||||
}
|
||||
|
||||
if parsedIP.To4() != nil {
|
||||
return IPv4
|
||||
}
|
||||
return IPv6
|
||||
}
|
||||
|
||||
// IsIPv4 判断是否为IPv4
|
||||
func IsIPv4(ip string) bool {
|
||||
return GetIPVersion(ip) == IPv4
|
||||
}
|
||||
|
||||
// NewXDBDetector 创建 xdb 检测器
|
||||
func NewXDBDetectorV6(dbPath string) (*XDBDetector, error) {
|
||||
// 自动查找数据库文件
|
||||
if dbPath == "" {
|
||||
dbPath = findXDBFile()
|
||||
if dbPath == "" {
|
||||
return nil, fmt.Errorf("未找到 ip2region.xdb 数据库文件")
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("数据库文件不存在: %s", dbPath)
|
||||
}
|
||||
|
||||
// 读取整个 xdb 文件到内存
|
||||
cBuff, err := xdb.LoadContentFromFile(dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载 xdb 内容失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建搜索器
|
||||
searcher, err := xdb.NewWithBuffer(xdb.IPv6, cBuff)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建搜索器失败: %v", err)
|
||||
}
|
||||
|
||||
detector := &XDBDetector{
|
||||
searcher: searcher,
|
||||
cacheTTL: 24 * time.Hour,
|
||||
}
|
||||
|
||||
log.Printf("XDBDetector 初始化成功,数据库: %s", dbPath)
|
||||
return detector, nil
|
||||
}
|
||||
|
||||
// NewXDBDetector 创建 xdb 检测器
|
||||
func NewXDBDetectorV4(dbPath string) (*XDBDetector, error) {
|
||||
// 自动查找数据库文件
|
||||
if dbPath == "" {
|
||||
dbPath = findXDBFile()
|
||||
if dbPath == "" {
|
||||
return nil, fmt.Errorf("未找到 ip2region.xdb 数据库文件")
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("数据库文件不存在: %s", dbPath)
|
||||
}
|
||||
|
||||
// 读取整个 xdb 文件到内存
|
||||
cBuff, err := xdb.LoadContentFromFile(dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载 xdb 内容失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建搜索器
|
||||
searcher, err := xdb.NewWithBuffer(xdb.IPv4, cBuff)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建搜索器失败: %v", err)
|
||||
}
|
||||
|
||||
detector := &XDBDetector{
|
||||
searcher: searcher,
|
||||
cacheTTL: 24 * time.Hour,
|
||||
}
|
||||
|
||||
log.Printf("XDBDetector 初始化成功,数据库: %s", dbPath)
|
||||
return detector, nil
|
||||
}
|
||||
|
||||
// Search 搜索IP信息
|
||||
func (d *XDBDetector) Search(ip string) (string, error) {
|
||||
if d.searcher == nil {
|
||||
return "", fmt.Errorf("搜索器未初始化")
|
||||
}
|
||||
|
||||
// 验证IP格式
|
||||
if net.ParseIP(ip) == nil {
|
||||
return "", fmt.Errorf("无效的IP地址: %s", ip)
|
||||
}
|
||||
|
||||
return d.searcher.SearchByStr(ip)
|
||||
}
|
||||
|
||||
// SearchRegion 搜索并解析区域信息
|
||||
func (d *XDBDetector) SearchRegion(ip string) (*RegionInfo, error) {
|
||||
regionStr, err := d.Search(ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return d.parseRegionString(regionStr), nil
|
||||
}
|
||||
|
||||
// parseRegionString 解析区域字符串
|
||||
// 格式: 国家|区域|省份|城市|ISP
|
||||
func (d *XDBDetector) parseRegionString(regionStr string) *RegionInfo {
|
||||
parts := strings.Split(regionStr, "|")
|
||||
|
||||
// 确保有足够的字段
|
||||
for len(parts) < 5 {
|
||||
parts = append(parts, "")
|
||||
}
|
||||
|
||||
country := strings.TrimSpace(parts[0])
|
||||
region := strings.TrimSpace(parts[1])
|
||||
province := strings.TrimSpace(parts[2])
|
||||
city := strings.TrimSpace(parts[3])
|
||||
isp := strings.TrimSpace(parts[4])
|
||||
|
||||
isChinaMainland := d.isChinaMainland(country)
|
||||
isOverseas := !isChinaMainland
|
||||
|
||||
// 特别行政区处理
|
||||
if !isChinaMainland && d.isChinaSpecialRegion(country) {
|
||||
// 可以根据业务需求调整特别行政区是否视为海外
|
||||
isOverseas = false
|
||||
}
|
||||
|
||||
return &RegionInfo{
|
||||
Country: country,
|
||||
Region: region,
|
||||
Province: province,
|
||||
City: city,
|
||||
ISP: isp,
|
||||
IsOverseas: isOverseas,
|
||||
IsChinaMainland: isChinaMainland,
|
||||
}
|
||||
}
|
||||
|
||||
// IsOverseasIP 判断是否为海外IP
|
||||
func (d *XDBDetector) IsOverseasIP(ip string) (bool, error) {
|
||||
// 检查缓存
|
||||
if cached, ok := d.getFromCache(ip); ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
// 内网IP默认为国内
|
||||
if d.isPrivateIP(ip) {
|
||||
d.setCache(ip, false)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
regionInfo, err := d.SearchRegion(ip)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
d.setCache(ip, regionInfo.IsOverseas)
|
||||
return regionInfo.IsOverseas, nil
|
||||
}
|
||||
|
||||
// isChinaMainland 判断是否为中国大陆
|
||||
func (d *XDBDetector) isChinaMainland(country string) bool {
|
||||
if country == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
chinaKeywords := []string{"中国", "China", "CN"}
|
||||
for _, keyword := range chinaKeywords {
|
||||
if country == keyword {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
lowerCountry := strings.ToLower(country)
|
||||
for _, keyword := range []string{"china", "cn", "中国"} {
|
||||
if strings.Contains(lowerCountry, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isChinaSpecialRegion 判断是否为中国特别行政区
|
||||
func (d *XDBDetector) isChinaSpecialRegion(region string) bool {
|
||||
specialRegions := []string{
|
||||
"台湾", "台湾省", "Taiwan", "TW",
|
||||
"香港", "Hong Kong", "HK",
|
||||
"澳门", "Macao", "Macau", "MO",
|
||||
}
|
||||
|
||||
for _, keyword := range specialRegions {
|
||||
if region == keyword {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// BatchIsOverseas 批量判断海外IP
|
||||
func (d *XDBDetector) BatchIsOverseas(ips []string) (map[string]bool, error) {
|
||||
results := make(map[string]bool)
|
||||
|
||||
for _, ip := range ips {
|
||||
isOverseas, err := d.IsOverseasIP(ip)
|
||||
if err != nil {
|
||||
// 失败时默认为海外(安全策略)
|
||||
results[ip] = true
|
||||
continue
|
||||
}
|
||||
results[ip] = isOverseas
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// isPrivateIP 判断内网IP
|
||||
func (d *XDBDetector) isPrivateIP(ip string) bool {
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
privateBlocks := []string{
|
||||
"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
|
||||
"127.0.0.0/8", "169.254.0.0/16",
|
||||
"::1/128", "fc00::/7", "fe80::/10",
|
||||
}
|
||||
|
||||
for _, block := range privateBlocks {
|
||||
_, ipNet, err := net.ParseCIDR(block)
|
||||
if err == nil && ipNet.Contains(parsedIP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 缓存管理
|
||||
func (d *XDBDetector) getFromCache(ip string) (bool, bool) {
|
||||
if item, ok := d.cache.Load(ip); ok {
|
||||
if cacheItem, ok := item.(*cacheResult); ok {
|
||||
if time.Since(cacheItem.timestamp) < d.cacheTTL {
|
||||
return cacheItem.isOverseas, true
|
||||
}
|
||||
d.cache.Delete(ip)
|
||||
}
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
func (d *XDBDetector) setCache(ip string, isOverseas bool) {
|
||||
d.cache.Store(ip, &cacheResult{
|
||||
isOverseas: isOverseas,
|
||||
timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
type cacheResult struct {
|
||||
isOverseas bool
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
// Close 关闭检测器
|
||||
func (d *XDBDetector) Close() {
|
||||
if d.searcher != nil {
|
||||
d.searcher.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// findXDBFile 查找 xdb 文件
|
||||
func findXDBFile() string {
|
||||
paths := []string{
|
||||
"ip2region.xdb",
|
||||
"data/ip2region.xdb",
|
||||
"./ip2region.xdb",
|
||||
"../ip2region.xdb",
|
||||
"/usr/local/share/ip2region/ip2region.xdb",
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ResolveClientIP 解析客户端 IP,用于 m3u8 限流、token 签发与 ipstat 统计。
|
||||
func ResolveClientIP(c *gin.Context) string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
remoteIP := normalizeIPString(c.RemoteIP())
|
||||
clientIP := normalizeIPString(c.ClientIP())
|
||||
|
||||
if shouldTrustForwardedHeaders(remoteIP, clientIP) {
|
||||
if ip := firstPublicIPFromHeader(c.GetHeader("X-Forwarded-For")); ip != "" {
|
||||
return ip
|
||||
}
|
||||
if ip := normalizeIPString(c.GetHeader("X-Real-IP")); ip != "" && !isPrivateOrLoopbackIP(ip) {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
|
||||
if clientIP != "" && !isPrivateOrLoopbackIP(clientIP) {
|
||||
return clientIP
|
||||
}
|
||||
if clientIP != "" {
|
||||
return clientIP
|
||||
}
|
||||
return remoteIP
|
||||
}
|
||||
|
||||
func shouldTrustForwardedHeaders(remoteIP, clientIP string) bool {
|
||||
if isPrivateOrLoopbackIP(remoteIP) || isLocalMachineIP(remoteIP) {
|
||||
return true
|
||||
}
|
||||
return clientIP != "" && remoteIP != "" && clientIP != remoteIP
|
||||
}
|
||||
|
||||
// isLocalMachineIP 判断对端是否本机网卡地址。
|
||||
// H5 与 1043 一样用本机公网 IP 回源 app,此时对端不是 127.0.0.1,但仍应信任 X-Real-IP。
|
||||
func isLocalMachineIP(ipStr string) bool {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
var candidate net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
candidate = v.IP
|
||||
case *net.IPAddr:
|
||||
candidate = v.IP
|
||||
}
|
||||
if candidate != nil && candidate.Equal(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeIPString(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
ip := net.ParseIP(raw)
|
||||
if ip == nil {
|
||||
return raw
|
||||
}
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
return v4.String()
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
func firstPublicIPFromHeader(xff string) string {
|
||||
xff = strings.TrimSpace(xff)
|
||||
if xff == "" {
|
||||
return ""
|
||||
}
|
||||
for _, part := range strings.Split(xff, ",") {
|
||||
ip := normalizeIPString(strings.TrimSpace(part))
|
||||
if ip != "" && !isPrivateOrLoopbackIP(ip) {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isPrivateOrLoopbackIP(ipStr string) bool {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// LaosijiDomains 老司机上传凭证缓存。由 web server 初始化。
|
||||
LaosijiDomains *Domains
|
||||
// LaosijiClient 老司机业务 API 客户端。由 web server 初始化。
|
||||
LaosijiClient *Client
|
||||
)
|
||||
|
||||
// Client 老司机「业务 API」客户端(区别于上传站 upload_url)。
|
||||
type Client struct {
|
||||
baseURL string
|
||||
appid string
|
||||
appkey string
|
||||
hc *http.Client
|
||||
}
|
||||
|
||||
// New 创建业务 API 客户端。baseURL 含 /lsjapi 前缀。
|
||||
func New(baseURL, appid, appkey string) *Client {
|
||||
return &Client{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
appid: appid,
|
||||
appkey: appkey,
|
||||
hc: &http.Client{Timeout: 40 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
// SetHTTPClient 替换底层 http.Client(测试/自定义超时用)。
|
||||
func (c *Client) SetHTTPClient(hc *http.Client) { c.hc = hc }
|
||||
|
||||
type apiResp struct {
|
||||
Status string `json:"status"`
|
||||
Data string `json:"data"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// ErrEmptyData status=y 但 data 为空。
|
||||
var ErrEmptyData = errors.New("laosiji: empty response data")
|
||||
|
||||
// Call 发起一次业务 API 调用。
|
||||
func (c *Client) Call(ctx context.Context, path string, req, out interface{}) error {
|
||||
reqJSON := []byte("{}")
|
||||
if req != nil {
|
||||
b, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("laosiji: marshal req: %w", err)
|
||||
}
|
||||
reqJSON = b
|
||||
}
|
||||
encBody, err := EncryptBase64(reqJSON, c.appkey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("laosiji: encrypt req: %w", err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, strings.NewReader(encBody))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpReq.Header.Set("appid", c.appid)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.hc.Do(httpReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("laosiji: http do: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("laosiji: read resp: %w", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("laosiji: http %d: %s", resp.StatusCode, truncate(body, 200))
|
||||
}
|
||||
|
||||
var outer apiResp
|
||||
if err := json.Unmarshal(body, &outer); err != nil {
|
||||
return fmt.Errorf("laosiji: unmarshal outer: %w (body=%s)", err, truncate(body, 200))
|
||||
}
|
||||
if outer.Status != "y" {
|
||||
return fmt.Errorf("laosiji: api status=%q error=%q", outer.Status, outer.Error)
|
||||
}
|
||||
if out == nil {
|
||||
return nil
|
||||
}
|
||||
if outer.Data == "" {
|
||||
return ErrEmptyData
|
||||
}
|
||||
plain, err := DecryptBase64(outer.Data, c.appkey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("laosiji: decrypt data: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(plain, out); err != nil {
|
||||
return fmt.Errorf("laosiji: unmarshal data: %w (plain=%s)", err, truncate(plain, 200))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func truncate(b []byte, n int) string {
|
||||
if len(b) > n {
|
||||
return string(b[:n]) + "..."
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidCipher = errors.New("laosiji crypto: invalid ciphertext length")
|
||||
ErrInvalidPadding = errors.New("laosiji crypto: invalid pkcs7 padding")
|
||||
)
|
||||
|
||||
func AesKey(appkey string) []byte {
|
||||
key := make([]byte, 16)
|
||||
copy(key, appkey)
|
||||
return key
|
||||
}
|
||||
|
||||
func EncryptBase64(plain []byte, appkey string) (string, error) {
|
||||
block, err := aes.NewCipher(AesKey(appkey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
padded := pkcs7Pad(plain, block.BlockSize())
|
||||
out := make([]byte, len(padded))
|
||||
ecbEncrypt(block, out, padded)
|
||||
return base64.StdEncoding.EncodeToString(out), nil
|
||||
}
|
||||
|
||||
func DecryptBase64(b64 string, appkey string) ([]byte, error) {
|
||||
ct, err := base64.StdEncoding.DecodeString(b64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block, err := aes.NewCipher(AesKey(appkey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bs := block.BlockSize()
|
||||
if len(ct) == 0 || len(ct)%bs != 0 {
|
||||
return nil, ErrInvalidCipher
|
||||
}
|
||||
out := make([]byte, len(ct))
|
||||
ecbDecrypt(block, out, ct)
|
||||
return pkcs7Unpad(out, bs)
|
||||
}
|
||||
|
||||
func ecbEncrypt(block cipher.Block, dst, src []byte) {
|
||||
bs := block.BlockSize()
|
||||
for len(src) > 0 {
|
||||
block.Encrypt(dst[:bs], src[:bs])
|
||||
src = src[bs:]
|
||||
dst = dst[bs:]
|
||||
}
|
||||
}
|
||||
|
||||
func ecbDecrypt(block cipher.Block, dst, src []byte) {
|
||||
bs := block.BlockSize()
|
||||
for len(src) > 0 {
|
||||
block.Decrypt(dst[:bs], src[:bs])
|
||||
src = src[bs:]
|
||||
dst = dst[bs:]
|
||||
}
|
||||
}
|
||||
|
||||
func pkcs7Pad(data []byte, blockSize int) []byte {
|
||||
pad := blockSize - len(data)%blockSize
|
||||
return append(data, bytes.Repeat([]byte{byte(pad)}, pad)...)
|
||||
}
|
||||
|
||||
func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
|
||||
n := len(data)
|
||||
if n == 0 || n%blockSize != 0 {
|
||||
return nil, ErrInvalidPadding
|
||||
}
|
||||
pad := int(data[n-1])
|
||||
if pad == 0 || pad > blockSize {
|
||||
return nil, ErrInvalidPadding
|
||||
}
|
||||
for i := n - pad; i < n; i++ {
|
||||
if int(data[i]) != pad {
|
||||
return nil, ErrInvalidPadding
|
||||
}
|
||||
}
|
||||
return data[:n-pad], nil
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const SystemDomainsPath = "/lsjapi/system/domains"
|
||||
|
||||
type SystemDomains struct {
|
||||
ImgFreeCdn string `json:"img_free_cdn"`
|
||||
ImgVipCdn string `json:"img_vip_cdn"`
|
||||
ImgCnCdn string `json:"img_cn_cdn"`
|
||||
ImgSourceCdn string `json:"img_source_cdn"`
|
||||
MovieFreeCdn string `json:"movie_free_cdn"`
|
||||
MovieVipCdn string `json:"movie_vip_cdn"`
|
||||
MovieCnCdn string `json:"movie_cn_cdn"`
|
||||
MovieSourceCdn string `json:"movie_source_cdn"`
|
||||
UploadURL string `json:"upload_url"`
|
||||
MediaDir string `json:"media_dir"`
|
||||
UploadKey string `json:"upload_key"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
type Domains struct {
|
||||
client *Client
|
||||
|
||||
mu sync.RWMutex
|
||||
cur *SystemDomains
|
||||
fetchedAt time.Time
|
||||
}
|
||||
|
||||
func NewDomains(client *Client) *Domains {
|
||||
return &Domains{client: client}
|
||||
}
|
||||
|
||||
func (d *Domains) Refresh(ctx context.Context) (*SystemDomains, error) {
|
||||
var sd SystemDomains
|
||||
if err := d.client.Call(ctx, SystemDomainsPath, map[string]string{}, &sd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.mu.Lock()
|
||||
d.cur = &sd
|
||||
d.fetchedAt = time.Now()
|
||||
d.mu.Unlock()
|
||||
return &sd, nil
|
||||
}
|
||||
|
||||
func (d *Domains) Get(ctx context.Context) (*SystemDomains, error) {
|
||||
d.mu.RLock()
|
||||
cur := d.cur
|
||||
d.mu.RUnlock()
|
||||
if cur != nil {
|
||||
return cur, nil
|
||||
}
|
||||
return d.Refresh(ctx)
|
||||
}
|
||||
|
||||
func (d *Domains) StartAutoRefresh(interval time.Duration) (stop func()) {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
t := time.NewTicker(interval)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-t.C:
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
_, _ = d.Refresh(ctx)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}()
|
||||
return func() { close(done) }
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
videoTranscodingVersion = "2.0"
|
||||
transcodeSuccessStatus = 5
|
||||
transcodeNoNeedStatus = 4
|
||||
transcodeFailedStatus = -1
|
||||
transcodeQueueLimit = 20
|
||||
)
|
||||
|
||||
var errTranscodeTaskNotFound = errors.New("laosiji transcode task not found")
|
||||
|
||||
type uploadAPIResp struct {
|
||||
Status string `json:"status"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Time string `json:"time"`
|
||||
Error string `json:"error"`
|
||||
ErrorCode int `json:"errorCode"`
|
||||
}
|
||||
|
||||
type transcodeTask struct {
|
||||
ID string `json:"id"`
|
||||
FileID string `json:"file_id"`
|
||||
FileURL string `json:"file_url"`
|
||||
Status int `json:"status"`
|
||||
Duration int `json:"duration"`
|
||||
Height int `json:"height"`
|
||||
Width int `json:"width"`
|
||||
TranscodeError string `json:"transcode_error"`
|
||||
TranscodeFile string `json:"transcode_file"`
|
||||
PreviewVtt string `json:"preview_vtt"`
|
||||
AudioFile string `json:"audio_file"`
|
||||
SmartCover string `json:"smart_cover"`
|
||||
PreviewDir string `json:"preview_dir"`
|
||||
SrcMetaInfo *MetaInfo `json:"src_meta_info"`
|
||||
HevcMetaInfo *MetaInfo `json:"hevc_meta_info"`
|
||||
}
|
||||
|
||||
// MetaInfo 视频元信息(src_meta_info / hevc_meta_info 共用结构)。
|
||||
type MetaInfo struct {
|
||||
Codec string `json:"codec"`
|
||||
Profile string `json:"profile"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Fps float64 `json:"fps"`
|
||||
VideoBitrate int `json:"video_bitrate"`
|
||||
PixFmt string `json:"pix_fmt"`
|
||||
AudioCodec string `json:"audio_codec"`
|
||||
AudioBitrate int `json:"audio_bitrate"`
|
||||
AudioSampleRate int `json:"audio_sample_rate"`
|
||||
AudioChannels int `json:"audio_channels"`
|
||||
Bitrate int `json:"bitrate"`
|
||||
Size int64 `json:"size"`
|
||||
Duration float64 `json:"duration"`
|
||||
}
|
||||
|
||||
type transcodeQueueInfo struct {
|
||||
Waiting int `json:"waiting"`
|
||||
Done int `json:"done"`
|
||||
Error int `json:"error"`
|
||||
}
|
||||
|
||||
// TranscodeResult 转码任务状态快照。
|
||||
type TranscodeResult struct {
|
||||
Status int
|
||||
Done bool
|
||||
Failed bool
|
||||
HevcURL string
|
||||
PreviewVtt string
|
||||
AudioFile string
|
||||
SmartCover string
|
||||
HevcMeta *MetaInfo
|
||||
}
|
||||
|
||||
// ResolveTranscodeFileID 优先使用媒资 fileId,缺失时回退为 fileURL 的 md5。
|
||||
func ResolveTranscodeFileID(fileID, fileURL string) string {
|
||||
if id := strings.TrimSpace(fileID); id != "" {
|
||||
return id
|
||||
}
|
||||
return md5Hex(fileURL)
|
||||
}
|
||||
|
||||
func (c *Client) SubmitTranscoding(ctx context.Context, fileURL, fileID string, domains SystemDomains, videoId int64) (TranscodeResult, error) {
|
||||
fileURL = strings.TrimSpace(fileURL)
|
||||
if fileURL == "" {
|
||||
return TranscodeResult{}, fmt.Errorf("empty transcode source url")
|
||||
}
|
||||
fileID = ResolveTranscodeFileID(fileID, fileURL)
|
||||
|
||||
task, err := c.queryVideoTranscoding(ctx, domains, fileID)
|
||||
if err == nil {
|
||||
return taskToResult(task), nil
|
||||
}
|
||||
if !errors.Is(err, errTranscodeTaskNotFound) {
|
||||
return TranscodeResult{}, err
|
||||
}
|
||||
task, err = c.createVideoTranscoding(ctx, domains, fileID, fileURL, videoId)
|
||||
if err != nil {
|
||||
return TranscodeResult{}, err
|
||||
}
|
||||
return taskToResult(task), nil
|
||||
}
|
||||
|
||||
func (c *Client) QueryTranscoding(ctx context.Context, fileURL, fileID string, domains SystemDomains) (result TranscodeResult, found bool, err error) {
|
||||
fileURL = strings.TrimSpace(fileURL)
|
||||
if fileURL == "" {
|
||||
return TranscodeResult{}, false, fmt.Errorf("empty transcode source url")
|
||||
}
|
||||
fileID = ResolveTranscodeFileID(fileID, fileURL)
|
||||
|
||||
task, err := c.queryVideoTranscoding(ctx, domains, fileID)
|
||||
if err != nil {
|
||||
if errors.Is(err, errTranscodeTaskNotFound) {
|
||||
return TranscodeResult{}, false, nil
|
||||
}
|
||||
return TranscodeResult{}, false, err
|
||||
}
|
||||
return taskToResult(task), true, nil
|
||||
}
|
||||
|
||||
func (c *Client) createVideoTranscoding(ctx context.Context, domains SystemDomains, fileID, fileURL string, videoId int64) (transcodeTask, error) {
|
||||
if q, err := c.getVideoTranscodingQueue(ctx, domains); err == nil && q.Waiting >= transcodeQueueLimit {
|
||||
return transcodeTask{}, fmt.Errorf("h264toh265 queue full waiting:%d limit:%d", q.Waiting, transcodeQueueLimit)
|
||||
}
|
||||
endpoint := joinURLPath(domains.UploadURL, "upload/videoTranscoding")
|
||||
extData, _ := json.Marshal(map[string]interface{}{
|
||||
"project": "ttrgw",
|
||||
"url": fileURL,
|
||||
"type": "h265",
|
||||
"videoId": videoId,
|
||||
})
|
||||
return c.uploadAPIGet(ctx, endpoint, map[string]string{
|
||||
"v": videoTranscodingVersion,
|
||||
"key": domains.UploadKey,
|
||||
"file_id": fileID,
|
||||
"file_url": fileURL,
|
||||
"notice_url": "",
|
||||
"ext_data": string(extData),
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) queryVideoTranscoding(ctx context.Context, domains SystemDomains, fileID string) (transcodeTask, error) {
|
||||
endpoint := joinURLPath(domains.UploadURL, "upload/queryVideoTranscoding")
|
||||
return c.uploadAPIGet(ctx, endpoint, map[string]string{
|
||||
"v": videoTranscodingVersion,
|
||||
"key": domains.UploadKey,
|
||||
"file_id": fileID,
|
||||
})
|
||||
}
|
||||
|
||||
func taskToResult(task transcodeTask) TranscodeResult {
|
||||
r := TranscodeResult{Status: task.Status}
|
||||
switch {
|
||||
case task.Status == transcodeSuccessStatus || task.Status == transcodeNoNeedStatus:
|
||||
if task.Status == transcodeSuccessStatus {
|
||||
if f := strings.TrimSpace(task.TranscodeFile); f != "" {
|
||||
r.Done = true
|
||||
r.HevcURL = joinURLPath("/laosiji/m3m", f)
|
||||
}
|
||||
} else {
|
||||
r.Done = true
|
||||
}
|
||||
if r.Done {
|
||||
r.PreviewVtt = laosijiPreviewURL(task.PreviewDir, task.PreviewVtt)
|
||||
r.AudioFile = laosijiURL(task.AudioFile)
|
||||
r.SmartCover = laosijiPreviewURL(task.PreviewDir, task.SmartCover)
|
||||
r.HevcMeta = task.HevcMetaInfo
|
||||
}
|
||||
case task.Status <= transcodeFailedStatus:
|
||||
r.Failed = true
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func laosijiURL(name string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return ""
|
||||
}
|
||||
return joinURLPath("/laosiji", name)
|
||||
}
|
||||
|
||||
func laosijiPreviewURL(previewDir, name string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return ""
|
||||
}
|
||||
return joinURLPath(joinURLPath("/laosiji", strings.TrimSpace(previewDir)), name)
|
||||
}
|
||||
|
||||
func (c *Client) getVideoTranscodingQueue(ctx context.Context, domains SystemDomains) (transcodeQueueInfo, error) {
|
||||
endpoint := joinURLPath(domains.UploadURL, "upload/getVideoTranscodingQueueInfo")
|
||||
data, err := c.uploadAPICall(ctx, endpoint, map[string]string{
|
||||
"v": videoTranscodingVersion,
|
||||
"key": domains.UploadKey,
|
||||
})
|
||||
if err != nil {
|
||||
return transcodeQueueInfo{}, err
|
||||
}
|
||||
var q transcodeQueueInfo
|
||||
if len(data) > 0 {
|
||||
if err = json.Unmarshal(data, &q); err != nil {
|
||||
return transcodeQueueInfo{}, err
|
||||
}
|
||||
}
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func (c *Client) uploadAPIGet(ctx context.Context, endpoint string, params map[string]string) (transcodeTask, error) {
|
||||
data, err := c.uploadAPICall(ctx, endpoint, params)
|
||||
if err != nil {
|
||||
return transcodeTask{}, err
|
||||
}
|
||||
var task transcodeTask
|
||||
if len(data) > 0 {
|
||||
if err = json.Unmarshal(data, &task); err != nil {
|
||||
return transcodeTask{}, err
|
||||
}
|
||||
}
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func (c *Client) uploadAPICall(ctx context.Context, endpoint string, params map[string]string) (json.RawMessage, error) {
|
||||
parsed, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := parsed.Query()
|
||||
for k, v := range params {
|
||||
query.Set(k, v)
|
||||
}
|
||||
parsed.RawQuery = query.Encode()
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.hc.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("upload api http status:%d body:%s", resp.StatusCode, truncate(body, 300))
|
||||
}
|
||||
var apiResp uploadAPIResp
|
||||
if err = json.Unmarshal(body, &apiResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if apiResp.Status != "y" {
|
||||
if apiResp.ErrorCode == 2000 {
|
||||
return nil, errTranscodeTaskNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("upload api errorCode:%d error:%s", apiResp.ErrorCode, apiResp.Error)
|
||||
}
|
||||
return apiResp.Data, nil
|
||||
}
|
||||
|
||||
func md5Hex(value string) string {
|
||||
sum := md5.Sum([]byte(value))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func joinURLPath(host, uri string) string {
|
||||
host = strings.TrimRight(strings.TrimSpace(host), "/")
|
||||
uri = strings.TrimLeft(strings.TrimSpace(uri), "/")
|
||||
if host == "" {
|
||||
return uri
|
||||
}
|
||||
if uri == "" {
|
||||
return host
|
||||
}
|
||||
return host + "/" + uri
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package linode
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"mime"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"videoweb/pub/common/fconst"
|
||||
"videoweb/pub/common/log"
|
||||
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/karrick/godirwalk"
|
||||
"github.com/xxjwxc/gowp/workpool"
|
||||
)
|
||||
|
||||
const (
|
||||
//region = "default"
|
||||
//endpoint = "https://ap-south-1.linodeobjects.com"
|
||||
//accessKey = "0RALF74H45ANVYPRQA1Y"
|
||||
//secretKey = "S1yCKUqxFJHqlFPZXJ3BbD8T6P0Yz6loUDntUrsS"
|
||||
//bucket = "fengshang"
|
||||
M = 1024 * 1024
|
||||
G = M * 1024
|
||||
defaultConcurrency = 150
|
||||
defaultTryMax = 5
|
||||
defaultContentType = "application/octet-stream"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
session *session.Session
|
||||
cfg Config
|
||||
}
|
||||
type UploadReq struct {
|
||||
Key string
|
||||
ContentType string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (u *UploadReq) Valid() bool {
|
||||
return u.Key != "" && len(u.Data) != 0
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Region string
|
||||
Endpoint string
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
Bucket string
|
||||
Concurrency int
|
||||
TryMax int
|
||||
}
|
||||
|
||||
func traverseDir(d string) ([]string, error) {
|
||||
var fileNames []string
|
||||
fn := func(name string, entry *godirwalk.Dirent) error {
|
||||
if !entry.IsRegular() {
|
||||
return nil
|
||||
}
|
||||
fileNames = append(fileNames, name)
|
||||
return nil
|
||||
}
|
||||
pathName := d
|
||||
err := godirwalk.Walk(pathName, &godirwalk.Options{
|
||||
Unsorted: true,
|
||||
Callback: fn,
|
||||
})
|
||||
if err != nil {
|
||||
log.Info("list files failed", log.Any("path", pathName), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
return fileNames, nil
|
||||
}
|
||||
|
||||
func New(cfg Config) (*Client, error) {
|
||||
if cfg.TryMax <= 0 {
|
||||
cfg.TryMax = defaultTryMax
|
||||
}
|
||||
if cfg.Concurrency <= 0 {
|
||||
cfg.Concurrency = defaultConcurrency
|
||||
}
|
||||
s, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(cfg.Region),
|
||||
Endpoint: aws.String(cfg.Endpoint),
|
||||
Credentials: credentials.NewStaticCredentials(cfg.AccessKey, cfg.SecretKey, ""),
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("Linode NewSession err", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
return &Client{session: s, cfg: cfg}, nil
|
||||
}
|
||||
|
||||
//func WithDefault() (*Client, error) {
|
||||
// return New(&Config{Concurrency: defaultConcurrency, TryMax: defaultTryMax})
|
||||
//}
|
||||
|
||||
func (c *Client) UploadFolder(folderName string) error {
|
||||
absFiles, err := traverseDir(folderName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(absFiles) == 0 {
|
||||
return nil
|
||||
}
|
||||
wp := workpool.New(c.cfg.Concurrency)
|
||||
for _, absFilename := range absFiles {
|
||||
var absName = absFilename
|
||||
wp.Do(func() error {
|
||||
var err error
|
||||
for i := 0; i < c.cfg.TryMax; i++ {
|
||||
start := time.Now()
|
||||
err = c.uploadToLinNode(absName)
|
||||
diff := time.Now().Sub(start)
|
||||
if diff < time.Second {
|
||||
time.Sleep(time.Second - diff)
|
||||
}
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
return wp.Wait()
|
||||
}
|
||||
|
||||
func (c *Client) UploadRaw(key, contentType string, body io.Reader) (string, error) {
|
||||
if contentType == "" {
|
||||
contentType = defaultContentType
|
||||
}
|
||||
input := &s3manager.UploadInput{
|
||||
Bucket: aws.String(c.cfg.Bucket),
|
||||
Key: aws.String(key),
|
||||
ACL: aws.String("public-read"),
|
||||
Body: body,
|
||||
ContentType: aws.String(contentType),
|
||||
}
|
||||
result, err := s3manager.NewUploader(c.session, func(uploader *s3manager.Uploader) {
|
||||
uploader.PartSize = 1 * G
|
||||
}).Upload(input)
|
||||
if err != nil {
|
||||
log.Info("UpdateRaw Upload Err", log.Any("key", key), log.E(err))
|
||||
return "", err
|
||||
}
|
||||
log.Info("UpdateRaw Upload OK", log.Any("key", key), log.Any("Location", result.Location))
|
||||
return "", nil
|
||||
}
|
||||
func (c *Client) Upload(req UploadReq) error {
|
||||
if !req.Valid() {
|
||||
return errors.New("invalid upload request")
|
||||
}
|
||||
if req.ContentType == "" {
|
||||
req.ContentType = defaultContentType
|
||||
ext := path.Ext(req.Key)
|
||||
if ext != "" {
|
||||
req.ContentType = mime.TypeByExtension(ext)
|
||||
}
|
||||
}
|
||||
var err error
|
||||
for i := 0; i < defaultTryMax; i++ {
|
||||
buf := bytes.NewReader(req.Data)
|
||||
input := &s3.PutObjectInput{
|
||||
Bucket: aws.String(c.cfg.Bucket),
|
||||
Key: aws.String(req.Key),
|
||||
ACL: aws.String("public-read"),
|
||||
Body: buf,
|
||||
ContentType: aws.String(req.ContentType),
|
||||
}
|
||||
|
||||
_, err := s3.New(c.session).PutObject(input)
|
||||
if err == nil {
|
||||
log.Info("Linode Upload OK", log.Any("key", req.Key))
|
||||
return nil
|
||||
}
|
||||
time.Sleep(time.Millisecond) //出让CPU
|
||||
log.Error("Linode Upload Try err", log.Any("key", req.Key), log.Any("tryCount", i+1), log.E(err))
|
||||
}
|
||||
log.Error("Linode Upload err", log.Any("key", req.Key), log.E(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 多线程调用
|
||||
func (m *Client) uploadToLinNodeWithKey(fileName, key string) error {
|
||||
f, err := os.OpenFile(fileName, os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
log.Info("uploadToLinNodeWithKey OpenFile", log.Any("fileName", fileName), log.E(err))
|
||||
return err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
ext := filepath.Ext(fileName)
|
||||
contentType := ""
|
||||
if ext != "" {
|
||||
contentType = mime.TypeByExtension(ext)
|
||||
}
|
||||
_, err = m.UploadRaw(key, contentType, f)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Client) uploadToLinNode(absFilename string) error {
|
||||
key := strings.TrimPrefix(absFilename, fconst.FsDir)
|
||||
return m.uploadToLinNodeWithKey(absFilename, key)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package linode
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/xxjwxc/gowp/workpool"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAA(t *testing.T) {
|
||||
wp := workpool.New(100)
|
||||
for i := 0; i < 200000; i++ {
|
||||
var t = i
|
||||
wp.Do(func() error {
|
||||
fmt.Println(t)
|
||||
if t == 22 {
|
||||
return errors.New("111")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
err := wp.Wait()
|
||||
if err != nil {
|
||||
fmt.Println("fail", err)
|
||||
} else {
|
||||
fmt.Println("OK")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package lock
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type IIdent interface {
|
||||
GetID() int64
|
||||
}
|
||||
|
||||
type Cache[T IIdent] struct {
|
||||
dict map[int64]T
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewCache[T IIdent]() *Cache[T] {
|
||||
return &Cache[T]{
|
||||
dict: make(map[int64]T),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache[T]) Get(Id int64) (T, bool) {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
t, ok := c.dict[Id]
|
||||
return t, ok
|
||||
}
|
||||
|
||||
func (c *Cache[T]) Keys() []int64 {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
keys := make([]int64, 0, len(c.dict))
|
||||
for k := range c.dict {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (c *Cache[T]) Set(items ...T) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
for _, item := range items {
|
||||
c.dict[item.GetID()] = item
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache[T]) Del(items ...T) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
for _, item := range items {
|
||||
delete(c.dict, item.GetID())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache[T]) Update(dict map[int64]T) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.dict = dict
|
||||
}
|
||||
|
||||
func (c *Cache[T]) SetItems(items []T) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
for _, item := range items {
|
||||
c.dict[item.GetID()] = item
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache[T]) Has(Id int64) bool {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
_, ok := c.dict[Id]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (c *Cache[T]) GetItems(Ids []int64) []T {
|
||||
ret := make([]T, 0)
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
for _, id := range Ids {
|
||||
if item, ok := c.dict[id]; ok {
|
||||
ret = append(ret, item)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package lock
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Dict[K comparable, V any] struct {
|
||||
dict map[K]V
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewDict[K comparable, V any]() *Dict[K, V] {
|
||||
return &Dict[K, V]{
|
||||
dict: make(map[K]V),
|
||||
}
|
||||
}
|
||||
|
||||
// Count get dict key count
|
||||
func (d *Dict[K, V]) Count() int {
|
||||
d.lock.RLock()
|
||||
defer d.lock.RUnlock()
|
||||
return len(d.dict)
|
||||
}
|
||||
|
||||
// Keys get dict keys
|
||||
func (d *Dict[K, V]) Keys() []K {
|
||||
d.lock.RLock()
|
||||
defer d.lock.RUnlock()
|
||||
keys := make([]K, 0, len(d.dict))
|
||||
for k := range d.dict {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// Set set k v to dict, overwrite old value if exists
|
||||
func (d *Dict[K, V]) Set(key K, value V) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
d.dict[key] = value
|
||||
}
|
||||
|
||||
// Has if exists, return true
|
||||
func (d *Dict[K, V]) Has(key K) bool {
|
||||
d.lock.RLock()
|
||||
defer d.lock.RUnlock()
|
||||
_, exists := d.dict[key]
|
||||
return exists
|
||||
}
|
||||
|
||||
// Get if exists, return value and true
|
||||
func (d *Dict[K, V]) Get(key K) (V, bool) {
|
||||
d.lock.RLock()
|
||||
defer d.lock.RUnlock()
|
||||
value, exists := d.dict[key]
|
||||
return value, exists
|
||||
}
|
||||
|
||||
// Del if exists and del success, return true
|
||||
func (d *Dict[K, V]) Del(key K) bool {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
_, exists := d.dict[key]
|
||||
delete(d.dict, key)
|
||||
return exists
|
||||
}
|
||||
|
||||
// Remove if exists and remove success, return old value and true
|
||||
func (d *Dict[K, V]) Remove(key K) (V, bool) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
value, exists := d.dict[key]
|
||||
delete(d.dict, key)
|
||||
return value, exists
|
||||
}
|
||||
|
||||
// SetIfNotExists if not exists and set success, return true
|
||||
func (d *Dict[K, V]) SetIfNotExists(key K, value V) bool {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
_, exists := d.dict[key]
|
||||
if !exists {
|
||||
d.dict[key] = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetOrSetIfNotExists if key exists return old:true, or, set new value, and return new:false
|
||||
func (d *Dict[K, V]) GetOrSetIfNotExists(key K, new V) (V, bool) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
old, exists := d.dict[key]
|
||||
if !exists {
|
||||
d.dict[key] = new
|
||||
return new, false
|
||||
}
|
||||
return old, true
|
||||
}
|
||||
|
||||
// Travel travels all dict keys and values
|
||||
func (d *Dict[K, V]) Travel(travel func(key K, value V) bool) {
|
||||
d.lock.RLock()
|
||||
defer d.lock.RUnlock()
|
||||
for key, value := range d.dict {
|
||||
cont := travel(key, value)
|
||||
if !cont {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear dict directly
|
||||
func (d *Dict[K, V]) Clear() {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
d.dict = map[K]V{}
|
||||
}
|
||||
|
||||
// Update replace dict directly
|
||||
func (d *Dict[K, V]) Update(dict map[K]V) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
d.dict = dict
|
||||
}
|
||||
|
||||
// Replace replace dict directly and return old dict
|
||||
func (d *Dict[K, V]) Replace(dict map[K]V) map[K]V {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
old := d.dict
|
||||
d.dict = dict
|
||||
return old
|
||||
}
|
||||
|
||||
// Append dict directly
|
||||
func (d *Dict[K, V]) Append(dict map[K]V) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
for k, v := range dict {
|
||||
d.dict[k] = v
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package lock
|
||||
|
||||
import "sync"
|
||||
|
||||
// Queue 定义一个泛型队列结构
|
||||
type Queue[T any] struct {
|
||||
items []T
|
||||
lock sync.RWMutex // 添加读写锁
|
||||
}
|
||||
|
||||
// NewQueue 创建一个新的队列
|
||||
func NewQueue[T any]() *Queue[T] {
|
||||
return &Queue[T]{
|
||||
items: make([]T, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue 入队操作 - 将元素添加到队列末尾
|
||||
func (q *Queue[T]) Enqueue(item T) {
|
||||
q.lock.Lock() // 写操作需要写锁
|
||||
defer q.lock.Unlock()
|
||||
|
||||
q.items = append(q.items, item)
|
||||
}
|
||||
|
||||
// Dequeue 出队操作 - 从队列头部移除并返回元素
|
||||
func (q *Queue[T]) Dequeue() (T, bool) {
|
||||
q.lock.Lock() // 写操作需要写锁
|
||||
defer q.lock.Unlock()
|
||||
|
||||
var zero T
|
||||
if len(q.items) == 0 {
|
||||
return zero, false
|
||||
}
|
||||
|
||||
item := q.items[0]
|
||||
q.items = q.items[1:]
|
||||
return item, true
|
||||
}
|
||||
|
||||
// Peek 查看队列头部元素但不移除
|
||||
func (q *Queue[T]) Peek() (T, bool) {
|
||||
q.lock.RLock() // 读操作使用读锁
|
||||
defer q.lock.RUnlock()
|
||||
|
||||
var zero T
|
||||
if len(q.items) == 0 {
|
||||
return zero, false
|
||||
}
|
||||
return q.items[0], true
|
||||
}
|
||||
|
||||
// IsEmpty 检查队列是否为空
|
||||
func (q *Queue[T]) IsEmpty() bool {
|
||||
q.lock.RLock() // 读操作使用读锁
|
||||
defer q.lock.RUnlock()
|
||||
|
||||
return len(q.items) == 0
|
||||
}
|
||||
|
||||
// Size 返回队列中的元素个数
|
||||
func (q *Queue[T]) Size() int {
|
||||
q.lock.RLock() // 读操作使用读锁
|
||||
defer q.lock.RUnlock()
|
||||
|
||||
return len(q.items)
|
||||
}
|
||||
|
||||
// Clear 清空队列
|
||||
func (q *Queue[T]) Clear() {
|
||||
q.lock.Lock() // 写操作需要写锁
|
||||
defer q.lock.Unlock()
|
||||
|
||||
q.items = make([]T, 0)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
"videoweb/pub/common/log/zapcorex"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
var (
|
||||
zapLogger *zap.Logger
|
||||
zapLoggerS *zap.SugaredLogger
|
||||
ZapLog *zap.Logger
|
||||
ZapLogS *zap.SugaredLogger
|
||||
Debug = zapLogger.Debug
|
||||
Info = zapLogger.Info
|
||||
Warn = zapLogger.Warn
|
||||
Error = zapLogger.Error
|
||||
Panic = zapLogger.Panic
|
||||
Fatal = zapLogger.Fatal
|
||||
Debugf = zapLoggerS.Debugf
|
||||
Infof = zapLoggerS.Infof
|
||||
Warnf = zapLoggerS.Warnf
|
||||
Errorf = zapLoggerS.Errorf
|
||||
Fatalf = zapLoggerS.Fatalf
|
||||
Debugw = zapLoggerS.Debugw
|
||||
Infow = zapLoggerS.Infow
|
||||
Warnw = zapLoggerS.Warnw
|
||||
Errorw = zapLoggerS.Errorw
|
||||
Fatalw = zapLoggerS.Fatalw
|
||||
)
|
||||
|
||||
func newEncoderConfig() zapcore.EncoderConfig {
|
||||
return zapcore.EncoderConfig{
|
||||
// Keys can be anything except the empty string.
|
||||
TimeKey: "T",
|
||||
LevelKey: "L",
|
||||
NameKey: "N",
|
||||
CallerKey: "C",
|
||||
MessageKey: "M",
|
||||
StacktraceKey: "S",
|
||||
FunctionKey: "F",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.CapitalColorLevelEncoder,
|
||||
EncodeTime: timeEncoder,
|
||||
EncodeDuration: zapcore.StringDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
ConsoleSeparator: " ",
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
core := zapcore.NewCore(
|
||||
zapcorex.NewConsoleEncoder(newEncoderConfig()),
|
||||
zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout)),
|
||||
zap.DebugLevel,
|
||||
)
|
||||
zapLogger = zap.New(core, zap.AddCaller())
|
||||
zapLoggerS = zapLogger.Sugar()
|
||||
ZapLog = zapLogger
|
||||
ZapLogS = zapLoggerS
|
||||
Debug = zapLogger.Debug
|
||||
Info = zapLogger.Info
|
||||
Warn = zapLogger.Warn
|
||||
Error = zapLogger.Error
|
||||
Panic = zapLogger.Panic
|
||||
Fatal = zapLogger.Fatal
|
||||
Debugf = zapLoggerS.Debugf
|
||||
Infof = zapLoggerS.Infof
|
||||
Warnf = zapLoggerS.Warnf
|
||||
Errorf = zapLoggerS.Errorf
|
||||
Fatalf = zapLoggerS.Fatalf
|
||||
Debugw = zapLoggerS.Debugw
|
||||
Infow = zapLoggerS.Infow
|
||||
Warnw = zapLoggerS.Warnw
|
||||
Errorw = zapLoggerS.Errorw
|
||||
Fatalw = zapLoggerS.Fatalw
|
||||
}
|
||||
|
||||
func timeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
|
||||
enc.AppendString(t.In(time.Local).Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
type Field = zap.Field
|
||||
|
||||
func Any(key string, value interface{}) Field {
|
||||
return zap.Any(key, value)
|
||||
}
|
||||
|
||||
func E(err error) Field {
|
||||
return Any("err", err)
|
||||
}
|
||||
|
||||
type contextKey int64
|
||||
|
||||
var (
|
||||
loggerContextKey contextKey
|
||||
)
|
||||
|
||||
func ContextWithLogger(ctx context.Context, l *zap.SugaredLogger) context.Context {
|
||||
return context.WithValue(ctx, loggerContextKey, l)
|
||||
}
|
||||
|
||||
func LoggerFromContext(ctx context.Context) *zap.SugaredLogger {
|
||||
v := ctx.Value(loggerContextKey)
|
||||
if v == nil {
|
||||
return ZapLogS
|
||||
}
|
||||
return v.(*zap.SugaredLogger)
|
||||
}
|
||||
|
||||
type Logger struct{}
|
||||
|
||||
// Info logs routine messages about cron's operation.
|
||||
func (s *Logger) Info(msg string, keysAndValues ...interface{}) {
|
||||
_ = log.Default().Output(2, msg+" "+fmt.Sprint(keysAndValues...))
|
||||
}
|
||||
|
||||
// Error logs an error condition.
|
||||
func (s *Logger) Error(err error, msg string, keysAndValues ...interface{}) {
|
||||
line := fmt.Sprintf("%s %v %s", msg, err, fmt.Sprint(keysAndValues...))
|
||||
_ = log.Default().Output(2, line)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
// Package bufferpool houses zap's shared internal buffer pool. Third-party
|
||||
// packages can recreate the same functionality with buffers.NewPool.
|
||||
package bufferpool
|
||||
|
||||
import "go.uber.org/zap/buffer"
|
||||
|
||||
var (
|
||||
_pool = buffer.NewPool()
|
||||
// Get retrieves a buffer from the pool, creating one if necessary.
|
||||
Get = _pool.Get
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
// Package color adds coloring functionality for TTY output.
|
||||
package zapcorex
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Foreground colors.
|
||||
const (
|
||||
Black Color = iota + 30
|
||||
Red
|
||||
Green
|
||||
Yellow
|
||||
Blue
|
||||
Magenta
|
||||
Cyan
|
||||
White
|
||||
)
|
||||
|
||||
// Color represents a text color.
|
||||
type Color uint8
|
||||
|
||||
// Add adds the coloring to the given string.
|
||||
func (c Color) Add(s string) string {
|
||||
return fmt.Sprintf("\x1b[%dm%s\x1b[0m", uint8(c), s)
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package zapcorex
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"videoweb/pub/common/log/zapcorex/bufferpool"
|
||||
|
||||
"go.uber.org/zap/buffer"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
var _sliceEncoderPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &sliceArrayEncoder{elems: make([]interface{}, 0, 2)}
|
||||
},
|
||||
}
|
||||
|
||||
func getSliceEncoder() *sliceArrayEncoder {
|
||||
return _sliceEncoderPool.Get().(*sliceArrayEncoder)
|
||||
}
|
||||
|
||||
func putSliceEncoder(e *sliceArrayEncoder) {
|
||||
e.elems = e.elems[:0]
|
||||
_sliceEncoderPool.Put(e)
|
||||
}
|
||||
|
||||
type consoleEncoder struct {
|
||||
*jsonEncoder
|
||||
}
|
||||
|
||||
// NewConsoleEncoder creates an encoder whose output is designed for human -
|
||||
// rather than machine - consumption. It serializes the core log entry data
|
||||
// (message, level, timestamp, etc.) in a plain-text format and leaves the
|
||||
// structured context as JSON.
|
||||
//
|
||||
// Note that although the console encoder doesn't use the keys specified in the
|
||||
// encoder configuration, it will omit any element whose key is set to the empty
|
||||
// string.
|
||||
func NewConsoleEncoder(cfg zapcore.EncoderConfig) zapcore.Encoder {
|
||||
if cfg.ConsoleSeparator == "" {
|
||||
// Use a default delimiter of '\t' for backwards compatibility
|
||||
cfg.ConsoleSeparator = "\t"
|
||||
}
|
||||
return consoleEncoder{newJSONEncoder(cfg, true)}
|
||||
}
|
||||
|
||||
func (c consoleEncoder) Clone() zapcore.Encoder {
|
||||
return consoleEncoder{c.jsonEncoder.Clone().(*jsonEncoder)}
|
||||
}
|
||||
|
||||
func (c consoleEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
|
||||
line := bufferpool.Get()
|
||||
|
||||
// We don't want the entry's metadata to be quoted and escaped (if it's
|
||||
// encoded as strings), which means that we can't use the JSON encoder. The
|
||||
// simplest option is to use the memory encoder and fmt.Fprint.
|
||||
//
|
||||
// If this ever becomes a performance bottleneck, we can implement
|
||||
// ArrayEncoder for our plain-text format.
|
||||
arr := getSliceEncoder()
|
||||
if c.TimeKey != "" && c.EncodeTime != nil {
|
||||
c.EncodeTime(ent.Time, arr)
|
||||
}
|
||||
if c.LevelKey != "" && c.EncodeLevel != nil {
|
||||
c.EncodeLevel(ent.Level, arr)
|
||||
}
|
||||
if ent.LoggerName != "" && c.NameKey != "" {
|
||||
nameEncoder := c.EncodeName
|
||||
|
||||
if nameEncoder == nil {
|
||||
// Fall back to FullNameEncoder for backward compatibility.
|
||||
nameEncoder = zapcore.FullNameEncoder
|
||||
}
|
||||
|
||||
nameEncoder(ent.LoggerName, arr)
|
||||
}
|
||||
|
||||
for i := range arr.elems {
|
||||
if i > 0 {
|
||||
line.AppendString(c.ConsoleSeparator)
|
||||
}
|
||||
fmt.Fprint(line, arr.elems[i])
|
||||
}
|
||||
|
||||
putSliceEncoder(arr)
|
||||
|
||||
// Add the message itself.
|
||||
if c.MessageKey != "" {
|
||||
c.addSeparatorIfNecessary(line)
|
||||
line.AppendString(ent.Message)
|
||||
}
|
||||
|
||||
// Add any structured context.
|
||||
c.writeContext(line, fields)
|
||||
|
||||
if ent.Caller.Defined {
|
||||
if c.FunctionKey != "" {
|
||||
c.addSeparatorIfNecessary(line)
|
||||
if idx := strings.LastIndex(ent.Caller.Function, "/"); idx > 0 {
|
||||
line.AppendString(White.Add(ent.Caller.Function[idx+1:]))
|
||||
} else {
|
||||
line.AppendString(White.Add(ent.Caller.Function))
|
||||
}
|
||||
}
|
||||
|
||||
line.AppendString(c.ConsoleSeparator)
|
||||
if c.CallerKey != "" && c.EncodeCaller != nil {
|
||||
line.AppendString(White.Add(ent.Caller.TrimmedPath()))
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no stacktrace key, honor that; this allows users to force
|
||||
// single-line output.
|
||||
if ent.Stack != "" && c.StacktraceKey != "" {
|
||||
line.AppendByte('\n')
|
||||
line.AppendString(ent.Stack)
|
||||
}
|
||||
|
||||
line.AppendString(c.LineEnding)
|
||||
return line, nil
|
||||
}
|
||||
|
||||
func (c consoleEncoder) writeContext(line *buffer.Buffer, extra []zapcore.Field) {
|
||||
context := c.jsonEncoder.Clone().(*jsonEncoder)
|
||||
defer func() {
|
||||
// putJSONEncoder assumes the buffer is still used, but we write out the buffer so
|
||||
// we can free it.
|
||||
context.buf.Free()
|
||||
putJSONEncoder(context)
|
||||
}()
|
||||
|
||||
addFields(context, extra)
|
||||
context.closeOpenNamespaces()
|
||||
if context.buf.Len() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
c.addSeparatorIfNecessary(line)
|
||||
line.AppendByte('{')
|
||||
line.Write(context.buf.Bytes())
|
||||
line.AppendByte('}')
|
||||
}
|
||||
|
||||
func (c consoleEncoder) addSeparatorIfNecessary(line *buffer.Buffer) {
|
||||
if line.Len() > 0 {
|
||||
line.AppendString(c.ConsoleSeparator)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package zapcorex
|
||||
|
||||
import "go.uber.org/zap/zapcore"
|
||||
|
||||
func addFields(enc zapcore.ObjectEncoder, fields []zapcore.Field) {
|
||||
for i := range fields {
|
||||
fields[i].AddTo(enc)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
package zapcorex
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"videoweb/pub/common/log/zapcorex/bufferpool"
|
||||
|
||||
"go.uber.org/zap/buffer"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// For JSON-escaping; see jsonEncoder.safeAddString below.
|
||||
const _hex = "0123456789abcdef"
|
||||
|
||||
var _jsonPool = sync.Pool{New: func() interface{} {
|
||||
return &jsonEncoder{}
|
||||
}}
|
||||
|
||||
func getJSONEncoder() *jsonEncoder {
|
||||
return _jsonPool.Get().(*jsonEncoder)
|
||||
}
|
||||
|
||||
func putJSONEncoder(enc *jsonEncoder) {
|
||||
if enc.reflectBuf != nil {
|
||||
enc.reflectBuf.Free()
|
||||
}
|
||||
enc.EncoderConfig = nil
|
||||
enc.buf = nil
|
||||
enc.spaced = false
|
||||
enc.openNamespaces = 0
|
||||
enc.reflectBuf = nil
|
||||
enc.reflectEnc = nil
|
||||
_jsonPool.Put(enc)
|
||||
}
|
||||
|
||||
type jsonEncoder struct {
|
||||
*zapcore.EncoderConfig
|
||||
buf *buffer.Buffer
|
||||
spaced bool // include spaces after colons and commas
|
||||
openNamespaces int
|
||||
|
||||
// for encoding generic values by reflection
|
||||
reflectBuf *buffer.Buffer
|
||||
reflectEnc *json.Encoder
|
||||
}
|
||||
|
||||
// NewJSONEncoder creates a fast, low-allocation JSON encoder. The encoder
|
||||
// appropriately escapes all field keys and values.
|
||||
//
|
||||
// Note that the encoder doesn't deduplicate keys, so it's possible to produce
|
||||
// a message like
|
||||
//
|
||||
// {"foo":"bar","foo":"baz"}
|
||||
//
|
||||
// This is permitted by the JSON specification, but not encouraged. Many
|
||||
// libraries will ignore duplicate key-value pairs (typically keeping the last
|
||||
// pair) when unmarshaling, but users should attempt to avoid adding duplicate
|
||||
// keys.
|
||||
func NewJSONEncoder(cfg zapcore.EncoderConfig) zapcore.Encoder {
|
||||
return newJSONEncoder(cfg, false)
|
||||
}
|
||||
|
||||
func newJSONEncoder(cfg zapcore.EncoderConfig, spaced bool) *jsonEncoder {
|
||||
return &jsonEncoder{
|
||||
EncoderConfig: &cfg,
|
||||
buf: bufferpool.Get(),
|
||||
spaced: spaced,
|
||||
}
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AddArray(key string, arr zapcore.ArrayMarshaler) error {
|
||||
enc.addKey(key)
|
||||
return enc.AppendArray(arr)
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AddObject(key string, obj zapcore.ObjectMarshaler) error {
|
||||
enc.addKey(key)
|
||||
return enc.AppendObject(obj)
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AddBinary(key string, val []byte) {
|
||||
enc.AddString(key, base64.StdEncoding.EncodeToString(val))
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AddByteString(key string, val []byte) {
|
||||
enc.addKey(key)
|
||||
enc.AppendByteString(val)
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AddBool(key string, val bool) {
|
||||
enc.addKey(key)
|
||||
enc.AppendBool(val)
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AddComplex128(key string, val complex128) {
|
||||
enc.addKey(key)
|
||||
enc.AppendComplex128(val)
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AddDuration(key string, val time.Duration) {
|
||||
enc.addKey(key)
|
||||
enc.AppendDuration(val)
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AddFloat64(key string, val float64) {
|
||||
enc.addKey(key)
|
||||
enc.AppendFloat64(val)
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AddInt64(key string, val int64) {
|
||||
enc.addKey(key)
|
||||
enc.AppendInt64(val)
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) resetReflectBuf() {
|
||||
if enc.reflectBuf == nil {
|
||||
enc.reflectBuf = bufferpool.Get()
|
||||
enc.reflectEnc = json.NewEncoder(enc.reflectBuf)
|
||||
|
||||
// For consistency with our custom JSON encoder.
|
||||
enc.reflectEnc.SetEscapeHTML(false)
|
||||
} else {
|
||||
enc.reflectBuf.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
var nullLiteralBytes = []byte("null")
|
||||
|
||||
// Only invoke the standard JSON encoder if there is actually something to
|
||||
// encode; otherwise write JSON null literal directly.
|
||||
func (enc *jsonEncoder) encodeReflected(obj interface{}) ([]byte, error) {
|
||||
if obj == nil {
|
||||
return nullLiteralBytes, nil
|
||||
}
|
||||
enc.resetReflectBuf()
|
||||
if err := enc.reflectEnc.Encode(obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enc.reflectBuf.TrimNewline()
|
||||
return enc.reflectBuf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AddReflected(key string, obj interface{}) error {
|
||||
valueBytes, err := enc.encodeReflected(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enc.addKey(key)
|
||||
_, err = enc.buf.Write(valueBytes)
|
||||
return err
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) OpenNamespace(key string) {
|
||||
enc.addKey(key)
|
||||
enc.buf.AppendByte('{')
|
||||
enc.openNamespaces++
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AddString(key, val string) {
|
||||
enc.addKey(key)
|
||||
enc.AppendString(val)
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AddTime(key string, val time.Time) {
|
||||
enc.addKey(key)
|
||||
enc.AppendTime(val)
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AddUint64(key string, val uint64) {
|
||||
enc.addKey(key)
|
||||
enc.AppendUint64(val)
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AppendArray(arr zapcore.ArrayMarshaler) error {
|
||||
enc.addElementSeparator()
|
||||
enc.buf.AppendByte('[')
|
||||
err := arr.MarshalLogArray(enc)
|
||||
enc.buf.AppendByte(']')
|
||||
return err
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AppendObject(obj zapcore.ObjectMarshaler) error {
|
||||
enc.addElementSeparator()
|
||||
enc.buf.AppendByte('{')
|
||||
err := obj.MarshalLogObject(enc)
|
||||
enc.buf.AppendByte('}')
|
||||
return err
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AppendBool(val bool) {
|
||||
enc.addElementSeparator()
|
||||
enc.buf.AppendBool(val)
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AppendByteString(val []byte) {
|
||||
enc.addElementSeparator()
|
||||
enc.buf.AppendByte('"')
|
||||
enc.safeAddByteString(val)
|
||||
enc.buf.AppendByte('"')
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AppendComplex128(val complex128) {
|
||||
enc.addElementSeparator()
|
||||
// Cast to a platform-independent, fixed-size type.
|
||||
r, i := float64(real(val)), float64(imag(val))
|
||||
enc.buf.AppendByte('"')
|
||||
// Because we're always in a quoted string, we can use strconv without
|
||||
// special-casing NaN and +/-Inf.
|
||||
enc.buf.AppendFloat(r, 64)
|
||||
enc.buf.AppendByte('+')
|
||||
enc.buf.AppendFloat(i, 64)
|
||||
enc.buf.AppendByte('i')
|
||||
enc.buf.AppendByte('"')
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AppendDuration(val time.Duration) {
|
||||
cur := enc.buf.Len()
|
||||
if e := enc.EncodeDuration; e != nil {
|
||||
e(val, enc)
|
||||
}
|
||||
if cur == enc.buf.Len() {
|
||||
// User-supplied EncodeDuration is a no-op. Fall back to nanoseconds to keep
|
||||
// JSON valid.
|
||||
enc.AppendInt64(int64(val))
|
||||
}
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AppendInt64(val int64) {
|
||||
enc.addElementSeparator()
|
||||
enc.buf.AppendInt(val)
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AppendReflected(val interface{}) error {
|
||||
valueBytes, err := enc.encodeReflected(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enc.addElementSeparator()
|
||||
_, err = enc.buf.Write(valueBytes)
|
||||
return err
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AppendString(val string) {
|
||||
enc.addElementSeparator()
|
||||
enc.buf.AppendByte('"')
|
||||
enc.safeAddString(val)
|
||||
enc.buf.AppendByte('"')
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AppendTimeLayout(time time.Time, layout string) {
|
||||
enc.addElementSeparator()
|
||||
enc.buf.AppendByte('"')
|
||||
enc.buf.AppendTime(time, layout)
|
||||
enc.buf.AppendByte('"')
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AppendTime(val time.Time) {
|
||||
cur := enc.buf.Len()
|
||||
if e := enc.EncodeTime; e != nil {
|
||||
e(val, enc)
|
||||
}
|
||||
if cur == enc.buf.Len() {
|
||||
// User-supplied EncodeTime is a no-op. Fall back to nanos since epoch to keep
|
||||
// output JSON valid.
|
||||
enc.AppendInt64(val.UnixNano())
|
||||
}
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AppendUint64(val uint64) {
|
||||
enc.addElementSeparator()
|
||||
enc.buf.AppendUint(val)
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) AddComplex64(k string, v complex64) { enc.AddComplex128(k, complex128(v)) }
|
||||
func (enc *jsonEncoder) AddFloat32(k string, v float32) { enc.AddFloat64(k, float64(v)) }
|
||||
func (enc *jsonEncoder) AddInt(k string, v int) { enc.AddInt64(k, int64(v)) }
|
||||
func (enc *jsonEncoder) AddInt32(k string, v int32) { enc.AddInt64(k, int64(v)) }
|
||||
func (enc *jsonEncoder) AddInt16(k string, v int16) { enc.AddInt64(k, int64(v)) }
|
||||
func (enc *jsonEncoder) AddInt8(k string, v int8) { enc.AddInt64(k, int64(v)) }
|
||||
func (enc *jsonEncoder) AddUint(k string, v uint) { enc.AddUint64(k, uint64(v)) }
|
||||
func (enc *jsonEncoder) AddUint32(k string, v uint32) { enc.AddUint64(k, uint64(v)) }
|
||||
func (enc *jsonEncoder) AddUint16(k string, v uint16) { enc.AddUint64(k, uint64(v)) }
|
||||
func (enc *jsonEncoder) AddUint8(k string, v uint8) { enc.AddUint64(k, uint64(v)) }
|
||||
func (enc *jsonEncoder) AddUintptr(k string, v uintptr) { enc.AddUint64(k, uint64(v)) }
|
||||
func (enc *jsonEncoder) AppendComplex64(v complex64) { enc.AppendComplex128(complex128(v)) }
|
||||
func (enc *jsonEncoder) AppendFloat64(v float64) { enc.appendFloat(v, 64) }
|
||||
func (enc *jsonEncoder) AppendFloat32(v float32) { enc.appendFloat(float64(v), 32) }
|
||||
func (enc *jsonEncoder) AppendInt(v int) { enc.AppendInt64(int64(v)) }
|
||||
func (enc *jsonEncoder) AppendInt32(v int32) { enc.AppendInt64(int64(v)) }
|
||||
func (enc *jsonEncoder) AppendInt16(v int16) { enc.AppendInt64(int64(v)) }
|
||||
func (enc *jsonEncoder) AppendInt8(v int8) { enc.AppendInt64(int64(v)) }
|
||||
func (enc *jsonEncoder) AppendUint(v uint) { enc.AppendUint64(uint64(v)) }
|
||||
func (enc *jsonEncoder) AppendUint32(v uint32) { enc.AppendUint64(uint64(v)) }
|
||||
func (enc *jsonEncoder) AppendUint16(v uint16) { enc.AppendUint64(uint64(v)) }
|
||||
func (enc *jsonEncoder) AppendUint8(v uint8) { enc.AppendUint64(uint64(v)) }
|
||||
func (enc *jsonEncoder) AppendUintptr(v uintptr) { enc.AppendUint64(uint64(v)) }
|
||||
|
||||
func (enc *jsonEncoder) Clone() zapcore.Encoder {
|
||||
clone := enc.clone()
|
||||
clone.buf.Write(enc.buf.Bytes())
|
||||
return clone
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) clone() *jsonEncoder {
|
||||
clone := getJSONEncoder()
|
||||
clone.EncoderConfig = enc.EncoderConfig
|
||||
clone.spaced = enc.spaced
|
||||
clone.openNamespaces = enc.openNamespaces
|
||||
clone.buf = bufferpool.Get()
|
||||
return clone
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
|
||||
final := enc.clone()
|
||||
final.buf.AppendByte('{')
|
||||
|
||||
if final.LevelKey != "" {
|
||||
final.addKey(final.LevelKey)
|
||||
cur := final.buf.Len()
|
||||
final.EncodeLevel(ent.Level, final)
|
||||
if cur == final.buf.Len() {
|
||||
// User-supplied EncodeLevel was a no-op. Fall back to strings to keep
|
||||
// output JSON valid.
|
||||
final.AppendString(ent.Level.String())
|
||||
}
|
||||
}
|
||||
if final.TimeKey != "" {
|
||||
final.AddTime(final.TimeKey, ent.Time)
|
||||
}
|
||||
if ent.LoggerName != "" && final.NameKey != "" {
|
||||
final.addKey(final.NameKey)
|
||||
cur := final.buf.Len()
|
||||
nameEncoder := final.EncodeName
|
||||
|
||||
// if no name encoder provided, fall back to FullNameEncoder for backwards
|
||||
// compatibility
|
||||
if nameEncoder == nil {
|
||||
nameEncoder = zapcore.FullNameEncoder
|
||||
}
|
||||
|
||||
nameEncoder(ent.LoggerName, final)
|
||||
if cur == final.buf.Len() {
|
||||
// User-supplied EncodeName was a no-op. Fall back to strings to
|
||||
// keep output JSON valid.
|
||||
final.AppendString(ent.LoggerName)
|
||||
}
|
||||
}
|
||||
if ent.Caller.Defined {
|
||||
if final.CallerKey != "" {
|
||||
final.addKey(final.CallerKey)
|
||||
cur := final.buf.Len()
|
||||
final.EncodeCaller(ent.Caller, final)
|
||||
if cur == final.buf.Len() {
|
||||
// User-supplied EncodeCaller was a no-op. Fall back to strings to
|
||||
// keep output JSON valid.
|
||||
final.AppendString(ent.Caller.String())
|
||||
}
|
||||
}
|
||||
if final.FunctionKey != "" {
|
||||
final.addKey(final.FunctionKey)
|
||||
final.AppendString(ent.Caller.Function)
|
||||
}
|
||||
}
|
||||
if final.MessageKey != "" {
|
||||
final.addKey(enc.MessageKey)
|
||||
final.AppendString(ent.Message)
|
||||
}
|
||||
if enc.buf.Len() > 0 {
|
||||
final.addElementSeparator()
|
||||
final.buf.Write(enc.buf.Bytes())
|
||||
}
|
||||
addFields(final, fields)
|
||||
final.closeOpenNamespaces()
|
||||
if ent.Stack != "" && final.StacktraceKey != "" {
|
||||
final.AddString(final.StacktraceKey, ent.Stack)
|
||||
}
|
||||
final.buf.AppendByte('}')
|
||||
if final.LineEnding != "" {
|
||||
final.buf.AppendString(final.LineEnding)
|
||||
} else {
|
||||
final.buf.AppendString(zapcore.DefaultLineEnding)
|
||||
}
|
||||
|
||||
ret := final.buf
|
||||
putJSONEncoder(final)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) truncate() {
|
||||
enc.buf.Reset()
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) closeOpenNamespaces() {
|
||||
for i := 0; i < enc.openNamespaces; i++ {
|
||||
enc.buf.AppendByte('}')
|
||||
}
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) addKey(key string) {
|
||||
enc.addElementSeparator()
|
||||
enc.buf.AppendByte('"')
|
||||
enc.safeAddString(key)
|
||||
enc.buf.AppendByte('"')
|
||||
enc.buf.AppendByte(':')
|
||||
if enc.spaced {
|
||||
enc.buf.AppendByte(' ')
|
||||
}
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) addElementSeparator() {
|
||||
last := enc.buf.Len() - 1
|
||||
if last < 0 {
|
||||
return
|
||||
}
|
||||
switch enc.buf.Bytes()[last] {
|
||||
case '{', '[', ':', ',', ' ':
|
||||
return
|
||||
default:
|
||||
enc.buf.AppendByte(',')
|
||||
if enc.spaced {
|
||||
enc.buf.AppendByte(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) appendFloat(val float64, bitSize int) {
|
||||
enc.addElementSeparator()
|
||||
switch {
|
||||
case math.IsNaN(val):
|
||||
enc.buf.AppendString(`"NaN"`)
|
||||
case math.IsInf(val, 1):
|
||||
enc.buf.AppendString(`"+Inf"`)
|
||||
case math.IsInf(val, -1):
|
||||
enc.buf.AppendString(`"-Inf"`)
|
||||
default:
|
||||
enc.buf.AppendFloat(val, bitSize)
|
||||
}
|
||||
}
|
||||
|
||||
// safeAddString JSON-escapes a string and appends it to the internal buffer.
|
||||
// Unlike the standard library's encoder, it doesn't attempt to protect the
|
||||
// user from browser vulnerabilities or JSONP-related problems.
|
||||
func (enc *jsonEncoder) safeAddString(s string) {
|
||||
for i := 0; i < len(s); {
|
||||
if enc.tryAddRuneSelf(s[i]) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
r, size := utf8.DecodeRuneInString(s[i:])
|
||||
if enc.tryAddRuneError(r, size) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
enc.buf.AppendString(s[i : i+size])
|
||||
i += size
|
||||
}
|
||||
}
|
||||
|
||||
// safeAddByteString is no-alloc equivalent of safeAddString(string(s)) for s []byte.
|
||||
func (enc *jsonEncoder) safeAddByteString(s []byte) {
|
||||
for i := 0; i < len(s); {
|
||||
if enc.tryAddRuneSelf(s[i]) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
r, size := utf8.DecodeRune(s[i:])
|
||||
if enc.tryAddRuneError(r, size) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
enc.buf.Write(s[i : i+size])
|
||||
i += size
|
||||
}
|
||||
}
|
||||
|
||||
// tryAddRuneSelf appends b if it is valid UTF-8 character represented in a single byte.
|
||||
func (enc *jsonEncoder) tryAddRuneSelf(b byte) bool {
|
||||
if b >= utf8.RuneSelf {
|
||||
return false
|
||||
}
|
||||
if 0x20 <= b && b != '\\' && b != '"' {
|
||||
enc.buf.AppendByte(b)
|
||||
return true
|
||||
}
|
||||
switch b {
|
||||
case '\\', '"':
|
||||
enc.buf.AppendByte('\\')
|
||||
enc.buf.AppendByte(b)
|
||||
case '\n':
|
||||
enc.buf.AppendByte('\\')
|
||||
enc.buf.AppendByte('n')
|
||||
case '\r':
|
||||
enc.buf.AppendByte('\\')
|
||||
enc.buf.AppendByte('r')
|
||||
case '\t':
|
||||
enc.buf.AppendByte('\\')
|
||||
enc.buf.AppendByte('t')
|
||||
default:
|
||||
// Encode bytes < 0x20, except for the escape sequences above.
|
||||
enc.buf.AppendString(`\u00`)
|
||||
enc.buf.AppendByte(_hex[b>>4])
|
||||
enc.buf.AppendByte(_hex[b&0xF])
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (enc *jsonEncoder) tryAddRuneError(r rune, size int) bool {
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
enc.buf.AppendString(`\ufffd`)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
package zapcorex
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// MapObjectEncoder is an ObjectEncoder backed by a simple
|
||||
// map[string]interface{}. It's not fast enough for production use, but it's
|
||||
// helpful in tests.
|
||||
type MapObjectEncoder struct {
|
||||
// Fields contains the entire encoded log context.
|
||||
Fields map[string]interface{}
|
||||
// cur is a pointer to the namespace we're currently writing to.
|
||||
cur map[string]interface{}
|
||||
}
|
||||
|
||||
// NewMapObjectEncoder creates a new map-backed ObjectEncoder.
|
||||
func NewMapObjectEncoder() *MapObjectEncoder {
|
||||
m := make(map[string]interface{})
|
||||
return &MapObjectEncoder{
|
||||
Fields: m,
|
||||
cur: m,
|
||||
}
|
||||
}
|
||||
|
||||
// AddArray implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddArray(key string, v zapcore.ArrayMarshaler) error {
|
||||
arr := &sliceArrayEncoder{elems: make([]interface{}, 0)}
|
||||
err := v.MarshalLogArray(arr)
|
||||
m.cur[key] = arr.elems
|
||||
return err
|
||||
}
|
||||
|
||||
// AddObject implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddObject(k string, v zapcore.ObjectMarshaler) error {
|
||||
newMap := NewMapObjectEncoder()
|
||||
m.cur[k] = newMap.Fields
|
||||
return v.MarshalLogObject(newMap)
|
||||
}
|
||||
|
||||
// AddBinary implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddBinary(k string, v []byte) { m.cur[k] = v }
|
||||
|
||||
// AddByteString implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddByteString(k string, v []byte) { m.cur[k] = string(v) }
|
||||
|
||||
// AddBool implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddBool(k string, v bool) { m.cur[k] = v }
|
||||
|
||||
// AddDuration implements ObjectEncoder.
|
||||
func (m MapObjectEncoder) AddDuration(k string, v time.Duration) { m.cur[k] = v }
|
||||
|
||||
// AddComplex128 implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddComplex128(k string, v complex128) { m.cur[k] = v }
|
||||
|
||||
// AddComplex64 implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddComplex64(k string, v complex64) { m.cur[k] = v }
|
||||
|
||||
// AddFloat64 implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddFloat64(k string, v float64) { m.cur[k] = v }
|
||||
|
||||
// AddFloat32 implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddFloat32(k string, v float32) { m.cur[k] = v }
|
||||
|
||||
// AddInt implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddInt(k string, v int) { m.cur[k] = v }
|
||||
|
||||
// AddInt64 implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddInt64(k string, v int64) { m.cur[k] = v }
|
||||
|
||||
// AddInt32 implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddInt32(k string, v int32) { m.cur[k] = v }
|
||||
|
||||
// AddInt16 implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddInt16(k string, v int16) { m.cur[k] = v }
|
||||
|
||||
// AddInt8 implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddInt8(k string, v int8) { m.cur[k] = v }
|
||||
|
||||
// AddString implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddString(k string, v string) { m.cur[k] = v }
|
||||
|
||||
// AddTime implements ObjectEncoder.
|
||||
func (m MapObjectEncoder) AddTime(k string, v time.Time) { m.cur[k] = v }
|
||||
|
||||
// AddUint implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddUint(k string, v uint) { m.cur[k] = v }
|
||||
|
||||
// AddUint64 implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddUint64(k string, v uint64) { m.cur[k] = v }
|
||||
|
||||
// AddUint32 implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddUint32(k string, v uint32) { m.cur[k] = v }
|
||||
|
||||
// AddUint16 implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddUint16(k string, v uint16) { m.cur[k] = v }
|
||||
|
||||
// AddUint8 implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddUint8(k string, v uint8) { m.cur[k] = v }
|
||||
|
||||
// AddUintptr implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddUintptr(k string, v uintptr) { m.cur[k] = v }
|
||||
|
||||
// AddReflected implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) AddReflected(k string, v interface{}) error {
|
||||
m.cur[k] = v
|
||||
return nil
|
||||
}
|
||||
|
||||
// OpenNamespace implements ObjectEncoder.
|
||||
func (m *MapObjectEncoder) OpenNamespace(k string) {
|
||||
ns := make(map[string]interface{})
|
||||
m.cur[k] = ns
|
||||
m.cur = ns
|
||||
}
|
||||
|
||||
// sliceArrayEncoder is an ArrayEncoder backed by a simple []interface{}. Like
|
||||
// the MapObjectEncoder, it's not designed for production use.
|
||||
type sliceArrayEncoder struct {
|
||||
elems []interface{}
|
||||
}
|
||||
|
||||
func (s *sliceArrayEncoder) AppendArray(v zapcore.ArrayMarshaler) error {
|
||||
enc := &sliceArrayEncoder{}
|
||||
err := v.MarshalLogArray(enc)
|
||||
s.elems = append(s.elems, enc.elems)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sliceArrayEncoder) AppendObject(v zapcore.ObjectMarshaler) error {
|
||||
m := NewMapObjectEncoder()
|
||||
err := v.MarshalLogObject(m)
|
||||
s.elems = append(s.elems, m.Fields)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sliceArrayEncoder) AppendReflected(v interface{}) error {
|
||||
s.elems = append(s.elems, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sliceArrayEncoder) AppendBool(v bool) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendByteString(v []byte) { s.elems = append(s.elems, string(v)) }
|
||||
func (s *sliceArrayEncoder) AppendComplex128(v complex128) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendComplex64(v complex64) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendDuration(v time.Duration) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendFloat64(v float64) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendFloat32(v float32) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendInt(v int) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendInt64(v int64) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendInt32(v int32) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendInt16(v int16) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendInt8(v int8) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendString(v string) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendTime(v time.Time) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendUint(v uint) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendUint64(v uint64) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendUint32(v uint32) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendUint16(v uint16) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendUint8(v uint8) { s.elems = append(s.elems, v) }
|
||||
func (s *sliceArrayEncoder) AppendUintptr(v uintptr) { s.elems = append(s.elems, v) }
|
||||
@@ -0,0 +1,63 @@
|
||||
package m3u8
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
cacheEnkey []byte
|
||||
cacheLsjEnkey []byte
|
||||
initOnce sync.Once
|
||||
cacheNewEnkey []byte
|
||||
newEnkeyErr error
|
||||
newEnkeyOnce sync.Once
|
||||
)
|
||||
|
||||
func initEnkey() {
|
||||
initOnce.Do(func() {
|
||||
if len(cacheEnkey) == 0 {
|
||||
data, err := ioutil.ReadFile("conf/m3u8.key")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cacheEnkey = data
|
||||
}
|
||||
if len(cacheLsjEnkey) == 0 {
|
||||
data, err := ioutil.ReadFile("conf/lsjm3u8.key")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cacheLsjEnkey = data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Enkey(ctx context.Context) (buffer *bytes.Buffer, err error) {
|
||||
if cacheEnkey == nil {
|
||||
initEnkey()
|
||||
}
|
||||
buffer = bytes.NewBuffer(cacheEnkey)
|
||||
return
|
||||
}
|
||||
|
||||
// NewEnkey 返回短剧原片使用的新 M3U8 解密密钥。
|
||||
func NewEnkey(ctx context.Context) (buffer *bytes.Buffer, err error) {
|
||||
newEnkeyOnce.Do(func() {
|
||||
cacheNewEnkey, newEnkeyErr = ioutil.ReadFile("conf/newm3u8.key")
|
||||
})
|
||||
if newEnkeyErr != nil {
|
||||
return nil, newEnkeyErr
|
||||
}
|
||||
return bytes.NewBuffer(cacheNewEnkey), nil
|
||||
}
|
||||
|
||||
func LsjEnkey(ctx context.Context) (buffer *bytes.Buffer, err error) {
|
||||
if cacheLsjEnkey == nil {
|
||||
initEnkey()
|
||||
}
|
||||
buffer = bytes.NewBuffer(cacheLsjEnkey)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package m3u8
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/utils"
|
||||
|
||||
"github.com/grafov/m3u8"
|
||||
)
|
||||
|
||||
func makeMediaPlayList(from *m3u8.MediaPlaylist, cdnDomain string, dirPath string, keyUri string, authKey string) (media *m3u8.MediaPlaylist, err error) {
|
||||
media, err = m3u8.NewMediaPlaylist(0, from.Count())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
base := utils.JoinUrlPath(cdnDomain, dirPath)
|
||||
now := time.Now()
|
||||
|
||||
for _, segment := range from.Segments {
|
||||
if segment == nil {
|
||||
continue
|
||||
}
|
||||
sg := new(m3u8.MediaSegment)
|
||||
if strings.Contains(dirPath, "laosiji") {
|
||||
segment.URI = removeDomainPrefix(segment.URI)
|
||||
}
|
||||
sg.URI = utils.JoinUrlPath(base, segment.URI)
|
||||
if len(authKey) > 0 {
|
||||
urlPath := utils.JoinUrlPath(dirPath, segment.URI)
|
||||
urlAuth := generateUrlAuth(now, urlPath, authKey)
|
||||
sg.URI = sg.URI + urlAuth
|
||||
}
|
||||
sg.Title = segment.Title
|
||||
sg.Duration = segment.Duration
|
||||
if err = media.AppendSegment(sg); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
_ = media.SetDefaultKey("AES-128", keyUri, "", "", "")
|
||||
media.Close()
|
||||
return
|
||||
}
|
||||
|
||||
func makeMediaPlayListLaosiji(from *m3u8.MediaPlaylist, cdnDomain string, dirPath string, keyUri string, authKey string) (media *m3u8.MediaPlaylist, err error) {
|
||||
media, err = m3u8.NewMediaPlaylist(0, from.Count())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
isFMP4 := from.Map != nil
|
||||
media.SetVersion(from.Version())
|
||||
if isFMP4 && media.Version() < 5 {
|
||||
media.SetVersion(5)
|
||||
}
|
||||
media.SeqNo = from.SeqNo
|
||||
media.MediaType = from.MediaType
|
||||
media.DiscontinuitySeq = from.DiscontinuitySeq
|
||||
media.StartTime = from.StartTime
|
||||
media.StartTimePrecise = from.StartTimePrecise
|
||||
media.Iframe = from.Iframe
|
||||
|
||||
if from.Map != nil {
|
||||
mapURI, mapErr := rewriteLaosijiMediaURI(from.Map.URI, cdnDomain, dirPath, "")
|
||||
if mapErr != nil {
|
||||
return nil, mapErr
|
||||
}
|
||||
mapURI = appendLaosijiURLAuth(mapURI, now, authKey)
|
||||
media.SetDefaultMap(mapURI, from.Map.Limit, from.Map.Offset)
|
||||
}
|
||||
|
||||
for _, segment := range from.Segments {
|
||||
if segment == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
sg := new(m3u8.MediaSegment)
|
||||
sg.URI, err = rewriteLaosijiMediaURI(segment.URI, cdnDomain, dirPath, fromMapURI(from))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sg.URI = appendLaosijiURLAuth(sg.URI, now, authKey)
|
||||
if from.Map == nil && segment.Map != nil {
|
||||
segmentMapURI, mapErr := rewriteLaosijiMediaURI(segment.Map.URI, cdnDomain, dirPath, "")
|
||||
if mapErr != nil {
|
||||
return nil, mapErr
|
||||
}
|
||||
sg.Map = &m3u8.Map{
|
||||
URI: appendLaosijiURLAuth(segmentMapURI, now, authKey),
|
||||
Limit: segment.Map.Limit,
|
||||
Offset: segment.Map.Offset,
|
||||
}
|
||||
isFMP4 = true
|
||||
}
|
||||
sg.Title = segment.Title
|
||||
sg.Duration = segment.Duration
|
||||
sg.Limit = segment.Limit
|
||||
sg.Offset = segment.Offset
|
||||
sg.Discontinuity = segment.Discontinuity
|
||||
if err = media.AppendSegment(sg); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if from.TargetDuration > media.TargetDuration {
|
||||
media.TargetDuration = from.TargetDuration
|
||||
}
|
||||
if isFMP4 && media.Version() < 5 {
|
||||
media.SetVersion(5)
|
||||
}
|
||||
// 老司机旧 TS 仍使用 AES-128 key;fMP4/HEVC 依赖 EXT-X-MAP,不能再强制插入 EXT-X-KEY。
|
||||
if !isFMP4 {
|
||||
_ = media.SetDefaultKey("AES-128", keyUri, "0x64cf692cebc516e4a86ac6081aae50ce", "", "")
|
||||
}
|
||||
media.Close()
|
||||
return
|
||||
}
|
||||
|
||||
func rewriteLaosijiMediaURI(originalURI, cdnDomain, playlistDir, mapURI string) (string, error) {
|
||||
originalURI = strings.TrimSpace(originalURI)
|
||||
if originalURI == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(originalURI)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !parsed.IsAbs() {
|
||||
if mapURL, mapErr := url.Parse(strings.TrimSpace(mapURI)); mapErr == nil && mapURL.IsAbs() {
|
||||
base := *mapURL
|
||||
base.RawQuery = ""
|
||||
base.Fragment = ""
|
||||
base.Path = path.Dir(base.Path) + "/"
|
||||
parsed = base.ResolveReference(parsed)
|
||||
} else {
|
||||
cleanDir := strings.TrimPrefix(strings.TrimSpace(playlistDir), "/")
|
||||
cleanDir = strings.TrimPrefix(cleanDir, "laosiji/m3m/")
|
||||
cleanDir = strings.TrimPrefix(cleanDir, "laosiji/")
|
||||
cleanDir = strings.TrimPrefix(cleanDir, "m3m/")
|
||||
parsed.Path = path.Join("/", cleanDir, parsed.Path)
|
||||
}
|
||||
}
|
||||
|
||||
return replaceDomainAndPath(parsed.String(), utils.JoinUrlPath(cdnDomain, "/laosiji"))
|
||||
}
|
||||
|
||||
func appendLaosijiURLAuth(mediaURL string, now time.Time, authKey string) string {
|
||||
if mediaURL == "" || authKey == "" {
|
||||
return mediaURL
|
||||
}
|
||||
signPath := mediaURL
|
||||
if parsed, err := url.Parse(mediaURL); err == nil {
|
||||
signPath = parsed.Path
|
||||
}
|
||||
return mediaURL + generateUrlAuth(now, signPath, authKey)
|
||||
}
|
||||
|
||||
func fromMapURI(playlist *m3u8.MediaPlaylist) string {
|
||||
if playlist == nil || playlist.Map == nil {
|
||||
return ""
|
||||
}
|
||||
return playlist.Map.URI
|
||||
}
|
||||
|
||||
func replaceDomainAndPath(originalURL, newBase string) (string, error) {
|
||||
parsedURL, err := url.Parse(originalURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
newBaseURL, err := url.Parse(newBase)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
parsedURL.Scheme = newBaseURL.Scheme
|
||||
parsedURL.Host = newBaseURL.Host
|
||||
parsedURL.Path = path.Join(newBaseURL.Path, strings.TrimPrefix(parsedURL.Path, ""))
|
||||
parsedURL.RawQuery = ""
|
||||
|
||||
return parsedURL.String(), nil
|
||||
}
|
||||
|
||||
func removeDomainPrefix(originalURL string) string {
|
||||
parsedURL, err := url.Parse(originalURL)
|
||||
if err != nil {
|
||||
return originalURL
|
||||
}
|
||||
return strings.TrimPrefix(parsedURL.Path, "")
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package m3u8
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"videoweb/pub/common/enum/redisconst"
|
||||
"videoweb/pub/common/errors"
|
||||
"videoweb/pub/common/http"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/redis"
|
||||
"videoweb/pub/storage/proxy"
|
||||
|
||||
"github.com/grafov/m3u8"
|
||||
)
|
||||
|
||||
const (
|
||||
keyUri = "/enkey"
|
||||
webkeyUri = "/api/web/enkey"
|
||||
h5KeyUri = "/enkey"
|
||||
AuthKey = "fT5xSg4hltHpzVy6aV9rVECDJ1J1pN"
|
||||
keyUriLsj = "/lsjenkey"
|
||||
keyUriWebLsj = "/api/web/lsjenkey"
|
||||
)
|
||||
|
||||
// resolveTsAuthKey 优先使用传入的 tsAuthKey,为空则回退默认 AuthKey。
|
||||
func resolveTsAuthKey(tsAuthKey string) string {
|
||||
if key := strings.TrimSpace(tsAuthKey); key != "" {
|
||||
return key
|
||||
}
|
||||
return AuthKey
|
||||
}
|
||||
|
||||
var headerMediaPlayList *m3u8.MediaPlaylist
|
||||
|
||||
// TransFormWithSafely 被检查权限的播放列表。
|
||||
// tsAuthKey: 普通 TS CDN 鉴权;lsjM3u8Yuan: 老司机源站;cdnAuthKey: 老司机分片 CDN 鉴权。
|
||||
func TransFormWithSafely(req *Req, cdnDomain, origDomain string, web bool, tsAuthKey, lsjM3u8Yuan, cdnAuthKey string) (*m3u8.MediaPlaylist, *bytes.Buffer, error) {
|
||||
if req.Name == "" {
|
||||
return nil, nil, errors.MakeWithMsg(errors.M3u8FileName, "file name is empty")
|
||||
}
|
||||
if cdnDomain == "" || origDomain == "" {
|
||||
return nil, nil, errors.MakeWithMsg(errors.DomainErr, "cdnDomain or origDomain is empty")
|
||||
}
|
||||
if strings.Contains(req.Name, "hc237") {
|
||||
req.Name = "/laosiji" + req.Name
|
||||
}
|
||||
if strings.Contains(req.Name, "laosiji") {
|
||||
return TransformLaosiji(req.Name, cdnDomain, web, lsjM3u8Yuan, cdnAuthKey)
|
||||
}
|
||||
return transform(req.Name, cdnDomain, origDomain, web, tsAuthKey)
|
||||
}
|
||||
|
||||
func TransformLaosiji(name, cdnDomain string, web bool, lsjM3u8Yuan, cdnAuthKey string, customKeyUri ...string) (media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
|
||||
// fMP4/HEVC 分片 CDN 使用传入的视频 CDN(web 为 MediaWebCdn,app/h5 为 TsCdn),
|
||||
// 与 mdsm GetCDNUrlList()[0] 一致;拉取源 m3u8 仍走 lsjM3u8Yuan。
|
||||
m3u8reader, err := readFromRedis(laosijiUpstreamPath(name), lsjM3u8Yuan)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p, t, err := m3u8.DecodeFrom(m3u8reader, false)
|
||||
if err != nil {
|
||||
err = errors.MakeWithErr(errors.M3u8Read, err)
|
||||
return
|
||||
}
|
||||
|
||||
authKey := strings.TrimSpace(cdnAuthKey)
|
||||
keyurl := keyUriLsj
|
||||
if web {
|
||||
keyurl = keyUriWebLsj
|
||||
}
|
||||
if len(customKeyUri) > 0 && customKeyUri[0] != "" {
|
||||
keyurl = customKeyUri[0]
|
||||
}
|
||||
|
||||
switch t {
|
||||
case m3u8.MEDIA:
|
||||
pl := p.(*m3u8.MediaPlaylist)
|
||||
dirPath := utils.PathForInfo(name).Dir
|
||||
media, err = makeMediaPlayListLaosiji(pl, cdnDomain, dirPath, keyurl, authKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buff = media.Encode()
|
||||
case m3u8.MASTER:
|
||||
mpl := p.(*m3u8.MasterPlaylist)
|
||||
buff = mpl.Encode()
|
||||
default:
|
||||
err = errors.MakeWithMsg(errors.M3u8FileName, fmt.Sprintf("invalid m3u8 type %d", t))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func transform(name, cdnDomain, origDomain string, web bool, tsAuthKey string) (media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
|
||||
mediaVerson := proxy.AppendsMediaS3Version(name)
|
||||
if len(mediaVerson) > 0 {
|
||||
name = mediaVerson + name
|
||||
}
|
||||
|
||||
m3u8reader, err := readFromRedis(name, origDomain)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p, t, err := m3u8.DecodeFrom(m3u8reader, false)
|
||||
if err != nil {
|
||||
err = errors.MakeWithErr(errors.M3u8Read, err)
|
||||
return
|
||||
}
|
||||
|
||||
authKey := resolveTsAuthKey(tsAuthKey)
|
||||
keyurl := keyUri
|
||||
if web {
|
||||
keyurl = webkeyUri
|
||||
}
|
||||
|
||||
switch t {
|
||||
case m3u8.MEDIA:
|
||||
pl := p.(*m3u8.MediaPlaylist)
|
||||
dirPath := utils.PathForInfo(name).Dir
|
||||
media, err = makeMediaPlayList(pl, cdnDomain, dirPath, keyurl, authKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buff = media.Encode()
|
||||
case m3u8.MASTER:
|
||||
mpl := p.(*m3u8.MasterPlaylist)
|
||||
buff = mpl.Encode()
|
||||
default:
|
||||
err = errors.MakeWithMsg(errors.M3u8FileName, fmt.Sprintf("invalid m3u8 type %d", t))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func readFromCDN(name, domain string) (reader io.Reader, err error) {
|
||||
requestUrl := utils.JoinUrlPath(domain, name)
|
||||
resp, err := http.GetLargeFile(requestUrl, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
msg := fmt.Sprintf("read m3u8 fail, url: %s, status code %d", requestUrl, resp.StatusCode)
|
||||
err = errors.MakeWithMsg(errors.M3u8Read, msg)
|
||||
return
|
||||
}
|
||||
reader = bytes.NewReader(resp.Data)
|
||||
return
|
||||
}
|
||||
|
||||
func readFromRedis(name, domain string) (reader io.Reader, err error) {
|
||||
m3u8Key := redisconst.MediaM3U8Key(name)
|
||||
data, err := redis.GetBytes(m3u8Key)
|
||||
if err == nil && len(data) > 0 {
|
||||
reader = bytes.NewReader(data)
|
||||
return
|
||||
}
|
||||
if strings.Contains(name, "hc237") {
|
||||
name = "/m3m/" + name
|
||||
}
|
||||
requestUrl := utils.JoinUrlPath(domain, name)
|
||||
resp, err := http.GetLargeFile(requestUrl, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
msg := fmt.Sprintf("read m3u8 fail, url: %s, status code %d", requestUrl, resp.StatusCode)
|
||||
err = errors.MakeWithMsg(errors.M3u8Read, msg)
|
||||
return
|
||||
}
|
||||
if err := redis.Set(m3u8Key, resp.Data,
|
||||
redisconst.MediaM3U8CacheExpire); err != nil {
|
||||
log.Info("m3u8:readFromRedis redis set err", log.E(err))
|
||||
}
|
||||
reader = bytes.NewReader(resp.Data)
|
||||
return
|
||||
}
|
||||
|
||||
func GetHeaderMediaSegUrls() []string {
|
||||
urls := make([]string, 0)
|
||||
if headerMediaPlayList == nil {
|
||||
return urls
|
||||
}
|
||||
for _, segment := range headerMediaPlayList.Segments {
|
||||
if segment == nil {
|
||||
continue
|
||||
}
|
||||
urls = append(urls, segment.URI)
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
// laosijiUpstreamPath maps internal laosiji m3u8 path to upstream m3m path.
|
||||
func laosijiUpstreamPath(name string) string {
|
||||
name = strings.TrimPrefix(name, "/")
|
||||
name = strings.TrimPrefix(name, "laosiji/m3m/")
|
||||
name = strings.TrimPrefix(name, "laosiji/")
|
||||
return "m3m/" + name
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package m3u8
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"videoweb/pub/common/errors"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/storage/proxy"
|
||||
|
||||
"github.com/grafov/m3u8"
|
||||
)
|
||||
|
||||
const lsjKeyIV = "0x64cf692cebc516e4a86ac6081aae50ce"
|
||||
|
||||
// TransformForTranscode 生成转码专用播放列表,供老司机转码服务器拉取。
|
||||
func TransformForTranscode(name, origDomain, cdnDomain, webM3u8Host, lsjM3u8Yuan, lsjTsYuan string) (media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
|
||||
if strings.Contains(name, "hc237") {
|
||||
name = "/laosiji" + name
|
||||
}
|
||||
if strings.Contains(name, "laosiji") {
|
||||
return transcodeLaosiji(name, webM3u8Host, lsjM3u8Yuan, lsjTsYuan)
|
||||
}
|
||||
return transcodeNormal(name, origDomain, cdnDomain, webM3u8Host)
|
||||
}
|
||||
|
||||
func transcodeLaosiji(name, webM3u8Host, lsjM3u8Yuan, lsjTsYuan string) (media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
|
||||
m3u8reader, err := readFromRedis(laosijiUpstreamPath(name), lsjM3u8Yuan)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p, t, err := m3u8.DecodeFrom(m3u8reader, false)
|
||||
if err != nil {
|
||||
err = errors.MakeWithErr(errors.M3u8Read, err)
|
||||
return
|
||||
}
|
||||
switch t {
|
||||
case m3u8.MEDIA:
|
||||
pl := p.(*m3u8.MediaPlaylist)
|
||||
keyURI := utils.JoinUrlPath(webM3u8Host, keyUriWebLsj)
|
||||
// ts 走 CDN(TsYuan/laosiji),与正常播放的 CDN 路径一致。
|
||||
media, err = makeTranscodeSegList(pl, utils.JoinUrlPath(lsjTsYuan, "/laosiji"), "", keyURI, lsjKeyIV, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buff = media.Encode()
|
||||
case m3u8.MASTER:
|
||||
buff = p.(*m3u8.MasterPlaylist).Encode()
|
||||
default:
|
||||
err = errors.MakeWithMsg(errors.M3u8FileName, fmt.Sprintf("invalid m3u8 type %d", t))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func transcodeNormal(name, origDomain, cdnDomain, webM3u8Host string) (media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
|
||||
if v := proxy.AppendsMediaS3Version(name); len(v) > 0 {
|
||||
name = v + name
|
||||
}
|
||||
m3u8reader, err := readFromRedis(name, origDomain)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p, t, err := m3u8.DecodeFrom(m3u8reader, false)
|
||||
if err != nil {
|
||||
err = errors.MakeWithErr(errors.M3u8Read, err)
|
||||
return
|
||||
}
|
||||
switch t {
|
||||
case m3u8.MEDIA:
|
||||
pl := p.(*m3u8.MediaPlaylist)
|
||||
dirPath := utils.PathForInfo(name).Dir
|
||||
keyURI := utils.JoinUrlPath(webM3u8Host, webkeyUri)
|
||||
// ts 走 CDN(CdnDomain),与正常播放一致。
|
||||
media, err = makeTranscodeSegList(pl, cdnDomain, dirPath, keyURI, "", false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buff = media.Encode()
|
||||
case m3u8.MASTER:
|
||||
buff = p.(*m3u8.MasterPlaylist).Encode()
|
||||
default:
|
||||
err = errors.MakeWithMsg(errors.M3u8FileName, fmt.Sprintf("invalid m3u8 type %d", t))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func makeTranscodeSegList(from *m3u8.MediaPlaylist, srcDomain, dirPath, keyURI, iv string, laosiji bool) (media *m3u8.MediaPlaylist, err error) {
|
||||
media, err = m3u8.NewMediaPlaylist(0, from.Count())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
base := utils.JoinUrlPath(srcDomain, dirPath)
|
||||
for _, segment := range from.Segments {
|
||||
if segment == nil {
|
||||
continue
|
||||
}
|
||||
sg := new(m3u8.MediaSegment)
|
||||
if laosiji {
|
||||
if u, e := replaceDomainAndPath(segment.URI, srcDomain); e == nil {
|
||||
sg.URI = u
|
||||
} else {
|
||||
sg.URI = segment.URI
|
||||
}
|
||||
} else {
|
||||
sg.URI = utils.JoinUrlPath(base, segment.URI)
|
||||
}
|
||||
sg.Title = segment.Title
|
||||
sg.Duration = segment.Duration
|
||||
if err = media.AppendSegment(sg); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
_ = media.SetDefaultKey("AES-128", keyURI, iv, "", "")
|
||||
media.Close()
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package m3u8
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/pub/common/errors"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/pubdef"
|
||||
"videoweb/pub/storage/proxy"
|
||||
|
||||
"github.com/grafov/m3u8"
|
||||
)
|
||||
|
||||
// TransformEx m3u8 文件转换。
|
||||
// apiType(1:app,2:h5app,3:web)
|
||||
// authKey: TS CDN 鉴权(空则回退默认 AuthKey)
|
||||
// lsjM3u8Yuan / cdnAuthKey: 老司机源站与分片鉴权
|
||||
// customKeyUri: 非空时直接使用,用于将鉴权参数嵌入 EXT-X-KEY URI
|
||||
func TransformEx(name, cdnDomain, origDomain string, apiType int, authKey, lsjM3u8Yuan, cdnAuthKey string, customKeyUri ...string) (
|
||||
media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
|
||||
return transformEx(name, cdnDomain, origDomain, apiType, authKey, lsjM3u8Yuan, cdnAuthKey, true, customKeyUri...)
|
||||
}
|
||||
|
||||
// TransformExOriginalPath 按数据库中的原始路径读取 m3u8,不自动追加 v1/v2/v3 前缀。
|
||||
// 短剧水印未完成时仍播放原片,原片路径如 k12/...,需要保留原路径。
|
||||
func TransformExOriginalPath(name, cdnDomain, origDomain string, apiType int, authKey, lsjM3u8Yuan, cdnAuthKey string, customKeyUri ...string) (
|
||||
media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
|
||||
return transformEx(name, cdnDomain, origDomain, apiType, authKey, lsjM3u8Yuan, cdnAuthKey, false, customKeyUri...)
|
||||
}
|
||||
|
||||
func transformEx(name, cdnDomain, origDomain string, apiType int, authKey, lsjM3u8Yuan, cdnAuthKey string, appendMediaVersion bool, customKeyUri ...string) (
|
||||
media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
|
||||
|
||||
if name == "" {
|
||||
err = errors.MakeWithMsg(errors.M3u8FileName, "m3u8 file name is empty")
|
||||
return
|
||||
}
|
||||
if cdnDomain == "" || origDomain == "" {
|
||||
err = errors.MakeWithMsg(errors.DomainErr, "cdnDomain or origDomain is empty")
|
||||
return
|
||||
}
|
||||
if strings.Contains(name, "hc237") {
|
||||
name = "/laosiji" + name
|
||||
}
|
||||
if strings.Contains(name, "laosiji") {
|
||||
return TransformLaosiji(name, cdnDomain, false, lsjM3u8Yuan, cdnAuthKey, customKeyUri...)
|
||||
}
|
||||
|
||||
if appendMediaVersion {
|
||||
mediaVerson := proxy.AppendsMediaS3Version(name)
|
||||
if len(mediaVerson) > 0 {
|
||||
name = mediaVerson + name
|
||||
}
|
||||
}
|
||||
authKey = resolveTsAuthKey(authKey)
|
||||
|
||||
m3u8reader, err := readFromRedis(name, origDomain)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p, t, err := m3u8.DecodeFrom(m3u8reader, false)
|
||||
if err != nil {
|
||||
err = errors.MakeWithErr(errors.M3u8Read, err)
|
||||
return
|
||||
}
|
||||
apiPath := keyUri
|
||||
if len(customKeyUri) > 0 && customKeyUri[0] != "" {
|
||||
apiPath = customKeyUri[0]
|
||||
} else if apiType == 2 {
|
||||
apiPath = h5KeyUri
|
||||
} else if apiType == 3 {
|
||||
apiPath = webkeyUri
|
||||
}
|
||||
|
||||
switch t {
|
||||
case m3u8.MEDIA:
|
||||
pl := p.(*m3u8.MediaPlaylist)
|
||||
dirPath := utils.PathForInfo(name).Dir
|
||||
media, err = makeTSList(pl, cdnDomain, dirPath, apiPath, authKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buff = media.Encode()
|
||||
case m3u8.MASTER:
|
||||
mpl := p.(*m3u8.MasterPlaylist)
|
||||
buff = mpl.Encode()
|
||||
default:
|
||||
err = errors.MakeWithMsg(errors.M3u8FileName, fmt.Sprintf("invalid m3u8 type %d", t))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func makeTSList(from *m3u8.MediaPlaylist, cdnDomain string,
|
||||
dirPath string, apiPath string, authKey string) (
|
||||
media *m3u8.MediaPlaylist, err error) {
|
||||
media, err = m3u8.NewMediaPlaylist(from.WinSize(), from.Count())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
base := utils.JoinUrlPath(cdnDomain, dirPath)
|
||||
for _, segment := range from.Segments {
|
||||
if segment == nil {
|
||||
continue
|
||||
}
|
||||
sg := new(m3u8.MediaSegment)
|
||||
sg.URI = utils.JoinUrlPath(base, segment.URI)
|
||||
if len(authKey) > 0 {
|
||||
urlPath := utils.JoinUrlPath(dirPath, segment.URI)
|
||||
urlAuth := generateUrlAuth(now, urlPath, authKey)
|
||||
sg.URI = sg.URI + urlAuth
|
||||
}
|
||||
sg.Title = segment.Title
|
||||
sg.Duration = segment.Duration
|
||||
if err = media.AppendSegment(sg); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
_ = media.SetDefaultKey("AES-128", apiPath, "", "", "")
|
||||
media.Close()
|
||||
return
|
||||
}
|
||||
|
||||
func generateUrlAuth(now time.Time, path string, authKey string) string {
|
||||
timestamp := now.Unix()
|
||||
signStr := fmt.Sprintf("%s-%d-0-0-%s", path, timestamp, authKey)
|
||||
md5Str := getMD5Sign(signStr)
|
||||
// TS CDN 鉴权参数后追加应用标识,便于流量统计。
|
||||
urlAuth := fmt.Sprintf("?md=%d-0-0-%s&c=%d", timestamp, md5Str, pubdef.AppId)
|
||||
return urlAuth
|
||||
}
|
||||
|
||||
func generateUrlAuthQiniu(now time.Time, path string, authKey string) string {
|
||||
timestamp := strconv.FormatInt(now.Add(2*time.Hour).Unix(), 16)
|
||||
base64 := path
|
||||
signStr := authKey + base64 + timestamp
|
||||
md5Str := getMD5Sign(signStr)
|
||||
urlAuth := fmt.Sprintf("?sign=%s&t=%s", md5Str, timestamp)
|
||||
return urlAuth
|
||||
}
|
||||
|
||||
func getMD5Sign(buf string) string {
|
||||
md5Ctx := md5.New()
|
||||
md5Ctx.Write([]byte(buf))
|
||||
cipherStr := md5Ctx.Sum(nil)
|
||||
nsign := hex.EncodeToString(cipherStr)
|
||||
return nsign
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package m3u8
|
||||
|
||||
type Req struct {
|
||||
Token string `json:"token" form:"token"`
|
||||
VId int64 `json:"vId" form:"vId"`
|
||||
Name string `json:"name" form:"name"`
|
||||
NeedHeader bool `json:"needHeader" form:"needHeader"`
|
||||
}
|
||||
|
||||
type PostVideoReq struct {
|
||||
Token string `json:"token" form:"token"`
|
||||
Name string `json:"name" form:"name" binding:"required" comment:"must be set"`
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package m3u8
|
||||
|
||||
//sErr "sz-app-server/pub/common/errors"
|
||||
|
||||
// func CheckParams(req *Req) (err error) {
|
||||
// if req.Token == "" {
|
||||
// err = sErr.MakeWithMsg(sErr.NoToken, "no token")
|
||||
// return
|
||||
// }
|
||||
// if req.VId <= 0 {
|
||||
// err = sErr.MakeWithMsg(sErr.VideoId, "invalid video id")
|
||||
// return
|
||||
// }
|
||||
// if req.Name == "" {
|
||||
// err = sErr.MakeWithMsg(sErr.VideoName, "invalid name")
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
@@ -0,0 +1,96 @@
|
||||
package m3u8
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"videoweb/pub/common/errors"
|
||||
"videoweb/pub/common/utils"
|
||||
"videoweb/pub/storage/proxy"
|
||||
)
|
||||
|
||||
// TransformVtt 读取雪碧图 vtt 文件,并把其中的缩略图路径替换为图片 cdn 地址。
|
||||
// 兼容现有 /laosiji 路径资源:laosiji 的 vtt 需移除 /laosiji 前缀后,从 lsjImageYuan 源站读取。
|
||||
// imgCdn 图片 cdn 域名(缩略图访问地址)
|
||||
// origDomain 普通媒资的源站域名
|
||||
// lsjImageYuan 老司机图片源站域名(由 app/web 项目从配置 lsjMediaConf.imageYuan 传入)
|
||||
func TransformVtt(name, imgCdn, origDomain, lsjImageYuan string) (buff []byte, err error) {
|
||||
if name == "" {
|
||||
err = errors.MakeWithMsg(errors.M3u8FileName, "vtt file name is empty")
|
||||
return
|
||||
}
|
||||
if imgCdn == "" || origDomain == "" {
|
||||
err = errors.MakeWithMsg(errors.DomainErr, "imgCdn or origDomain is empty")
|
||||
return
|
||||
}
|
||||
|
||||
mediaVersion := proxy.AppendsMediaS3Version(name)
|
||||
if len(mediaVersion) > 0 {
|
||||
name = mediaVersion + name
|
||||
}
|
||||
|
||||
originName := name
|
||||
if strings.Contains(name, "laosiji") {
|
||||
if lsjImageYuan == "" {
|
||||
err = errors.MakeWithMsg(errors.DomainErr, "lsjImageYuan is empty")
|
||||
return
|
||||
}
|
||||
originName = stripLaosijiPrefix(name)
|
||||
origDomain = lsjImageYuan
|
||||
}
|
||||
|
||||
srcReader, err := readFromRedis(originName, origDomain)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(srcReader)
|
||||
if err != nil {
|
||||
return nil, errors.MakeWithMsg(errors.M3u8Read, "read vtt reader fail: "+err.Error())
|
||||
}
|
||||
|
||||
dirPath := utils.PathForInfo(name).Dir
|
||||
buff = rewriteVttWithCDN(raw, imgCdn, dirPath)
|
||||
return
|
||||
}
|
||||
|
||||
func stripLaosijiPrefix(name string) string {
|
||||
p := strings.TrimPrefix(name, "/")
|
||||
p = strings.TrimPrefix(p, "laosiji")
|
||||
p = strings.TrimPrefix(p, "/")
|
||||
return p
|
||||
}
|
||||
|
||||
func rewriteVttWithCDN(vttData []byte, imgCdn, dirPath string) []byte {
|
||||
imgCdn = strings.TrimRight(strings.TrimSpace(imgCdn), "/")
|
||||
|
||||
var out bytes.Buffer
|
||||
scanner := bufio.NewScanner(bytes.NewReader(vttData))
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
trim := strings.TrimSpace(line)
|
||||
|
||||
if idx := strings.Index(trim, "#xywh="); idx >= 0 {
|
||||
p := strings.TrimSpace(trim[:idx])
|
||||
xywh := trim[idx:]
|
||||
|
||||
var newPath string
|
||||
switch {
|
||||
case strings.HasPrefix(p, "http://") || strings.HasPrefix(p, "https://"):
|
||||
newPath = p
|
||||
case strings.HasPrefix(p, "/"):
|
||||
newPath = imgCdn + p
|
||||
default:
|
||||
newPath = utils.JoinUrlPath(utils.JoinUrlPath(imgCdn, strings.TrimRight(dirPath, "/")), p)
|
||||
}
|
||||
out.WriteString(newPath + xywh + "\n")
|
||||
continue
|
||||
}
|
||||
|
||||
out.WriteString(line + "\n")
|
||||
}
|
||||
|
||||
return out.Bytes()
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package maths
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
const CODE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
const CodeLenth = 62
|
||||
|
||||
/**
|
||||
* 10进制 为 62进制 字符串。大数字类型
|
||||
*
|
||||
* param: *big.Int number 待转换的数字,非常大的
|
||||
* return: string
|
||||
*/
|
||||
func From10To62Big(number *big.Int) string {
|
||||
tmpNumber := big.NewInt(0)
|
||||
var result []byte
|
||||
for number.Cmp(big.NewInt(0)) != 0 {
|
||||
var tmp []byte
|
||||
tmpNumber.Rem(number, big.NewInt(CodeLenth))
|
||||
tmp = append(tmp, CODE62[tmpNumber.Int64()])
|
||||
result = append(tmp, result...)
|
||||
number.Div(number, big.NewInt(CodeLenth))
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 10进制 为 62进制 字符串。大数字类型
|
||||
*
|
||||
* param: int64 num 待转换的数字
|
||||
* return: string
|
||||
*/
|
||||
func From10To62(number int64) string {
|
||||
var result []byte
|
||||
for number != 0 {
|
||||
var tmp []byte
|
||||
number1 := number % CodeLenth
|
||||
tmp = append(tmp, CODE62[number1])
|
||||
result = append(tmp, result...)
|
||||
number = number / CodeLenth
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func GenUUID() string {
|
||||
u1 := uuid.Must(uuid.NewV4(), nil).String()
|
||||
uuidStr := strings.Replace(u1, "-", "", -1)
|
||||
number, _ := new(big.Int).SetString(uuidStr, 16)
|
||||
return From10To62Big(number)
|
||||
}
|
||||
|
||||
func Abs(n int64) int64 {
|
||||
if n < 0 {
|
||||
return -n
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func Gen32String() string {
|
||||
u1 := uuid.Must(uuid.NewV4(), nil).String()
|
||||
uuidStr := strings.Replace(u1, "-", "", -1)
|
||||
return uuidStr
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package netutil
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetLocalIP() string {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, address := range addrs {
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
return ipnet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func MergeURL(baseURL, relativePath string) string {
|
||||
if len(relativePath) == 0 {
|
||||
return ""
|
||||
}
|
||||
// 解析基础URL
|
||||
base, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 如果相对路径是绝对URL,直接返回
|
||||
if strings.HasPrefix(relativePath, "http://") || strings.HasPrefix(relativePath, "https://") {
|
||||
return relativePath
|
||||
}
|
||||
|
||||
// 合并路径
|
||||
mergedPath := path.Join(base.Path, relativePath)
|
||||
|
||||
// 处理路径中的 ./ 和 ../
|
||||
mergedPath = path.Clean(mergedPath)
|
||||
|
||||
// 确保路径以/开头(如果原始base.Path以/结尾)
|
||||
if strings.HasSuffix(base.Path, "/") && !strings.HasPrefix(mergedPath, "/") {
|
||||
mergedPath = "/" + mergedPath
|
||||
}
|
||||
|
||||
// 更新URL的Path部分
|
||||
base.Path = mergedPath
|
||||
|
||||
return base.String()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package netutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_GetLocalIP(t *testing.T) {
|
||||
fmt.Println(GetLocalIP())
|
||||
}
|
||||
@@ -0,0 +1,580 @@
|
||||
package pageopt
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// A A from Array
|
||||
type A = []interface{}
|
||||
|
||||
// M M from Map
|
||||
type M = bson.M
|
||||
|
||||
// *******************************************//
|
||||
// Matcher
|
||||
type Matcher interface {
|
||||
Filter() M
|
||||
}
|
||||
|
||||
// MergeM
|
||||
func MergeM(mats []Matcher) M {
|
||||
merge := M{}
|
||||
for _, m := range mats {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
ql := m.Filter()
|
||||
for k, v := range ql {
|
||||
merge[k] = v
|
||||
}
|
||||
}
|
||||
return merge
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
type MergeMatch struct {
|
||||
merge M
|
||||
}
|
||||
|
||||
func NewMergeMatch(mats ...Matcher) Matcher {
|
||||
mergeM := MergeM(mats)
|
||||
return &MergeMatch{mergeM}
|
||||
}
|
||||
|
||||
func (m *MergeMatch) Filter() M {
|
||||
return m.merge
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
// AssignMatch
|
||||
type AssignMatch struct {
|
||||
Key string
|
||||
Val interface{}
|
||||
}
|
||||
|
||||
func NewAssignMatch(key string, val interface{}) Matcher {
|
||||
return &AssignMatch{
|
||||
key,
|
||||
val,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *AssignMatch) Filter() M {
|
||||
if IsEmpt(m.Val) {
|
||||
return M{}
|
||||
}
|
||||
return M{
|
||||
m.Key: m.Val,
|
||||
}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
|
||||
type Sort = bson.D
|
||||
|
||||
func SortDesc(Key string) Sort {
|
||||
return Sort{{Key, -1}}
|
||||
}
|
||||
|
||||
func SortAsc(Key string) Sort {
|
||||
return Sort{{Key, 1}}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
// RegexMatch
|
||||
type RegexMatch struct {
|
||||
Key string
|
||||
Val interface{}
|
||||
Options string
|
||||
}
|
||||
|
||||
func NewRegexMatch(key string, val interface{}, opt string) Matcher {
|
||||
return &RegexMatch{
|
||||
Key: key,
|
||||
Val: val,
|
||||
Options: opt,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RegexMatch) Filter() M {
|
||||
if IsEmpt(r.Val) {
|
||||
return M{}
|
||||
}
|
||||
return M{
|
||||
r.Key: M{"$regex": r.Val, "$options": r.Options},
|
||||
}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
func IsNil(i interface{}) bool {
|
||||
vi := reflect.ValueOf(i)
|
||||
return vi.IsNil()
|
||||
}
|
||||
|
||||
func IsEmpt(i interface{}) bool {
|
||||
v := reflect.ValueOf(i)
|
||||
switch v.Kind() {
|
||||
case reflect.Ptr:
|
||||
return v.IsNil()
|
||||
case reflect.Slice, reflect.Array:
|
||||
return v.Len() == 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
|
||||
type NEMatch struct {
|
||||
Key string
|
||||
NE interface{}
|
||||
}
|
||||
|
||||
func (m *NEMatch) Filter() M {
|
||||
if IsEmpt(m.NE) {
|
||||
return M{}
|
||||
}
|
||||
return M{m.Key: M{"$ne": m.NE}}
|
||||
}
|
||||
|
||||
func NewNEMatch(Key string, ne interface{}) Matcher {
|
||||
return &NEMatch{
|
||||
Key: Key,
|
||||
NE: ne,
|
||||
}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
// GEMatch
|
||||
type GEMatch struct {
|
||||
Key string
|
||||
GT interface{}
|
||||
}
|
||||
|
||||
func NewGEMatch(Key string, gt interface{}) Matcher {
|
||||
return &GEMatch{
|
||||
Key: Key,
|
||||
GT: gt,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *GEMatch) Filter() M {
|
||||
if IsEmpt(m.GT) {
|
||||
return M{}
|
||||
}
|
||||
return M{m.Key: M{"$gt": m.GT}}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
// GTEMatch
|
||||
type GTEMatch struct {
|
||||
Key string
|
||||
GTE interface{}
|
||||
}
|
||||
|
||||
func NewGTEMatch(Key string, gte interface{}) Matcher {
|
||||
return >EMatch{
|
||||
Key: Key,
|
||||
GTE: gte,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *GTEMatch) Filter() M {
|
||||
if IsEmpt(m.GTE) {
|
||||
return M{}
|
||||
}
|
||||
return M{m.Key: M{"$gte": m.GTE}}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
|
||||
// GTMatch
|
||||
type GTMatch struct {
|
||||
Key string
|
||||
GT interface{}
|
||||
}
|
||||
|
||||
func NewGTMatch(Key string, gt interface{}) Matcher {
|
||||
return >Match{
|
||||
Key: Key,
|
||||
GT: gt,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *GTMatch) Filter() M {
|
||||
if IsEmpt(m.GT) {
|
||||
return M{}
|
||||
}
|
||||
return M{m.Key: M{"$gt": m.GT}}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
// LTMatch
|
||||
type LTMatch struct {
|
||||
Key string
|
||||
LT interface{}
|
||||
}
|
||||
|
||||
func NewLTMatch(Key string, lt interface{}) Matcher {
|
||||
return <Match{
|
||||
Key: Key,
|
||||
LT: lt,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *LTMatch) Filter() M {
|
||||
if IsEmpt(m.LT) {
|
||||
return M{}
|
||||
}
|
||||
return M{m.Key: M{"$lt": m.LT}}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
|
||||
// LTEMatch
|
||||
type LTEMatch struct {
|
||||
Key string
|
||||
LTE interface{}
|
||||
}
|
||||
|
||||
func NewLTEMatch(Key string, lte interface{}) Matcher {
|
||||
return <EMatch{
|
||||
Key: Key,
|
||||
LTE: lte,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *LTEMatch) Filter() M {
|
||||
if IsEmpt(m.LTE) {
|
||||
return M{}
|
||||
}
|
||||
return M{m.Key: M{"$lte": m.LTE}}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
// GTEAndLTEMatch
|
||||
type GTEAndLTEMatch struct {
|
||||
Key string
|
||||
GTE interface{}
|
||||
LTE interface{}
|
||||
}
|
||||
|
||||
func NewGTEAndLTEMatch(Key string, gte, lte interface{}) Matcher {
|
||||
return >EAndLTEMatch{
|
||||
Key,
|
||||
gte,
|
||||
lte,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *GTEAndLTEMatch) Filter() M {
|
||||
if IsEmpt(m.GTE) || IsEmpt(m.LTE) {
|
||||
return M{}
|
||||
}
|
||||
|
||||
return M{
|
||||
m.Key: M{
|
||||
"$gte": m.GTE,
|
||||
"$lte": m.LTE,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
// GTEAndLTMatch
|
||||
type GTEAndLTMatch struct {
|
||||
Key string
|
||||
GTE interface{}
|
||||
LT interface{}
|
||||
}
|
||||
|
||||
func NewGTEAndLTMatch(key string, gte, lt interface{}) Matcher {
|
||||
return >EAndLTMatch{
|
||||
key,
|
||||
gte,
|
||||
lt,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *GTEAndLTMatch) Filter() M {
|
||||
if IsEmpt(m.GTE) || IsEmpt(m.LT) {
|
||||
return M{}
|
||||
}
|
||||
|
||||
return M{
|
||||
m.Key: M{
|
||||
"$gte": m.GTE,
|
||||
"$lt": m.LT,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
|
||||
// ExistsMatch
|
||||
type ExistsMatch struct {
|
||||
Key string
|
||||
IsExisted *bool
|
||||
}
|
||||
|
||||
func NewExistsMatch(key string, isExisted *bool) Matcher {
|
||||
return &ExistsMatch{
|
||||
Key: key,
|
||||
IsExisted: isExisted,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ExistsMatch) Filter() M {
|
||||
if IsEmpt(e.IsExisted) {
|
||||
return M{}
|
||||
}
|
||||
return M{
|
||||
e.Key: M{"$exists": e.IsExisted},
|
||||
}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
// InMatch
|
||||
type InMatch struct {
|
||||
Key string
|
||||
Slice interface{}
|
||||
}
|
||||
|
||||
func NewInMatch(key string, slice interface{}) Matcher {
|
||||
return &InMatch{
|
||||
Key: key,
|
||||
Slice: slice,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *InMatch) Filter() M {
|
||||
if IsNil(i.Slice) {
|
||||
return M{}
|
||||
}
|
||||
return M{
|
||||
i.Key: M{"$in": i.Slice},
|
||||
}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
// NinMatch
|
||||
type NinMatch struct {
|
||||
Key string
|
||||
Slice interface{}
|
||||
}
|
||||
|
||||
func NewNinMatch(key string, slice interface{}) Matcher {
|
||||
return &NinMatch{
|
||||
Key: key,
|
||||
Slice: slice,
|
||||
}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
func (n *NinMatch) Filter() M {
|
||||
if IsNil(n.Slice) {
|
||||
return M{}
|
||||
}
|
||||
return M{
|
||||
n.Key: M{"$nin": n.Slice},
|
||||
}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
// ManualOrMatch
|
||||
type ManualOrMatch struct {
|
||||
Key string
|
||||
Mats []Matcher
|
||||
}
|
||||
|
||||
func NewManualOrMatch(key string, mats []Matcher) Matcher {
|
||||
return &ManualOrMatch{
|
||||
Key: key,
|
||||
Mats: mats,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *ManualOrMatch) Filter() M {
|
||||
if len(o.Mats) == 0 {
|
||||
return M{}
|
||||
}
|
||||
fs := []interface{}{}
|
||||
for _, v := range o.Mats {
|
||||
f := v.Filter()
|
||||
if len(f) != 0 {
|
||||
fs = append(fs, v.Filter())
|
||||
}
|
||||
}
|
||||
if len(fs) == 0 {
|
||||
return M{}
|
||||
}
|
||||
return M{"$or": fs}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
// BitsAllSetMatch
|
||||
type BitsAllSetMatch struct {
|
||||
Key string
|
||||
Bits []int
|
||||
}
|
||||
|
||||
func NewBitsAllSetMatch(key string, bits []int) Matcher {
|
||||
return &BitsAllSetMatch{
|
||||
Key: key,
|
||||
Bits: bits,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BitsAllSetMatch) Filter() M {
|
||||
if len(b.Bits) == 0 {
|
||||
return M{}
|
||||
}
|
||||
return M{
|
||||
b.Key: M{"$bitsAllSet": b.Bits},
|
||||
}
|
||||
}
|
||||
|
||||
// BitsAllClearMatch
|
||||
type BitsAllClearMatch struct {
|
||||
Key string
|
||||
Bits []int
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
func NewBitsAllClearMatch(key string, bits []int) Matcher {
|
||||
return &BitsAllClearMatch{
|
||||
Key: key,
|
||||
Bits: bits,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BitsAllClearMatch) Filter() M {
|
||||
if len(b.Bits) == 0 {
|
||||
return M{}
|
||||
}
|
||||
return M{
|
||||
b.Key: M{"$bitsAllClear": b.Bits},
|
||||
}
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
|
||||
// UIDMatch
|
||||
type UIDMatch struct {
|
||||
UID *int64
|
||||
}
|
||||
|
||||
func (u *UIDMatch) New() Matcher {
|
||||
return NewAssignMatch("userId", u.UID)
|
||||
}
|
||||
|
||||
// UIDInMatch
|
||||
type UIDInMatch struct {
|
||||
UIDS []int64
|
||||
}
|
||||
|
||||
func (u *UIDInMatch) New() Matcher {
|
||||
return NewInMatch("userId", u.UIDS)
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
// UIDMatch
|
||||
type UserUIDMatch struct {
|
||||
UID *int64
|
||||
}
|
||||
|
||||
func (u *UserUIDMatch) New() Matcher {
|
||||
return NewAssignMatch("id", u.UID)
|
||||
}
|
||||
|
||||
// UIDInMatch
|
||||
type UserUIDInMatch struct {
|
||||
UIDS []int64
|
||||
}
|
||||
|
||||
func (u *UserUIDInMatch) New() Matcher {
|
||||
return NewInMatch("id", u.UIDS)
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
|
||||
// IDMatch
|
||||
type IDMatch struct {
|
||||
ID *primitive.ObjectID
|
||||
}
|
||||
|
||||
func (i *IDMatch) New() Matcher {
|
||||
return NewAssignMatch("_id", i.ID)
|
||||
}
|
||||
|
||||
// IDMatch
|
||||
type IDint64Match struct {
|
||||
Id *int64
|
||||
}
|
||||
|
||||
func (i *IDint64Match) New() Matcher {
|
||||
return NewAssignMatch("id", i.Id)
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
// IDInMatch
|
||||
type IDInMatch struct {
|
||||
IDs []primitive.ObjectID
|
||||
}
|
||||
|
||||
func (c *IDInMatch) New() Matcher {
|
||||
return NewInMatch("_id", c.IDs)
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
// CreatedAtGTEMatch
|
||||
type CreatedAtGTEMatch struct {
|
||||
GTE *time.Time
|
||||
}
|
||||
|
||||
func (c *CreatedAtGTEMatch) New() Matcher {
|
||||
return NewGTEMatch("createdAt", c.GTE)
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
// CreatedAtLTMatch
|
||||
type CreatedAtLTMatch struct {
|
||||
LT *time.Time
|
||||
}
|
||||
|
||||
func (c *CreatedAtLTMatch) New() Matcher {
|
||||
return NewLTMatch("createdAt", c.LT)
|
||||
}
|
||||
|
||||
// *******************************************//
|
||||
// CreatedAtGTEAndLTMatch
|
||||
type CreatedAtGTEAndLTMatch struct {
|
||||
GTE *time.Time
|
||||
LT *time.Time
|
||||
}
|
||||
|
||||
func (c *CreatedAtGTEAndLTMatch) New() Matcher {
|
||||
return NewGTEAndLTMatch("createdAt", c.GTE, c.LT)
|
||||
}
|
||||
|
||||
// // *******************************************//
|
||||
// type Int32Match struct {
|
||||
// key string //值
|
||||
// value *int32
|
||||
// }
|
||||
//
|
||||
// func (i *Int32Match) New() Matcher {
|
||||
// return NewAssignMatch("_id", i.ID)
|
||||
// }
|
||||
//
|
||||
// type Int32InMatch struct {
|
||||
// key string //值
|
||||
// values []int32
|
||||
// }
|
||||
//
|
||||
// func (c *Int32InMatch) New() Matcher {
|
||||
// return NewInMatch("_id", c.IDs)
|
||||
// }
|
||||
@@ -0,0 +1,20 @@
|
||||
package playletcache
|
||||
|
||||
import (
|
||||
"videoweb/pub/common/enum/redisconst"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/redis"
|
||||
)
|
||||
|
||||
// ClearHomeCache 清除app首页短剧缓存
|
||||
func ClearHomeCache() {
|
||||
if redis.Client() == nil {
|
||||
return
|
||||
}
|
||||
if err := redis.Del(redisconst.AppPlayletHomeCacheKey()); err != nil {
|
||||
log.Warn("清除app首页短剧缓存失败", log.E(err))
|
||||
}
|
||||
if err := redis.Client().Incr(redisconst.AppPlayletHomeCacheVersionKey()).Err(); err != nil {
|
||||
log.Warn("更新app首页短剧缓存版本失败", log.E(err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package pointers
|
||||
|
||||
import "time"
|
||||
|
||||
func Bool(v bool) *bool { return &v }
|
||||
func Int(v int) *int { return &v }
|
||||
func Int32(v int32) *int32 { return &v }
|
||||
func Int64(v int64) *int64 { return &v }
|
||||
func Uint32(v uint32) *uint32 { return &v }
|
||||
func Uint64(v uint64) *uint64 { return &v }
|
||||
func Float32(v float32) *float32 { return &v }
|
||||
func Float64(v float64) *float64 { return &v }
|
||||
func String(v string) *string { return &v }
|
||||
func Time(v time.Time) *time.Time { return &v }
|
||||
func StringSlice(v []string) *[]string { return &v }
|
||||
func Int64Slice(v []int64) *[]int64 { return &v }
|
||||
|
||||
func GetBool(v *bool) bool {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return false
|
||||
}
|
||||
func GetInt(v *int) int {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
func GetInt32(v *int32) int32 {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
func GetInt64(v *int64) int64 {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
func GetInt64Slice(v *[]int64) []int64 {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return []int64{}
|
||||
}
|
||||
func GetUint32(v *uint32) uint32 {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
func GetUint64(v *uint64) uint64 {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
func GetFloat32(v *float32) float32 {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
func GetFloat64(v *float64) float64 {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
func GetString(v *string) string {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func GetStringSlice(v *[]string) []string {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package random
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
const letterBytesAll = "0123456789abcdefghijklmnopqrstvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().Unix())
|
||||
}
|
||||
|
||||
func Int(n int) int {
|
||||
return rand.Intn(n)
|
||||
}
|
||||
|
||||
func SliceRand(ids []int64) (result []int64) {
|
||||
result = make([]int64, 0)
|
||||
if len(ids) <= 0 {
|
||||
return
|
||||
}
|
||||
copy := make([]int64, 0)
|
||||
for _, id := range ids {
|
||||
copy = append(copy, id)
|
||||
}
|
||||
num := Int(len(copy)) + 1
|
||||
for i := 0; i < num; i++ {
|
||||
index := Int(len(copy))
|
||||
result = append(result, copy[index])
|
||||
copy = append(copy[:index], copy[index+1:]...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func RandDigits(n uint) string {
|
||||
s := ""
|
||||
for n != 0 {
|
||||
s += fmt.Sprintf("%d", rand.Intn(10))
|
||||
n--
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// RandStrRandrAll 生成指定位数的随机数
|
||||
func RandStrRandrAll(n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytesAll[rand.Int63()%int64(len(letterBytesAll))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package recomapi
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
FBT_Star string = "star"
|
||||
FBT_Like string = "like"
|
||||
FBT_Read string = "read"
|
||||
)
|
||||
|
||||
type RSItemInfo struct {
|
||||
Comment string `json:"Comment"`
|
||||
ItemId string `json:"ItemId"`
|
||||
Labels []string `json:"Labels"`
|
||||
Timestamp time.Time `json:"Timestamp"`
|
||||
}
|
||||
|
||||
type RSItemInfoList []*RSItemInfo
|
||||
|
||||
type RSUserInfo struct {
|
||||
Comment string `json:"Comment"`
|
||||
UserId string `json:"UserId"`
|
||||
Labels []string `json:"Labels"`
|
||||
Subscribe []string `json:"Subscribe"`
|
||||
Timestamp time.Time `json:"Timestamp"`
|
||||
}
|
||||
|
||||
type RSUserInfoList []*RSUserInfo
|
||||
|
||||
type APIResult struct {
|
||||
RowAffected int64 `json:"RowAffected"`
|
||||
}
|
||||
|
||||
type FeedbackInfo struct {
|
||||
Comment string `json:"Comment"`
|
||||
FeedbackType string `json:"FeedbackType"`
|
||||
ItemId string `json:"ItemId"`
|
||||
Timestamp time.Time `json:"Timestamp"`
|
||||
UserId string `json:"UserId"`
|
||||
}
|
||||
|
||||
type FeedbackInfoList []*FeedbackInfo
|
||||
@@ -0,0 +1,45 @@
|
||||
package recomapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
)
|
||||
|
||||
// AddOneFeedback 添加一个推荐反馈
|
||||
func AddOneFeedback(url string, key string,
|
||||
userId int64, mediaId int64, fbType string, comment string) (
|
||||
int64, *srvdef.SrvErr) {
|
||||
feedBack := &FeedbackInfo{
|
||||
UserId: strconv.FormatInt(userId, 10),
|
||||
ItemId: strconv.FormatInt(mediaId, 10),
|
||||
FeedbackType: fbType,
|
||||
Comment: comment,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
return AddManyFeedbacks(url, key, FeedbackInfoList{feedBack})
|
||||
}
|
||||
|
||||
func AddManyFeedbacks(url string, key string, feedbacks FeedbackInfoList) (
|
||||
int64, *srvdef.SrvErr) {
|
||||
reqUrl := url + "/api/feedback"
|
||||
resp := &APIResult{}
|
||||
reqBody, _ := json.Marshal(feedbacks)
|
||||
header := map[string]string{"X-API-Key": key}
|
||||
code, err := utils.POSTJsonWithJResp(resp, reqUrl, header, reqBody)
|
||||
if err != nil {
|
||||
log.Error("recomapi:AddManyFeedbacks POSTJsonWithJResp failed",
|
||||
log.Any("reqUrl", reqUrl), log.E(err))
|
||||
return 0, srvdef.New(srvdef.Err, "", "")
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
log.Error("recomapi:AddManyFeedbacks POSTJsonWithJResp code error.",
|
||||
log.Any("reqUrl", reqUrl), log.Any("code", code))
|
||||
return 0, srvdef.New(srvdef.Err, "", "")
|
||||
}
|
||||
return resp.RowAffected, nil
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package recomapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
)
|
||||
|
||||
func InsertOneItem(url string, key string, itemInfo *RSItemInfo) *srvdef.SrvErr {
|
||||
reqUrl := url + "/api/item"
|
||||
resp := &APIResult{}
|
||||
reqBody, _ := json.Marshal(itemInfo)
|
||||
header := map[string]string{"X-API-Key": key}
|
||||
code, err := utils.POSTJsonWithJResp(resp, reqUrl, header, reqBody)
|
||||
if err != nil {
|
||||
log.Error("recomapi:InsertOneItem POSTJsonWithJResp failed",
|
||||
log.Any("reqUrl", reqUrl), log.E(err))
|
||||
return srvdef.New(srvdef.Err, "", "")
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
log.Error("recomapi:InsertOneItem POSTJsonWithJResp code error.",
|
||||
log.Any("reqUrl", reqUrl), log.Any("code", code))
|
||||
return srvdef.New(srvdef.Err, "", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func InsertManyItems(url string, key string, itemInfos RSItemInfoList) *srvdef.SrvErr {
|
||||
reqUrl := url + "/api/items"
|
||||
reqBody, _ := json.Marshal(itemInfos)
|
||||
header := map[string]string{"X-API-Key": key}
|
||||
code, err := utils.POSTJsonWithJResp(nil, reqUrl, header, reqBody)
|
||||
if err != nil {
|
||||
log.Error("recomapi:InsertManyItems POSTJsonWithJResp failed",
|
||||
log.Any("reqUrl", reqUrl), log.E(err))
|
||||
return srvdef.New(srvdef.Err, "", "")
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
log.Error("recomapi:InsertManyItems POSTJsonWithJResp code error.",
|
||||
log.Any("reqUrl", reqUrl), log.Any("code", code))
|
||||
return srvdef.New(srvdef.Err, "", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetOneItem(url string, key string, itemId string) (*RSItemInfo, *srvdef.SrvErr) {
|
||||
reqUrl := fmt.Sprintf("%s/api/item/%s", url, itemId)
|
||||
resp := &RSItemInfo{}
|
||||
header := map[string]string{"X-API-Key": key}
|
||||
code, err := utils.GetWithJResp(resp, reqUrl, header, nil)
|
||||
if err != nil {
|
||||
log.Error("recomapi:GetOneItem GetWithJResp failed",
|
||||
log.Any("reqUrl", reqUrl), log.E(err))
|
||||
return nil, srvdef.New(srvdef.Err, "", "")
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
log.Error("recomapi:GetOneItem GetWithJResp code error.",
|
||||
log.Any("reqUrl", reqUrl), log.Any("code", code))
|
||||
return nil, srvdef.New(srvdef.Err, "", "")
|
||||
}
|
||||
log.Info("Test", log.Any("resp", resp))
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func GetManyItems(url string, key string, cursor string, size int64) (RSItemInfoList, string, *srvdef.SrvErr) {
|
||||
reqUrl := fmt.Sprintf("%s/api/items?n=%d&cursor=%s", url, size, cursor)
|
||||
type Resp struct {
|
||||
Cursor string `json:"Cursor"`
|
||||
Items RSItemInfoList `json:"Items"`
|
||||
}
|
||||
var resp Resp
|
||||
header := map[string]string{"X-API-Key": key}
|
||||
code, err := utils.GetWithJResp(&resp, reqUrl, header, nil)
|
||||
if err != nil {
|
||||
log.Error("recomapi:GetOneItem GetWithJResp failed",
|
||||
log.Any("reqUrl", reqUrl), log.E(err))
|
||||
return nil, "", srvdef.New(srvdef.Err, "", "")
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
log.Error("recomapi:GetOneItem GetWithJResp code error.",
|
||||
log.Any("reqUrl", reqUrl), log.Any("code", code))
|
||||
return nil, "", srvdef.New(srvdef.Err, "", "")
|
||||
}
|
||||
return resp.Items, resp.Cursor, nil
|
||||
}
|
||||
|
||||
func DeleteOneItem(url string, key string, itemId string) (int64, *srvdef.SrvErr) {
|
||||
reqUrl := fmt.Sprintf("%s/api/item/%s", url, itemId)
|
||||
resp := &APIResult{}
|
||||
header := map[string]string{"X-API-Key": key}
|
||||
code, err := utils.DeleteWithJResp(resp, reqUrl, header, nil)
|
||||
if err != nil {
|
||||
log.Error("recomapi:DeleteOneItem DeleteWithJResp failed",
|
||||
log.Any("reqUrl", reqUrl), log.E(err))
|
||||
return 0, srvdef.New(srvdef.Err, "", "")
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
log.Error("recomapi:DeleteOneItem DeleteWithJResp code error.",
|
||||
log.Any("reqUrl", reqUrl), log.Any("code", code))
|
||||
return 0, srvdef.New(srvdef.Err, "", "")
|
||||
}
|
||||
return resp.RowAffected, nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package recomapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
)
|
||||
|
||||
func GetRecomItems(url string, key string, userId int64, writeBack string, size int64) (
|
||||
[]int64, *srvdef.SrvErr) {
|
||||
reqUrl := fmt.Sprintf("%s/api/recommend/%d?n=%d&write-back=%s", url, userId, size, writeBack)
|
||||
var resp []string
|
||||
header := map[string]string{"X-API-Key": key}
|
||||
code, err := utils.GetWithJResp(&resp, reqUrl, header, nil)
|
||||
if err != nil {
|
||||
log.Error("recomapi:GetRecomItems GetWithJResp failed",
|
||||
log.Any("reqUrl", reqUrl), log.E(err))
|
||||
return nil, srvdef.New(srvdef.Err, "", "")
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
log.Error("recomapi:GetRecomItems GetWithJResp code error.",
|
||||
log.Any("reqUrl", reqUrl), log.Any("code", code))
|
||||
return nil, srvdef.New(srvdef.Err, "", "")
|
||||
}
|
||||
itemIds := make([]int64, 0)
|
||||
for _, v := range resp {
|
||||
id, err := strconv.ParseInt(v, 10, 64)
|
||||
if err == nil {
|
||||
itemIds = append(itemIds, id)
|
||||
}
|
||||
}
|
||||
return itemIds, nil
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package recomapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
)
|
||||
|
||||
var (
|
||||
testUrl = "http://139.177.187.219:8087"
|
||||
testKey = ""
|
||||
)
|
||||
|
||||
func Test_InsertOneItem(t *testing.T) {
|
||||
itemInfo := &RSItemInfo{
|
||||
Comment: "Comment1",
|
||||
ItemId: "10004",
|
||||
Labels: []string{"lable1", "lable2", "lable3"},
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
serr := InsertOneItem(testUrl, testKey, itemInfo)
|
||||
fmt.Printf("\nTest_pingReq %s %v\n", itemInfo, serr)
|
||||
}
|
||||
|
||||
func Test_InsertManyItems(t *testing.T) {
|
||||
list := make(RSItemInfoList, 0)
|
||||
itemInfo1 := &RSItemInfo{
|
||||
Comment: "Comment1",
|
||||
ItemId: "10005",
|
||||
Labels: []string{"lable1", "lable2", "lable3"},
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
list = append(list, itemInfo1)
|
||||
itemInfo2 := &RSItemInfo{
|
||||
Comment: "Comment1",
|
||||
ItemId: "10006",
|
||||
Labels: []string{"lable1", "lable2", "lable3"},
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
list = append(list, itemInfo2)
|
||||
serr := InsertManyItems(testUrl, testKey, list)
|
||||
fmt.Printf("\nTest_InsertManyItems %v\n", serr)
|
||||
}
|
||||
|
||||
func Test_GetOneItem(t *testing.T) {
|
||||
itemInfo, serr := GetOneItem(testUrl, testKey, "10004")
|
||||
fmt.Printf("\nTest_GetOneItem %v %v\n", itemInfo, serr)
|
||||
}
|
||||
|
||||
func Test_GetManyItem(t *testing.T) {
|
||||
itemInfos, cursor, serr := GetManyItems(testUrl, testKey, "10003", 2)
|
||||
log.Info("Test_GetManyItem", log.Any("itemInfos", itemInfos),
|
||||
log.Any("cursor", cursor), log.E(serr))
|
||||
//fmt.Printf("\nTest_GetOneItem %v %v\n", itemInfos, serr)
|
||||
}
|
||||
|
||||
func Test_DeleteOneItem(t *testing.T) {
|
||||
count, serr := DeleteOneItem(testUrl, testKey, "10004")
|
||||
log.Info("Test_DeleteOneItem", log.Any("count", count), log.E(serr))
|
||||
//fmt.Printf("\nTest_GetOneItem %v %v\n", itemInfos, serr)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
//feedback
|
||||
|
||||
func Test_AddOneFeedback(t *testing.T) {
|
||||
count, serr := AddOneFeedback(testUrl, testKey, 18531, 16346, FBT_Star, "feedback1")
|
||||
fmt.Printf("\nTest_pingReq %d %v\n", count, serr)
|
||||
}
|
||||
|
||||
// /reccommend
|
||||
func Test_GetManyRecomItems(t *testing.T) {
|
||||
itemIds, serr := GetRecomItems(testUrl, testKey, 18531, "test", 10)
|
||||
log.Info("Test_GetManyRecomItems", log.Any("itemIds", itemIds), log.E(serr))
|
||||
//fmt.Printf("\nTest_GetOneItem %v %v\n", itemInfos, serr)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package recomapi
|
||||
@@ -0,0 +1,26 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// type FunHandle func(*gin.Context) (proto.Message, string, string, pkt_pub.EType)
|
||||
|
||||
type ApiOpType int
|
||||
|
||||
const (
|
||||
ApiGet ApiOpType = 1
|
||||
ApiPost ApiOpType = 2
|
||||
ApiPut ApiOpType = 3
|
||||
)
|
||||
|
||||
// API接口设置
|
||||
type ApiSet struct {
|
||||
Path string // 路径
|
||||
OpType ApiOpType // 操作类型
|
||||
Fun gin.HandlerFunc // 函数句柄
|
||||
IsWhite bool // 是否白名单
|
||||
Desc string // 描述
|
||||
}
|
||||
|
||||
var ApiSetMaps = map[string]ApiSet{}
|
||||
@@ -0,0 +1,11 @@
|
||||
package response
|
||||
|
||||
// BasePageResp
|
||||
//
|
||||
// 分页数据返回结构体
|
||||
type BasePageResp struct {
|
||||
TotalCount int64 `json:"totalCount"` // 总共数量
|
||||
PageSize int `json:"pageSize"` // 每页数
|
||||
PageNum int `json:"pageNum"` // 页数
|
||||
List interface{} `json:"list"` // 集合列表
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package slugx
|
||||
|
||||
import (
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/utils"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// Exists 判断 slug 是否已被占用
|
||||
type Exists func(slug string) bool
|
||||
|
||||
// Ensure 新增时保证 slug:有值则校验唯一且不能全数字,无值则按 source 自动生成
|
||||
func Ensure(slug *string, source string, exists Exists) *srvdef.SrvErr {
|
||||
if slug == nil {
|
||||
return srvdef.New(srvdef.ErrParamError, "Slug不能为空", "Slug不能为空")
|
||||
}
|
||||
*slug = utils.SanitizeSlug(*slug)
|
||||
if *slug != "" {
|
||||
if utils.IsSlugAllDigits(*slug) {
|
||||
return srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
|
||||
}
|
||||
if exists(*slug) {
|
||||
return srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
*slug = utils.MakeUniqueSlug(source, exists)
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrepareOnUpdate 更新时处理 slug:手动传入则校验,历史为空则自动生成
|
||||
func PrepareOnUpdate(current string, reqSlug *string, source string, exists Exists) (string, bool, *srvdef.SrvErr) {
|
||||
slug := current
|
||||
if reqSlug != nil {
|
||||
slug = utils.SanitizeSlug(*reqSlug)
|
||||
}
|
||||
if slug != "" {
|
||||
if reqSlug != nil {
|
||||
if utils.IsSlugAllDigits(slug) {
|
||||
return "", false, srvdef.New(srvdef.ErrParamError, "Slug不能全是数字", "Slug不能全是数字")
|
||||
}
|
||||
if exists(slug) {
|
||||
return "", false, srvdef.New(srvdef.ErrDbInputExist, "Slug已存在", "Slug已存在")
|
||||
}
|
||||
return slug, true, nil
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
return utils.MakeUniqueSlug(source, exists), true, nil
|
||||
}
|
||||
|
||||
// Filter 构造按 slug 查重条件,excludeId>0 时排除自身
|
||||
func Filter(slugField, idField, slug string, excludeId int64) bson.M {
|
||||
filter := bson.M{slugField: slug}
|
||||
if excludeId > 0 {
|
||||
filter[idField] = bson.M{"$ne": excludeId}
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
// EmptyFilter 查询 slug 为空或不存在的历史数据
|
||||
func EmptyFilter() bson.M {
|
||||
return bson.M{
|
||||
"$or": []bson.M{
|
||||
{"slug": ""},
|
||||
{"slug": bson.M{"$exists": false}},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package fengniao
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"videoweb/pub/common/httputil"
|
||||
"videoweb/pub/common/log"
|
||||
)
|
||||
|
||||
type requestBody struct {
|
||||
MercId string `json:"mercId"`
|
||||
Secret string `json:"secret"`
|
||||
TemplateId string `json:"templateId"`
|
||||
Code string `json:"smsCode"`
|
||||
Mobile string `json:"mobile"`
|
||||
SignType string `json:"signType"` // 固定sha1
|
||||
Sign string `json:"sign"` // 签名
|
||||
}
|
||||
|
||||
type msg struct {
|
||||
Status string `json:"status"`
|
||||
TaskId string `json:"taskId"`
|
||||
}
|
||||
|
||||
type responseBody struct {
|
||||
Code int `json:"code"`
|
||||
Msg msg `json:"msg"`
|
||||
Err string `json:"err"`
|
||||
}
|
||||
|
||||
type FengNiao struct {
|
||||
Url string
|
||||
MercId string
|
||||
Secret string
|
||||
Tmpl string
|
||||
}
|
||||
|
||||
// Send 发送, 目前蜂鸟貌似只支持数字
|
||||
func (f *FengNiao) Send(mobile string, code string) error {
|
||||
content := fmt.Sprintf(f.Tmpl, code)
|
||||
data := fmt.Sprintf("mercId=%s&templateId=%s&smsCode=%s&mobile=%s&secret=%s", f.MercId, f.Tmpl, code, mobile, f.Secret)
|
||||
|
||||
sha := sha1.New()
|
||||
_, err := sha.Write([]byte(data))
|
||||
if err != nil {
|
||||
log.Error("fengniao send error", log.Any("mobile", mobile), log.Any("content", content), log.E(err))
|
||||
return err
|
||||
}
|
||||
|
||||
body := &requestBody{
|
||||
MercId: f.MercId,
|
||||
Secret: f.Secret,
|
||||
TemplateId: f.Tmpl,
|
||||
Code: code,
|
||||
Mobile: mobile,
|
||||
SignType: "sha1",
|
||||
Sign: fmt.Sprintf("%x", sha.Sum(nil)),
|
||||
}
|
||||
rp := responseBody{}
|
||||
statusCode, err := httputil.Client().POSTJsonWithJResp(&rp, f.Url, nil, body)
|
||||
if err != nil {
|
||||
log.Error("fengniao POSTJsonWithResp err", log.E(err))
|
||||
return err
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
log.Error("fengniao status code not 200")
|
||||
return errors.New("fengniao statusCode not 200")
|
||||
}
|
||||
if rp.Code != 200 {
|
||||
log.Error("fengniao code not 200", log.Any("errdesc", rp.Err))
|
||||
err = errors.New(rp.Err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package sms
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"videoweb/pub/common/sms/fengniao"
|
||||
)
|
||||
|
||||
type Provider = int
|
||||
|
||||
const (
|
||||
tmplID1 = "6029f19a99fd8b404062e4d2" // 大陆
|
||||
tmplID2 = "5f8412c39396d16f787e561b" // 非大陆
|
||||
)
|
||||
|
||||
const (
|
||||
FengNiao Provider = 1 //蜂鸟
|
||||
YunPian Provider = 2 //云片
|
||||
)
|
||||
|
||||
func sendByFengNiao(mobile string, code string) error {
|
||||
var tmplID string
|
||||
if strings.HasPrefix(mobile, "+86") {
|
||||
tmplID = tmplID1
|
||||
return (&fengniao.FengNiao{
|
||||
Url: "https://t9x6k.cc/api/sms/sendSms",
|
||||
MercId: "10177",
|
||||
Secret: "2dfb7ec44ed59ee3c1fe8dc533384e16",
|
||||
Tmpl: tmplID,
|
||||
}).Send(mobile, code)
|
||||
} else {
|
||||
tmplID = tmplID2
|
||||
return (&fengniao.FengNiao{
|
||||
Url: "https://t9x6k.cc/api/sms/sendSms",
|
||||
MercId: "10228",
|
||||
Secret: "3ea58a27a162dad4ed3e18c076648742",
|
||||
Tmpl: tmplID,
|
||||
}).Send(mobile, code)
|
||||
}
|
||||
// return
|
||||
}
|
||||
|
||||
// Send 发送短信
|
||||
func Send(mobile string, code string) error {
|
||||
return sendByFengNiao(mobile, code)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package sms
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSend(t *testing.T) {
|
||||
err := Send("15008279990", "654321")
|
||||
t.Log(err)
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
package srvdef
|
||||
|
||||
import "fmt"
|
||||
|
||||
type CodeType int
|
||||
|
||||
const NotFind = "mongo: no documents in result"
|
||||
|
||||
// 错误级别
|
||||
const (
|
||||
ParamLevel = 1000 // 参数级
|
||||
StoreLevel = 3000 // 存储级
|
||||
DBLevel = 5000 // 数据库
|
||||
ServerLevel = 4000 // 服务级
|
||||
BusinessLevel = 6000 // 业务逻辑级
|
||||
NewErrorLevelV2 = 7000 // 新版错误
|
||||
NewErrorLevelV3 = 8000 // 新版错误
|
||||
)
|
||||
|
||||
// 通用code
|
||||
const (
|
||||
Success CodeType = 200 //Success 通用成功
|
||||
Failure CodeType = 400 //Failure 通用失败
|
||||
NotFound CodeType = 404
|
||||
)
|
||||
|
||||
func (c CodeType) Msg() string {
|
||||
msg, ok := codeAndMsg[c]
|
||||
if ok {
|
||||
return msg
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c CodeType) Tip() string {
|
||||
tip, ok := codeAndTip[c]
|
||||
if ok {
|
||||
return tip
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c CodeType) Error() string {
|
||||
return fmt.Sprintf("code=%d,msg=%v,tip=%v", c, c.Msg(), c.Tip())
|
||||
}
|
||||
|
||||
type ErrInfoer interface {
|
||||
GetMsg() string
|
||||
GetTip() string
|
||||
GetCode() CodeType
|
||||
}
|
||||
|
||||
// GetMsg alias of Msg()
|
||||
func (c CodeType) GetMsg() string { return c.Msg() }
|
||||
|
||||
// GetTip alias of Tip()
|
||||
func (c CodeType) GetTip() string { return c.Tip() }
|
||||
func (c CodeType) GetCode() CodeType { return c }
|
||||
|
||||
const (
|
||||
InvalidToken CodeType = ParamLevel + iota // 框架错误
|
||||
ErrParamError // 参数错误
|
||||
NoToken // 无token
|
||||
RoleErr // 角色错误
|
||||
ReLogin // 重复登陆
|
||||
UploadToFileSrvFailed // 文件上传失败
|
||||
ErrCaptcha // 验证码为空
|
||||
ErrCaptchaInvalid // 验证码非法
|
||||
ErrCaptchaGet // 验证码获取失败
|
||||
ErrCaptchaResend // 验证码重复发送
|
||||
ErrCaptchaSendFail // 验证码发送失败
|
||||
InvalidateToken // 非法token
|
||||
NoUA // 没有UA信息
|
||||
UploadFile // 文件上传失败
|
||||
AlreadyRegisered // 已经组册
|
||||
TokenRemoved // 你已经挤下线,请重新登陆
|
||||
PostCommentNotUse //发送失败,服务器错误,请稍后再试
|
||||
DBTransaction // 事务错误
|
||||
ErrCrawlerUrl // 爬虫URL错误
|
||||
ErrCrawler // 爬取数据错误
|
||||
)
|
||||
|
||||
const (
|
||||
DBQuery CodeType = StoreLevel + iota
|
||||
DBInsert
|
||||
DBUpdate
|
||||
DBDelete
|
||||
RedisSet // 设置redis错误
|
||||
RedisGet // 获取redis值错误
|
||||
RedisDel // 删除redis值错误
|
||||
EtcdGet // 获取etcd错误
|
||||
MongoDBQueryErr // 数据库查询失败
|
||||
)
|
||||
|
||||
const (
|
||||
ServerError CodeType = ServerLevel + iota // 用户相关错误
|
||||
)
|
||||
|
||||
const (
|
||||
_ CodeType = DBLevel + iota
|
||||
ErrDbQueryError //ErrDbQueryError 数据库查询错误 5001
|
||||
ErrDbInsertError //ErrDbInsertError 数据库插入错误 5002
|
||||
ErrDbUpdateError //ErrDbUpdateError 数据库修改错误 5003
|
||||
ErrDbDeleteError //ErrDbDeleteError 数据库删除错误 5004
|
||||
ErrDbInputExist //ErrDbInputExist 数据库输入值已经存在 5005
|
||||
ErrRdsOperError //ErrRdsOperError redis操作错误 5006
|
||||
ErrElsOperError //ErrElsOperError redis操作错误 5007
|
||||
ErrTransactionsError //ErrTransactionsError 数据库事务操作错误
|
||||
ErrNoData //无数据
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrESSearchError ES搜索失败,单独用于搜索接口,避免和通用ES操作错误混淆。
|
||||
ErrESSearchError CodeType = DBLevel + 100
|
||||
)
|
||||
const (
|
||||
ErrUserPermission CodeType = BusinessLevel + iota // 用户权限
|
||||
ErrUserNotExsit // 用户不存在
|
||||
ErrUserTypeNotUP // 用户不是UP主
|
||||
AccountIsNotExists // 账户不存在
|
||||
AccountErrAccessForbid // 账户禁止登陆
|
||||
AccountErrPasswordError // 账户密码错误
|
||||
GenerateTokenFailed // 生成二维码错误
|
||||
ErrCardNotExists // 没有会员卡信息
|
||||
ErrMadouCoinNotExists // 没有钻石信息
|
||||
ErrMediaNotExsit // 视频不存在
|
||||
ErrTopicInfoNotExsit // 视频专题不存在
|
||||
ErrData // 数据错误
|
||||
Err // 一般错误
|
||||
ErrRchgOrderNotExist // 充值订单不存在
|
||||
ErrSubmitRchgOrder // 发起充值订单错误
|
||||
InvalidRequestReplayAttack // 非法请求
|
||||
ErrBuyVIPCardFail // 购买会员卡失败
|
||||
ErrOperationTooFrequently // 用户操作太频繁
|
||||
ErrMediaSyncServer // 视频同步服务器错误
|
||||
ErrUserType // 用户无改权限
|
||||
DownloadM3u8File // 文件服务器访问失败
|
||||
MediaSyncing // 视频同步中
|
||||
MediaConnectServerFaild // 媒资库链接失败
|
||||
AtressNotExist // 演员不存在
|
||||
ErrAnnouncementNotExsit // 公告不存在
|
||||
ErrContactDetailsNotExsit // 联系方式不存在
|
||||
ErrPostCommentNotExsit // 评论不存在
|
||||
HistoryNotExist // 记录不存在
|
||||
ErrHotsearchNotExsit // 热搜不存在
|
||||
ActivityDoesNotExist // 活动不存在
|
||||
UserWatchNumNotEnough // 免费观看次数不足
|
||||
MediaIsVip // 用户不是会员
|
||||
MediaIsNeedPay // 视频需要付费
|
||||
RepeatBuy // 重复购买
|
||||
MovieTicketsNotExits // 观影券不足
|
||||
MediaIsNotNeedPay // 视频不需要支付
|
||||
PostNoNeed2Pay // 帖子不需要支付
|
||||
SystemMsgNotExist // 消息不存在
|
||||
ForbidlogNotExist // 封禁消息不存在
|
||||
AttachmentDoesNotExist // 导出记录
|
||||
UserDownloadNotEnough // 下载次数不足
|
||||
UserDownloadNeedPay // 付费视频需要先购买
|
||||
RedeemCodeNotExists // 兑换码不存在
|
||||
RedeemCodeTimesUsedUp // 兑换码次数使用完了
|
||||
RedeemCodeAlreadyUsed // 兑换码你已经使用过兑换码了
|
||||
RedeemCodeRecordNotExists // 兑换记录不存在
|
||||
MediaIsNotNeedPreSale // 视频不支持预售
|
||||
MediaIsPreSale // 预售视频
|
||||
QRcodeError // 二维码错误
|
||||
ModifyUserNameError // 修改
|
||||
ErrSkdRecordNotExsit // 任务不存在
|
||||
ErrContainIllegalWord // 包含非法字符
|
||||
DatingOrderNotExist // 约会购买记录不存在
|
||||
DatingHasBuy // 重复购买
|
||||
DatingCollectNotExist // 收藏记录不存在
|
||||
DatingBossNotExist // 经纪人不存在
|
||||
DatingReportNotExist // 验证报告不存在
|
||||
DatingReportDouble // 重复验证
|
||||
DatingAlreadyComplaint // 已经提交举报
|
||||
SortNotExist // 视频排序不存在
|
||||
MessageDialogNotExist // 私信对话框不存在
|
||||
VideoUploadFaild // 文件上传失败
|
||||
PosoStatusFaild // 帖子状态错误
|
||||
WtdrFrequencyLimits // 提现频率限制
|
||||
WtdrErroTip1 // 提现不是100整数倍
|
||||
WtdrErroTip2 // 赠送3元金额要满500才能提现
|
||||
WtdrErroTip3 // 请填写银行名字或升级App版本
|
||||
GameWtdrErroTip7 // 银行卡必须与您以前的提现卡一致,如需更改请联系客服
|
||||
ProxyUserNotExist // 代理不存在
|
||||
ErrPostReportNotExsit // 举报信息不存在
|
||||
FileUploadDouble // 文件重复上传
|
||||
UserRepeatSubmitRchgOrder // 未支付订单太多,有问题请联系客服
|
||||
Limiting // 限流
|
||||
ErrIpForbid // Ip禁止
|
||||
DeviceIdInvalid // 设备id无效
|
||||
UserScoreNotEnough // 用户积分不足
|
||||
UserSocreLVLimit // 用户积分等级限制
|
||||
UploaderNeedActive // Up主没有激活
|
||||
UploaderNeedBind // 后台用户没有up主绑定
|
||||
PostCollectionNotExists // 图文专辑不存在
|
||||
MediaCollectionNotExist // 该合集或选集不存在
|
||||
ContestNotExists // 该比赛不存在
|
||||
CannotJoinContest // 该比赛无法参加
|
||||
ContestIsVip // 该比赛只限VIP参加
|
||||
AppInfoNotExist // 该App不存在
|
||||
MediaNotVerified // 视频未上架
|
||||
PCCategoryUnmatch // 专辑的分类和作品的分类不一致
|
||||
ErrMediaRecommendNotExsit // 推荐不存在
|
||||
DatingBuyHistoryNotExist // 购买记录不存在
|
||||
DatingWatchHistoryNotExist // 观看记录不存在
|
||||
CollectIsExist // 收藏已经存在
|
||||
CollectNotExist // 收藏不存在
|
||||
DiscussRankDoesNotExist // 话题视频排名
|
||||
DiscussActivityDoesNotExist // 话题活动不存在
|
||||
UpDayStatDoesNotExist // 创作者收益
|
||||
DiscussObjectRankDoesNotExist // 话题视频
|
||||
DiscussCategoryDoesNotExist // 话题分类不存在
|
||||
ErrCareNotExist // 取消关注对象不存在
|
||||
ErrUpMasterTaskNotCompleted // up主申请任务未完成
|
||||
HGameNotExist // 游戏不存在
|
||||
HGameIsVip // 游戏需要会员
|
||||
HGameIsNeedPay // 游戏需要付费
|
||||
ErrApplyUpIsVip // UP主申请需要vip
|
||||
UserLevelCardNotExist // 用户等级卡不存在
|
||||
UserCheckinSetupNotExist // 签到配置不存在
|
||||
PriceMediaNotCancel // 钻石视频不能下架
|
||||
PriceMediaHasBuy // 钻石视频已经被购买
|
||||
GiftWeekIsExist // 礼包已领取
|
||||
UserLevelPointTooLow // 用户等级积分太低
|
||||
UserGiftHaveReceive // 任务奖励已经领取
|
||||
UserGiftExistNoReceive // 没有领取资格
|
||||
DatingUpMasterNotExist // up主不存在
|
||||
UserIsUpMaster // 用户已经是up主
|
||||
UserUpCerting // up主认证中
|
||||
MissionSetupNotExist // 任务配置不存在
|
||||
UserIsUpMasterBind // 用户已被up主绑定
|
||||
ErrCareIsExist // 不可重复关注
|
||||
ObjectDayStatDoesNotExist // 作品统计记录不存在
|
||||
ObjectDayViewIncStatDoesNotExist // 作品增量记录不存在
|
||||
UserNotEnoughFreeLotteryCount // 免费抽奖次数不足
|
||||
PrizeOutOfStore // 奖品库存不足
|
||||
MediaSubscribedNotNeedPay // 订阅的视频不需要支付
|
||||
DatingComplaintNotExist // 举报帖子不存在
|
||||
DatingUserNotExist // 楼凤不存在
|
||||
UserDauInfoNotExist // 搜寻日期没有资料
|
||||
ParseJsonError // 数据解析错误
|
||||
AlreadyRefund // 已退款
|
||||
AiCountTypeNotSupport // ai内容不支持
|
||||
TodayPontNotExist // 积分记录不存在
|
||||
RedeemPrizeHistoryNotExist // 奖品兑换记录不存在
|
||||
PrizeNotExist // 奖品不存在
|
||||
ErrUserPointsNotExist // 积分不足
|
||||
AIFreeCountNotEnough // ai免费次使用数不足
|
||||
AIFreeCountNOtSupportRefund // AI免费使用次数不支持退款
|
||||
CrowdfundStatusFail // 众筹女神的状态不在可修改的范围内
|
||||
CrowdfundNotOpenPrizeFail // 众筹女神的订单开奖失败
|
||||
CrowdfundPrizeNumberError // 众筹女神的开奖号码不对
|
||||
RedeemFail // 积分兑换失败
|
||||
CrowdfundNotBuy // 当前女神状态不能购买
|
||||
CrowdfundNumberHaveBuy // 这个号码已经被其他人购买了
|
||||
UserCheckinDisabled // 用户签到功能已关闭
|
||||
PrizeTypeUnsupported // 奖品类型不支持自动发放
|
||||
CouponExpired // 卡券已过期
|
||||
CouponUsed // 卡券已被使用
|
||||
CouponNotFound // 没有卡券信息
|
||||
JumpTabNotExist // 首页跳转tab不存在
|
||||
CouponCanNotUse // 卡券无法使用
|
||||
SysConfigError //系统配置错误
|
||||
ComicsCateNotExist // 漫画分类不存在
|
||||
ComicsTagNotExist // 漫画标签不存在
|
||||
ComicsTopicNotExist // 漫画主题不存在
|
||||
ErrComicsChapterNotExist // 漫画章节不存在
|
||||
ComicsNotExist // 漫画不存在
|
||||
ComicsCVNotExist // 漫画CV不存在
|
||||
ComicsMediaNotExist // 漫画视频不存在
|
||||
ComicsRankNotExist // 排行榜不存在
|
||||
VomicsTopicNotExist // 有声漫不存在
|
||||
ComicsIsBlack // 漫画在黑名单中
|
||||
CommentTextIsEmpty // 评论内容为空
|
||||
ComicsIsVip // 漫画需要会员
|
||||
ComicsIsNeedPay // 漫画需要付费
|
||||
ComicsChapterExist // 漫画章节已存在
|
||||
ObjectNotExist // 数据不存在
|
||||
GameWtdrErroTip3 // 请填写银行名字或升级App版本
|
||||
UserDoTaskErr // 用户任务出错
|
||||
GetPrizeFail // 获取奖品失败
|
||||
NovelIsNeedPay // 小说需要付费
|
||||
NovelIsVip // 小说需要vip
|
||||
LotteryActivityNotEnable // 抽奖活动没有启用
|
||||
CurrTimeNotActivity // 当前时间没有活动
|
||||
AIOrderStripsCountLimit // 今天AI脱衣提交次数达到上限
|
||||
NotWtdrChannel // 没有匹配到提现通过
|
||||
ExcelExportErr // 文件导出错误
|
||||
ComicsNotJoinActivity // 这本漫画没有参与活动
|
||||
NoGiftToReceive // 没有奖品可领取
|
||||
UserRepeatDrawDiscountCard // 用户重复翻牌
|
||||
NoUseDiscountCard // 不可使用折扣卡
|
||||
AIOrderNotDel // 未完成的订单不能删除
|
||||
AIWaitOrderTooMuch // 排队中的AI订单太多,请稍后再试
|
||||
ActorNotExist // 演员不存在
|
||||
MediaFSServerHandleIng // 文件服务器处理中
|
||||
VideoRepeat // 视频重复
|
||||
ErrReqForbidden // 请求被拒
|
||||
APPIDError // APPID异常
|
||||
RepeatActress // 重复的女优
|
||||
CreateNotExist // 作者不存在
|
||||
)
|
||||
const (
|
||||
ErrRecharge CodeType = iota + 18000 // 充值相关错误
|
||||
ErrGetRechargeType // 充值方式获取错误
|
||||
UserPhoneNotBind // 手机号码未注册
|
||||
PostCommentPermissionDenied // 禁止评论
|
||||
UserLoginPasswordErr // 密码错误
|
||||
ErrMediaRecordNotExsit // 视频记录不存在
|
||||
ErrThressServerHttpErr // 第三方服务错误
|
||||
UserPhoneBeenBound // 手机号码已被绑定
|
||||
WeekCommentNumNotEnough // 评论条数不足
|
||||
ErrAccessForbid //ErrAccessForbid 没有访问权限 1000
|
||||
MediaAlreadyInEpisode // 该视频已经有选集
|
||||
MediaNotInEpisode // 选集中不包含该视频
|
||||
MediaCollectNotNull // 合集视频不为空
|
||||
)
|
||||
|
||||
const (
|
||||
//
|
||||
CircleErrCode CodeType = iota + 20000 // 圈子错误码 20000
|
||||
PublistNotVIPPermissionDenied // 不是会员禁止发布
|
||||
PublistVIPExpired // 会员已过期禁止发布
|
||||
PublistContentErrDenied // 包含敏感词汇
|
||||
PostRepeatBuy // 帖子重复购买
|
||||
UserNotEnoughMoney // 用户的钻石数量不足
|
||||
PostStatusError // 订单状态错误
|
||||
PostNotAllowComplaint // 不允许举报
|
||||
UserNotEnoughScore // 用户的钻石数量不足
|
||||
ErrUserAccExist // 账号已存在
|
||||
UserAccountNotBind // 账号未注册
|
||||
VerifyOKPostCountLimit //上架的帖子数量达到限制
|
||||
ErrUserBeBanned //ErrUserBeBanned 用户已经禁言 1004
|
||||
ErrCustomerBanned //ErrCustomerBanned 客服已休息 1005
|
||||
ErrVersionUpdate //ErrVersionUpdate 版本强制更新 1006
|
||||
DarkAlleyNotExist // 暗巷攻略信息不存在
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
package srvdef
|
||||
|
||||
// error tips
|
||||
const (
|
||||
Ok = "成功"
|
||||
//基础错误提示
|
||||
ET_Store = "存储错误"
|
||||
ET_DBOp = "DB操作错误"
|
||||
ET_DBUpdate = "DB更新失败"
|
||||
ET_RdsOp = "Rds操作错误"
|
||||
ET_ESOp = "ES操作错误"
|
||||
ET_Param = "参数错误"
|
||||
ET_NoDocment = "无数据不存在"
|
||||
ET_NoCache = "缓存数据不存在"
|
||||
ET_MoneyNotEnough = "余额不足"
|
||||
//----------------------------------------------
|
||||
ET_UserNoExist = "用户不存在"
|
||||
ET_IsntBlogger = "用户不是博主"
|
||||
ET_BloggerApply = "博主申请条件不满足"
|
||||
ET_BloggerAlreadyIs = "用户已经是博主"
|
||||
ET_VipLevel2Low = "用户的VIP等级不够"
|
||||
//----------------------care------------------------
|
||||
ET_StatusEqual = "已经是请求状态"
|
||||
ET_AlreadyCare = "已关注"
|
||||
ET_AlreadyJoinFG = "您已加入了该粉丝团"
|
||||
)
|
||||
|
||||
var codeAndMsg = map[CodeType]string{
|
||||
Success: "success",
|
||||
Failure: "Failed",
|
||||
ErrDbQueryError: "database query error",
|
||||
ErrDbInsertError: "database insert error",
|
||||
ErrDbUpdateError: "database update error",
|
||||
ErrDbDeleteError: "database delete error",
|
||||
ErrDbInputExist: "current input has already exists",
|
||||
ErrRdsOperError: "redis operate error",
|
||||
ErrElsOperError: "elastic operate error",
|
||||
ErrESSearchError: "elastic search error",
|
||||
BusinessLevel: "business error",
|
||||
ErrCardNotExists: "card does not exist",
|
||||
ErrMadouCoinNotExists: "madou coin does not exist",
|
||||
ErrGetRechargeType: "get recharge type error",
|
||||
ErrRchgOrderNotExist: "recharge order does not exist",
|
||||
ErrBuyVIPCardFail: "buy vip card failed",
|
||||
ErrOperationTooFrequently: "user operation is too frequent",
|
||||
PublistNotVIPPermissionDenied: "vip required to publish",
|
||||
PublistVIPExpired: "vip expired",
|
||||
DarkAlleyNotExist: "dark alley does not exist",
|
||||
UserNotEnoughMoney: "user's coin is not enough",
|
||||
RedeemCodeNotExists: "code does not exist",
|
||||
RedeemCodeTimesUsedUp: "code times used up",
|
||||
RedeemCodeAlreadyUsed: "you have used this code",
|
||||
RedeemCodeRecordNotExists: "code use record does not exist",
|
||||
ErrNoData: "no data",
|
||||
UserPhoneBeenBound: "phone number has been bound",
|
||||
WtdrFrequencyLimits: "wtdr frequency limits,please try again later",
|
||||
MediaIsNeedPay: "You have to buy the video.",
|
||||
UserWatchNumNotEnough: "More than three times",
|
||||
UserScoreNotEnough: "user score is not enough",
|
||||
UserSocreLVLimit: "user score level limit",
|
||||
AlreadyRefund: "already refund",
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package srvdef
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SrvErr 代替/common/error中的定义,逐渐在代码中弃掉(common/error模块)
|
||||
type SrvErr struct {
|
||||
Code CodeType `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Tips string `json:"tips"`
|
||||
}
|
||||
|
||||
// New create an error
|
||||
func New(code CodeType, msgAndTips ...string) *SrvErr {
|
||||
msg := code.Msg()
|
||||
tips := code.Tip()
|
||||
if len(msgAndTips) == 1 {
|
||||
msg = msgAndTips[0]
|
||||
} else if len(msgAndTips) == 2 {
|
||||
msg, tips = msgAndTips[0], msgAndTips[1]
|
||||
}
|
||||
return &SrvErr{
|
||||
Code: code,
|
||||
Msg: msg,
|
||||
Tips: tips,
|
||||
}
|
||||
}
|
||||
|
||||
// SrvErr 返回字符串
|
||||
func (s *SrvErr) Error() string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("code = %d, msg = %s ,tips = %v", s.Code, s.Msg, s.Tips)
|
||||
}
|
||||
|
||||
// SrvErr 返回error
|
||||
func (s *SrvErr) ToError() error {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("code = %d, msg = %s ,tips = %v", s.Code, s.Msg, s.Tips)
|
||||
}
|
||||
|
||||
func (c *SrvErr) GetMsg() string {
|
||||
// 有自定义的msg就用自定义的msg
|
||||
if c.Msg != "" {
|
||||
return c.Msg
|
||||
}
|
||||
return c.Code.Msg()
|
||||
}
|
||||
func (c *SrvErr) GetTip() string {
|
||||
// 有自定义的tip就用自定义的tip
|
||||
if c.Tips != "" {
|
||||
return c.Tips
|
||||
}
|
||||
return c.Code.Tip()
|
||||
}
|
||||
func (c *SrvErr) GetCode() CodeType { return c.Code }
|
||||
|
||||
var (
|
||||
ErrDbInsert = New(ErrDbInsertError, "", "")
|
||||
ErrDbQuery = New(ErrDbQueryError, "", "")
|
||||
ErrDbUpdate = New(ErrDbUpdateError, "", "")
|
||||
ErrDbDelete = New(ErrDbDeleteError, "", "")
|
||||
)
|
||||
@@ -0,0 +1,167 @@
|
||||
package srvdef
|
||||
|
||||
var codeAndTip = map[CodeType]string{
|
||||
Success: "成功",
|
||||
Failure: "失敗",
|
||||
ErrCardNotExists: "沒有會員卡信息",
|
||||
ErrMadouCoinNotExists: "沒有金幣信息",
|
||||
ErrGetRechargeType: "充值方式獲取錯誤",
|
||||
ErrRchgOrderNotExist: "充值訂單不存在",
|
||||
ErrBuyVIPCardFail: "購買會員卡失敗",
|
||||
ErrOperationTooFrequently: "用戶操作太頻繁",
|
||||
PublistNotVIPPermissionDenied: "请开通VIP后发布",
|
||||
PublistVIPExpired: "VIP已过期,请续费后发布",
|
||||
DarkAlleyNotExist: "信息不存在",
|
||||
UserNotEnoughMoney: "用户余额不足",
|
||||
UserPhoneNotBind: "手机号码未注册",
|
||||
PostCommentPermissionDenied: "非会员禁止发贴和评论",
|
||||
UserLoginPasswordErr: "密码错误",
|
||||
ErrUserTypeNotUP: "用户不是UP主",
|
||||
AccountIsNotExists: "账户不存在",
|
||||
AccountErrAccessForbid: "账户禁止登陆",
|
||||
AccountErrPasswordError: "账户密码错误",
|
||||
GenerateTokenFailed: "生成二维码错误",
|
||||
ErrMediaNotExsit: "视频不存在",
|
||||
ErrTopicInfoNotExsit: "视频专题不存在",
|
||||
ErrData: "数据错误",
|
||||
Err: "一般错误",
|
||||
ErrSubmitRchgOrder: "发起充值订单错误",
|
||||
InvalidRequestReplayAttack: "非法请求",
|
||||
ErrMediaSyncServer: "视频同步服务器错误",
|
||||
ErrUserType: "用户无改权限",
|
||||
DownloadM3u8File: "文件服务器访问失败",
|
||||
MediaSyncing: "视频同步中",
|
||||
MediaConnectServerFaild: "媒资库链接失败",
|
||||
AtressNotExist: "演员不存在",
|
||||
ErrAnnouncementNotExsit: "公告不存在",
|
||||
ErrContactDetailsNotExsit: "联系方式不存在",
|
||||
ErrPostCommentNotExsit: "评论不存在",
|
||||
HistoryNotExist: "记录不存在",
|
||||
ErrHotsearchNotExsit: "热搜不存在",
|
||||
ActivityDoesNotExist: "活动不存在",
|
||||
UserWatchNumNotEnough: "免费观看次数不足",
|
||||
MediaIsVip: "用户不是会员",
|
||||
MediaIsNeedPay: "视频需要付费",
|
||||
RepeatBuy: "重复购买",
|
||||
MovieTicketsNotExits: "观影券不足",
|
||||
MediaIsNotNeedPay: "视频不需要支付",
|
||||
PostNoNeed2Pay: "帖子不需要支付",
|
||||
SystemMsgNotExist: "消息不存在",
|
||||
ForbidlogNotExist: "封禁消息不存在",
|
||||
AttachmentDoesNotExist: "导出记录",
|
||||
UserDownloadNotEnough: "下载次数不足",
|
||||
UserDownloadNeedPay: "付费视频需要先购买",
|
||||
ErrNoData: "无数据",
|
||||
ModifyUserNameError: "昵称!每月只能修改一次",
|
||||
RedeemCodeTimesUsedUp: "該兌換碼已使用",
|
||||
RedeemCodeNotExists: "兌換碼已使用不存在",
|
||||
RedeemCodeAlreadyUsed: "該兌換碼已使用",
|
||||
InvalidToken: "框架错误",
|
||||
ErrParamError: "参数错误",
|
||||
NoToken: "无token",
|
||||
RoleErr: "角色错误",
|
||||
ReLogin: "重复登陆",
|
||||
UploadToFileSrvFailed: "文件上传失败",
|
||||
ErrCaptcha: "验证码为空",
|
||||
ErrCaptchaInvalid: "验证码非法",
|
||||
ErrCaptchaGet: "验证码获取失败",
|
||||
ErrCaptchaResend: "验证码重复发送",
|
||||
ErrCaptchaSendFail: "验证码发送失败",
|
||||
InvalidateToken: "非法token",
|
||||
NoUA: "没有UA信息",
|
||||
UploadFile: "文件上传失败",
|
||||
RedisSet: "设置redis错误",
|
||||
RedisGet: "获取redis值错误",
|
||||
RedisDel: "删除redis值错误",
|
||||
EtcdGet: "获取etcd错误",
|
||||
MongoDBQueryErr: "数据库查询失败",
|
||||
ErrDbQueryError: "数据库查询错误",
|
||||
ErrDbInsertError: "数据库插入错误",
|
||||
ErrDbUpdateError: "数据库修改错误",
|
||||
ErrDbDeleteError: "数据库删除错误",
|
||||
ErrDbInputExist: "数据库输入值已经存在",
|
||||
ErrRdsOperError: "redis操作错误",
|
||||
ErrElsOperError: "redis操作错误",
|
||||
ErrESSearchError: "搜索服务暂不可用",
|
||||
ErrTransactionsError: "数据库事务操作错误",
|
||||
ErrUserPermission: "用户权限",
|
||||
ErrUserNotExsit: "用户不存在",
|
||||
RedeemCodeRecordNotExists: "兑换记录不存在",
|
||||
MediaIsNotNeedPreSale: "视频不支持预售",
|
||||
MediaIsPreSale: "预售视频",
|
||||
QRcodeError: "二维码错误",
|
||||
ErrRecharge: "充值相关错误",
|
||||
ErrMediaRecordNotExsit: "视频记录不存在",
|
||||
UserPhoneBeenBound: "手机号码已被绑定",
|
||||
DatingAlreadyComplaint: "已经举报过",
|
||||
WtdrFrequencyLimits: "提现操作频率限制,请稍后再试",
|
||||
WtdrErroTip1: "提现数额必须是100的整数倍",
|
||||
WtdrErroTip2: "赠送6元体验金,满500可兑换钻石",
|
||||
WtdrErroTip3: "请填写银行名字或升级App版本",
|
||||
GameWtdrErroTip7: "银行卡必须与您以前的提现卡一致,如需更改请联系客服",
|
||||
FileUploadDouble: "文件重复上传",
|
||||
UserRepeatSubmitRchgOrder: "未支付订单太多,有问题请联系客服",
|
||||
ErrIpForbid: "IP被禁止",
|
||||
UserScoreNotEnough: "用户积分不足",
|
||||
UserSocreLVLimit: "用户积分等级限制",
|
||||
UploaderNeedBind: "后台用户没有up主绑定",
|
||||
UploaderNeedActive: "Up主没有激活",
|
||||
PostCollectionNotExists: "图文专辑不存在",
|
||||
MediaCollectionNotExist: "该合集或选集不存在",
|
||||
ContestNotExists: "该比赛不存在",
|
||||
CannotJoinContest: "该比赛无法参加",
|
||||
ContestIsVip: "该比赛只限VIP参加",
|
||||
MediaNotVerified: "视频未上架",
|
||||
PCCategoryUnmatch: "专辑的分类和作品的分类不一致",
|
||||
ErrCareNotExist: "取消关注对象不存在",
|
||||
ErrUpMasterTaskNotCompleted: "up主申请任务未完成",
|
||||
HGameIsVip: "游戏需要会员",
|
||||
HGameIsNeedPay: "游戏需要付费",
|
||||
HGameNotExist: "游戏不存在",
|
||||
ErrApplyUpIsVip: "UP主申请需要vip",
|
||||
UserLevelCardNotExist: "用户等级卡不存在",
|
||||
UserCheckinSetupNotExist: "签到配置不存在",
|
||||
GiftWeekIsExist: "礼包已领取",
|
||||
UserLevelPointTooLow: "用户等级积分太低",
|
||||
UserGiftHaveReceive: "任务奖励已领取",
|
||||
UserGiftExistNoReceive: "没有领取资格",
|
||||
DatingUpMasterNotExist: "up主不存在",
|
||||
UserIsUpMaster: "用户已经是up主",
|
||||
UserUpCerting: "up主认证中",
|
||||
MissionSetupNotExist: "任务配置不存在",
|
||||
UserIsUpMasterBind: "用户已被up主绑定",
|
||||
ErrCareIsExist: "不可重复关注",
|
||||
ObjectDayStatDoesNotExist: "作品统计记录不存在",
|
||||
ObjectDayViewIncStatDoesNotExist: "作品增量记录不存在",
|
||||
UserNotEnoughFreeLotteryCount: "免费抽奖次数不足",
|
||||
PrizeOutOfStore: "奖品库存不足",
|
||||
MediaSubscribedNotNeedPay: "订阅的视频无需支付",
|
||||
DatingComplaintNotExist: "举报帖子不存在",
|
||||
DatingUserNotExist: "楼凤不存在",
|
||||
WeekCommentNumNotEnough: "评论条数不足",
|
||||
UserDauInfoNotExist: "搜寻日期没有资料",
|
||||
PostCommentNotUse: "发送失败,服务器错误,请稍后再试",
|
||||
AlreadyRefund: "已退款",
|
||||
ErrUserPointsNotExist: "积分不足",
|
||||
RedeemFail: "积分兑换失败",
|
||||
CrowdfundNumberHaveBuy: "购买的号码已经出售",
|
||||
CouponExpired: "卡券已过期",
|
||||
CouponUsed: "卡券已使用",
|
||||
CouponNotFound: "没有卡券信息",
|
||||
VerifyOKPostCountLimit: "上架的帖子数量达到限制",
|
||||
CouponCanNotUse: "卡券无法使用",
|
||||
LotteryActivityNotEnable: "抽奖活动没有启用",
|
||||
CurrTimeNotActivity: "当前时间没有活动",
|
||||
AIOrderStripsCountLimit: "今天AI脱衣提交次数达到上限",
|
||||
NotWtdrChannel: "没有匹配到提现通过",
|
||||
ComicsNotJoinActivity: "这本漫画没有参与活动",
|
||||
UserRepeatDrawDiscountCard: "重复翻牌",
|
||||
NoUseDiscountCard: "不可使用折扣卡",
|
||||
ErrUserAccExist: "账号已存在",
|
||||
AIOrderNotDel: "未完成的订单不能删除",
|
||||
AIWaitOrderTooMuch: "排队中的AI订单太多,请稍后再试",
|
||||
ActorNotExist: "演员不存在",
|
||||
VideoRepeat: "视频重复",
|
||||
ErrReqForbidden: "请求被拒",
|
||||
RepeatActress: "重复的女优",
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package srvversion
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
//"os"
|
||||
)
|
||||
|
||||
var (
|
||||
//每次编译时需要手动修改本字段,否则每次编译得到的服务版本号都会是默认值
|
||||
SERVICE_VERSION = "V1.001.T"
|
||||
|
||||
//以下几个字段编译期间自动替换,无需手动操作
|
||||
GIT_BRANCH = "GIT_BRANCH_FLAG"
|
||||
GIT_VERSION = "GIT_VERSION_FLAG"
|
||||
BUILD_DATE = "BUILD_DATE_FLAG"
|
||||
BUILD_IP = "BUILD_IP_FLAG"
|
||||
)
|
||||
|
||||
func Info() {
|
||||
fmt.Printf("version:%s\n", SERVICE_VERSION)
|
||||
fmt.Printf("git branch:%s\n", GIT_BRANCH)
|
||||
fmt.Printf("git version:%s\n", GIT_VERSION)
|
||||
fmt.Printf("build date:%s\n", BUILD_DATE)
|
||||
fmt.Printf("build ip:%s\n", BUILD_IP)
|
||||
|
||||
os.Exit(0) //退出进程
|
||||
}
|
||||
|
||||
func GetVersion() *Version {
|
||||
return &Version{
|
||||
ServiceVer: SERVICE_VERSION,
|
||||
GitVer: GIT_VERSION,
|
||||
BuildDate: BUILD_DATE,
|
||||
BuildIp: BUILD_IP,
|
||||
Branch: GIT_BRANCH,
|
||||
}
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
ServiceVer string `json:"serviceVer"`
|
||||
GitVer string `json:"gitVer"`
|
||||
BuildDate string `json:"buildDate"`
|
||||
BuildIp string `json:"buildIp"`
|
||||
Branch string `json:"branch"`
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package statcenter
|
||||
|
||||
// IOSRegistrationDistrictCode 原生 iOS 商店包用户的固定渠道码。
|
||||
// 未配置时不覆盖用户原 DistrictCode。
|
||||
const IOSRegistrationDistrictCode = ""
|
||||
@@ -0,0 +1,227 @@
|
||||
package statcenter
|
||||
|
||||
import (
|
||||
"time"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type KFKTopic string
|
||||
|
||||
const (
|
||||
STAT_APPID int32 = pubdef.AppId
|
||||
USER_REG KFKTopic = "user_register" // 用户注册
|
||||
USER_RECH KFKTopic = "user_recharge" // 用户充值(成功)
|
||||
USER_RECH_ALL KFKTopic = "user_recharge_all" // 全部充值订单
|
||||
USER_ACCE KFKTopic = "user_access" // 用户访问
|
||||
USER_BINDING KFKTopic = "user_binding" // 用户绑定(邮箱/正式注册)
|
||||
USER_INVITE KFKTopic = "user_invite" // 用户邀请绑定
|
||||
VipJob KFKTopic = "vip" // VIP 购买(兼容旧任务)
|
||||
RechargeAll KFKTopic = "rechargeAll" // 全部充值(兼容旧任务名)
|
||||
RegisterLightJob KFKTopic = "registerLight" // H5 轻量注册
|
||||
ConsumeRecordJob KFKTopic = "consume_record" // 产品消费流水
|
||||
CardSellJob KFKTopic = "card_sell" // 会员卡销售流水
|
||||
AI_SELL_JOB KFKTopic = "ai_sell" // AI 消费(本项目无此业务)
|
||||
)
|
||||
|
||||
const (
|
||||
StatVipCard = iota // 会员卡
|
||||
StatValueAddSer // 增值/其他消费
|
||||
StatVideoUnlock // 视频解锁
|
||||
)
|
||||
|
||||
const (
|
||||
CurrencyTypeGold = iota // 金币
|
||||
CurrencyTypeCash // 现金
|
||||
)
|
||||
|
||||
// StatcenterSyncReq 统计中心增量同步请求
|
||||
type StatcenterSyncReq struct {
|
||||
Job string `json:"job" bson:"job"` // 同步任务类型
|
||||
UserId uint64 `json:"userId" bson:"userId"` // 用户(注册)
|
||||
PlatformId string `json:"platformId" bson:"platformId"` // 原始平台Id(访问/流水)
|
||||
SuccessTime time.Time `json:"successTime"` // 游标时间(购买会员、提现、充值)
|
||||
MaxSize int64 `json:"maxSize" bson:"maxSize"` // 最大条数
|
||||
}
|
||||
|
||||
// StatcenterSyncResp 统计中心增量同步响应
|
||||
type StatcenterSyncResp struct {
|
||||
Code int `json:"code"` // 200正常 其他异常
|
||||
Msg string `json:"msg"` // 错误消息
|
||||
Job string `json:"job" bson:"job"` // 类型
|
||||
VisitList []UserVisitMsg `json:"visitList"` // 日活记录(旧字段,兼容)
|
||||
AccessList []UserVisitMsg `json:"accessList"` // 日活记录
|
||||
RegisterList []UserRegisterMsg `json:"registerList"` // 注册记录
|
||||
BindingList []UserBindingMsg `json:"bindingList"` // 用户绑定记录
|
||||
InviteList []UserInviteBindMsg `json:"inviteList"` // 邀请绑定
|
||||
ConsumeList []ConsumeRecordMsg `json:"consumeList"` // 消费流水
|
||||
RechargeList []UserRechargeMsg `json:"rechargeList"` // 充值记录
|
||||
AllRechargeList []UserRechargeMsg `json:"allRechargeList"` // 全部支付订单
|
||||
VipList []UserBuyVIPMsg `json:"buyVipList"` // VIP购买记录
|
||||
CardSellList []CardSellMsg `json:"cardSellList"` // 会员卡销售流水
|
||||
AiSellList []AiSellMsg `json:"aiSellList"` // AI销售流水
|
||||
}
|
||||
|
||||
// 用户访问消息
|
||||
type UserVisitMsg struct {
|
||||
UserId uint64 `json:"userId" bson:"userId"` // 用户ID
|
||||
PlatformId string `json:"platformId" bson:"platformId"` // 原始平台流水Id
|
||||
AppId int32 `json:"appId" bson:"appId"` // appID
|
||||
SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS
|
||||
DevType string `json:"devType,omitempty" bson:"devType"` // 设备型号
|
||||
IP string `json:"ip,omitempty" bson:"ip"` // IP
|
||||
Version string `json:"version,omitempty" bson:"version"` // APP版本号
|
||||
DevID string `json:"devID,omitempty" bson:"devID"` // 设备ID
|
||||
VisitAt time.Time `json:"visitAt,omitempty" bson:"visitAt"` // 访问时间
|
||||
IsDirect bool `json:"isDirect,omitempty"` // true:是直推用户
|
||||
DistrictCode string `json:"districtCode,omitempty"` // 渠道码
|
||||
RegisterTime time.Time `json:"registerTime,omitempty"` // 用户注册时间
|
||||
}
|
||||
|
||||
// 用户注册消息
|
||||
type UserRegisterMsg struct {
|
||||
UserId uint64 `json:"userId" bson:"userId" binding:"required"` // 用户ID
|
||||
AppId int32 `json:"appId" bson:"appId" binding:"required"` // appID
|
||||
SysType string `json:"sysType" bson:"sysType" binding:"required"` // 操作系统类型 安卓 IOS
|
||||
PlatformId string `json:"platformId" bson:"platformId"` // 原始平台流水Id
|
||||
DevType string `json:"devType" bson:"devType"` // 设备类型
|
||||
Mobile string `json:"mobile" bson:"mobile"` // 手机号
|
||||
IP string `json:"ip,omitempty" bson:"ip"` // IP
|
||||
Name string `json:"name" bson:"name"` // 名称
|
||||
IsDirect bool `json:"isDirect" bson:"isDirect" binding:"required"` // true:是直推用户
|
||||
DistrictCode string `json:"districtCode" bson:"districtCode" ` // 渠道码
|
||||
PromSeqe string `json:"promSeqe,omitempty" bson:"promSeqe"` // 全名代理
|
||||
PromCode string `json:"promCode,omitempty" bson:"promCode"` // 推广序列
|
||||
PUC string `json:"puc" bson:"puc"` // 应用中心推广
|
||||
RegisterTime time.Time `json:"registerTime,omitempty" bson:"registerTime" binding:"required"` // 注册时间
|
||||
}
|
||||
|
||||
// UserBindingMsg 用户绑定(本项目对应邮箱正式注册)
|
||||
type UserBindingMsg struct {
|
||||
UserId uint64 `json:"userId"` // 用户ID
|
||||
AppId int32 `json:"appId"` // appID
|
||||
PlatformId string `json:"platformId"` // 原始平台流水Id
|
||||
SysType string `json:"sysType"` // 操作系统类型
|
||||
DevType string `json:"devType"` // 设备类型
|
||||
Mobile string `json:"mobile"` // 手机号/绑定账号(本项目填邮箱)
|
||||
BindingTime *time.Time `json:"bindingTime"` // 绑定时间
|
||||
}
|
||||
|
||||
// UserInviteBindMsg 邀请绑定
|
||||
type UserInviteBindMsg struct {
|
||||
UserId uint64 `json:"userId" bson:"userId" binding:"required"`
|
||||
AppId int32 `json:"appId" bson:"appId" binding:"required"`
|
||||
ParentPromCode string `json:"parentPromCode,omitempty" bson:"promSeqe"`
|
||||
InviteTime time.Time `json:"inviteTime,omitempty" bson:"inviteTime" binding:"required"`
|
||||
}
|
||||
|
||||
// ConsumeRecordMsg 消费流水
|
||||
type ConsumeRecordMsg struct {
|
||||
AppID int32 `json:"appID" bson:"appID"`
|
||||
UID uint64 `json:"uid" bson:"uid"`
|
||||
Type int `json:"type" bson:"type"` // 消费类型
|
||||
CurrencyType int `json:"currencyType" bson:"currencyType"` // 0金币 1人民币
|
||||
Money decimal.Decimal `json:"money" bson:"money"` // 人民币 元
|
||||
Amount decimal.Decimal `json:"amount" bson:"amount"` // 金币
|
||||
Uniq string `json:"uniq" bson:"uniq"`
|
||||
CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
|
||||
}
|
||||
|
||||
// AiSellMsg AI 销售流水
|
||||
type AiSellMsg struct {
|
||||
AppID int32 `json:"appID" bson:"appID"`
|
||||
UID uint64 `json:"uid" bson:"uid"`
|
||||
UniqID string `json:"uniqID" bson:"uniqID"`
|
||||
Amount int64 `json:"amount" bson:"amount"`
|
||||
TranType string `json:"tranType" bson:"tranType"`
|
||||
SysType string `json:"sysType" bson:"sysType"`
|
||||
DevType string `json:"devType" bson:"devType"`
|
||||
CurrencyType string `json:"currencyType" bson:"currencyType"`
|
||||
TranCreatedAt time.Time `json:"tranCreatedAt" bson:"tranCreatedAt"`
|
||||
IsRepurchase string `json:"isRepurchase" bson:"isRepurchase"`
|
||||
}
|
||||
|
||||
// CardSellMsg 会员卡销售流水
|
||||
type CardSellMsg struct {
|
||||
AppID int32 `json:"appID" bson:"appID"`
|
||||
UID uint64 `json:"uid" bson:"uid"`
|
||||
UniqID string `json:"uniqID" bson:"uniqID"`
|
||||
Amount int64 `json:"amount" bson:"amount"`
|
||||
TranTypeInt int64 `json:"tranTypeInt" bson:"tranTypeInt"`
|
||||
TranType string `json:"tranType" bson:"tranType"`
|
||||
SysType string `json:"sysType" bson:"sysType"`
|
||||
CurrencyType int `json:"currencyType" bson:"currencyType"` // 0金币 1现金
|
||||
TranCreatedAt time.Time `json:"tranCreatedAt" bson:"tranCreatedAt"`
|
||||
Product Product `json:"product" bson:"product"`
|
||||
}
|
||||
|
||||
// Product 售卡产品信息
|
||||
type Product struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
DiscountedPrice int64 `json:"discountedPrice"`
|
||||
}
|
||||
|
||||
// 用户充值消息
|
||||
type UserRechargeMsg struct {
|
||||
UserId uint64 `json:"userId" bson:"userId" binding:"required"`
|
||||
AppId int32 `json:"appId" bson:"appId" binding:"required"`
|
||||
PlatformId string `json:"platformId" bson:"platformId"`
|
||||
SysType string `json:"sysType" bson:"sysType" binding:"required"`
|
||||
DevType string `json:"devType" bson:"devType"`
|
||||
ChannelName string `json:"channelName" bson:"channelName"`
|
||||
CID string `json:"cid" bson:"cid"`
|
||||
Type string `bson:"type" json:"type"`
|
||||
OrderId string `json:"orderId" bson:"orderId" binding:"required"`
|
||||
OID string `json:"oid" bson:"oid"`
|
||||
Money int64 `json:"money" bson:"money" binding:"required"`
|
||||
PayMoney int64 `json:"payMoney" bson:"payMoney" binding:"required"`
|
||||
Status int `json:"status" bson:"status"`
|
||||
Rate string `json:"rate"`
|
||||
SuccessAt time.Time `json:"successAt,omitempty" bson:"successAt"`
|
||||
PaymentAt time.Time `json:"paymentAt"`
|
||||
ProductType int `json:"productType" bson:"productType"`
|
||||
ChanShareMod int `json:"chanShareMod" bson:"chanShareMod"`
|
||||
IsNewOrOldUser bool `json:"isNewOrOldUser" bson:"isNewOrOldUser"`
|
||||
CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
|
||||
DistrictCode string `json:"districtCode" bson:"districtCode"`
|
||||
RegisterTime time.Time `json:"registerTime,omitempty" bson:"registerTime"`
|
||||
}
|
||||
|
||||
type BuyVIPType int64
|
||||
|
||||
const (
|
||||
BuyVIPTypeMoney BuyVIPType = 0 + iota // 现金
|
||||
BuyVIPTypeGold // 金币
|
||||
)
|
||||
|
||||
// 用户购买VIP
|
||||
type UserBuyVIPMsg struct {
|
||||
AppId int32 `json:"appId" bson:"appId"`
|
||||
UID uint64 `json:"uid" bson:"uid"`
|
||||
PlatformId string `json:"platformId" bson:"platformId"`
|
||||
Name string `json:"name" bson:"name"`
|
||||
Money int64 `json:"money" bson:"money"`
|
||||
Amount int64 `json:"amount" bson:"amount"`
|
||||
BuyVIPType BuyVIPType `json:"buyVIPType" bson:"buyVIPType"`
|
||||
OID string `json:"oid" bson:"oid"`
|
||||
DeviceType string `json:"deviceType" bson:"deviceType"`
|
||||
Status int `json:"status" bson:"status"`
|
||||
SuccessAt time.Time `json:"successTime"`
|
||||
IsDirect bool `json:"isDirect" bson:"isDirect"`
|
||||
DistrictCode string `json:"districtCode" bson:"districtCode"`
|
||||
PromCode string `json:"promCode" bson:"promCode"`
|
||||
}
|
||||
|
||||
// MapOrderStatusToStat 将本项目订单状态映射为统计中心约定:2失败 3成功
|
||||
func MapOrderStatusToStat(status pubdef.VipOrderStatus) int {
|
||||
switch status {
|
||||
case pubdef.VipOrderSuccess:
|
||||
return 3
|
||||
case pubdef.VipOrderFailed:
|
||||
return 2
|
||||
default:
|
||||
return int(status)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package swaggerutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"videoweb/pub/common/netutil"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
func isDevHost(runMode string, svcListenIp string) bool {
|
||||
localIp := netutil.GetLocalIP()
|
||||
return (runMode == "dev" || runMode == "debug") && localIp != svcListenIp
|
||||
}
|
||||
|
||||
func SetupGinSwagger(runMode, svcListenIp, svcListenPort string) func(r *gin.Engine) {
|
||||
return func(r *gin.Engine) {
|
||||
if isDevHost(runMode, svcListenIp) {
|
||||
url := ginSwagger.URL(fmt.Sprintf("http://%s:%s/swagger/doc.json", netutil.GetLocalIP(), svcListenPort))
|
||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
|
||||
} else {
|
||||
url := ginSwagger.URL(fmt.Sprintf("http://%s:%s/swagger/doc.json", svcListenIp, svcListenPort))
|
||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package synclock
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"videoweb/pub/common/errors"
|
||||
"videoweb/pub/redis"
|
||||
)
|
||||
|
||||
const (
|
||||
UserExtSyncLockFmt = "userExtSyncLock:%d" // 用户账户扩展信息
|
||||
UserExtSyncLockExpire = 5 * time.Minute // 用户账户扩展信息 过期时间
|
||||
|
||||
UserAccountLockFmt = "UserAccountLockFmt:%d" // 用户账户
|
||||
UserAccountLockExpire = 5 * time.Minute // 用户账户 过期时间
|
||||
TraderAccountLockFmt = "TraderAccountLockFmt:%s" //商人账户
|
||||
SpinLockExpire = 3 * time.Second
|
||||
)
|
||||
|
||||
// UserExtSyncLock 通过redis按照用户ID加锁
|
||||
func UserExtcLock(userId int64) bool {
|
||||
v, e := redis.Setnx_NewOK(fmt.Sprintf(UserExtSyncLockFmt, userId), true, UserExtSyncLockExpire)
|
||||
if !v || e != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// UserExtSpinLock 通过redis按照用户ID加锁
|
||||
func UserExtSpinLock(userId int64, lockTimeOut time.Duration) (bool, error) {
|
||||
endTime := time.Now().Add(lockTimeOut)
|
||||
for time.Now().Before(endTime) {
|
||||
value, err := redis.Setnx_NewOK(fmt.Sprintf(UserExtSyncLockFmt, userId), true, UserExtSyncLockExpire)
|
||||
if err != nil {
|
||||
return false, errors.MakeWithMsg(errors.RedisError, "redis未启动")
|
||||
}
|
||||
if value {
|
||||
return true, nil
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
|
||||
return false, errors.MakeWithMsg(errors.RedisError, "用户操作繁忙")
|
||||
}
|
||||
|
||||
// UserExtUnlock 按照用户ID释放锁
|
||||
func UserExtUnlock(userId int64) {
|
||||
redis.Del(fmt.Sprintf(UserExtSyncLockFmt, userId))
|
||||
}
|
||||
|
||||
// UserAccountLock 通过redis按照用户ID加锁
|
||||
func UserAccountLock(userId int64) bool {
|
||||
v, e := redis.Setnx_NewOK(fmt.Sprintf(UserAccountLockFmt, userId), true, UserExtSyncLockExpire)
|
||||
if !v || e != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// UserAccountSpinLock 通过redis按照用户ID加锁
|
||||
func UserAccountSpinLock(userId int64, lockTimeOut time.Duration) (bool, error) {
|
||||
endTime := time.Now().Add(lockTimeOut)
|
||||
for time.Now().Before(endTime) {
|
||||
value, err := redis.Setnx_NewOK(fmt.Sprintf(UserAccountLockFmt, userId), true, UserExtSyncLockExpire)
|
||||
if err != nil {
|
||||
return false, errors.MakeWithMsg(errors.RedisError, "redis未启动")
|
||||
}
|
||||
if value {
|
||||
return true, nil
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
return false, errors.MakeWithMsg(errors.RedisError, "用户操作繁忙")
|
||||
}
|
||||
|
||||
// 通过redis按照商人ID加锁
|
||||
func TraderAccountSpinLock(userId string, lockTimeOut time.Duration) (bool, error) {
|
||||
endTime := time.Now().Add(lockTimeOut)
|
||||
for time.Now().Before(endTime) {
|
||||
value, err := redis.Setnx_NewOK(fmt.Sprintf(TraderAccountLockFmt, userId), true, SpinLockExpire)
|
||||
if err != nil {
|
||||
return false, errors.MakeWithMsg(errors.RedisError, "redis未启动")
|
||||
}
|
||||
if value {
|
||||
return true, nil
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
return false, errors.MakeWithMsg(errors.RedisError, "用户操作繁忙")
|
||||
}
|
||||
|
||||
// 按照商人ID释放锁
|
||||
func TraderAccountUnlock(userId string) {
|
||||
redis.Del(fmt.Sprintf(TraderAccountLockFmt, userId))
|
||||
}
|
||||
|
||||
// UserAccountUnlock 按照用户ID释放锁
|
||||
func UserAccountUnlock(userId int64) {
|
||||
redis.Del(fmt.Sprintf(UserAccountLockFmt, userId))
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package tgbot
|
||||
|
||||
import (
|
||||
"videoweb/pub/common/srvdef"
|
||||
)
|
||||
|
||||
//获取群聊id
|
||||
//https://api.telegram.org/bot1803190372:AAGmrrN6xUoh7IKiv4qCVJYdCs8wLt1ffCw/getUpdates
|
||||
|
||||
var (
|
||||
//配置
|
||||
//zhihu app机器人 2048234434:AAF3XoW4iE6vAOmttTT_bofgan_PleKuaEk
|
||||
//zhihu web机器人 1973289619:AAHjCZ8KQOewVfr20foHKlKVwJoulrf_OFc
|
||||
tgToken = "2048234434:AAF3XoW4iE6vAOmttTT_bofgan_PleKuaEk"
|
||||
//程序支付监控群tgids
|
||||
programTgGroupIds = []int64{-778146822}
|
||||
//天行监控组tgids
|
||||
monitorTgGroupIds = []int64{-1001373963166}
|
||||
//管理人员tgid(yl:1652126945)
|
||||
adminTgIds = []int64{1652126945}
|
||||
prefix = "[汁乎App]"
|
||||
tgBot *TgBot = nil
|
||||
)
|
||||
|
||||
func InitTgBot(token string, prefixStr string) *srvdef.SrvErr {
|
||||
if tgBot != nil {
|
||||
return nil
|
||||
}
|
||||
if prefixStr != "" {
|
||||
prefix = prefixStr
|
||||
}
|
||||
tgBot = NewTelegramBot(token)
|
||||
if serr := tgBot.Init(); nil != serr {
|
||||
return serr
|
||||
}
|
||||
|
||||
//SendToAll("支付监控测试", "Test")
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReleaseTgBot() {
|
||||
if tgBot == nil {
|
||||
return
|
||||
}
|
||||
tgBot.Release()
|
||||
}
|
||||
|
||||
// SendToAdmins 发到管理人员
|
||||
func SendToAdmins(title string, info string) {
|
||||
sendTo(adminTgIds, title, info)
|
||||
}
|
||||
|
||||
// SendToProgram 发到程序
|
||||
func SendToProgram(title string, info string) {
|
||||
sendTo(programTgGroupIds, title, info)
|
||||
}
|
||||
|
||||
// SendToMonitor 发到监控群组
|
||||
func SendToMonitor(title string, info string) {
|
||||
sendTo(monitorTgGroupIds, title, info)
|
||||
}
|
||||
|
||||
// SendToProgram 发到所有
|
||||
func SendToAll(title string, info string) {
|
||||
groupIds := programTgGroupIds
|
||||
groupIds = append(groupIds, monitorTgGroupIds...)
|
||||
//groupIds = append(groupIds, adminTgIds...)
|
||||
sendTo(groupIds, title, info)
|
||||
}
|
||||
|
||||
func sendTo(ids []int64, title string, info string) {
|
||||
if tgBot == nil {
|
||||
return
|
||||
}
|
||||
//异步发送
|
||||
go func() {
|
||||
msg := prefix + "\n" + title + "\n" + info
|
||||
tgBot.Send(ids, msg)
|
||||
}()
|
||||
}
|
||||
|
||||
func RegisterCommands(commands []*BotCmd) {
|
||||
if tgBot == nil {
|
||||
return
|
||||
}
|
||||
tgBot.RegisterCommands(commands)
|
||||
}
|
||||
|
||||
func Receive() {
|
||||
if tgBot == nil {
|
||||
return
|
||||
}
|
||||
tgBot.Receive()
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package tgbot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
)
|
||||
|
||||
type RSP_TYPE int
|
||||
|
||||
const (
|
||||
TextType RSP_TYPE = 0
|
||||
FileType RSP_TYPE = 1
|
||||
)
|
||||
|
||||
type TgCmdRsp struct {
|
||||
Buf []byte
|
||||
BufType RSP_TYPE
|
||||
}
|
||||
|
||||
// TgBot Telegram机器人.
|
||||
type TgBot struct {
|
||||
// 机器人标示;
|
||||
token string
|
||||
// // 管理员分组;
|
||||
// admins []int64
|
||||
// // 管理群分组;
|
||||
// groups []int64
|
||||
// 机器人引擎;
|
||||
engine *tgbotapi.BotAPI
|
||||
// 命令存储集;
|
||||
command map[string]func(string, int64) TgCmdRsp
|
||||
help string
|
||||
exitRcv chan interface{}
|
||||
}
|
||||
|
||||
// BotCmd 命令.
|
||||
type BotCmd struct {
|
||||
// 指令;
|
||||
Key string
|
||||
// 摘要;
|
||||
Summary string
|
||||
// 处理;
|
||||
Handle func(string, int64) TgCmdRsp
|
||||
}
|
||||
|
||||
type BotCmdList []*BotCmd
|
||||
|
||||
// NewBotCmd 新的命令.
|
||||
func NewBotCmd(key, summary string, handle func(string, int64) TgCmdRsp) *BotCmd {
|
||||
return &BotCmd{
|
||||
Key: key,
|
||||
Summary: summary,
|
||||
Handle: handle,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTelegramBot 新的Telegram机器人.
|
||||
func NewTelegramBot(token string) *TgBot {
|
||||
return &TgBot{
|
||||
token: token,
|
||||
exitRcv: make(chan interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Init 机器人初始化.
|
||||
func (tb *TgBot) Init() *srvdef.SrvErr {
|
||||
var err error
|
||||
tb.engine, err = tgbotapi.NewBotAPI(tb.token)
|
||||
if err != nil {
|
||||
log.Warn("(tgbot:Init) tgbotapi.NewBotAPI failed", log.E(err))
|
||||
return srvdef.New(srvdef.BusinessLevel, "", "")
|
||||
}
|
||||
tb.command = make(map[string]func(string, int64) TgCmdRsp)
|
||||
tb.command["help"] = tb.menu
|
||||
return nil
|
||||
}
|
||||
|
||||
// Release 机器人退出释放
|
||||
func (tb *TgBot) Release() *srvdef.SrvErr {
|
||||
tb.engine.StopReceivingUpdates()
|
||||
close(tb.exitRcv)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tb *TgBot) menu(string, int64) TgCmdRsp {
|
||||
return TgCmdRsp{Buf: []byte(tb.help), BufType: TextType}
|
||||
}
|
||||
|
||||
// RegisterCommands 注册命令并且自动生成help.
|
||||
func (tb *TgBot) RegisterCommands(commands []*BotCmd) {
|
||||
for _, cmd := range commands {
|
||||
tb.command[cmd.Key] = cmd.Handle
|
||||
tb.help += fmt.Sprintln("/" + cmd.Key + ": " + cmd.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
// Receive 接收消息(阻塞).
|
||||
func (tb *TgBot) Receive() error {
|
||||
log.Info("(tgbot:Receive) start")
|
||||
// 设置更新频率;
|
||||
cfg := tgbotapi.NewUpdate(0)
|
||||
//cfg.Timeout = 1
|
||||
|
||||
// 得到更新管道;
|
||||
updates, err := tb.engine.GetUpdatesChan(cfg)
|
||||
if err != nil {
|
||||
log.Warn("(tgbot:Receive) tb.engine.GetUpdatesChan failed", log.E(err))
|
||||
return srvdef.New(srvdef.BusinessLevel, "", "")
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case update := <-updates:
|
||||
tb.ProcessMessage(&update)
|
||||
case <-tb.exitRcv:
|
||||
log.Info("(tgbot:Receive) exist")
|
||||
break
|
||||
default:
|
||||
}
|
||||
time.Sleep(time.Second * 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tb *TgBot) ProcessMessage(update *tgbotapi.Update) {
|
||||
// 忽略非消息更新;
|
||||
if update.Message == nil {
|
||||
log.Info("(tgbot:ProcessMessage) message is nil")
|
||||
return
|
||||
}
|
||||
info := fmt.Sprintf("[Name: %s%s][ID: %d][Text: %s]\n",
|
||||
update.Message.From.FirstName, update.Message.From.LastName,
|
||||
update.Message.Chat.ID, update.Message.Text)
|
||||
log.Info(info)
|
||||
if update.Message.IsCommand() {
|
||||
message := tgbotapi.NewMessage(update.Message.Chat.ID, "")
|
||||
handle, ok := tb.command[update.Message.Command()]
|
||||
if !ok {
|
||||
message.Text = "unknown command"
|
||||
} else {
|
||||
param := update.Message.CommandArguments()
|
||||
rsp := handle(param, update.Message.Chat.ID)
|
||||
switch rsp.BufType {
|
||||
case FileType:
|
||||
|
||||
default:
|
||||
message.Text = string(rsp.Buf)
|
||||
}
|
||||
//message.Text = handle(param)
|
||||
}
|
||||
tb.engine.Send(message)
|
||||
}
|
||||
}
|
||||
|
||||
func (tb *TgBot) StopReceive() {
|
||||
tb.engine.StopReceivingUpdates()
|
||||
close(tb.exitRcv)
|
||||
}
|
||||
|
||||
// Send 发送消息.
|
||||
func (tb *TgBot) Send(ids []int64, text string) {
|
||||
for _, id := range ids {
|
||||
message := tgbotapi.NewMessage(id, text)
|
||||
tb.engine.Send(message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package tgbot
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func create(t *testing.T) *TgBot {
|
||||
token := "1014745091:AAFkw5j3Fw4ytvWgIll6iRH1q5WooS6JO74"
|
||||
robot := NewTelegramBot(token)
|
||||
if err := robot.Init(); err != nil {
|
||||
t.Error(err)
|
||||
t.Fail()
|
||||
}
|
||||
// 注册命令.
|
||||
cmds := BotCmdList{NewBotCmd("test", "return testfunc result", testfunc)}
|
||||
robot.RegisterCommands(cmds)
|
||||
return robot
|
||||
}
|
||||
|
||||
func testfunc(string, int64) TgCmdRsp {
|
||||
return TgCmdRsp{Buf: []byte("help"), BufType: TextType}
|
||||
}
|
||||
|
||||
func TestReceive(t *testing.T) {
|
||||
robot := create(t)
|
||||
if err := robot.Receive(); err != nil {
|
||||
t.Error(err)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestSend(t *testing.T) {
|
||||
robot := create(t)
|
||||
robot.Send([]int64{790603940}, "test send")
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func BindUrl(hostname string, path ...string) string {
|
||||
if hostname == "" {
|
||||
return strings.TrimLeft(filepath.Join(path...), "/")
|
||||
}
|
||||
if len(path) == 0 {
|
||||
return hostname
|
||||
}
|
||||
su := strings.TrimLeft(filepath.Join(path...), "/")
|
||||
pr := strings.TrimRight(hostname, "/")
|
||||
return pr + "/" + su
|
||||
}
|
||||
|
||||
// 将time.Time转换成int64
|
||||
func Time2Stamp(t time.Time) int64 {
|
||||
return t.UnixNano() / 1e9
|
||||
}
|
||||
|
||||
/**时间对象->字符串*/
|
||||
func Time2Str() string {
|
||||
const shortForm = "2006-01-01 15:04:05"
|
||||
t := time.Now()
|
||||
temp := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.Local)
|
||||
str := temp.Format(shortForm)
|
||||
return str
|
||||
}
|
||||
|
||||
/*时间戳->字符串*/
|
||||
func Stamp2Str(stamp int64) string {
|
||||
timeLayout := "2006-01-02 15:04:05"
|
||||
str := time.Unix(stamp/1000, 0).Format(timeLayout)
|
||||
return str
|
||||
}
|
||||
|
||||
/*时间戳->时间对象*/
|
||||
func Stamp2Time(stamp int64) time.Time {
|
||||
stampStr := Stamp2Str(stamp)
|
||||
timer := Str2Time(stampStr)
|
||||
return timer
|
||||
}
|
||||
|
||||
/**字符串->时间对象*/
|
||||
func Str2Time(formatTimeStr string) time.Time {
|
||||
timeLayout := "2006-01-02 15:04:05"
|
||||
loc, _ := time.LoadLocation("Local")
|
||||
theTime, _ := time.ParseInLocation(timeLayout, formatTimeStr, loc) //使用模板在对应时区转化为time.time类型
|
||||
return theTime
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package topicmessage
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
/* 通信协议,第一版*/
|
||||
|
||||
// 用户用户注册消息
|
||||
type UserRegisterMsg struct {
|
||||
UserId int64 `json:"userId" bson:"userId"` // 用户ID
|
||||
AppId int32 `json:"appId" bson:"appId"` // appID
|
||||
SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS
|
||||
PlatformId string `json:"platformId" bson:"platformId"` // 原始平台流水Id
|
||||
DevType string `json:"devType" bson:"devType"` // 设备类型
|
||||
Name string `json:"name" bson:"name"` // 名称
|
||||
IsDirect bool `json:"isDirect" bson:"isDirect"` // true:是直推用户
|
||||
DeviceId string `json:"deviceId" bson:"deviceId"` // 设备Id
|
||||
IP string `json:"ip,omitempty" bson:"ip"` // IP
|
||||
DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码
|
||||
InviteCode string `json:"inviteCode" bson:"inviteCode"` // 被邀请码
|
||||
RegisterAt time.Time `json:"registerAt,omitempty" bson:"registerAt"` // 注册时间
|
||||
UserAgent string `json:"userAgent" bson:"userAgent"` // User-Agent识别
|
||||
}
|
||||
|
||||
// 用户第一次访问
|
||||
type UserVisitMsg struct {
|
||||
UserId int64 `bson:"userId" json:"userId"` // 用户Id
|
||||
SumDate time.Time `bson:"sumDate" json:"sumDate"` // 记录时间
|
||||
AppId int32 `json:"appId" bson:"appId"` // appID
|
||||
SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS
|
||||
DevType string `json:"devType" bson:"devType"` // 设备类型
|
||||
IP string `json:"ip,omitempty" bson:"ip"` // IP
|
||||
Version string `json:"version,omitempty" bson:"version"` // APP版本号
|
||||
VisitAt time.Time `json:"visitAt,omitempty" bson:"visitAt"` // 访问时间
|
||||
DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码
|
||||
}
|
||||
|
||||
type UserOrderMsg struct {
|
||||
UserId int64 `json:"userId" bson:"userId"` // 用户ID
|
||||
AppId int32 `json:"appId" bson:"appId"` // appID
|
||||
PlatformId string `json:"platformId" bson:"platformId"` // 原始平台流水Id
|
||||
SysType string `json:"sysType" bson:"sysType"` // 操作系统类型 安卓 IOS
|
||||
DevType string `json:"devType" bson:"devType"` // 设备类型
|
||||
ChannelName string `json:"channelName" bson:"channelName"` // 支付渠道名字
|
||||
CID string `json:"cid" bson:"cid"` // 渠道id
|
||||
Type string `bson:"type" json:"type"` // 充值方式
|
||||
RchgUse int64 `json:"rchgUse" bson:"rchgUse"` // 充值用途 1:金币 2:VIP
|
||||
Repeat bool `bson:"repeat" json:"repeat"` // 复冲: true 重复充值 false 第一次
|
||||
OrderId string `json:"orderId" bson:"orderId"` // 流水id
|
||||
Money int64 `json:"money" bson:"money"` // 充值金额 订单金额
|
||||
PayMoney int64 `json:"payMoney" bson:"payMoney"` // 实际到账金额 用户实际支付金额
|
||||
Status int `json:"status" bson:"status"` // 2付款失败 3付款成功(目前只有成功才发送)
|
||||
DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码
|
||||
Rate string `json:"rate" bson:"rate"` // 渠道费率
|
||||
SuccessAt time.Time `json:"successAt" bson:"successAt"` // 成功时间
|
||||
CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
|
||||
}
|
||||
|
||||
type UserChannelEvent struct {
|
||||
UserId int64 `bson:"userId" json:"userId"` // 用户Id
|
||||
AppId int32 `json:"appId" bson:"appId"` // appID
|
||||
DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码
|
||||
WatchCount int64 `json:"watchCount,omitempty" bson:"watchCount"` // 观看次数
|
||||
WatchTime int64 `json:"watchTime,omitempty" bson:"watchTime"` // 观看时长
|
||||
AdClick int64 `json:"adClick,omitempty" bson:"adClick"` // 广告点击
|
||||
AppClick int64 `json:"appClick,omitempty" bson:"appClick"` // APP点击
|
||||
RequestCount int64 `json:"requestCount,omitempty" bson:"requestCount"` // 请求次数
|
||||
}
|
||||
|
||||
type MediaPlayEvent struct {
|
||||
MediaId int64 `json:"mediaId" bson:"mediaId"` // 视频Id
|
||||
Title string `json:"title" bson:"title"` // 标题
|
||||
FileId string `json:"fileId" bson:"fileId"` // 媒资库ID
|
||||
ActionType int64 `json:"actionType" bson:"actionType"` // 类型:0:播放 1:点赞 2:购买
|
||||
UserId int64 `json:"userId" bson:"userId"` // 用户Id
|
||||
AppId int32 `json:"appId" bson:"appId"` // appID
|
||||
WatchAt time.Time `json:"watchAt" bson:"watchAt"` // 时间
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// StructToMap 对象转map,去掉无效值
|
||||
func StructToMap(u interface{}) (m map[string]interface{}) {
|
||||
t := reflect.TypeOf(u)
|
||||
v := reflect.ValueOf(u)
|
||||
p := make(map[string]interface{})
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
fv := v.Field(i)
|
||||
ft := t.Field(i)
|
||||
jsonTag, _ := ft.Tag.Lookup("json")
|
||||
switch fv.Kind() {
|
||||
case reflect.String:
|
||||
if fv.String() != "" {
|
||||
p[jsonTag] = fv.Interface()
|
||||
}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if fv.Int() != 0 {
|
||||
p[jsonTag] = fv.Interface()
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if fv.Uint() != 0 {
|
||||
p[jsonTag] = fv.Interface()
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if fv.Float() != 0 {
|
||||
p[jsonTag] = fv.Interface()
|
||||
}
|
||||
default:
|
||||
p[jsonTag] = fv.Interface()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// Ucfirst () 字符串首字母大写
|
||||
func Ucfirst(str string) string {
|
||||
for i, v := range str {
|
||||
return string(unicode.ToUpper(v)) + str[i+1:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Lcfirst () 字符串首字母小写
|
||||
func Lcfirst(str string) string {
|
||||
for i, v := range str {
|
||||
return string(unicode.ToLower(v)) + str[i+1:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// MobileBlurry ()
|
||||
func MobileBlurry(str string) string {
|
||||
|
||||
return str[0:3] + "****" + str[7:]
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package useragent
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// SDKCommonHeader 由 H5/Flutter 将 SDK 完整公共参数转为 UTF-8 JSON,再用标准 Base64 编码后传入。
|
||||
const SDKCommonHeader = "X-SDK-Common"
|
||||
|
||||
// 限制解析开销;正常 SDK 公共参数远小于此大小。
|
||||
const maxSDKCommonHeaderSize = 16 * 1024
|
||||
|
||||
// SDKCommonFields 接收五项 SDK 公共字段及客户端 app_version,完整 fields 中的其他字段自动忽略。
|
||||
// 业务 DevID、用户 ID、渠道等继续走原有业务参数,不能被 SDK 对象覆盖。
|
||||
type SDKCommonFields struct {
|
||||
SID string `json:"sid" bson:"sid"`
|
||||
DeviceID string `json:"device_id" bson:"device_id"`
|
||||
Device string `json:"device" bson:"device"`
|
||||
DeviceFingerprint string `json:"device_fingerprint" bson:"device_fingerprint"`
|
||||
FPVersion string `json:"fp_version" bson:"fp_version"`
|
||||
AppVersion string `json:"app_version" bson:"app_version"` // 客户端版本随下单快照保存,支付回调不使用当前版本补值。
|
||||
}
|
||||
|
||||
// ParseSDKCommonHeader 缺少请求头返回 nil;格式错误交给调用方记录,不阻断业务请求。
|
||||
// 不修剪、截断或推算字段值,SDK 指纹生成失败时也保留其空值。
|
||||
func ParseSDKCommonHeader(header string) (*SDKCommonFields, error) {
|
||||
header = strings.TrimSpace(header)
|
||||
if header == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if len(header) > maxSDKCommonHeaderSize {
|
||||
return nil, fmt.Errorf("SDK 公共参数请求头超过 %d 字节", maxSDKCommonHeaderSize)
|
||||
}
|
||||
data, err := base64.StdEncoding.DecodeString(header)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SDK 公共参数 Base64 解码失败: %w", err)
|
||||
}
|
||||
if !utf8.Valid(data) {
|
||||
return nil, fmt.Errorf("SDK 公共参数不是有效的 UTF-8")
|
||||
}
|
||||
var fields *SDKCommonFields
|
||||
if err := json.Unmarshal(data, &fields); err != nil {
|
||||
return nil, fmt.Errorf("SDK 公共参数 JSON 解析失败: %w", err)
|
||||
}
|
||||
if fields == nil {
|
||||
return nil, fmt.Errorf("SDK 公共参数必须是 JSON 对象,不能为 null")
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
// Clone 下单时保存独立快照,避免请求对象后续变化影响支付事件。
|
||||
func (fields *SDKCommonFields) Clone() *SDKCommonFields {
|
||||
if fields == nil {
|
||||
return nil
|
||||
}
|
||||
snapshot := *fields
|
||||
return &snapshot
|
||||
}
|
||||
|
||||
// MissingFields 仅用于缺失观测;合法空值仍原样上报,不作为业务失败。
|
||||
func (fields *SDKCommonFields) MissingFields() []string {
|
||||
if fields == nil {
|
||||
fields = &SDKCommonFields{}
|
||||
}
|
||||
var missing []string
|
||||
for _, field := range []struct{ name, value string }{
|
||||
{"sid", fields.SID},
|
||||
{"device_id", fields.DeviceID},
|
||||
{"device", fields.Device},
|
||||
{"device_fingerprint", fields.DeviceFingerprint},
|
||||
{"fp_version", fields.FPVersion},
|
||||
{"app_version", fields.AppVersion},
|
||||
} {
|
||||
if strings.TrimSpace(field.value) == "" {
|
||||
missing = append(missing, field.name)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package useragent
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"videoweb/pub/common/enum/consts"
|
||||
"videoweb/pub/common/log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type UA struct {
|
||||
UserAgent string
|
||||
DevID string
|
||||
DevType string
|
||||
SysType string
|
||||
Ver string
|
||||
BuildID string
|
||||
SystemName string
|
||||
SystemVersion string
|
||||
DeviceBrand string
|
||||
DeviceModel string
|
||||
SessionID string
|
||||
SDKCommon *SDKCommonFields // SDK 公共参数独立保存,不能与原业务 UA 字段混用。
|
||||
RequestPath string // 仅用于定位埋点参数缺失的业务接口。
|
||||
}
|
||||
|
||||
func IsH5(sysType string) bool {
|
||||
if sysType == consts.H5IOS || sysType == consts.H5Android || sysType == consts.H5PC || sysType == consts.H5 || sysType == consts.Light {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s UA) IsH5() bool {
|
||||
return IsH5(s.SysType)
|
||||
}
|
||||
|
||||
func UserAgent(c *gin.Context) {
|
||||
var uastr string
|
||||
ua := UA{RequestPath: c.FullPath()}
|
||||
var sdkErr error
|
||||
ua.SDKCommon, sdkErr = ParseSDKCommonHeader(c.GetHeader(SDKCommonHeader))
|
||||
if sdkErr != nil {
|
||||
log.Error("【SDK公共参数解析失败】UserAgent", log.Any("接口", ua.RequestPath), log.E(sdkErr))
|
||||
}
|
||||
ua.SessionID = c.GetHeader("sid")
|
||||
userAgent := c.GetHeader("X-User-Agent") // 兼容 H5 UserAgent
|
||||
if userAgent == "" {
|
||||
userAgent = c.Request.UserAgent()
|
||||
}
|
||||
c.Set(consts.CtxUAH5, c.Request.UserAgent())
|
||||
if userAgent != "" {
|
||||
decodeUa, err := url.QueryUnescape(userAgent)
|
||||
if err != nil {
|
||||
log.Warn("URLDecode user-agent error", log.Any("ua", userAgent))
|
||||
uastr = userAgent
|
||||
} else {
|
||||
uastr = decodeUa
|
||||
}
|
||||
uas := strings.Split(uastr, ";")
|
||||
for _, v := range uas {
|
||||
segment := strings.TrimSpace(v)
|
||||
if segment == "" {
|
||||
continue
|
||||
}
|
||||
vss := strings.SplitN(segment, "=", 2)
|
||||
if len(vss) < 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(vss[0])
|
||||
value := strings.TrimSpace(vss[1])
|
||||
lowerKey := strings.ToLower(key)
|
||||
switch lowerKey {
|
||||
case "devid":
|
||||
ua.DevID = value
|
||||
c.Set(consts.CtxDevId, ua.DevID)
|
||||
case "devtype":
|
||||
ua.DevType = value
|
||||
c.Set(consts.CtxDevType, ua.DevType)
|
||||
case "systype":
|
||||
inputSysType := strings.ToLower(strings.TrimSpace(value))
|
||||
switch {
|
||||
case strings.Contains(inputSysType, "h5_android"):
|
||||
ua.SysType = consts.H5Android
|
||||
case strings.Contains(inputSysType, "h5_ios"):
|
||||
ua.SysType = consts.H5IOS
|
||||
case strings.Contains(inputSysType, "h5_pc"):
|
||||
ua.SysType = consts.H5PC
|
||||
case strings.Contains(inputSysType, "android"):
|
||||
ua.SysType = consts.Android
|
||||
case strings.Contains(inputSysType, "ios"):
|
||||
ua.SysType = consts.IOS
|
||||
case strings.Contains(inputSysType, "h5"):
|
||||
ua.SysType = consts.H5
|
||||
case strings.Contains(inputSysType, "pc"):
|
||||
ua.SysType = consts.PC
|
||||
case strings.Contains(inputSysType, "light"):
|
||||
ua.SysType = consts.Light
|
||||
}
|
||||
c.Set(consts.CtxSysType, ua.SysType)
|
||||
case "ver":
|
||||
ua.Ver = value
|
||||
c.Set(consts.CtxVersion, ua.Ver)
|
||||
case "buildid":
|
||||
ua.BuildID = value
|
||||
c.Set(consts.CtxBuildId, ua.BuildID)
|
||||
case "system_name", "system-name", "systemname":
|
||||
if ua.SystemName == "" {
|
||||
ua.SystemName = value
|
||||
}
|
||||
case "system_version", "system-version", "systemversion":
|
||||
if ua.SystemVersion == "" {
|
||||
ua.SystemVersion = value
|
||||
}
|
||||
case "device_brand", "device-brand", "devicebrand":
|
||||
if ua.DeviceBrand == "" {
|
||||
ua.DeviceBrand = value
|
||||
}
|
||||
case "device_model", "device-model", "devicemodel":
|
||||
if ua.DeviceModel == "" {
|
||||
ua.DeviceModel = value
|
||||
}
|
||||
case "sid":
|
||||
if ua.SessionID == "" {
|
||||
ua.SessionID = value
|
||||
}
|
||||
default:
|
||||
log.Warn("user-agent unknown", log.Any("ua", userAgent), log.Any("segment", segment))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Warn("user-agent empty")
|
||||
}
|
||||
ua.UserAgent = uastr
|
||||
if ua.DeviceBrand == "" {
|
||||
brand := strings.TrimSpace(c.GetHeader("device_brand"))
|
||||
if brand == "" {
|
||||
brand = strings.TrimSpace(c.GetHeader("device-brand"))
|
||||
}
|
||||
ua.DeviceBrand = brand
|
||||
}
|
||||
if ua.DeviceModel == "" {
|
||||
model := strings.TrimSpace(c.GetHeader("device_model"))
|
||||
if model == "" {
|
||||
model = strings.TrimSpace(c.GetHeader("device-model"))
|
||||
}
|
||||
ua.DeviceModel = model
|
||||
}
|
||||
if ua.SystemName == "" {
|
||||
name := strings.TrimSpace(c.GetHeader("system_name"))
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(c.GetHeader("system-name"))
|
||||
}
|
||||
ua.SystemName = name
|
||||
}
|
||||
if ua.SystemVersion == "" {
|
||||
version := strings.TrimSpace(c.GetHeader("system_version"))
|
||||
if version == "" {
|
||||
version = strings.TrimSpace(c.GetHeader("system-version"))
|
||||
}
|
||||
ua.SystemVersion = version
|
||||
}
|
||||
if ua.SystemName == "" {
|
||||
ua.SystemName = ua.SysType
|
||||
}
|
||||
if ua.SystemVersion == "" {
|
||||
ua.SystemVersion = ua.Ver
|
||||
}
|
||||
c.Set(consts.CtxSystemName, ua.SystemName)
|
||||
c.Set(consts.CtxSystemVersion, ua.SystemVersion)
|
||||
c.Set(consts.CtxUA, ua)
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
//数组求交集,差集,并集
|
||||
/*求并集**/
|
||||
func Union(slice1, slice2 []string) []string {
|
||||
m := make(map[string]int)
|
||||
for _, v := range slice1 {
|
||||
m[v]++
|
||||
}
|
||||
|
||||
for _, v := range slice2 {
|
||||
times, _ := m[v]
|
||||
if times == 0 {
|
||||
slice1 = append(slice1, v)
|
||||
}
|
||||
}
|
||||
return slice1
|
||||
}
|
||||
|
||||
// 求交集
|
||||
func Intersect(slice1, slice2 []string) []string {
|
||||
m := make(map[string]int)
|
||||
nn := make([]string, 0)
|
||||
for _, v := range slice1 {
|
||||
m[v]++
|
||||
}
|
||||
|
||||
for _, v := range slice2 {
|
||||
times, _ := m[v]
|
||||
if times == 1 {
|
||||
nn = append(nn, v)
|
||||
}
|
||||
}
|
||||
return nn
|
||||
}
|
||||
|
||||
// 求交集
|
||||
func IntersectInt64(slice1, slice2 []int64) []int64 {
|
||||
m := make(map[int64]int)
|
||||
nn := make([]int64, 0)
|
||||
for _, v := range slice1 {
|
||||
m[v]++
|
||||
}
|
||||
|
||||
for _, v := range slice2 {
|
||||
times, _ := m[v]
|
||||
if times == 1 {
|
||||
nn = append(nn, v)
|
||||
}
|
||||
}
|
||||
return nn
|
||||
}
|
||||
|
||||
// 求差集 slice1-并集
|
||||
func Difference(slice1, slice2 []string) []string {
|
||||
m := make(map[string]int)
|
||||
nn := make([]string, 0)
|
||||
inter := Intersect(slice1, slice2)
|
||||
for _, v := range inter {
|
||||
m[v]++
|
||||
}
|
||||
|
||||
for _, value := range slice1 {
|
||||
times, _ := m[value]
|
||||
if times == 0 {
|
||||
nn = append(nn, value)
|
||||
}
|
||||
}
|
||||
return nn
|
||||
}
|
||||
|
||||
// 求差集 slice1-并集
|
||||
func DifferenceInt64(slice1, slice2 []int64) []int64 {
|
||||
m := make(map[int64]int)
|
||||
nn := make([]int64, 0)
|
||||
inter := IntersectInt64(slice1, slice2)
|
||||
for _, v := range inter {
|
||||
m[v]++
|
||||
}
|
||||
|
||||
for _, value := range slice1 {
|
||||
times, _ := m[value]
|
||||
if times == 0 {
|
||||
nn = append(nn, value)
|
||||
}
|
||||
}
|
||||
return nn
|
||||
}
|
||||
|
||||
// 去重
|
||||
func RemoveRepeat(slc []int64) []int64 {
|
||||
result := []int64{} // 存放结果
|
||||
for i := range slc {
|
||||
flag := true
|
||||
for j := range result {
|
||||
if slc[i] == result[j] {
|
||||
flag = false // 存在重复元素,标识为false
|
||||
break
|
||||
}
|
||||
}
|
||||
if flag { // 标识为false,不添加进结果
|
||||
result = append(result, slc[i])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func IncludeInt32(slc []int32, n int32) bool {
|
||||
for _, v := range slc {
|
||||
if v == n {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IncludeInt64(slc []int64, n int64) bool {
|
||||
for _, v := range slc {
|
||||
if v == n {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func RemoveInt64(slc []int64, n int64) []int64 {
|
||||
for i := 0; i < len(slc); {
|
||||
if slc[i] == n {
|
||||
slc = append(slc[:i], slc[i+1:]...)
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return slc
|
||||
}
|
||||
|
||||
func RemoveString(slc []string, n string) []string {
|
||||
for i := 0; i < len(slc); {
|
||||
if slc[i] == n {
|
||||
slc = append(slc[:i], slc[i+1:]...)
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return slc
|
||||
}
|
||||
|
||||
// RemoveRepeatString 去重string
|
||||
func RemoveRepeatString(slc []string) []string {
|
||||
result := []string{} // 存放结果
|
||||
for i := range slc {
|
||||
flag := true
|
||||
for j := range result {
|
||||
if slc[i] == result[j] {
|
||||
flag = false // 存在重复元素,标识为false
|
||||
break
|
||||
}
|
||||
}
|
||||
if flag { // 标识为false,不添加进结果
|
||||
result = append(result, slc[i])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func IncludeString(slc []string, s string) bool {
|
||||
for _, v := range slc {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func EqualInt64s(slice1, slice2 []int64) bool {
|
||||
if len(slice1) != len(slice2) {
|
||||
return false
|
||||
}
|
||||
for _, v1 := range slice1 {
|
||||
find := false
|
||||
for _, v2 := range slice2 {
|
||||
if v1 == v2 {
|
||||
find = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !find {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func EqualInt64WithOrder(slice1, slice2 []int64) bool {
|
||||
if len(slice1) != len(slice2) {
|
||||
return false
|
||||
}
|
||||
for k, v1 := range slice1 {
|
||||
if slice2[k] != v1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func EqualString(slice1, slice2 []string) bool {
|
||||
if len(slice1) != len(slice2) {
|
||||
return false
|
||||
}
|
||||
for _, v1 := range slice1 {
|
||||
find := false
|
||||
for _, v2 := range slice2 {
|
||||
if v1 == v2 {
|
||||
find = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if find == false {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 切片乱序
|
||||
func ShuffleSlice(slice interface{}) {
|
||||
rv := reflect.ValueOf(slice)
|
||||
if rv.Type().Kind() != reflect.Slice {
|
||||
return
|
||||
}
|
||||
|
||||
length := rv.Len()
|
||||
if length < 2 {
|
||||
return
|
||||
}
|
||||
swap := reflect.Swapper(slice)
|
||||
//rand.Seed(time.Now().Unix())
|
||||
for i := length - 1; i >= 0; i-- {
|
||||
j := rand.Intn(length)
|
||||
swap(i, j)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func RemoveOneInt64(slc []int64, n int64) []int64 {
|
||||
for i := 0; i < len(slc); {
|
||||
if slc[i] == n {
|
||||
slc = append(slc[:i], slc[i+1:]...)
|
||||
break
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return slc
|
||||
}
|
||||
|
||||
// 求最大值
|
||||
func SliceMaxInt64(slc []int64) int64 {
|
||||
maxVal := slc[0]
|
||||
for i := 1; i < len(slc); i++ {
|
||||
//从第二个 元素开始循环比较,如果发现有更大的,则交换
|
||||
if maxVal < slc[i] {
|
||||
maxVal = slc[i]
|
||||
}
|
||||
}
|
||||
return maxVal
|
||||
}
|
||||
|
||||
// 求最大值
|
||||
func SliceMinInt64(slc []int64) int64 {
|
||||
minVal := slc[0]
|
||||
for i := 1; i < len(slc); i++ {
|
||||
//从第二个 元素开始循环比较,如果发现有更小的,则交换
|
||||
if minVal > slc[i] {
|
||||
minVal = slc[i]
|
||||
}
|
||||
}
|
||||
return minVal
|
||||
}
|
||||
|
||||
func ReverseAny(s interface{}) {
|
||||
n := reflect.ValueOf(s).Len()
|
||||
swap := reflect.Swapper(s)
|
||||
for i, j := 0, n-1; i < j; i, j = i+1, j-1 {
|
||||
swap(i, j)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type ID = primitive.ObjectID
|
||||
|
||||
// ToJsonM 将struct转换为 Json Map
|
||||
func ToJsonM(obj interface{}) (map[string]interface{}, error) {
|
||||
j, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]interface{})
|
||||
if err := json.Unmarshal(j, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, err
|
||||
}
|
||||
|
||||
// ToBsonM 将struct转换为 Bson Map
|
||||
func ToBsonM(s interface{}) (bson.M, error) {
|
||||
data, err := bson.Marshal(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(bson.M)
|
||||
if err := bson.Unmarshal(data, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func MapToJsonString(m map[string]string) (string, error) {
|
||||
if data, err := json.Marshal(m); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return string(data), nil
|
||||
}
|
||||
}
|
||||
|
||||
func StructToJsonString(s interface{}) (string, error) {
|
||||
j, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m := make(map[string]interface{})
|
||||
if err := json.Unmarshal(j, &m); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if data, err := json.Marshal(m); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return string(data), nil
|
||||
}
|
||||
}
|
||||
|
||||
// BMapToStruct s is struct ptr
|
||||
func BMapToStruct(s interface{}, m map[string]interface{}) error {
|
||||
bsonBytes, err := bson.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bson.Unmarshal(bsonBytes, s)
|
||||
}
|
||||
|
||||
// JMapToStruct s is struct ptr
|
||||
func JMapToStruct(s interface{}, m map[string]interface{}) error {
|
||||
bsonBytes, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(bsonBytes, s)
|
||||
}
|
||||
|
||||
// JSONStruct2Map 将struct转换为Map
|
||||
func JSONStruct2Map(obj interface{}) (map[string]interface{}, error) {
|
||||
m := make(map[string]interface{})
|
||||
j, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(j, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Map2JSONStruct 将map转换为Json struct
|
||||
func Map2JSONStruct(v interface{}, m map[string]interface{}) error {
|
||||
j, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(j, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func IDArray(ids []string) ([]ID, error) {
|
||||
var idArray []ID
|
||||
for _, id := range ids {
|
||||
_id, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idArray = append(idArray, _id)
|
||||
}
|
||||
|
||||
return idArray, nil
|
||||
}
|
||||
|
||||
// 拼接https://
|
||||
func BindHttpSch(path string) string {
|
||||
if strings.HasPrefix(path, "http") {
|
||||
return path
|
||||
} else {
|
||||
return "https://" + path
|
||||
}
|
||||
}
|
||||
|
||||
func BindUrl(hostname string, path ...string) string {
|
||||
if hostname == "" {
|
||||
return strings.TrimLeft(filepath.Join(path...), "/")
|
||||
}
|
||||
if len(path) == 0 {
|
||||
return hostname
|
||||
}
|
||||
su := strings.TrimLeft(filepath.Join(path...), "/")
|
||||
pr := strings.TrimRight(hostname, "/")
|
||||
return pr + "/" + su
|
||||
}
|
||||
|
||||
func ObjectIDs2String(obj []primitive.ObjectID) []string {
|
||||
arr := []string{}
|
||||
if len(obj) == 0 {
|
||||
return arr
|
||||
}
|
||||
for _, o := range obj {
|
||||
arr = append(arr, o.Hex())
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
func String2ObjectID(obj []string) []primitive.ObjectID {
|
||||
arr := []primitive.ObjectID{}
|
||||
for _, o := range obj {
|
||||
oid, err := primitive.ObjectIDFromHex(o)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
arr = append(arr, oid)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// PercentOf - calculate what percent [number1] is of [number2].
|
||||
// ex. 300 is 12.5% of 2400
|
||||
func PercentOf(part int, total int) float64 {
|
||||
return (float64(part) * float64(100)) / float64(total)
|
||||
}
|
||||
|
||||
func MergeMap(src, dest map[string]interface{}) map[string]interface{} {
|
||||
if len(src) == 0 {
|
||||
return dest
|
||||
}
|
||||
if len(dest) == 0 {
|
||||
return src
|
||||
}
|
||||
if len(src) == 0 && len(dest) == 0 {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
for k, v := range dest {
|
||||
src[k] = v
|
||||
}
|
||||
return src
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DeepCopy 深copy
|
||||
func DeepCopy(dst, src interface{}) error {
|
||||
var buf bytes.Buffer
|
||||
if err := gob.NewEncoder(&buf).Encode(src); err != nil {
|
||||
return err
|
||||
}
|
||||
return gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(dst)
|
||||
}
|
||||
|
||||
// Interface for delegating copy process to type
|
||||
type Interface interface {
|
||||
DeepCopy() interface{}
|
||||
}
|
||||
|
||||
// Iface is an alias to Copy; this exists for backwards compatibility reasons.
|
||||
func Iface(iface interface{}) interface{} {
|
||||
return Copy(iface)
|
||||
}
|
||||
|
||||
// Copy creates a deep copy of whatever is passed to it and returns the copy
|
||||
// in an interface{}. The returned value will need to be asserted to the
|
||||
// correct type.
|
||||
func Copy(src interface{}) interface{} {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make the interface a reflect.Value
|
||||
original := reflect.ValueOf(src)
|
||||
|
||||
// Make a copy of the same type as the original.
|
||||
cpy := reflect.New(original.Type()).Elem()
|
||||
|
||||
// Recursively copy the original.
|
||||
copyRecursive(original, cpy)
|
||||
|
||||
// Return the copy as an interface.
|
||||
return cpy.Interface()
|
||||
}
|
||||
|
||||
// copyRecursive does the actual copying of the interface. It currently has
|
||||
// limited support for what it can handle. Add as needed.
|
||||
func copyRecursive(original, cpy reflect.Value) {
|
||||
// check for implement deepcopy.Interface
|
||||
if original.CanInterface() {
|
||||
if copier, ok := original.Interface().(Interface); ok {
|
||||
cpy.Set(reflect.ValueOf(copier.DeepCopy()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handle according to original's Kind
|
||||
switch original.Kind() {
|
||||
case reflect.Ptr:
|
||||
// Get the actual value being pointed to.
|
||||
originalValue := original.Elem()
|
||||
|
||||
// if it isn't valid, return.
|
||||
if !originalValue.IsValid() {
|
||||
return
|
||||
}
|
||||
cpy.Set(reflect.New(originalValue.Type()))
|
||||
copyRecursive(originalValue, cpy.Elem())
|
||||
|
||||
case reflect.Interface:
|
||||
// If this is a nil, don't do anything
|
||||
if original.IsNil() {
|
||||
return
|
||||
}
|
||||
// Get the value for the interface, not the pointer.
|
||||
originalValue := original.Elem()
|
||||
|
||||
// Get the value by calling Elem().
|
||||
copyValue := reflect.New(originalValue.Type()).Elem()
|
||||
copyRecursive(originalValue, copyValue)
|
||||
cpy.Set(copyValue)
|
||||
|
||||
case reflect.Struct:
|
||||
t, ok := original.Interface().(time.Time)
|
||||
if ok {
|
||||
cpy.Set(reflect.ValueOf(t))
|
||||
return
|
||||
}
|
||||
// Go through each field of the struct and copy it.
|
||||
for i := 0; i < original.NumField(); i++ {
|
||||
// The Type's StructField for a given field is checked to see if StructField.PkgPath
|
||||
// is set to determine if the field is exported or not because CanSet() returns false
|
||||
// for settable fields. I'm not sure why. -mohae
|
||||
if original.Type().Field(i).PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
copyRecursive(original.Field(i), cpy.Field(i))
|
||||
}
|
||||
|
||||
case reflect.Slice:
|
||||
if original.IsNil() {
|
||||
return
|
||||
}
|
||||
// Make a new slice and copy each element.
|
||||
cpy.Set(reflect.MakeSlice(original.Type(), original.Len(), original.Cap()))
|
||||
for i := 0; i < original.Len(); i++ {
|
||||
copyRecursive(original.Index(i), cpy.Index(i))
|
||||
}
|
||||
|
||||
case reflect.Map:
|
||||
if original.IsNil() {
|
||||
return
|
||||
}
|
||||
cpy.Set(reflect.MakeMap(original.Type()))
|
||||
for _, key := range original.MapKeys() {
|
||||
originalValue := original.MapIndex(key)
|
||||
copyValue := reflect.New(originalValue.Type()).Elem()
|
||||
copyRecursive(originalValue, copyValue)
|
||||
copyKey := Copy(key.Interface())
|
||||
cpy.SetMapIndex(reflect.ValueOf(copyKey), copyValue)
|
||||
}
|
||||
|
||||
default:
|
||||
cpy.Set(original)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package utils
|
||||
|
||||
import "os"
|
||||
|
||||
// CreateEmptyFile 创建一个固定size大小的空文件
|
||||
// 如果seek文件失败,则删除文件,并返回错误信息
|
||||
func CreateEmptyFile(name string, size int64) (f *os.File, err error) {
|
||||
defer func() {
|
||||
if err != nil && f != nil {
|
||||
f.Close()
|
||||
os.Remove(name)
|
||||
}
|
||||
}()
|
||||
f, err = os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = f.Seek(size-1, 0); err != nil {
|
||||
return
|
||||
}
|
||||
_, err = f.Write([]byte{0})
|
||||
return
|
||||
}
|
||||
|
||||
func WriteFile(data []byte, filename string) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
os.Remove(filename)
|
||||
}
|
||||
}()
|
||||
|
||||
pi := PathForInfo(filename)
|
||||
if pi.Dir != "" {
|
||||
if err = os.MkdirAll(pi.Dir, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = f.Write(data)
|
||||
f.Close()
|
||||
return
|
||||
}
|
||||
func PathExists(path string) (bool, error) {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package utils
|
||||
|
||||
// // MakeCrc
|
||||
// func MakeCrc(s string) uint32 {
|
||||
// return crc32.ChecksumIEEE([]byte(s))
|
||||
// }
|
||||
|
||||
// func Md5V(str string) string {
|
||||
// h := md5.New()
|
||||
// h.Write([]byte(str))
|
||||
// return hex.EncodeToString(h.Sum(nil))
|
||||
// }
|
||||
@@ -0,0 +1,283 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var once sync.Once
|
||||
var _client *http.Client
|
||||
|
||||
func GetHttpClient() *http.Client {
|
||||
once.Do(func() {
|
||||
_client = &http.Client{
|
||||
Timeout: time.Minute * 1, //设置超时时间
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
TLSHandshakeTimeout: 10 * time.Second, //限制 TLS握手的时间
|
||||
ResponseHeaderTimeout: 60 * time.Second, //限制读取response header的时间,默认 timeout + 5*time.Second
|
||||
ExpectContinueTimeout: 1 * time.Second, //限制client在发送包含 Expect: 100-continue的header到收到继续发送body的response之间的时间等待。
|
||||
MaxIdleConns: 100, //所有host的连接池最大连接数量,默认无穷大
|
||||
MaxIdleConnsPerHost: 300, //每个host的连接池最大空闲连接数,默认2
|
||||
MaxConnsPerHost: 100, //每个host的最大连接数量
|
||||
IdleConnTimeout: 3 * time.Minute, //how long an idle connection is kept in the connection pool.
|
||||
},
|
||||
}
|
||||
})
|
||||
return _client
|
||||
}
|
||||
|
||||
func getBytes(request interface{}) ([]byte, error) {
|
||||
var body []byte
|
||||
switch t := request.(type) {
|
||||
case []byte:
|
||||
body = t
|
||||
case string:
|
||||
body = []byte(t)
|
||||
default:
|
||||
var err error
|
||||
body, err = json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
type OnHttpResponse func(res *http.Response) error
|
||||
|
||||
func HttpRequest(req *http.Request, onResponse OnHttpResponse) error {
|
||||
client := GetHttpClient()
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if onResponse != nil {
|
||||
err = onResponse(res)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func HttpMethod(url, method string, header http.Header, body io.Reader, onResponse OnHttpResponse) error {
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if header != nil {
|
||||
req.Header = header
|
||||
}
|
||||
return HttpRequest(req, onResponse)
|
||||
}
|
||||
|
||||
func HttpGet(url string, header http.Header, body io.Reader, onResponse OnHttpResponse) error {
|
||||
return HttpMethod(url, http.MethodGet, header, body, onResponse)
|
||||
}
|
||||
|
||||
func HttpPost(url string, header http.Header, body io.Reader, onResponse OnHttpResponse) error {
|
||||
return HttpMethod(url, http.MethodPost, header, body, onResponse)
|
||||
}
|
||||
|
||||
func HttpDelete(url string, header http.Header, body io.Reader, onResponse OnHttpResponse) error {
|
||||
return HttpMethod(url, http.MethodDelete, header, body, onResponse)
|
||||
}
|
||||
|
||||
func HttpPostRequestJson(url string, request interface{}, resp interface{}) error {
|
||||
header := make(http.Header)
|
||||
header.Set("Content-Type", "application/json")
|
||||
body, err := getBytes(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return HttpPost(url, header, bytes.NewReader(body), func(res *http.Response) error {
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("http request (%s) error %s", url, res.Status)
|
||||
}
|
||||
content, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(content, resp)
|
||||
})
|
||||
}
|
||||
|
||||
func POSTJson(url string, headers map[string]string, data interface{}) (*http.Response, error) {
|
||||
header := make(http.Header)
|
||||
for k, v := range headers {
|
||||
header.Set(k, v)
|
||||
}
|
||||
body, err := getBytes(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = header
|
||||
client := GetHttpClient()
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func GetWithParam(url string, headers map[string]string, params ...interface{}) (*http.Response, error) {
|
||||
header := make(http.Header)
|
||||
for k, v := range headers {
|
||||
header.Set(k, v)
|
||||
}
|
||||
for _, p := range params {
|
||||
url = addParams(url, toUrlValues(p))
|
||||
}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = header
|
||||
client := GetHttpClient()
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func GetWithJResp(bind interface{}, url string, headers map[string]string, params ...interface{}) (int, error) {
|
||||
header := make(http.Header)
|
||||
for k, v := range headers {
|
||||
header.Set(k, v)
|
||||
}
|
||||
for _, p := range params {
|
||||
url = addParams(url, toUrlValues(p))
|
||||
}
|
||||
code := http.StatusBadRequest
|
||||
err := HttpGet(url, header, nil, func(res *http.Response) error {
|
||||
code = res.StatusCode
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("http request (%s) error %s", url, res.Status)
|
||||
}
|
||||
content, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(content, bind)
|
||||
})
|
||||
return code, err
|
||||
}
|
||||
|
||||
func POSTWithJResp(bind interface{}, url string, headers map[string]string, params interface{}) (int, error) {
|
||||
header := make(http.Header)
|
||||
for k, v := range headers {
|
||||
header.Set(k, v)
|
||||
}
|
||||
body, err := getBytes(params)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
code := http.StatusBadRequest
|
||||
err = HttpPost(url, header, bytes.NewReader(body), func(res *http.Response) error {
|
||||
code = res.StatusCode
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("http request (%s) error %s", url, res.Status)
|
||||
}
|
||||
content, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(content, bind)
|
||||
})
|
||||
return code, err
|
||||
}
|
||||
|
||||
func POSTJsonWithJResp(bind interface{}, url string, headers map[string]string, data interface{}) (int, error) {
|
||||
header := make(http.Header)
|
||||
for k, v := range headers {
|
||||
header.Set(k, v)
|
||||
}
|
||||
header.Set("Content-Type", "application/json")
|
||||
body, err := getBytes(data)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
code := http.StatusBadRequest
|
||||
err = HttpPost(url, header, bytes.NewReader(body), func(res *http.Response) error {
|
||||
code = res.StatusCode
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("http request (%s) error %s", url, res.Status)
|
||||
}
|
||||
content, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(content, bind)
|
||||
})
|
||||
return code, err
|
||||
}
|
||||
|
||||
func DeleteWithJResp(bind interface{}, url string, headers map[string]string, params interface{}) (int, error) {
|
||||
header := make(http.Header)
|
||||
for k, v := range headers {
|
||||
header.Set(k, v)
|
||||
}
|
||||
body, err := getBytes(params)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
code := http.StatusBadRequest
|
||||
err = HttpDelete(url, header, bytes.NewReader(body), func(res *http.Response) error {
|
||||
code = res.StatusCode
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("http request (%s) error %s", url, res.Status)
|
||||
}
|
||||
content, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(content, bind)
|
||||
})
|
||||
return code, err
|
||||
}
|
||||
|
||||
// Add params to a url string.
|
||||
func addParams(url_ string, params url.Values) string {
|
||||
if len(params) == 0 {
|
||||
return url_
|
||||
}
|
||||
|
||||
if !strings.Contains(url_, "?") {
|
||||
url_ += "?"
|
||||
}
|
||||
|
||||
if strings.HasSuffix(url_, "?") || strings.HasSuffix(url_, "&") {
|
||||
url_ += params.Encode()
|
||||
} else {
|
||||
url_ += "&" + params.Encode()
|
||||
}
|
||||
|
||||
return url_
|
||||
}
|
||||
|
||||
func toUrlValues(v interface{}) url.Values {
|
||||
switch t := v.(type) {
|
||||
case url.Values:
|
||||
return t
|
||||
case map[string][]string:
|
||||
return url.Values(t)
|
||||
case map[string]string:
|
||||
rst := make(url.Values)
|
||||
for k, v := range t {
|
||||
rst.Add(k, v)
|
||||
}
|
||||
return rst
|
||||
case nil:
|
||||
return make(url.Values)
|
||||
default:
|
||||
panic("Invalid value")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
const (
|
||||
userIDKey = "userId"
|
||||
ipKey = "ip"
|
||||
uaKey = "ua"
|
||||
)
|
||||
|
||||
type IPClaims struct {
|
||||
Ip string
|
||||
Expire time.Duration
|
||||
UA string
|
||||
}
|
||||
|
||||
func GenerateIpToken(claims IPClaims, secret string) (string, error) {
|
||||
return generateToken(jwt.MapClaims{
|
||||
ipKey: claims.Ip,
|
||||
uaKey: claims.UA,
|
||||
"exp": Now().Add(claims.Expire).Unix(),
|
||||
"issuer": "com.butterfly",
|
||||
"sub": "asign",
|
||||
"aud": "public",
|
||||
}, []byte(secret))
|
||||
}
|
||||
|
||||
func ParseIpToken(tokenString string, secret string) (pass bool, ip, ua string, err error) {
|
||||
t, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
var isExpire *bool
|
||||
if vErr, ok := err.(*jwt.ValidationError); ok {
|
||||
e := vErr.Errors&jwt.ValidationErrorExpired > 0
|
||||
isExpire = &e
|
||||
}
|
||||
if err != nil || isExpire == nil {
|
||||
return
|
||||
}
|
||||
var ok bool
|
||||
var jc jwt.MapClaims
|
||||
jc, ok = t.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
err = errors.New("parse token fail")
|
||||
return
|
||||
}
|
||||
ip, ok = jc[ipKey].(string)
|
||||
if !ok {
|
||||
err = errors.New("parse token fail")
|
||||
return
|
||||
}
|
||||
ua, ok = jc[uaKey].(string)
|
||||
if !ok {
|
||||
err = errors.New("parse token fail")
|
||||
return
|
||||
}
|
||||
pass = true
|
||||
return
|
||||
}
|
||||
|
||||
type UserClaims struct {
|
||||
UserID int64
|
||||
Expire time.Duration
|
||||
}
|
||||
|
||||
func generateToken(claims jwt.MapClaims, secret []byte) (string, error) {
|
||||
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secret)
|
||||
}
|
||||
|
||||
func GenerateUserToken(claims UserClaims, secret string) (string, error) {
|
||||
return generateToken(jwt.MapClaims{
|
||||
userIDKey: claims.UserID,
|
||||
"exp": Now().Add(claims.Expire).Unix(),
|
||||
"issuer": "com.butterfly",
|
||||
"sub": "asign",
|
||||
"aud": "public",
|
||||
}, []byte(secret))
|
||||
}
|
||||
|
||||
// ParseToken 解析Token
|
||||
func ParseUserToken(token string, secret string) (userId int64, newToken string, err error) {
|
||||
t, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
var isExpire *bool
|
||||
if vErr, ok := err.(jwt.ValidationError); ok {
|
||||
e := vErr.Errors&jwt.ValidationErrorExpired > 0
|
||||
isExpire = &e
|
||||
}
|
||||
if err != nil && isExpire == nil {
|
||||
return
|
||||
}
|
||||
|
||||
jc, ok := t.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
err = errors.New("unknown token error1")
|
||||
return
|
||||
}
|
||||
|
||||
id, ok := jc[userIDKey].(float64)
|
||||
if !ok {
|
||||
err = errors.New("unknown token error1")
|
||||
return
|
||||
}
|
||||
userId = int64(id)
|
||||
if isExpire != nil && *isExpire {
|
||||
newToken, err = GenerateUserToken(UserClaims{UserID: userId, Expire: 30 * 24 * time.Hour}, secret)
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
|
||||
// Strips 'Bearer ' prefix from bearer token string
|
||||
func StripBearerPrefixFromTokenString(tok string) string {
|
||||
if len(tok) > 6 && strings.ToUpper(tok[0:7]) == "BEARER " {
|
||||
return tok[7:]
|
||||
} else {
|
||||
return tok
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CountDigits 有几位数字
|
||||
func CountDigits(i int) (count int) {
|
||||
for i != 0 {
|
||||
i /= 10
|
||||
count++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func RandInt(min int, max int) int {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
return min + rand.Intn(max-min)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package utils
|
||||
|
||||
func GetPageRange(totalLen, pageNum, pageSize int) (start, end int) {
|
||||
if pageNum < 1 {
|
||||
pageNum = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 10
|
||||
}
|
||||
start = (pageNum - 1) * pageSize
|
||||
if start > totalLen {
|
||||
start = totalLen
|
||||
}
|
||||
end = start + pageSize
|
||||
if end > totalLen {
|
||||
end = totalLen
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/purell"
|
||||
)
|
||||
|
||||
// PathSegment 路由路径段:有 slug 用 slug,否则回退为数字 id。
|
||||
func PathSegment(slug string, id int64) string {
|
||||
if s := strings.TrimSpace(slug); s != "" {
|
||||
return s
|
||||
}
|
||||
if id > 0 {
|
||||
return strconv.FormatInt(id, 10)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// PathInfo 路径信息
|
||||
type PathInfo struct {
|
||||
Dir string // 目录
|
||||
Ext string // 扩展名
|
||||
Filename string // 没有扩展名的文件名名称
|
||||
FullFileName string // 文件全名
|
||||
}
|
||||
|
||||
// PathForInfo 解析路径
|
||||
// 返回文件,路径,文件名等信息
|
||||
func PathForInfo(path string) (pi PathInfo) {
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
ext := filepath.Ext(path)
|
||||
dir, name := filepath.Split(path)
|
||||
|
||||
pi.Dir = dir
|
||||
pi.Ext = ext
|
||||
pi.Filename = strings.TrimRight(name, ext)
|
||||
pi.FullFileName = name
|
||||
return
|
||||
}
|
||||
|
||||
// JoinUrlPath 域名与uri拼接(走 purell 标准化,会剥掉尾部斜杠)。
|
||||
// 适用于 CDN 文件链接(m3u8 分段、vtt、静态图等)这类"不需要尾部斜杠"的场景。
|
||||
// 需要保留尾部斜杠的页面 URL(sitemap/rss/indexnow)请使用 JoinUrlPathKeepSlash。
|
||||
func JoinUrlPath(host string, uri string) (urlpath string) {
|
||||
urlpath = joinRawUrlPath(host, uri)
|
||||
if urlpath == "" {
|
||||
return
|
||||
}
|
||||
// 标准化 URL
|
||||
normalized, err := purell.NormalizeURLString(urlpath, purell.FlagsUsuallySafeGreedy)
|
||||
if err != nil {
|
||||
return urlpath
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
// JoinUrlPathKeepSlash 域名与uri拼接,保留 uri 原始的尾部斜杠,不做任何标准化。
|
||||
// sitemap/rss/indexnow 的页面 URL 必须与路由注册的 /archives/:id/ 等完全一致,
|
||||
// 否则爬虫每次访问都会被 gin 的 RedirectTrailingSlash 301 到带 / 的地址,
|
||||
// 浪费抓取预算并造成重定向链。
|
||||
func JoinUrlPathKeepSlash(host string, uri string) string {
|
||||
return joinRawUrlPath(host, uri)
|
||||
}
|
||||
|
||||
// joinRawUrlPath 纯拼接,不做任何标准化。
|
||||
func joinRawUrlPath(host string, uri string) (urlpath string) {
|
||||
if uri == "" {
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case strings.HasSuffix(host, "/") && strings.HasPrefix(uri, "/"):
|
||||
urlpath = host[0:len(host)-1] + uri
|
||||
case !strings.HasSuffix(host, "/") && !strings.HasPrefix(uri, "/"):
|
||||
urlpath = host + "/" + uri
|
||||
default:
|
||||
urlpath = host + uri
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func JoinUrlPathV2(host string, uri string) string {
|
||||
return path.Join(host, uri)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"time"
|
||||
"videoweb/pub/common/enum/redisconst"
|
||||
sErr "videoweb/pub/common/errors"
|
||||
"videoweb/pub/redis"
|
||||
)
|
||||
|
||||
const (
|
||||
expireTime = 30 * 24 * time.Hour
|
||||
)
|
||||
|
||||
type TokenRedis struct {
|
||||
}
|
||||
|
||||
func NewTokenRedis() *TokenRedis {
|
||||
return &TokenRedis{}
|
||||
}
|
||||
|
||||
func (tr *TokenRedis) Save(userId int64, token string) (err error) {
|
||||
if err = tr.Delete(userId); err != nil {
|
||||
return
|
||||
}
|
||||
err = redis.Set(redisconst.AppUserTokenKey(userId), token, expireTime)
|
||||
if err != nil {
|
||||
err = sErr.MakeWithErr(sErr.RedisSet, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (tr *TokenRedis) Exists(userId int64, token string) (has bool, err error) {
|
||||
tokenbytes, err := tr.Get(userId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
has = bytes.Compare(tokenbytes, []byte(token)) == 0
|
||||
return
|
||||
}
|
||||
|
||||
func (tr *TokenRedis) Get(userId int64) (token []byte, err error) {
|
||||
token, err = redis.GetBytes(redisconst.AppUserTokenKey(userId))
|
||||
if err != nil {
|
||||
err = sErr.MakeWithErr(sErr.RedisGet, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (tr *TokenRedis) Delete(userId int64) (err error) {
|
||||
err = redis.Del(redisconst.AppUserTokenKey(userId))
|
||||
if err != nil {
|
||||
err = sErr.MakeWithErr(sErr.RedisDel, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func ShellCmd(cmdStr string) (string, error) {
|
||||
cmd := exec.Command("/bin/bash", "-c", cmdStr)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
ec := cmd.Run()
|
||||
|
||||
return out.String(), ec
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user