Initial commit: web_hghl_server

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 16:28:14 +08:00
co-authored by Claude Opus 4.7
commit 65fca60622
653 changed files with 135901 additions and 0 deletions
+138
View File
@@ -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 // 删除一个媒体
_ // 已移除帖子新增消息,保留原数值
_ // 已移除帖子删除消息,保留原数值
)
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
}
}
}
})
}
+184
View File
@@ -0,0 +1,184 @@
package common
import (
"errors"
"fmt"
"net/http"
"os"
"time"
"videoweb/pub/common/enum/consts"
"videoweb/pub/common/log"
"videoweb/pub/common/srvdef"
"videoweb/pub/common/useragent"
"strconv"
"github.com/gin-gonic/gin"
)
var (
cryptSecret string
env consts.RunMod
responseDataTransform func(*gin.Context, interface{}) interface{}
//routineCount int64
)
// SetResponseDataTransform 设置当前服务的成功响应数据转换函数
func SetResponseDataTransform(transform func(*gin.Context, interface{}) interface{}) {
responseDataTransform = transform
}
// 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()
if code == srvdef.Success && responseDataTransform != nil {
data = responseDataTransform(c, data)
}
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 = string(ua.SysType)
}
if devType != "" {
devType = ua.DevType
}
log.Warn("Error:",
log.Any("UID", strconv.FormatInt(id, 10)),
log.Any("IP", c.ClientIP()),
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()
if code == srvdef.Success && responseDataTransform != nil {
data = responseDataTransform(c, data)
}
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 = string(ua.SysType)
}
if devType != "" {
devType = ua.DevType
}
log.Warn("Error:",
log.Any("UID", strconv.FormatInt(id, 10)),
log.Any("IP", c.ClientIP()),
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 = ctx.ClientIP()
}
}
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"}
}
+135
View File
@@ -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
}
+119
View File
@@ -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)
}
+148
View File
@@ -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
}
+43
View File
@@ -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
}
+615
View File
@@ -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
}
+27
View File
@@ -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))
}
+145
View File
@@ -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
}
+126
View File
@@ -0,0 +1,126 @@
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"
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
}
+226
View File
@@ -0,0 +1,226 @@
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
//禁止登陆设置
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
// 评论缓存key和过期时间
CommentListKey = "commentList_%d_%d"
CommentListKeyExpire = time.Minute * 5
// 视频收藏列表分页缓存key、版本key和过期时间
FavoriteListVersionKeyExpire = time.Hour * 24
FavoriteVideoListKey = "favoriteVideoList:v%d:%d:%d:%d"
FavoriteVideoListVersionKey = "favoriteVideoListVersion:%d"
FavoriteVideoListKeyExpire = time.Minute * 30
// 问卷提交频率限制(同一 IP / 用户每分钟只能提交一次)
questionnaireLimitIp = "questionnaireLimit:ip:%s"
questionnaireLimitUser = "questionnaireLimit:user:%d"
QuestionnaireLimitExpire = time.Minute
// m3u8 / enkey 防盗链
// IP 自然日正片去重计数(key 含日期,次日自动换桶)
m3u8DayRateLimitIp = "m3u8dayrate:ip:%s:%s" // %s=ip, %s=日期 20060102
m3u8DailyMediaIpFmt = "m3u8:daily:media:ip:%s:%d:%s" // ip, mediaId, 日期
M3U8DayRateLimitMax = int64(80) // 每 IP 每天最多 80 个不同正片(sys_cfg 未配置时的默认值)
// enkey 一次性票据:/h5/m3u8 鉴权通过后写入,/enkey 消费后删除
enkeyTicketKey = "enkey:ticket:%s" // %s = token 前16位
EnkeyTicketExpire = 15 * time.Minute
// 访问IP去重统计:每个 appId 每天一个 SET
ipStatKey = "ipstat:%d:%s" // %d=appId, %s=日期 "20060102"
IpStatExpire = 49 * time.Hour // 保留49小时,确保昨日数据在今天仍可读
)
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)
}
// AppPlayletHomeCacheVersionKey 用于后台变更后切换短剧首页缓存版本。
func AppPlayletHomeCacheVersionKey() string {
return fmt.Sprintf("app:playlet:home:version:%d", pubdef.AppId)
}
// AppPlayletRankCacheVersionKey 用于短剧删除后刷新榜单缓存
func AppPlayletRankCacheVersionKey() string {
return fmt.Sprintf("app:playlet:rank:version:%d", pubdef.AppId)
}
func MediaM3U8Key(name string) string {
return fmt.Sprintf(mediaM3U8Fmt, name)
}
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)
}
// CommentListCacheKey 返回评论列表缓存key
func CommentListCacheKey(objectId int64, objectType pubdef.CommentObjectType) string {
return fmt.Sprintf(CommentListKey, objectId, objectType)
}
func FavoriteVideoListCacheKey(userId, version, pageNum, pageSize int64) string {
return fmt.Sprintf(FavoriteVideoListKey, version, userId, pageNum, pageSize)
}
func FavoriteVideoListVersionCacheKey(userId int64) string {
return fmt.Sprintf(FavoriteVideoListVersionKey, userId)
}
func QuestionnaireLimitIpKey(ip string) string {
ip = strings.ReplaceAll(ip, ":", "-")
return fmt.Sprintf(questionnaireLimitIp, ip)
}
func QuestionnaireLimitUserKey(userId int64) string {
return fmt.Sprintf(questionnaireLimitUser, userId)
}
// M3U8DayRateLimitIpKey 按自然日(Asia/Shanghai)生成 IP 日限流 key
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)
}
// M3U8DayRateLimitExpire 返回距下一个自然日 0 点的时长,用于 key TTL
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
}
// EnkeyTicketKey token 的前 16 个字符作为 key,避免存完整 token 太长
func EnkeyTicketKey(token string) string {
if len(token) > 16 {
token = token[:16]
}
return fmt.Sprintf(enkeyTicketKey, token)
}
func normalizeM3U8Ip(ip string) string {
return strings.ReplaceAll(ip, ":", "-")
}
// M3U8DailyMediaIpKey 同一 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)
}
// IpStatKey 返回指定 appId 在某天的访问IP集合 key
// date 格式: "20060102"
func IpStatKey(appId int64, date string) string {
return fmt.Sprintf(ipStatKey, appId, date)
}
+27
View File
@@ -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
)
+169
View File
@@ -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 //
_ // 已移除帖子参数错误码,保留原数值
_ // 已移除帖子重复发送错误码,保留原数值
_ // 已移除帖子不存在错误码,保留原数值
_ // 已移除帖子重复点赞错误码,保留原数值
_ // 已移除帖子取消点赞错误码,保留原数值
_ // 已移除帖子重复收藏错误码,保留原数值
_ // 已移除帖子取消收藏错误码,保留原数值
_ // 已移除帖子重复屏蔽错误码,保留原数值
_ // 已移除帖子取消屏蔽错误码,保留原数值
UserDoNotForSelf // 用户不能对自己进行此项操作
UserWatchDupi // 不可重复关注同一用户
UserNoWatch // 没有关注该用户,不可取消关注
_ // 已移除帖子回复参数错误码,保留原数值
_ // 已移除帖子评论不存在错误码,保留原数值
_ // 已移除帖子评论已存在错误码,保留原数值
NoWatchableUsers // 没有可以关注的用户池
_ // 已移除帖子评论点赞错误码,保留原数值
_ // 已移除帖子评论取消点赞错误码,保留原数值
_ // 已移除帖子定价错误码,保留原数值
_ // 已移除帖子已购买错误码,保留原数值
NoBalance // 余额不足
BalanceErr // 编辑余额错误
NoAccount // 没账户
AccountDisabled // 账户已被封禁
BuySelf // 不能购买自己的内容
RedisError // 服务器繁忙
Button // 落地页按钮
ReplyTimeShort // 回复间隔时间太短
OperateLog // 日志出错
RoleErr // 角色权限不够
DocExists // 查找的文档已存在
_ // 已移除帖子未知错误码,保留原数值
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
)
+39
View File
@@ -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)
}
+24
View File
@@ -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)
}
+19
View File
@@ -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 = "余额不足"
)
+6
View File
@@ -0,0 +1,6 @@
package fconst
const (
FileDir = "file-tmp"
FsDir = "/fs"
)
+26
View File
@@ -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()
}()
}
+34
View File
@@ -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)
}
+67
View File
@@ -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
}
+202
View File
@@ -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
}
+16
View File
@@ -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:
}
+321
View File
@@ -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
}
+105
View File
@@ -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)
}
+74
View File
@@ -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
}
+151
View File
@@ -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
}
+420
View File
@@ -0,0 +1,420 @@
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 统计。
// CDN/LB 链路下 Gin ClientIP 常返回离源站最近的节点 IP(如 CloudFront);
// 此时优先取 X-Forwarded-For 最左侧公网 IP(真实访客)。
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("CF-Connecting-IP")); ip != "" {
return ip
}
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
}
// shouldTrustForwardedHeaders 判断是否处于反代/CDN 链路,可信任 XFF/X-Real-IP。
// 公网直连时不信任,避免客户端伪造 XFF。
func shouldTrustForwardedHeaders(remoteIP, clientIP string) bool {
if isPrivateOrLoopbackIP(remoteIP) {
return true
}
// Gin 从 XFF 解析出的 ClientIP 与 TCP 直连 IP 不同,说明经过了 LB/CDN。
return clientIP != "" && remoteIP != "" && clientIP != remoteIP
}
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()
}
+115
View File
@@ -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)
}
+89
View File
@@ -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
}
+77
View File
@@ -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) }
}
+292
View File
@@ -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
}
+212
View File
@@ -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)
}
+28
View File
@@ -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")
}
}
+86
View File
@@ -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
}
+143
View File
@@ -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
}
}
+75
View File
@@ -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)
}
+128
View File
@@ -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
)
+44
View File
@@ -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)
}
+150
View File
@@ -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)
}
}
+9
View File
@@ -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)
}
}
+518
View File
@@ -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
}
+183
View File
@@ -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) }
+63
View File
@@ -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 返回短剧原片使用的解密密钥。
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
}
+192
View File
@@ -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, "")
}
+199
View File
@@ -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
}
+116
View File
@@ -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
}
+152
View File
@@ -0,0 +1,152 @@
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。
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
}
+13
View File
@@ -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"`
}
+18
View File
@@ -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
// }
+96
View File
@@ -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()
}
+68
View File
@@ -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
}
+55
View File
@@ -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 relativePath == "" {
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()
}
+10
View File
@@ -0,0 +1,10 @@
package netutil
import (
"fmt"
"testing"
)
func Test_GetLocalIP(t *testing.T) {
fmt.Println(GetLocalIP())
}
+580
View File
@@ -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 &GTEMatch{
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 &GTMatch{
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 &LTMatch{
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 &LTEMatch{
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 &GTEAndLTEMatch{
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 &GTEAndLTMatch{
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)
// }
+39
View File
@@ -0,0 +1,39 @@
package playletcache
import (
"videoweb/pub/common/enum/redisconst"
"videoweb/pub/common/log"
"videoweb/pub/redis"
)
// ClearHomeCache 切换缓存版本,旧数据由 TTL 自动过期
func ClearHomeCache() {
if redis.Client() == nil {
return
}
if err := redis.Client().Incr(redisconst.AppPlayletHomeCacheVersionKey()).Err(); err != nil {
log.Warn("更新短剧首页缓存版本失败", log.E(err))
}
}
// ClearRankCache 更新短剧榜单缓存版本
func ClearRankCache() {
if redis.Client() == nil {
return
}
if err := redis.Client().Incr(redisconst.AppPlayletRankCacheVersionKey()).Err(); err != nil {
log.Warn("更新短剧榜单缓存版本失败", log.E(err))
}
}
// RankCacheVersion 查询短剧榜单缓存版本
func RankCacheVersion() int64 {
if redis.Client() == nil {
return 0
}
version, err := redis.Client().Get(redisconst.AppPlayletRankCacheVersionKey()).Int64()
if err != nil {
return 0
}
return version
}
+83
View File
@@ -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{}
}
+53
View File
@@ -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)
}
+42
View File
@@ -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
+45
View File
@@ -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
}
+106
View File
@@ -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
}
+36
View File
@@ -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
}
+77
View File
@@ -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)
}
+1
View File
@@ -0,0 +1 @@
package recomapi
+26
View File
@@ -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{}
+9
View File
@@ -0,0 +1,9 @@
package response
// BasePageResp 分页数据返回结构体
type BasePageResp struct {
TotalCount int64 `json:"totalCount"` // 总数量
PageSize int `json:"pageSize"` // 每页数量
PageNum int `json:"pageNum"` // 当前页码
List interface{} `json:"list"` // 当前页数据
}
+70
View File
@@ -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}},
},
}
}
+76
View File
@@ -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
}
+45
View File
@@ -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)
}
+8
View File
@@ -0,0 +1,8 @@
package sms
import "testing"
func TestSend(t *testing.T) {
err := Send("15008279990", "654321")
t.Log(err)
}
+331
View File
@@ -0,0 +1,331 @@
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 (
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 // 视频不需要支付
_ // 已移除帖子不需要支付错误码,保留原数值
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 // 文件上传失败
_ // 已移除帖子状态错误码,保留原数值
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 //系统配置错误
_ // 已移除漫画分类错误码,保留原数值
_ // 已移除漫画标签错误码,保留原数值
_ // 已移除漫画主题错误码,保留原数值
_ // 已移除漫画章节不存在错误码,保留原数值
_ // 已移除漫画不存在错误码,保留原数值
_ // 已移除漫画CV错误码,保留原数值
_ // 已移除漫画视频错误码,保留原数值
_ // 已移除漫画排行榜错误码,保留原数值
VomicsTopicNotExist // 有声漫不存在
_ // 已移除漫画黑名单错误码,保留原数值
CommentTextIsEmpty // 评论内容为空
_ // 已移除漫画会员错误码,保留原数值
_ // 已移除漫画付费错误码,保留原数值
_ // 已移除漫画章节已存在错误码,保留原数值
ObjectNotExist // 数据不存在
GameWtdrErroTip3 // 请填写银行名字或升级App版本
UserDoTaskErr // 用户任务出错
GetPrizeFail // 获取奖品失败
_ // 已移除小说付费错误码,保留原数值
_ // 已移除小说会员错误码,保留原数值
LotteryActivityNotEnable // 抽奖活动没有启用
CurrTimeNotActivity // 当前时间没有活动
AIOrderStripsCountLimit // 今天AI脱衣提交次数达到上限
NotWtdrChannel // 没有匹配到提现通过
ExcelExportErr // 文件导出错误
_ // 已移除漫画活动错误码,保留原数值
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 // 不是会员禁止发布
PublistContentErrDenied // 包含敏感词汇
_ // 已移除帖子重复购买错误码,保留原数值
UserNotEnoughMoney // 用户的钻石数量不足
PostStatusError // 订单状态错误
PostNotAllowComplaint // 不允许举报
UserNotEnoughScore // 用户的钻石数量不足
ErrUserAccExist // 账号已存在
UserAccountNotBind // 账号未注册
_ // 已移除帖子上架数量限制错误码,保留原数值
ErrUserBeBanned //ErrUserBeBanned 用户已经禁言 1004
ErrCustomerBanned //ErrCustomerBanned 客服已休息 1005
ErrVersionUpdate //ErrVersionUpdate 版本强制更新 1006
)
+58
View File
@@ -0,0 +1,58 @@
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",
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",
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",
}
+67
View File
@@ -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, "", "")
)
+160
View File
@@ -0,0 +1,160 @@
package srvdef
var codeAndTip = map[CodeType]string{
Success: "成功",
Failure: "失敗",
ErrCardNotExists: "沒有會員卡信息",
ErrMadouCoinNotExists: "沒有金幣信息",
ErrGetRechargeType: "充值方式獲取錯誤",
ErrRchgOrderNotExist: "充值訂單不存在",
ErrBuyVIPCardFail: "購買會員卡失敗",
ErrOperationTooFrequently: "用戶操作太頻繁",
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: "视频不需要支付",
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操作错误",
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: "没有卡券信息",
CouponCanNotUse: "卡券无法使用",
LotteryActivityNotEnable: "抽奖活动没有启用",
CurrTimeNotActivity: "当前时间没有活动",
AIOrderStripsCountLimit: "今天AI脱衣提交次数达到上限",
NotWtdrChannel: "没有匹配到提现通过",
UserRepeatDrawDiscountCard: "重复翻牌",
NoUseDiscountCard: "不可使用折扣卡",
ErrUserAccExist: "账号已存在",
AIOrderNotDel: "未完成的订单不能删除",
AIWaitOrderTooMuch: "排队中的AI订单太多,请稍后再试",
ActorNotExist: "演员不存在",
VideoRepeat: "视频重复",
ErrReqForbidden: "请求被拒",
RepeatActress: "重复的女优",
}
+46
View File
@@ -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"`
}
+27
View File
@@ -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))
}
}
}
+102
View File
@@ -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))
}
+94
View File
@@ -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()
}
+169
View File
@@ -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)
}
}
+35
View File
@@ -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")
}
+55
View File
@@ -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
}
+79
View File
@@ -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"` // 时间
}
+64
View File
@@ -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:]
}
+83
View File
@@ -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
}
+109
View File
@@ -0,0 +1,109 @@
package useragent
import (
"net/url"
"strings"
"videoweb/pub/common/enum/consts"
"videoweb/pub/common/log"
"videoweb/pub/pubdef"
"github.com/gin-gonic/gin"
)
type UA struct {
DevID string
DevType string
SysType pubdef.SysType
Ver string
BuildID string
UserAgent string
SDKCommon *SDKCommonFields // SDK 公共参数独立保存,不能与原业务 UA 字段混用。
RequestPath string // 仅用于定位埋点参数缺失的业务接口。
}
func IsH5(sysType pubdef.SysType) bool {
return sysType.IsH5()
}
func (s UA) IsH5() bool {
return s.SysType.IsH5()
}
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))
}
userAgent := c.GetHeader("X-User-Agent") //兼容H5 UserAgent
if userAgent == "" {
userAgent = c.Request.UserAgent()
}
ua.UserAgent = userAgent
if userAgent != "" {
// "BuildID=com.abc.Butterfly;SysType=h5;DevID=B9A00938-FE68-4686-9461-A365C7E5110D;Ver=0.0.1;DevType=iPhone;"
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 {
vss := strings.Split(strings.TrimSpace(v), "=")
if len(vss) < 2 {
c.Set(consts.CtxUAH5, vss[0])
// log.Warn("user-agent miss", log.Any("ua", userAgent), log.Any("v", v))
continue
}
switch strings.TrimSpace(vss[0]) {
case "DevID":
ua.DevID = strings.TrimSpace(vss[1])
c.Set(consts.CtxDevId, ua.DevID)
case "DevType":
ua.DevType = strings.TrimSpace(vss[1])
c.Set(consts.CtxDevType, ua.DevType)
case "SysType":
input := strings.ToLower(strings.TrimSpace(vss[1]))
switch {
case strings.Contains(input, "h5_android"):
ua.SysType = pubdef.ST_H5Android
case strings.Contains(input, "h5_ios"):
ua.SysType = pubdef.ST_H5IOS
case strings.Contains(input, "h5_pc"):
ua.SysType = pubdef.ST_H5PC
case strings.Contains(input, "android"):
ua.SysType = pubdef.ST_Android
case strings.Contains(input, "ios"):
ua.SysType = pubdef.ST_IOS
case strings.Contains(input, "h5"):
ua.SysType = pubdef.ST_H5
case strings.Contains(input, "pc"):
ua.SysType = pubdef.ST_PC
case strings.Contains(input, "light"):
ua.SysType = pubdef.ST_Light
case strings.Contains(input, "tg"):
ua.SysType = pubdef.ST_TG
case strings.Contains(input, "web"):
ua.SysType = pubdef.ST_Web
}
// 存入 context 时用 string,保持 ctx.GetString 兼容
c.Set(consts.CtxSysType, string(ua.SysType))
case "Ver":
ua.Ver = strings.TrimSpace(vss[1])
c.Set(consts.CtxVersion, ua.Ver)
case "BuildID":
ua.BuildID = strings.TrimSpace(vss[1])
c.Set(consts.CtxBuildId, ua.BuildID)
default:
log.Warn("user-agent unknown", log.Any("ua", userAgent))
}
}
} else {
log.Warn("user-agent empty")
}
c.Set(consts.CtxUA, ua)
}
+292
View File
@@ -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)
}
}
+184
View File
@@ -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
}
+130
View File
@@ -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)
}
}
+56
View File
@@ -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
}
+12
View File
@@ -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))
// }
+283
View File
@@ -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")
}
}
+127
View File
@@ -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
}
}
+20
View File
@@ -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)
}
+19
View File
@@ -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
}
+109
View File
@@ -0,0 +1,109 @@
package utils
import (
"fmt"
"path/filepath"
"strconv"
"strings"
"github.com/PuerkitoBio/purell"
)
// PathSegment 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)
}
// JoinUrlPathAppendEnd 域名与uri拼接 追加/结尾
func JoinUrlPathAppendEnd(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
}
// 标准化 URL
normalized, err := purell.NormalizeURLString(urlpath, purell.FlagsUsuallySafeGreedy)
if err != nil {
return urlpath
}
if !strings.HasSuffix(normalized, "/") {
normalized = fmt.Sprintf("%s/", normalized)
}
return normalized
}
// 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
}
+56
View File
@@ -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
}
+15
View File
@@ -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
}
+116
View File
@@ -0,0 +1,116 @@
package utils
import (
"golang.org/x/exp/constraints"
"sort"
)
func SliceConstruct[T any](fn func() T) T {
return fn()
}
type SliceWalkCallback func(index int) bool // true 继续
func SliceWalk(length int, cb SliceWalkCallback) {
for i := 0; i < length; i++ {
if ok := cb(i); ok {
continue
} else {
break
}
}
}
type SliceIncludeFilter func(index int) bool // 返回 true 继续, false 中断
func SliceInclude(length int, filter SliceIncludeFilter) (int, bool) {
for i := 0; i < length; i++ {
if ok := filter(i); ok {
return i, true
}
}
return -1, false
}
type SliceConstructNFilter[T any] func(index int) (T, bool) // 返回 是否保留,项值
func SliceConstructN[T any](length int, filter SliceConstructNFilter[T]) []T {
ret := make([]T, 0)
for i := 0; i < length; i++ {
if item, keep := filter(i); keep {
ret = append(ret, item)
}
}
return ret
}
// SliceUnique 使用泛型实现切片去重
func SliceUnique[T comparable](s []T) []T {
seen := make(map[T]bool)
j := 0
for _, v := range s {
if _, ok := seen[v]; !ok {
seen[v] = true
s[j] = v
j++
}
}
return s[:j]
}
// SlicesDifference 求两个集合的差集
func SlicesDifference[T comparable](a, b []T) []T {
// 使用 map 存储集合 b 的元素,方便快速判断元素是否存在
seen := make(map[T]struct{}, len(b))
for _, v := range b {
seen[v] = struct{}{}
}
// 遍历集合 a,将不在集合 b 中的元素放入新的切片
diff := make([]T, 0)
for _, v := range a {
if _, ok := seen[v]; !ok {
diff = append(diff, v)
}
}
return diff
}
func RemoveSlicesElement[T comparable](slice []T, element T) []T {
// 使用两个指针,一个遍历,一个用来记录有效元素的位置
j := 0
for i := range slice {
if slice[i] != element {
slice[j] = slice[i]
j++
}
}
// 返回截取后的切片
return slice[:j]
}
func SlicesIntersect[T constraints.Ordered | ~string](slice1, slice2 []T) []T {
dict := make(map[T]int)
ret := make([]T, 0)
for _, v1 := range slice1 {
dict[v1]++
}
for _, v2 := range slice2 {
if _, exists := dict[v2]; exists {
ret = append(ret, v2)
}
}
return ret
}
func IsSliceEqual[T constraints.Ordered](src []T, dst []T) bool {
sort.Slice(src, func(i, j int) bool { return src[i] < src[j] })
sort.Slice(dst, func(i, j int) bool { return dst[i] < dst[j] })
if len(src) != len(dst) {
return false
}
for i := range src {
if src[i] != dst[i] {
return false
}
}
return true
}
+270
View File
@@ -0,0 +1,270 @@
package utils
import (
"regexp"
"strings"
"unicode"
)
const slugTitleRuneLimit = 10
var (
reSlugFC2Code = regexp.MustCompile(`(?i)fc2[-_ ]?ppv[-_ ]?\d{5,12}`)
reSlugNumLetterCode = regexp.MustCompile(`(?i)\d{3}[a-z]{2,6}[-_ ]?\d{2,7}`)
reSlugLetterNumCode = regexp.MustCompile(`(?i)[a-z]{2,8}[-_ ]?\d{2,7}`)
reSlugLeadingYear = regexp.MustCompile(`^20\d{2}`)
reSlugLeadingBracket = regexp.MustCompile(`^[【\[]([^】\]]+)[】\]]|^[((]([^))]+)[))]|^「([^」]+)」`)
)
// 标题开头常见的无意义前缀,长词优先。
var slugTitlePrefixNoise = []string{
"中文字幕", "中文翻譯", "中文翻译", "无码流出", "無碼流出",
"无码破解", "無碼破解", "无修正", "無修正", "完整版", "抢先版",
"高清版", "字幕版",
}
// 技术规格、版本标记,单独出现在标题开头时去掉。
var slugTitlePrefixTokens = []string{
"1080P", "2160P", "720P", "360P", "FHD", "UHD", "4K", "8K", "HD",
"FANZA", "MGS", "最新", "高清", "流出", "破解", "字幕", "中字",
}
// 关键词里过于通用、几乎每条都有的词,不单独拿来做 slug。
var slugGenericKeywords = map[string]struct{}{
"中文字幕": {}, "中文翻译": {}, "中文翻譯": {}, "无码流出": {}, "無碼流出": {},
"无码": {}, "無碼": {}, "有码": {}, "有碼": {}, "无修正": {}, "無修正": {},
"日本": {}, "国产": {}, "國產": {}, "素人": {}, "人妻": {}, "熟女": {},
"中文": {}, "字幕": {}, "高清": {}, "最新": {}, "流出": {}, "破解": {},
"hd": {}, "4k": {}, "fhd": {}, "fanza": {},
}
// SlugSourceFromKeywordsOrTitle 从标题和关键词提取适合转拼音的 slug 源文本。
// 优先用标题里的番号或去掉无意义前缀后的有效片段,不再固定取第一个关键词。
func SlugSourceFromKeywordsOrTitle(keywords []string, title string) string {
title = strings.TrimSpace(title)
if code := firstContentCode(title, keywords); code != "" {
return code
}
if slice := meaningfulTitleSlice(cleanTitleForSlug(title)); slice != "" {
return slice
}
if kw := pickDistinctKeyword(keywords); kw != "" {
return kw
}
if title != "" {
return GetPrefix(title, slugTitleRuneLimit)
}
return firstNonEmptyKeyword(keywords)
}
func firstContentCode(title string, keywords []string) string {
if code := extractContentCode(title); code != "" {
return code
}
for _, kw := range keywords {
if code := extractContentCode(kw); code != "" {
return code
}
}
return ""
}
func extractContentCode(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
if m := reSlugFC2Code.FindString(s); m != "" {
return m
}
if m := reSlugNumLetterCode.FindString(s); m != "" {
return m
}
if m := reSlugLetterNumCode.FindString(s); m != "" && !isQualityToken(m) {
return m
}
return ""
}
func isQualityToken(s string) bool {
s = strings.ToUpper(strings.TrimSpace(s))
s = strings.ReplaceAll(s, " ", "")
switch s {
case "1080P", "2160P", "720P", "360P", "4K", "8K", "HD", "FHD", "UHD":
return true
default:
return false
}
}
func cleanTitleForSlug(title string) string {
s := strings.TrimSpace(title)
for i := 0; i < 8 && s != ""; i++ {
next := stripOnceTitlePrefix(s)
if next == s {
break
}
s = next
}
return strings.TrimSpace(s)
}
func stripOnceTitlePrefix(s string) string {
s = strings.TrimLeftFunc(s, func(r rune) bool {
return unicode.IsSpace(r) || strings.ContainsRune(`-_~|/.,;::、·•*#@`, r)
})
if s == "" {
return ""
}
if loc := reSlugLeadingBracket.FindStringSubmatch(s); len(loc) > 0 {
inner := firstSubmatch(loc)
whole := loc[0]
if extractContentCode(inner) != "" {
return strings.TrimSpace(inner + s[len(whole):])
}
if isNoiseText(inner) || runeLen(inner) <= 4 {
return strings.TrimSpace(s[len(whole):])
}
}
lower := strings.ToLower(s)
for _, prefix := range slugTitlePrefixNoise {
if strings.HasPrefix(s, prefix) || strings.HasPrefix(lower, strings.ToLower(prefix)) {
return strings.TrimSpace(s[len([]byte(prefixMatch(s, prefix))):])
}
}
for _, token := range slugTitlePrefixTokens {
if consumed, ok := consumeLeadingToken(s, token); ok {
return consumed
}
}
if loc := reSlugLeadingYear.FindStringIndex(s); loc != nil && loc[0] == 0 {
rest := strings.TrimSpace(s[loc[1]:])
if rest != "" {
return rest
}
}
return s
}
func prefixMatch(s, prefix string) string {
if strings.HasPrefix(s, prefix) {
return prefix
}
lower := strings.ToLower(s)
p := strings.ToLower(prefix)
if strings.HasPrefix(lower, p) {
return s[:len(prefix)]
}
return prefix
}
func consumeLeadingToken(s, token string) (string, bool) {
if len(s) < len(token) {
return s, false
}
if !strings.HasPrefix(strings.ToLower(s), strings.ToLower(token)) {
return s, false
}
rest := s[len(token):]
if rest == "" {
return "", true
}
r := []rune(rest)[0]
if unicode.IsLetter(r) || unicode.IsDigit(r) {
return s, false
}
return strings.TrimSpace(rest), true
}
func firstSubmatch(m []string) string {
for i := 1; i < len(m); i++ {
if strings.TrimSpace(m[i]) != "" {
return strings.TrimSpace(m[i])
}
}
return ""
}
func meaningfulTitleSlice(title string) string {
title = strings.TrimSpace(title)
if title == "" {
return ""
}
return GetPrefix(title, slugTitleRuneLimit)
}
func pickDistinctKeyword(keywords []string) string {
best := ""
bestScore := 0
for _, kw := range keywords {
kw = strings.TrimSpace(kw)
if kw == "" || isGenericKeyword(kw) {
continue
}
score := runeLen(kw)
if score < 3 {
continue
}
if score > bestScore {
best = kw
bestScore = score
}
}
return best
}
func firstNonEmptyKeyword(keywords []string) string {
for _, kw := range keywords {
if s := strings.TrimSpace(kw); s != "" {
return s
}
}
return ""
}
func isGenericKeyword(s string) bool {
s = strings.TrimSpace(s)
if s == "" {
return true
}
return isGenericKeywordExact(s) || isNoiseText(s)
}
func isNoiseText(s string) bool {
s = strings.TrimSpace(s)
if s == "" {
return true
}
if isGenericKeywordExact(s) || isQualityToken(s) {
return true
}
for _, prefix := range slugTitlePrefixNoise {
if s == prefix {
return true
}
}
parts := strings.FieldsFunc(s, func(r rune) bool {
return strings.ContainsRune(` /|、,,-_+`, r)
})
if len(parts) > 1 {
for _, p := range parts {
if !isNoiseText(p) {
return false
}
}
return true
}
return false
}
func isGenericKeywordExact(s string) bool {
if _, ok := slugGenericKeywords[s]; ok {
return true
}
_, ok := slugGenericKeywords[strings.ToLower(s)]
return ok
}
func runeLen(s string) int {
return len([]rune(s))
}
+100
View File
@@ -0,0 +1,100 @@
package utils
import (
"math/rand/v2"
"strings"
"github.com/gosimple/slug"
)
// BinarySearch BinarySearch
func BinarySearch(s []string, t string, n int) (idx int) {
idx = -1
if n == 0 {
return
}
start, end := 0, n-1
for start <= end {
mid := (start + end) >> 1
if s[mid] < t {
start = mid + 1
} else if s[mid] > t {
end = mid - 1
} else {
idx = mid
break
}
}
return
}
var YesOrNo = map[bool]string{
true: "是",
false: "否",
}
func GetRandomLetters(n int) string {
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
b := make([]byte, n)
for i := range b {
b[i] = letters[rand.IntN(len(letters))]
}
return string(b)
}
func GetPrefix(s string, n int) string {
runes := []rune(s)
if len(runes) <= n {
return s
}
return string(runes[:n])
}
func GetSlugInfo(text string) string {
return SanitizeSlug(slug.Make(text))
}
// SanitizeSlug 去掉 slug 中的横杠等特殊符号,只保留字母和数字
func SanitizeSlug(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
var b strings.Builder
b.Grow(len(s))
for _, r := range s {
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' {
b.WriteRune(r)
}
}
return b.String()
}
// IsSlugAllDigits 判断 slug 是否全为数字
func IsSlugAllDigits(s string) bool {
s = strings.TrimSpace(s)
if s == "" {
return false
}
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return true
}
// MakeUniqueSlug 生成唯一 slug;exists 返回 true 表示已被占用。生成结果不能全是数字。
func MakeUniqueSlug(source string, exists func(string) bool) string {
pinyin := GetSlugInfo(source)
if pinyin == "" {
pinyin = GetRandomLetters(5)
} else if IsSlugAllDigits(pinyin) {
pinyin = pinyin + GetRandomLetters(3)
}
finalSlug := pinyin
for exists(finalSlug) {
finalSlug = pinyin + GetRandomLetters(3)
}
return finalSlug
}

Some files were not shown because too many files have changed in this diff Show More