@@ -0,0 +1,22 @@
|
||||
package common
|
||||
|
||||
import "math/rand"
|
||||
|
||||
/**
|
||||
* 生成邀请码
|
||||
*/
|
||||
var (
|
||||
//所有的字符
|
||||
allChars = []string{"2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
|
||||
//默认长度
|
||||
defaultLen = 6
|
||||
)
|
||||
|
||||
func InvitePromotionCodeGenera() string {
|
||||
result := ""
|
||||
for i := 0; i < defaultLen; i++ {
|
||||
index := rand.Intn(len(allChars))
|
||||
result += allChars[index]
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package aiMate
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
)
|
||||
|
||||
const (
|
||||
req_doman = "https://www.gpt4novel.com" //请求域名
|
||||
access_doman = "https://www.saibolaopo.com/tg-login" //访问域名
|
||||
get_token_url = "/api/dzmm/get-jwt-for-organization-subaccount" //获取用户登陆token
|
||||
transfer_points_url = "/api/dzmm/transfer-all-user-credit-to-organization" //转出用户所有积分
|
||||
get_points_url = "/api/dzmm/get-organization-user-balance" //获取用户积分
|
||||
get_transactions_url = "/api/dzmm/get-user-credit-transactions" //获取用户消费记录
|
||||
access_key = "c1c5f394-1a6a-4834-8ee8-cb2fb6325969" //密钥
|
||||
organization_id = "65443ba4-480a-4c62-8276-f5380bccc739" //组织id
|
||||
)
|
||||
|
||||
// 获取token请求参数
|
||||
type GetTokenReq struct {
|
||||
OrganizationId string `json:"organizationId"` // 组织id
|
||||
OrganizationExternalId string `json:"organizationExternalId"` // 用户唯一id,uuidv4
|
||||
First_name string `json:"first_name"` // 用户名
|
||||
Avatar_url string `json:"avatar_url"` // 用户头像
|
||||
Bio string `json:"bio"` // 简介
|
||||
CreditToUser float64 `json:"creditToUser"` // 转移给用户的积分数
|
||||
Sign string `json:"sign"` // sign
|
||||
}
|
||||
|
||||
// 获取token返回参数
|
||||
type GetTokenResp struct {
|
||||
StatusCode int `json:"statusCode"` // 状态码
|
||||
Message string `json:"message"` // 描述
|
||||
Error string `json:"error"` // 错误
|
||||
Jwt string `json:"jwt"` // jwt
|
||||
}
|
||||
|
||||
// 获取用户访问Ai女友的链接
|
||||
func GetUserAccessAiUrl(uid uint64, uuid, name string, amount float64) (access_url string, err error) {
|
||||
var (
|
||||
//请求参数
|
||||
req = GetTokenReq{
|
||||
OrganizationId: organization_id,
|
||||
OrganizationExternalId: uuid,
|
||||
First_name: "91PORN-" + name,
|
||||
Bio: "AiMate",
|
||||
Avatar_url: "https://example.com/avatar.jpg",
|
||||
CreditToUser: amount,
|
||||
}
|
||||
//返回参数
|
||||
resp = GetTokenResp{}
|
||||
//加密字符串
|
||||
signStr = "avatar_url=" + req.Avatar_url + "&bio=" + req.Bio + "&creditToUser=" + strconv.FormatFloat(req.CreditToUser, 'f', -1, 64) + "&first_name=" + req.First_name + "&organizationExternalId=" + req.OrganizationExternalId + "&organizationId=" + req.OrganizationId + access_key
|
||||
)
|
||||
//参数加密
|
||||
h := md5.New()
|
||||
h.Write([]byte(signStr))
|
||||
cipherStr := h.Sum(nil)
|
||||
req.Sign = hex.EncodeToString(cipherStr)
|
||||
//请求
|
||||
url := req_doman + get_token_url
|
||||
bodyStr, _ := json.Marshal(req)
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, bodyStr)
|
||||
if err != nil {
|
||||
log.Error("GetUserAccessAiUrl POSTWithJResp ", log.Any("url", url), log.Any("uid:", uid), log.E(err))
|
||||
return
|
||||
}
|
||||
if code != http.StatusCreated {
|
||||
log.Error("GetUserAccessAiUrl response status ", log.Any("code", code), log.Any("uid", uid))
|
||||
if strings.Contains(resp.Message, "积分不足") {
|
||||
err = fmt.Errorf("insufficient organization points")
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("response status err")
|
||||
return
|
||||
}
|
||||
//访问链接
|
||||
access_url = access_doman + "?token=" + resp.Jwt
|
||||
log.Info("GetUserAccessAiUrl success", log.Any("uid", uid))
|
||||
return
|
||||
}
|
||||
|
||||
// 划转积分请求参数
|
||||
type TransferIntegralReq struct {
|
||||
OrganizationId string `json:"organizationId"` // 组织id
|
||||
SourceAccountId string `json:"sourceAccountId"` // 用户唯一id,uuidv4
|
||||
Note string `json:"note"` // 备注
|
||||
Sign string `json:"sign"` // sign
|
||||
}
|
||||
|
||||
// 划转积分返回参数
|
||||
type TransferIntegralResp struct {
|
||||
StatusCode int `json:"statusCode"` // 状态码
|
||||
Message string `json:"message"` // 描述
|
||||
Error string `json:"error"` // 错误
|
||||
NewBalance float64 `json:"newBalance"` // 最新余额
|
||||
TransferredAmount float64 `json:"transferredAmount"` // 划转余额
|
||||
}
|
||||
|
||||
// 用户划转积分,从第三方转出用户所有积分
|
||||
func UserAiTransferIntegral(uid uint64, uuid string) (transferredAmount float64, err error) {
|
||||
var (
|
||||
//请求参数
|
||||
req = TransferIntegralReq{
|
||||
SourceAccountId: uuid,
|
||||
OrganizationId: organization_id,
|
||||
Note: "transfer-user-all-integral",
|
||||
}
|
||||
//返回参数
|
||||
resp = TransferIntegralResp{}
|
||||
//加密字符串
|
||||
signStr = "note=" + req.Note + "&organizationId=" + req.OrganizationId + "&sourceAccountId=" + req.SourceAccountId + access_key
|
||||
)
|
||||
//参数加密
|
||||
h := md5.New()
|
||||
h.Write([]byte(signStr))
|
||||
cipherStr := h.Sum(nil)
|
||||
req.Sign = hex.EncodeToString(cipherStr)
|
||||
//请求
|
||||
url := req_doman + transfer_points_url
|
||||
bodyStr, _ := json.Marshal(req)
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, bodyStr)
|
||||
if err != nil {
|
||||
log.Error("UserAiTransferIntegral POSTWithJResp ", log.Any("url", url), log.Any("uid:", uid), log.E(err))
|
||||
return
|
||||
}
|
||||
if code != http.StatusCreated {
|
||||
log.Error("UserAiTransferIntegral response status ", log.Any("code", code), log.Any("uid", uid))
|
||||
err = fmt.Errorf("response status err")
|
||||
return
|
||||
}
|
||||
log.Info("UserAiTransferIntegral success", log.Any("uid", uid))
|
||||
transferredAmount = resp.TransferredAmount
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户余额请求参数
|
||||
type GetUserAiBalanceReq struct {
|
||||
OrganizationId string `json:"organizationId"` // 组织id
|
||||
ExternalUserId string `json:"externalUserId"` // 用户唯一id,uuidv4
|
||||
Sign string `json:"sign"` // sign
|
||||
}
|
||||
|
||||
// 获取用户余额返回参数
|
||||
type GetUserAiBalanceResp struct {
|
||||
StatusCode int `json:"statusCode"` // 状态码
|
||||
Message string `json:"message"` // 描述
|
||||
Error string `json:"error"` // 错误
|
||||
Balance float64 `json:"balance"` // 余额
|
||||
}
|
||||
|
||||
// 获取用户余额
|
||||
func GetUserAiBalance(uid uint64, uuid string) (balance float64, err error) {
|
||||
var (
|
||||
//请求参数
|
||||
req = GetUserAiBalanceReq{
|
||||
OrganizationId: organization_id,
|
||||
ExternalUserId: uuid,
|
||||
}
|
||||
//返回参数
|
||||
resp = GetUserAiBalanceResp{}
|
||||
//加密字符串
|
||||
signStr = "externalUserId=" + req.ExternalUserId + "&organizationId=" + req.OrganizationId + access_key
|
||||
)
|
||||
//参数加密
|
||||
h := md5.New()
|
||||
h.Write([]byte(signStr))
|
||||
cipherStr := h.Sum(nil)
|
||||
req.Sign = hex.EncodeToString(cipherStr)
|
||||
//请求
|
||||
url := req_doman + get_points_url
|
||||
bodyStr, _ := json.Marshal(req)
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, bodyStr)
|
||||
if err != nil {
|
||||
log.Error("GetUserAiBalanceReq POSTWithJResp ", log.Any("url", url), log.Any("uid:", uid), log.E(err))
|
||||
return
|
||||
}
|
||||
if code != http.StatusCreated && code != 0 {
|
||||
log.Error("GetUserAiBalanceReq response status ", log.Any("code", code), log.Any("uid", uid))
|
||||
err = fmt.Errorf("response status err")
|
||||
return
|
||||
}
|
||||
log.Info("GetUserAiBalanceReq success", log.Any("uid", uid))
|
||||
balance = resp.Balance
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户交易记录请求参数
|
||||
type GetUserTransactionsReq struct {
|
||||
OrganizationId string `json:"organizationId"` // 组织id
|
||||
ExternalUserId string `json:"externalUserId"` // 用户唯一id,uuidv4
|
||||
Sign string `json:"sign"` // sign
|
||||
}
|
||||
|
||||
// 获取用户交易记录返回参数
|
||||
type GetUserTransactionsResp struct {
|
||||
StatusCode int `json:"statusCode"` // 状态码
|
||||
Message string `json:"message"` // 描述
|
||||
Error string `json:"error"` // 错误
|
||||
TotalCount int `json:"totalCount"` // 总条数
|
||||
Transactions []TransactionsInfo `json:"transactions"` // 交易列表
|
||||
}
|
||||
|
||||
// 用户交易记录对象
|
||||
type TransactionsInfo struct {
|
||||
OrderId int `json:"orderId"` // 订单Id
|
||||
Credit_delta string `json:"credit_delta"` // 此次操作积分数
|
||||
Post_credit string `json:"post_credit"` // 操作后剩余积分数
|
||||
Created_at string `json:"created_at"` // 操作时间
|
||||
Operation_type string `json:"operation_type"` // 操作类型
|
||||
}
|
||||
|
||||
// 获取用户交易记录
|
||||
func GetUserTransactions(uid uint64, uuid string) (totalCount int, list []TransactionsInfo, err error) {
|
||||
var (
|
||||
//请求参数
|
||||
req = GetUserTransactionsReq{
|
||||
OrganizationId: organization_id,
|
||||
ExternalUserId: uuid,
|
||||
}
|
||||
//返回参数
|
||||
resp = GetUserTransactionsResp{}
|
||||
//加密字符串
|
||||
signStr = "externalUserId=" + req.ExternalUserId + "&organizationId=" + req.OrganizationId + access_key
|
||||
)
|
||||
log.Info("GetUserTransactionsReq req", log.Any("signStr:", signStr), log.Any("uid:", uid))
|
||||
//参数加密
|
||||
h := md5.New()
|
||||
h.Write([]byte(signStr))
|
||||
cipherStr := h.Sum(nil)
|
||||
req.Sign = hex.EncodeToString(cipherStr)
|
||||
log.Info("GetUserTransactionsReq req", log.Any("params:", req), log.Any("uid:", uid))
|
||||
//请求
|
||||
url := req_doman + get_transactions_url
|
||||
bodyStr, _ := json.Marshal(req)
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, bodyStr)
|
||||
if err != nil {
|
||||
log.Error("GetUserTransactionsReq POSTWithJResp ", log.Any("url", url), log.Any("uid:", uid), log.E(err))
|
||||
return
|
||||
}
|
||||
if code != http.StatusCreated && code != 0 {
|
||||
log.Error("GetUserTransactionsReq response status ", log.Any("code", code), log.Any("uid:", uid), log.Any("resp", resp))
|
||||
err = fmt.Errorf("response status err")
|
||||
return
|
||||
}
|
||||
log.Info("GetUserTransactionsReq http", log.Any("resp:", resp), log.Any("uid:", uid))
|
||||
totalCount = resp.TotalCount
|
||||
list = resp.Transactions
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
package aiService
|
||||
|
||||
import (
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/redis"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type AiService struct {
|
||||
c *Conf
|
||||
}
|
||||
|
||||
type Conf struct {
|
||||
AppId int
|
||||
Url string
|
||||
Redis *redis.Client
|
||||
}
|
||||
|
||||
type Option func(c *Conf)
|
||||
|
||||
func NewAiService(opts ...Option) *AiService {
|
||||
c := &Conf{}
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
return &AiService{
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
func Redis(r *redis.Client) Option {
|
||||
return func(c *Conf) {
|
||||
c.Redis = r
|
||||
}
|
||||
}
|
||||
|
||||
func AppId(appId int) Option {
|
||||
return func(c *Conf) {
|
||||
c.AppId = appId
|
||||
}
|
||||
}
|
||||
|
||||
func Url(url string) Option {
|
||||
return func(c *Conf) {
|
||||
c.Url = url
|
||||
}
|
||||
}
|
||||
|
||||
type msg struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func (m *AiService) getParams(p interface{}) (params map[string]interface{}) {
|
||||
req := structs.New(p)
|
||||
fields := req.Fields()
|
||||
params = make(map[string]interface{})
|
||||
for _, field := range fields {
|
||||
jsonTag := field.Tag("json")
|
||||
tagList := strings.Split(jsonTag, ",")
|
||||
key := field.Name()
|
||||
omitempty := ""
|
||||
if len(tagList) > 0 {
|
||||
key = tagList[0]
|
||||
}
|
||||
if len(tagList) > 1 {
|
||||
omitempty = tagList[1]
|
||||
}
|
||||
kind := reflect.TypeOf(field.Value()).Kind()
|
||||
// 如果是0值忽略或者空指针,不需要传
|
||||
if field.IsZero() && (kind == reflect.Ptr || omitempty == "omitempty") {
|
||||
continue
|
||||
}
|
||||
if kind == reflect.Ptr {
|
||||
// 通过指针取值
|
||||
params[key] = reflect.ValueOf(field.Value()).Elem().Interface()
|
||||
} else {
|
||||
params[key] = field.Value()
|
||||
}
|
||||
}
|
||||
params["appId"] = m.c.AppId
|
||||
return
|
||||
}
|
||||
|
||||
func (s *AiService) getTemplateList(p *TemplateListReq) (result TemplateListResp, err error) {
|
||||
var retMsg msg
|
||||
path := "/api/ai/template/all"
|
||||
params := s.getParams(p)
|
||||
code, err := httputil.DefaultClientGetWithResp(&retMsg, common.BindUrl(s.c.Url, path), nil, params)
|
||||
if err != nil {
|
||||
log.Error("AiService GetTemplateList fail", log.Any("params", params), log.E(err))
|
||||
err = errors.New("AiService GetTemplateList fail")
|
||||
return
|
||||
}
|
||||
if code != 200 {
|
||||
log.Error("AiService GetTemplateList fail", log.Any("params", params), log.Any("code", code))
|
||||
err = errors.New("AiService GetTemplateList fail")
|
||||
return
|
||||
}
|
||||
|
||||
if retMsg.Code != 200 {
|
||||
log.Error("AiService GetTemplateList fail", log.Any("params", params), log.Any("retMsg", retMsg))
|
||||
err = errors.New("AiService GetTemplateList fail")
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(retMsg.Data)
|
||||
if err != nil {
|
||||
log.Error("AiService GetTemplateList json.Marshal fail", log.Any("params", params), log.E(err))
|
||||
err = errors.New("AiService GetTemplateList json.Marshal fail")
|
||||
return
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(b, &result); err != nil {
|
||||
log.Error("AiService GetTemplateList json.Unmarshal fail", log.Any("params", params), log.Any("data", string(b)), log.E(err))
|
||||
err = errors.New("AiService GetTemplateList json.Unmarshal fail")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type TemplateDetailReq struct {
|
||||
Ids []primitive.ObjectID `json:"ids" binding:"required"`
|
||||
}
|
||||
|
||||
func (s *AiService) getTemplateDetail(p *TemplateDetailReq) (result TemplateDetailResp, err error) {
|
||||
var retMsg msg
|
||||
path := "/api/ai/template/detail"
|
||||
params := s.getParams(p)
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&retMsg, common.BindUrl(s.c.Url, path), nil, params)
|
||||
if err != nil {
|
||||
log.Error("AiService GetTemplateDetail fail", log.Any("params", params), log.E(err))
|
||||
err = errors.New("AiService GetTemplateDetail fail")
|
||||
return
|
||||
}
|
||||
if code != 200 {
|
||||
log.Error("AiService GetTemplateDetail fail", log.Any("params", params), log.Any("code", code))
|
||||
err = errors.New("AiService GetTemplateDetail fail")
|
||||
return
|
||||
}
|
||||
|
||||
if retMsg.Code != 200 {
|
||||
log.Error("AiService GetTemplateDetail fail", log.Any("params", params), log.Any("retMsg", retMsg))
|
||||
err = errors.New("AiService GetTemplateDetail fail")
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(retMsg.Data)
|
||||
if err != nil {
|
||||
log.Error("AiService GetTemplateDetail json.Marshal fail", log.Any("params", params), log.E(err))
|
||||
err = errors.New("AiService GetTemplateDetail json.Marshal fail")
|
||||
return
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(b, &result); err != nil {
|
||||
log.Error("AiService GetTemplateDetail json.Unmarshal fail", log.Any("params", params), log.Any("data", string(b)), log.E(err))
|
||||
err = errors.New("AiService GetTemplateDetail json.Unmarshal fail")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *AiService) Sync() (err error) {
|
||||
if s.c.Redis == nil {
|
||||
return errors.New("redis is nil")
|
||||
}
|
||||
resp, err := s.getTemplateList(&TemplateListReq{})
|
||||
if err != nil {
|
||||
log.Error("AI Template sync fail", log.E(err))
|
||||
return
|
||||
}
|
||||
for _, template := range resp.TemplateList {
|
||||
b, err := json.Marshal(template)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// 将模版数据写入redis
|
||||
s.c.Redis.Set(AiTemplateKey(template.ID), string(b), time.Hour*24*7)
|
||||
}
|
||||
var imageCategoryList []Category
|
||||
var videoCategoryList []Category
|
||||
for _, v := range resp.CategoryList {
|
||||
if v.Type == 0 {
|
||||
imageCategoryList = append(imageCategoryList, v)
|
||||
} else {
|
||||
videoCategoryList = append(videoCategoryList, v)
|
||||
}
|
||||
}
|
||||
// 将数据写入写入redis
|
||||
b0, err := json.Marshal(imageCategoryList)
|
||||
if err != nil {
|
||||
log.Error("AI Template json.Marshal(imageCategoryList) fail", log.E(err))
|
||||
return
|
||||
}
|
||||
b1, err := json.Marshal(videoCategoryList)
|
||||
if err != nil {
|
||||
log.Error("AI Template json.Marshal(videoCategoryList) fail", log.E(err))
|
||||
return
|
||||
}
|
||||
err = s.c.Redis.Set(AiCategoryKey(0), string(b0), time.Hour*24*7)
|
||||
if err != nil {
|
||||
log.Error("AI Template Redis.Set(AiCategoryKey(0) fail", log.E(err))
|
||||
return
|
||||
}
|
||||
err = s.c.Redis.Set(AiCategoryKey(1), string(b1), time.Hour*24*7)
|
||||
if err != nil {
|
||||
log.Error("AI Template Redis.Set(AiCategoryKey(1) fail", log.E(err))
|
||||
return
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AiService) GetTemplateList(types int, categoryId string) (categoryList []*Category, queryCategoryId string, templateList []*Template, err error) {
|
||||
if s.c.Redis == nil {
|
||||
err = errors.New("redis nil")
|
||||
return
|
||||
}
|
||||
// 获取分类列表
|
||||
val, err := s.c.Redis.Get(AiCategoryKey(types))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if val == nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(*val), &categoryList)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(categoryList) == 0 {
|
||||
return
|
||||
}
|
||||
if categoryId == "" {
|
||||
queryCategoryId = categoryList[0].ID.Hex()
|
||||
} else {
|
||||
queryCategoryId = categoryId
|
||||
}
|
||||
templateIds := []primitive.ObjectID{}
|
||||
for _, v := range categoryList {
|
||||
if v.ID.Hex() == queryCategoryId {
|
||||
templateIds = v.TemplateIds
|
||||
break
|
||||
}
|
||||
}
|
||||
keys := []string{}
|
||||
for _, templateId := range templateIds {
|
||||
keys = append(keys, AiTemplateKey(templateId))
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return
|
||||
}
|
||||
// 通过id查找这个模版
|
||||
res, err := s.c.Redis.MGet(keys...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, v := range res {
|
||||
templateVal, ok := v.(string)
|
||||
if !ok || templateVal == "" {
|
||||
continue
|
||||
}
|
||||
item := Template{}
|
||||
err = json.Unmarshal([]byte(templateVal), &item)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if item.ModuleType != types {
|
||||
continue
|
||||
}
|
||||
templateList = append(templateList, &item)
|
||||
}
|
||||
return categoryList, queryCategoryId, templateList, nil
|
||||
}
|
||||
|
||||
func (s *AiService) GetTemplate(id primitive.ObjectID) (template Template, err error) {
|
||||
if id.IsZero() {
|
||||
return
|
||||
}
|
||||
if s.c.Redis == nil {
|
||||
err = errors.New("redis nil")
|
||||
return
|
||||
}
|
||||
// 从缓存中获取数据
|
||||
val, err := s.c.Redis.Get(AiTemplateKey(id))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if val != nil && *val != "" {
|
||||
err = json.Unmarshal([]byte(*val), &template)
|
||||
if err == nil {
|
||||
return template, nil
|
||||
}
|
||||
}
|
||||
// 如果获取不到,尝试从ai服务获取
|
||||
res, err := s.getTemplateDetail(&TemplateDetailReq{
|
||||
Ids: []primitive.ObjectID{id},
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(res.List) == 0 {
|
||||
return
|
||||
}
|
||||
template = res.List[0]
|
||||
common.Go(func() {
|
||||
b, err := json.Marshal(template)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 将模版数据写入redis
|
||||
s.c.Redis.Set(AiTemplateKey(template.ID), string(b), time.Hour*48)
|
||||
})
|
||||
return template, nil
|
||||
}
|
||||
|
||||
func (s *AiService) GetTemplateByIds(ids []primitive.ObjectID) (templateList []Template, err error) {
|
||||
if len(ids) == 0 {
|
||||
return
|
||||
}
|
||||
if s.c.Redis == nil {
|
||||
err = errors.New("redis nil")
|
||||
return
|
||||
}
|
||||
|
||||
keys := []string{}
|
||||
for _, id := range ids {
|
||||
keys = append(keys, AiTemplateKey(id))
|
||||
}
|
||||
// 从缓存中获取数据
|
||||
list, err := s.c.Redis.MGet(keys...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
noFoundKey := []int{}
|
||||
for k, v := range list {
|
||||
if v == nil {
|
||||
noFoundKey = append(noFoundKey, k)
|
||||
continue
|
||||
}
|
||||
val, ok := v.(string)
|
||||
if !ok || val == "" {
|
||||
noFoundKey = append(noFoundKey, k)
|
||||
continue
|
||||
}
|
||||
item := Template{}
|
||||
err = json.Unmarshal([]byte(val), &item)
|
||||
if err != nil {
|
||||
noFoundKey = append(noFoundKey, k)
|
||||
continue
|
||||
}
|
||||
templateList = append(templateList, item)
|
||||
}
|
||||
noFoundIds := []primitive.ObjectID{}
|
||||
for _, k := range noFoundKey {
|
||||
noFoundIds = append(noFoundIds, ids[k])
|
||||
}
|
||||
if len(noFoundIds) == 0 {
|
||||
return
|
||||
}
|
||||
// 如果获取不到,尝试从ai服务获取
|
||||
res, err := s.getTemplateDetail(&TemplateDetailReq{
|
||||
Ids: noFoundIds,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(res.List) == 0 {
|
||||
return
|
||||
}
|
||||
templateList = append(templateList, res.List...)
|
||||
common.Go(func() {
|
||||
// 将查到的写入redis
|
||||
for _, template := range res.List {
|
||||
b, err := json.Marshal(template)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 将模版数据写入redis
|
||||
s.c.Redis.Set(AiTemplateKey(template.ID), string(b), time.Hour*48)
|
||||
}
|
||||
|
||||
})
|
||||
return templateList, nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package aiService
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const (
|
||||
aiCategoryKey = "aiCategory:%v"
|
||||
aiTemplateKey = "aiTemplate:%v"
|
||||
aiCategoryTemplateKey = "aiCategoryTemplate:%v"
|
||||
)
|
||||
|
||||
func AiTemplateKey(id primitive.ObjectID) string {
|
||||
return fmt.Sprintf(aiTemplateKey, id.Hex())
|
||||
}
|
||||
|
||||
func AiCategoryKey(types int) string {
|
||||
return fmt.Sprintf(aiCategoryKey, types)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package aiService
|
||||
|
||||
import (
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 获取模版列表
|
||||
type TemplateListReq struct {
|
||||
}
|
||||
|
||||
type TemplateListResp struct {
|
||||
CategoryList []Category `json:"categoryList"` // 分类列表
|
||||
TemplateList []*Template `json:"templateList"` // 模版列表
|
||||
|
||||
}
|
||||
|
||||
type TemplateDetailResp struct {
|
||||
List []Template `json:"list"`
|
||||
}
|
||||
|
||||
type Template struct {
|
||||
ID primitive.ObjectID `json:"id" bson:"id"`
|
||||
Title string `json:"title" bson:"title"`
|
||||
Cover string `json:"cover" bson:"cover"` //封面
|
||||
M3u8Url string `json:"m3u8Url" bson:"m3u8_url"` // m3u8 地址
|
||||
Coin uint `json:"coin" bson:"coin"` // 价格(金币)
|
||||
ModuleType int `json:"moduleType" bson:"module_type"` //换脸模版类型 0 图片 1 视频
|
||||
VipCoin uint `json:"vipCoin" bson:"vipCoin"`
|
||||
CategoryId primitive.ObjectID `json:"categoryId" bson:"categoryId"` // 分类id
|
||||
CreatedAt time.Time `json:"createdAt,omitempty" bson:"createdAt"` //创建时间
|
||||
UsedCount uint64 `json:"usedCount" bson:"usedCount"` // 模版使用次数
|
||||
}
|
||||
|
||||
// Category 模版分类表
|
||||
type Category struct {
|
||||
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
|
||||
Name string `json:"name" bson:"name"` // 分类名
|
||||
Type int `json:"type" bson:"type"` // 0 图片 1 视频
|
||||
SortCode int `json:"sortCode" bson:"sortCode"` // 排序号
|
||||
AppId int `json:"appId" bson:"appId"` // 绑定appId
|
||||
Status int `json:"status" bson:"status"` // 0-不可用 1-可用
|
||||
UpdateTime time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
|
||||
CreateTime time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
|
||||
TemplateIds []primitive.ObjectID `json:"templateIds"`
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package bank
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
)
|
||||
|
||||
// Bank 充值订单请求体
|
||||
type Bank struct {
|
||||
CardNo string `json:"cardNo" form:"cardNo"`
|
||||
CardBinCheck string `json:"cardBinCheck" from:"cardBinCheck"`
|
||||
}
|
||||
|
||||
type MsgModel struct {
|
||||
CardType string `json:"cardType"`
|
||||
Bank string `json:"bank"`
|
||||
Key string `json:"key"`
|
||||
Messages []Message `json:"messages"`
|
||||
Validated bool `json:"validated"`
|
||||
Stat string `json:"stat"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
ErrorCodes string `json:"errorCodes"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type BankType struct {
|
||||
BankName string `form:"bankName" json:"bankName" bson:"bankName"` //银行名字
|
||||
BankCode string `form:"bankCode" json:"bankCode" bson:"bankCode"` //银行代码
|
||||
Icon string `form:"icon" json:"icon" bson:"icon"`
|
||||
Img string `form:"img" json:"img" bson:"img" `
|
||||
}
|
||||
|
||||
// GetBankCardInfo 银行卡信息获取
|
||||
func (b *Bank) GetBankCardCode() (msg MsgModel, err error) {
|
||||
b.Fill()
|
||||
code, err := httputil.DefaultClientGetWithResp(&msg, b.GetURL(), b.GetHeader(), b.GetQueryParam())
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("bank GetBankCardInfo Http.Get fail error:%+v/,data:%+v;", err, b))
|
||||
return
|
||||
}
|
||||
if code != 200 {
|
||||
log.Error(fmt.Sprintf("bank GetBankCardInfo Http.Get fail error:%+v/,data:%+v;", err, msg))
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetBankCardInfo 银行卡信息获取
|
||||
func (b *Bank) GetBankCardName(code string) (msg BankType, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Fill 补全配置信息和签名信息
|
||||
func (b *Bank) Fill() {
|
||||
}
|
||||
|
||||
// GetBody 获取请求body
|
||||
func (b *Bank) GetBody() interface{} {
|
||||
jsonStr, err := json.Marshal(b)
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Bank GetBody json.Marshal is fail error:%+v/data:%+v", err, b))
|
||||
}
|
||||
return jsonStr
|
||||
}
|
||||
|
||||
// GetBody 获取请求body
|
||||
func (b *Bank) GetQueryParam() map[string]string {
|
||||
j, err := json.Marshal(b)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
m := make(map[string]string)
|
||||
if err := json.Unmarshal(j, &m); err != nil {
|
||||
return nil
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// GetHeader 获取请求头信息
|
||||
func (b *Bank) GetHeader() map[string]string {
|
||||
header := make(map[string]string, 0)
|
||||
return header
|
||||
}
|
||||
|
||||
// GetURL 获取请求地址
|
||||
func (bank *Bank) GetURL() string {
|
||||
return "https://ccdcapi.alipay.com/validateAndCacheCardInfo.json"
|
||||
}
|
||||
|
||||
func (b *Bank) GetBankCardInfo() {
|
||||
}
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
cache "github.com/robfig/go-cache"
|
||||
)
|
||||
|
||||
// Cache 服务单机本地缓存
|
||||
type Cache struct {
|
||||
Expiration time.Duration
|
||||
CleanInterval time.Duration
|
||||
Cli *cache.Cache
|
||||
}
|
||||
|
||||
// New 创建缓存客户端
|
||||
func (c *Cache) New() {
|
||||
c.Cli = cache.New(c.Expiration, c.CleanInterval)
|
||||
}
|
||||
|
||||
// Set 设置键值
|
||||
func (c *Cache) Set(key string, value interface{}, expiration time.Duration) {
|
||||
c.Cli.Set(key, value, expiration)
|
||||
}
|
||||
|
||||
// Get 获取值
|
||||
func (c *Cache) Get(key string) (result interface{}, exists bool) {
|
||||
return c.Cli.Get(key)
|
||||
}
|
||||
|
||||
// Add 添加值
|
||||
func (c *Cache) Add(key string, value interface{}, expiration time.Duration) error {
|
||||
return c.Cli.Add(key, value, expiration)
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
package cachev2
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
// 默认的缓存时间
|
||||
defaultCacheTime = 1800
|
||||
// 空数据的缓存时间
|
||||
emptyDataCacheTime = 300
|
||||
|
||||
// 数据类型
|
||||
dataTypeList = "list"
|
||||
dataTypeInfo = "info"
|
||||
)
|
||||
|
||||
// BaseCache 缓存类定义
|
||||
// 集成到类黑料框架文件:
|
||||
//
|
||||
// copy common/cachev2/*
|
||||
// common/redis/redis.go +ScanKeys方法
|
||||
// web/main.go | app/main.go (+初始化缓存操作中间件)
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// cache object: cachev2.Classes().CacheTime(30*time.Minute).Key("cache-key").ResBind(&object).Cache(user_func, params...)
|
||||
// cache base type: cachev2.Classes().CacheTime(2*time.Hour).Key("cache-key").Cache(user_func, params...)
|
||||
// auto cache key(list): cachev2.Classes().CacheTime(600*time.Second).AutoListKey("table-name").Cache(user_func, params...)
|
||||
// auto cache key(info): cachev2.Classes().CacheTime(600*time.Second).AutoInfoKey("table-name", "id-string").Cache(user_func, params...)
|
||||
//
|
||||
// 清理缓存:
|
||||
//
|
||||
// clear cache key: cachev2.Classes().FussyClear("key-prefix*")
|
||||
// clear auto cache(just list cache): cachev2.Classes().Table("table-name").AutoClear(true, nil)
|
||||
// clear auto cache(just info cache): cachev2.Classes().Table("table-name").AutoClear(false, &idString)
|
||||
// clear auto cache(both of list and info): cachev2.Classes().Table("table-name").AutoClear(true, &idString)
|
||||
type BaseCache struct {
|
||||
// 缓存驱动器
|
||||
driver Driver
|
||||
// 缓存时间 s
|
||||
ttl int64
|
||||
// 是否随机时间
|
||||
random bool
|
||||
// 缓存随机时间 s
|
||||
randomTTL int
|
||||
// 是否需要刷新缓存
|
||||
refresh bool
|
||||
// 缓存key
|
||||
key string
|
||||
// 自动生成缓存key
|
||||
autoKey bool
|
||||
// 表名
|
||||
table string
|
||||
// 数据类型
|
||||
dataType string
|
||||
// 数据id
|
||||
dataID string
|
||||
// 缓存对象
|
||||
object interface{}
|
||||
// 日志驱动
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// basic 缓存基础
|
||||
var basic = &BaseCache{}
|
||||
|
||||
// Init 初始化
|
||||
func Init(d Driver, l *zap.Logger) {
|
||||
basic.driver = d
|
||||
basic.logger = l
|
||||
}
|
||||
|
||||
// Classes 获取操作类
|
||||
func Classes() *BaseCache {
|
||||
c := &BaseCache{
|
||||
driver: basic.driver,
|
||||
ttl: defaultCacheTime,
|
||||
random: true,
|
||||
logger: basic.logger,
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// New 新建缓存操作类
|
||||
func New(d Driver) *BaseCache {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
rnd := rand.Intn(30)
|
||||
c := &BaseCache{
|
||||
driver: d,
|
||||
ttl: defaultCacheTime,
|
||||
random: true,
|
||||
randomTTL: rnd,
|
||||
logger: basic.logger,
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Key 设置缓存键
|
||||
func (c *BaseCache) Key(k string) *BaseCache {
|
||||
c.key = k
|
||||
return c
|
||||
}
|
||||
|
||||
// AutoListKey 自动生成列表存储键
|
||||
func (c *BaseCache) AutoListKey(table string) *BaseCache {
|
||||
c.autoKey = true
|
||||
c.table = table
|
||||
c.dataType = dataTypeList
|
||||
return c
|
||||
}
|
||||
|
||||
// AutoInfoKey 自动生成单条数据键
|
||||
func (c *BaseCache) AutoInfoKey(table, id string) *BaseCache {
|
||||
c.autoKey = true
|
||||
c.table = table
|
||||
c.dataID = id
|
||||
c.dataType = dataTypeInfo
|
||||
return c
|
||||
}
|
||||
|
||||
// ResBind 设置缓存对象
|
||||
func (c *BaseCache) ResBind(o interface{}) *BaseCache {
|
||||
c.object = o
|
||||
return c
|
||||
}
|
||||
|
||||
// CacheTime 设定缓存时间
|
||||
func (c *BaseCache) CacheTime(t time.Duration) *BaseCache {
|
||||
c.ttl = int64(t.Seconds())
|
||||
c.random = true
|
||||
return c
|
||||
}
|
||||
|
||||
// CacheStair 阶梯式缓存
|
||||
func (c *BaseCache) CacheStair(td time.Duration) *BaseCache {
|
||||
second := int64(td.Seconds())
|
||||
t := time.Now()
|
||||
timeSecond := int64(math.Min(86400, math.Max(60, float64(second))))
|
||||
zeroTime := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()).Unix()
|
||||
over := t.Unix() - zeroTime
|
||||
|
||||
c.ttl = timeSecond - over%timeSecond
|
||||
c.random = false
|
||||
return c
|
||||
}
|
||||
|
||||
// Refresh 刷新缓存
|
||||
func (c *BaseCache) Refresh() *BaseCache {
|
||||
c.refresh = true
|
||||
return c
|
||||
}
|
||||
|
||||
// Cache 缓存并返回[对象]数据
|
||||
func (c *BaseCache) Cache(fn interface{}, p ...interface{}) (data interface{}, err error) {
|
||||
// 操作完成后需要重置缓存对象
|
||||
defer c.reset()
|
||||
|
||||
// 自动缓存key
|
||||
if c.autoKey {
|
||||
c.key, err = c.generalKey(false, p...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if c.key == "" {
|
||||
return nil, fmt.Errorf("缓存键未设置")
|
||||
}
|
||||
|
||||
var e error
|
||||
if c.refresh {
|
||||
_, _ = c.Delete(c.key)
|
||||
}
|
||||
// 获取缓存
|
||||
ok, d := c.getCache(c.key)
|
||||
// 存在缓在则进行反序列编码
|
||||
if ok {
|
||||
data, e = unSerialJson(d, c.object)
|
||||
if e == nil {
|
||||
//fmt.Println("data from cache")
|
||||
return
|
||||
}
|
||||
|
||||
c.LogError("unSerialJson error occur:", zap.Any("data", d), zap.Error(err))
|
||||
}
|
||||
|
||||
// 不存在缓存则获取锁
|
||||
lockKey := c.key + "_processing"
|
||||
ok, e = c.driver.SetNX(lockKey, 1, 5*time.Second)
|
||||
if !ok || e != nil {
|
||||
//fmt.Println("未获取到锁,等待中:", lockKey)
|
||||
i := 0
|
||||
expire := false
|
||||
var d string
|
||||
for expire == false {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
// 重新尝试获取缓存
|
||||
ok, d = c.getCache(c.key)
|
||||
i++
|
||||
expire = i >= 25 || ok
|
||||
//fmt.Printf("第 %d 次尝试获取缓存,key: %s \n", i, lockKey)
|
||||
}
|
||||
|
||||
if ok {
|
||||
data, e = unSerialJson(d, c.object)
|
||||
if e == nil {
|
||||
//fmt.Println("data from cache2")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//fmt.Println("已获取到cache锁:", lockKey)
|
||||
// 释放数据请求锁
|
||||
defer func(driver Driver, key string) {
|
||||
_, _ = driver.Del(key)
|
||||
}(c.driver, lockKey)
|
||||
|
||||
// 请求原始数据
|
||||
data, err = funcInvoke(fn, p...)
|
||||
if err != nil && err.Error() != "record not found" {
|
||||
c.LogError("acquire source data error occur:", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取原数据错误:%s", err.Error())
|
||||
}
|
||||
|
||||
// 缓存原始数据后释放锁
|
||||
d, e = serialJson(data)
|
||||
if e == nil {
|
||||
//fmt.Println("data form origin")
|
||||
// object 设置
|
||||
if c.object != nil {
|
||||
_, e = unSerialJson(d, c.object)
|
||||
if e != nil {
|
||||
c.LogError("unmarshal data to object error occur:", zap.Error(e))
|
||||
return data, fmt.Errorf("数据映射失败:%s", e.Error())
|
||||
}
|
||||
}
|
||||
e = c.setCache(c.key, d)
|
||||
if e != nil && c.logger != nil {
|
||||
c.LogError("set cache data error occur:", zap.Any("data", data), zap.Error(e))
|
||||
}
|
||||
} else {
|
||||
c.LogError("serialJson data error occur:", zap.Any("data", data), zap.Error(e))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// generalKey 自动生成缓存键
|
||||
func (c *BaseCache) generalKey(forFuzzyClear bool, p ...interface{}) (string, error) {
|
||||
var id, key string
|
||||
keyStr, e := serialJson(p)
|
||||
if e != nil {
|
||||
return "", fmt.Errorf("此类参数不支持自动生成key")
|
||||
}
|
||||
switch c.dataType {
|
||||
case dataTypeList:
|
||||
if forFuzzyClear {
|
||||
id = "*"
|
||||
} else {
|
||||
id = fmt.Sprintf("%x", md5.Sum([]byte(keyStr)))
|
||||
}
|
||||
key = fmt.Sprintf("table-%s:list-%s", c.table, id)
|
||||
case dataTypeInfo:
|
||||
if forFuzzyClear {
|
||||
id = c.dataID + ":*"
|
||||
} else {
|
||||
id = fmt.Sprintf("%s:%x", c.dataID, md5.Sum([]byte(keyStr)))
|
||||
}
|
||||
key = fmt.Sprintf("table-%s:info-%s", c.table, id)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// getCache 获取缓存
|
||||
func (c *BaseCache) getCache(key string) (ok bool, data string) {
|
||||
if !c.driver.IsExist(key) {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
res, err := c.driver.Get(key)
|
||||
if err != nil {
|
||||
c.LogError("get cache error:", zap.Any("cache key", key), zap.Error(err))
|
||||
return
|
||||
}
|
||||
if res == nil {
|
||||
return
|
||||
}
|
||||
|
||||
return true, *res
|
||||
}
|
||||
|
||||
// setCache 设置缓存
|
||||
func (c *BaseCache) setCache(key string, data string) error {
|
||||
if data == "" || data == "null" {
|
||||
c.ttl = emptyDataCacheTime
|
||||
} else if c.random {
|
||||
if c.randomTTL == 0 {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
rnd := rand.Intn(30)
|
||||
c.ttl += int64(rnd)
|
||||
} else {
|
||||
c.ttl += int64(c.randomTTL)
|
||||
}
|
||||
}
|
||||
|
||||
return c.driver.Set(key, data, time.Duration(c.ttl)*time.Second)
|
||||
}
|
||||
|
||||
// Delete 清除指定缓存
|
||||
func (c *BaseCache) Delete(key ...string) (int64, error) {
|
||||
c.refresh = false
|
||||
count, err := c.driver.Del(key...)
|
||||
if err != nil {
|
||||
c.LogError("delete cache key error", zap.Any("key", key), zap.Error(err))
|
||||
return count, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// DeleteCurrent 清除当前缓存
|
||||
func (c *BaseCache) DeleteCurrent(p ...interface{}) (int64, error) {
|
||||
var err error
|
||||
if c.key == "" {
|
||||
c.key, err = c.generalKey(false, p...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return c.Delete(c.key)
|
||||
}
|
||||
|
||||
// FussyClear 模糊匹配删除
|
||||
func (c *BaseCache) FussyClear(match string) (int64, error) {
|
||||
go func() {
|
||||
var err error
|
||||
var keys []string
|
||||
keys, err = c.driver.ScanKeys(match)
|
||||
if err != nil {
|
||||
c.LogError("scan cache key error", zap.Any("match", match), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// 分批删除
|
||||
var batch = 10
|
||||
for i := 0; i < len(keys); i += batch {
|
||||
if i+batch >= len(keys) {
|
||||
_, err = c.Delete(keys[i:]...)
|
||||
} else {
|
||||
_, err = c.Delete(keys[i : i+batch]...)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// Table 设置表名
|
||||
func (c *BaseCache) Table(table string) *BaseCache {
|
||||
c.table = table
|
||||
return c
|
||||
}
|
||||
|
||||
// AutoClear 清理自动生成的列表/详情缓存
|
||||
func (c *BaseCache) AutoClear(clearList bool, id *string) (count int64, err error) {
|
||||
// 判断是否设置表
|
||||
//if c.table == "" {
|
||||
// return 0, fmt.Errorf("清理表缓存失败,未设置表名")
|
||||
//}
|
||||
//
|
||||
//var tc int64
|
||||
//// 先清理列表缓存
|
||||
//if clearList {
|
||||
// c.dataType = dataTypeList
|
||||
// key, _ := c.generalKey(true)
|
||||
// tc, err = c.FussyClear(key)
|
||||
// count += tc
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
//}
|
||||
//// 清理详情数据
|
||||
//if id != nil {
|
||||
// c.dataType = dataTypeInfo
|
||||
// c.dataID = *id
|
||||
// key, _ := c.generalKey(true)
|
||||
// tc, err = c.FussyClear(key)
|
||||
// count += tc
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
//}
|
||||
return
|
||||
}
|
||||
|
||||
// LogError 日志错误记录
|
||||
func (c *BaseCache) LogError(msg string, fs ...zap.Field) {
|
||||
if c.logger != nil {
|
||||
dep := 0
|
||||
t := make([]string, 0, 10)
|
||||
for i := 1; i < 10; i++ {
|
||||
_, file, line, ok := runtime.Caller(i)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if strings.Contains(file, "/runtime/") || strings.Contains(file, "/reflect/") {
|
||||
continue
|
||||
}
|
||||
t = append(t, fmt.Sprintf("%s∟%s:%d", strings.Repeat(" ", dep), file, line))
|
||||
dep++
|
||||
}
|
||||
exception := fmt.Sprintf("[MSG]%s\n[Stack]\n%s", msg, strings.Join(t, "\n"))
|
||||
|
||||
c.logger.Error(exception, fs...)
|
||||
}
|
||||
}
|
||||
|
||||
// reset 重置缓存
|
||||
func (c *BaseCache) reset() {
|
||||
c.ttl = defaultCacheTime
|
||||
c.random = true
|
||||
c.randomTTL = 0
|
||||
c.refresh = false
|
||||
c.key = ""
|
||||
c.autoKey = false
|
||||
c.table = ""
|
||||
c.dataType = ""
|
||||
c.dataID = ""
|
||||
c.object = nil
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cachev2
|
||||
|
||||
import "time"
|
||||
|
||||
// Driver 缓存驱动接口定义
|
||||
type Driver interface {
|
||||
IsExist(key string) bool
|
||||
Get(key string) (*string, error)
|
||||
Set(key string, value interface{}, expire time.Duration) error
|
||||
SetNX(key string, value interface{}, expiration time.Duration) (bool, error)
|
||||
ScanKeys(match string) (keys []string, err error)
|
||||
Del(keys ...string) (n int64, err error)
|
||||
Close() error
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package cachev2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// funcInvoke 函数调用
|
||||
func funcInvoke(fun interface{}, args ...interface{}) (data interface{}, err error) {
|
||||
ft := reflect.TypeOf(fun)
|
||||
fv := reflect.ValueOf(fun)
|
||||
if ft.Kind() != reflect.Func {
|
||||
return nil, fmt.Errorf("CACHE:不是一个有效的函数")
|
||||
}
|
||||
funcArgsNum := ft.NumIn()
|
||||
if len(args) != funcArgsNum {
|
||||
return nil, fmt.Errorf("CACHE:参数未对齐错误,方法名:%s,方法参数数:%d,传入参数数:%d", runtime.FuncForPC(fv.Pointer()).Name(), funcArgsNum, len(args))
|
||||
}
|
||||
funcResNum := ft.NumOut()
|
||||
if funcResNum < 1 {
|
||||
return nil, fmt.Errorf("CACHE:没有返回值的无效调用,方法名:%s", runtime.FuncForPC(fv.Pointer()).Name())
|
||||
}
|
||||
|
||||
// 调用函数
|
||||
var funRes []reflect.Value
|
||||
if funcArgsNum == 0 {
|
||||
funRes = fv.Call(nil)
|
||||
} else {
|
||||
argsV := make([]reflect.Value, 0, len(args))
|
||||
for _, arg := range args {
|
||||
argsV = append(argsV, reflect.ValueOf(arg))
|
||||
}
|
||||
|
||||
funRes = fv.Call(argsV)
|
||||
}
|
||||
|
||||
data = funRes[0].Interface()
|
||||
for _, ret := range funRes {
|
||||
if ret.Type().String() == "error" {
|
||||
e, ok := ret.Interface().(error)
|
||||
if ok {
|
||||
err = e
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// serialJson 序列化为JSON
|
||||
func serialJson(m interface{}) (string, error) {
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// unSerialJson 从JSON字符串反序列化
|
||||
func unSerialJson(str string, resObj interface{}) (m interface{}, err error) {
|
||||
if resObj == nil {
|
||||
err = json.Unmarshal([]byte(str), &m)
|
||||
} else {
|
||||
err = json.Unmarshal([]byte(str), &resObj)
|
||||
m = resObj
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package checkWx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
)
|
||||
|
||||
const ApiToken string = "866mcJyA4teCehxivaCJec3qbghWStjA"
|
||||
|
||||
type Resp struct {
|
||||
Code stderr.Code `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type CheckResp struct {
|
||||
Code stderr.Code `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
Pass bool `json:"pass"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// 获取订单信息
|
||||
func CheckWx(execUrl, domian string) (checkResp CheckResp, err error) {
|
||||
c, cancle := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancle()
|
||||
code, err := httputil.DefaultClientGetWithRespWithCtx(c, &checkResp, execUrl+"?"+bindUrl(domian), nil)
|
||||
log.Info("http method CheckWx response code ==>", log.Any("statusCode", code), log.Any("checkResp", checkResp))
|
||||
if err != nil {
|
||||
log.Error("CheckWx failed", log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TgSend(msgPayload, text string) (resp Resp, err error) {
|
||||
params := map[string]interface{}{
|
||||
"tgName": "ys_bot",
|
||||
"text": text,
|
||||
"msgPayload": msgPayload,
|
||||
"chatId": -228259065,
|
||||
}
|
||||
c, cancle := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancle()
|
||||
code, err := httputil.DefaultClientPostJsonWithRespWithCtx(c, &resp, "http://uni.ztgba.com/api/tg/send", nil, params)
|
||||
log.Info("http method TgSend response code ==>", log.Any("statusCode", code), log.Any("resp", resp))
|
||||
if err != nil {
|
||||
log.Error("TgSend failed", log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func bindUrl(domain string) string {
|
||||
buf := bytes.Buffer{}
|
||||
buf.WriteString("apiToken=")
|
||||
buf.WriteString(ApiToken + "&")
|
||||
buf.WriteString("req_url=")
|
||||
buf.WriteString(domain)
|
||||
return buf.String()
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func LoadJSON(path string, cfg interface{}) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
bs, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = json.Unmarshal(bs, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package cacheconst
|
||||
|
||||
const LocalCdnCacheKey = "cdn:cache" //本地缓存cdn域名列表
|
||||
@@ -0,0 +1,375 @@
|
||||
package constant
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
//DevRunmod 开发环境
|
||||
DevRunmod = "dev"
|
||||
//TestRunmod 测试环境
|
||||
TestRunmod = "test"
|
||||
//RelRunmod 生产环境
|
||||
ReleaseRunmod = "prod"
|
||||
//ChannelTypeFile 渠道日志导出
|
||||
ChannelLogTypeFile = 1
|
||||
//ChannelNumTypeFile 渠道号导出
|
||||
ChannelNumTypeFile = 2
|
||||
//UserList 用户列表导出
|
||||
UserList = 3
|
||||
Ver1_6_4 = "1.6.4"
|
||||
Ver2_1_0 = "2.1.0"
|
||||
Ver3_0_7 = "3.0.7"
|
||||
Ver3_1_0 = "3.1.0"
|
||||
Ver3_6_0 = "3.6.0" //更换了加密方式, 增加了注册 devID 签名校验, 修正了x-api-key的问题
|
||||
)
|
||||
|
||||
const DefaultBloggerVideoIncomeTaxLevel int64 = 4
|
||||
|
||||
type NewsType = string
|
||||
type AcgNewsType = string
|
||||
type KeywordType = string
|
||||
|
||||
// 全局统一类型
|
||||
const (
|
||||
SP NewsType = "SP" // 长视频
|
||||
SHORT NewsType = "SHORT" // 短视频
|
||||
COVER NewsType = "COVER" // 图文帖子
|
||||
PIC NewsType = "PIC" // 图集帖子
|
||||
AD_COVER NewsType = "AD_COVER" // 图片广告
|
||||
AD_SP NewsType = "AD_SP" // 视频广告
|
||||
SEED_LINK NewsType = "SEED_LINK" // 种子/黄油帖子
|
||||
AiPlaza string = "AiPlaza" // ai广场
|
||||
|
||||
KeywordTag KeywordType = "TAG" // 标签关键词
|
||||
KeywordUser KeywordType = "USER" // 用户关键词
|
||||
KeywordComment KeywordType = "COMMENT" // 评论关键词
|
||||
|
||||
Cartoon AcgNewsType = "video" // 动漫
|
||||
Comics AcgNewsType = "image" // 漫画
|
||||
Text AcgNewsType = "text" // 小说
|
||||
Drama AcgNewsType = "drama" // 短剧
|
||||
)
|
||||
|
||||
type RealmType = string
|
||||
|
||||
const (
|
||||
SearchSP RealmType = SP
|
||||
SearchShort RealmType = SHORT
|
||||
SearchCover RealmType = COVER
|
||||
SearchPic RealmType = PIC
|
||||
|
||||
SearchTag RealmType = KeywordTag
|
||||
SearchUser RealmType = KeywordUser
|
||||
|
||||
// 单个功能特殊自定义
|
||||
//SearchAll = RealmType("ALL") // 搜索全部
|
||||
SearchComplex = RealmType("COMPLEX")
|
||||
|
||||
// Deprecated
|
||||
FictionRealmType RealmType = "fiction"
|
||||
// Deprecated
|
||||
AudioBookRealmType RealmType = "audiobook"
|
||||
)
|
||||
|
||||
// 收藏类型
|
||||
type CollectType = string
|
||||
|
||||
const (
|
||||
CollectTypeSP CollectType = SP // 影视
|
||||
CollectTypeShort CollectType = SHORT // 短视频
|
||||
CollectTypeCover CollectType = COVER // 图文
|
||||
CollectTypePIC CollectType = PIC // 图集帖子
|
||||
CollectTypeSEED_LINK CollectType = SEED_LINK // 黄油/种子帖子
|
||||
CollectTypeAiPlaza CollectType = AiPlaza // ai广场帖子
|
||||
|
||||
CollectTypeTag CollectType = KeywordTag // 话题
|
||||
|
||||
// Deprecated
|
||||
CollectTypeLocation = "location"
|
||||
)
|
||||
|
||||
// 点赞类型
|
||||
type LikeType = string
|
||||
|
||||
const (
|
||||
LikeTypeSP LikeType = SP // 长视频
|
||||
LikeTypeShort LikeType = SHORT // 短视频
|
||||
LikeTypeCover LikeType = COVER // 图文
|
||||
LikeTypePic LikeType = PIC // 图集
|
||||
LikeTypeSEED_LINK LikeType = SEED_LINK // 黄油种子帖
|
||||
LikeTypeAiPlaza LikeType = AiPlaza // ai广场帖子
|
||||
|
||||
// ACG "video":视频,"image":图片,"text":小说
|
||||
LikeTypeCartoon LikeType = Cartoon // 动漫
|
||||
LikeTypeComics LikeType = Comics // 漫画
|
||||
LikeTypeText LikeType = Text // 小说
|
||||
LikeTypeDrama LikeType = Drama // 短剧
|
||||
|
||||
LikeTypeComment LikeType = KeywordComment // 评论
|
||||
)
|
||||
|
||||
// 评论状态
|
||||
const (
|
||||
CommentSTSAccessed = 1 // 通过审核
|
||||
CommentSTSDeleted = 2 // 已删除
|
||||
CommentSTSAutoFiltered = 3 // 被自动过滤
|
||||
)
|
||||
|
||||
// 设备
|
||||
const (
|
||||
DeviceTypeIOS = "ios"
|
||||
DeviceTypeAndroid = "android"
|
||||
DeviceTypeH5 = "h5"
|
||||
)
|
||||
|
||||
const AdGroupClientVersion = "1.12.2" // 这里得改成自己上线的这个ab测安卓版本号
|
||||
|
||||
// 机型
|
||||
const (
|
||||
SysTypeIOS = "ios"
|
||||
SysTypeAndroid = "android"
|
||||
SysTypeH5 = "h5"
|
||||
)
|
||||
|
||||
// gin中ctx使用的key
|
||||
const (
|
||||
CtxUserID = "USER_ID"
|
||||
CtxAdminAct = "ADMIN_ACT"
|
||||
CtxDistrictName = "DISTRICT_USER_ID"
|
||||
CtxUA = "UA"
|
||||
CtxIP = "IP"
|
||||
CtxJuShangCID = "JU_SHANG_CID"
|
||||
CtxAdminRole = "ADMIN_ROLE"
|
||||
CtxNudeChatMerchant = "Nude_Chat_Merchant"
|
||||
)
|
||||
|
||||
const (
|
||||
CaptchaLen = 6
|
||||
)
|
||||
|
||||
const (
|
||||
ProdEnv = "prod"
|
||||
)
|
||||
|
||||
// 推广码链接参数
|
||||
const (
|
||||
PromotionField = "?pc="
|
||||
DiscField = "?dc="
|
||||
)
|
||||
|
||||
// web操作栏目
|
||||
const (
|
||||
Administrator = "系统管理员"
|
||||
AuthorManager = "权限管理"
|
||||
UserManageList = "用户列表"
|
||||
VideoManageCheck = "视频审核"
|
||||
VideoManageList = "视频列表"
|
||||
VideoDiscountArea = "折扣专区"
|
||||
VideoDiscountAreaVideo = "折扣专区视频"
|
||||
VideoManageTag = "标签管理"
|
||||
VideoManageCity = "城市列表"
|
||||
VideoManageHotCity = "热门城市"
|
||||
VideoManageComment = "评论管理"
|
||||
VideoManageSource = "资源管理"
|
||||
VideoManageTone = "音色最热"
|
||||
ProductManageVIP = "vip商品列表"
|
||||
ProductManageCoin = "金币列表"
|
||||
ProductManagePage = "推广落地页配置"
|
||||
ProductManagePayType = "支付方式"
|
||||
TradeManageRechargeOrder = "充值订单管理"
|
||||
TradeManageWithdrawOrder = "提现订单列表"
|
||||
FinancialTransferOrder = "财务转账列表"
|
||||
TradeManageWithdrawType = "提现方式配置"
|
||||
TradeManageWithdrawBank = "提现银行配置"
|
||||
TradeManageWithdrawRefund = "提现人工退款"
|
||||
RejectTemplate = "审核拒绝模板"
|
||||
|
||||
AdsManageAdsList = "广告列表"
|
||||
AdsManageAnnousList = "公告管理"
|
||||
AdsManageAnnounceList = "会员中心跑马灯管理"
|
||||
SystemManageVersion = "版本管理"
|
||||
ActiveManageList = "活动列表"
|
||||
IPBlockList = "IP限制列表"
|
||||
IPWhiteList = "IP白名单列表"
|
||||
IPWhiteRedisKey = "white_ip_"
|
||||
SwitchList = "开关量列表"
|
||||
AutoAccount = "自助结算审核"
|
||||
ExchangeCode = "兑换码管理"
|
||||
MerchantAdmin = "代充商人管理"
|
||||
Disc = "商区"
|
||||
GoldConfig = "支付优惠配置"
|
||||
FreeVidConfig = "免费观看配置"
|
||||
RoleConfig = "角色配置"
|
||||
JuShangManageList = "聚商管理"
|
||||
NewsModel = "嫩模活动"
|
||||
RewardList = "获奖列表"
|
||||
Loufeng = "楼风"
|
||||
VerifyReport = "验证报告"
|
||||
Fiction = "电子书小说"
|
||||
Audiobook = "有声小说"
|
||||
VIPConfig = "VIP配置"
|
||||
AiUndressList = "AI脱衣列表"
|
||||
IntegralConfig = "积分配置列表"
|
||||
IntegralExchange = "积分兑换列表"
|
||||
OfficialConfig = "官方配置"
|
||||
AiChangefaceVidMod = "AI换脸视频模版"
|
||||
AiChangeface = "AI视频换脸"
|
||||
AiChangeFaceImgList = "AI图片换脸列表"
|
||||
Section = "专题列表"
|
||||
VideoGoldCoinList = "金币视频列表"
|
||||
AdvanceConfig = "预售配置列表"
|
||||
OfficialWebsiteConfig = "官网配置"
|
||||
)
|
||||
|
||||
// web操作方式
|
||||
const (
|
||||
Add = "新增"
|
||||
Delete = "删除"
|
||||
Modify = "修改"
|
||||
)
|
||||
|
||||
// 推荐维度
|
||||
const (
|
||||
ChosenVideo = "chosenVideo"
|
||||
TagVideo = "tagVideo"
|
||||
SameCityVideo = "sameCityVideo"
|
||||
NewVideo = "newVideo"
|
||||
UnPopularVideo = "unPopularVideo"
|
||||
ForcePushSP = "forcePushSP" //强推视频纬度
|
||||
ChargeVideo = "chargeVideo"
|
||||
)
|
||||
|
||||
// IpBlock 类型
|
||||
const (
|
||||
BlockComment = "comment"
|
||||
FrequencyComment = "frequencyComment" // 评论频率
|
||||
Register = "login" //限制用户注册
|
||||
GlobalBlock = "global" //全局限制该IP用户访问
|
||||
)
|
||||
|
||||
// switch 落地页按钮类型
|
||||
type SwitchAct string
|
||||
|
||||
const (
|
||||
SwitchIosEnterprise SwitchAct = "iosEnterprise"
|
||||
SwitchIosStore SwitchAct = "iosStore" //商店包(TF包)
|
||||
SwitchAndroid SwitchAct = "android"
|
||||
)
|
||||
|
||||
// switch 落地页按钮样式
|
||||
type SwitchStyle string
|
||||
|
||||
const (
|
||||
SwitchStress SwitchStyle = "switchStress" //突出的开关风格
|
||||
SwitchSimple SwitchStyle = "switchSimple" //简单的开关风格
|
||||
)
|
||||
|
||||
// 机器人的uid最大值
|
||||
const (
|
||||
RobotUIDLimit = 111999
|
||||
)
|
||||
|
||||
// DeleteUID
|
||||
const (
|
||||
DeleteUID uint64 = 100008
|
||||
)
|
||||
|
||||
const (
|
||||
UserLowestTrueScore int64 = 10
|
||||
)
|
||||
|
||||
type DomainNameStatus int64
|
||||
|
||||
const (
|
||||
Normal DomainNameStatus = iota
|
||||
WxBlock
|
||||
)
|
||||
const (
|
||||
RCHG_Mode_SDK = "sdk"
|
||||
RCHG_Mode_URL = "url"
|
||||
RechargeAmtTolerance = 500
|
||||
)
|
||||
const (
|
||||
MongoRandomSpareLen int = 5
|
||||
)
|
||||
|
||||
const (
|
||||
Media_Del = 1 //"媒体删除"
|
||||
|
||||
Media_Edit_Status = 2 //"媒体更新状态"
|
||||
|
||||
Media_Edit_Price = 3 //"媒体更新价格"
|
||||
|
||||
Media_Edit_Content = 4 //"媒体更新标题内容"
|
||||
|
||||
Media_Edit_Remark = 5 //"媒体更新备注"
|
||||
|
||||
User_INCoin = 6 //"用户追加金币"
|
||||
|
||||
User_Edit_VIP_Type = 7 //"用户更新VIP类型"
|
||||
|
||||
User_Edit_VIP_PromoteEnd = 8 //"用户更新VIP新推广赠送免费到期时间"
|
||||
|
||||
User_Edit_VIP_Expire = 9 //"用户更新VIP过期时间"
|
||||
|
||||
User_Edit_VIP_RechargeLevel = 10 //"用户更新VIP充值等级"
|
||||
|
||||
User_INFruitCoin = 11 //"用户追加果币"
|
||||
|
||||
User_ReduceAmount = 12 //"回收用户金币"
|
||||
|
||||
User_ReduceIncome = 13 //"回收用户收益"
|
||||
|
||||
User_INAiMateBalance = 14 // 用户追加ai伴侣币
|
||||
|
||||
User_INIntegral = 15 //"用户追加积分"
|
||||
)
|
||||
|
||||
const (
|
||||
FakeMobilePrefix string = "+86122"
|
||||
)
|
||||
const (
|
||||
MediaSourceSP string = "sp"
|
||||
MediaSourceSPPrefixPath string = "/sp"
|
||||
MediaSourcePMS string = "pms"
|
||||
MediaSourcePMSPrefixPath string = "/pms"
|
||||
ImageSourceIMS string = "ims"
|
||||
ImageSourceIMSPrefixPath string = "/ims"
|
||||
MediaSourceJH1B string = "jh1b" // 嘉华1部
|
||||
MediaSourceLaoSiJi string = "laosiji" // 老司机
|
||||
MediaSourceAuthKey string = "fT5xSg4hltHpzVy6aV9rVECDJ1J1pN"
|
||||
)
|
||||
|
||||
// DefaultTsAuthKeyVersion 未配置密钥版本时的默认版本标识,随 TS URL 以 v={keyVersion} 下发。
|
||||
const DefaultTsAuthKeyVersion = "default"
|
||||
|
||||
// TsAuthKeyConfig 媒体分片(TS)鉴权签名密钥配置。
|
||||
type TsAuthKeyConfig struct {
|
||||
KeyVersion string `json:"keyVersion"` // 密钥版本,随 TS URL 以 v={keyVersion} 下发
|
||||
Key string `json:"key"` // 签名密钥
|
||||
}
|
||||
|
||||
// Resolve 返回生效的密钥版本与签名密钥:
|
||||
// 仅当未配置 Key(空或纯空白)时,才回退到内置默认——版本 DefaultTsAuthKeyVersion、密钥 MediaSourceAuthKey;
|
||||
// 配置了 Key 时,版本与密钥均按配置原样返回(版本可为空)。
|
||||
func (c TsAuthKeyConfig) Resolve() (keyVersion, key string) {
|
||||
if strings.TrimSpace(c.Key) == "" {
|
||||
return DefaultTsAuthKeyVersion, MediaSourceAuthKey
|
||||
}
|
||||
return strings.TrimSpace(c.KeyVersion), c.Key
|
||||
}
|
||||
|
||||
type Terminal = string //终端型号
|
||||
|
||||
const (
|
||||
TerminalAndroid Terminal = "0"
|
||||
TerminalH5 Terminal = "1"
|
||||
TerminalWeb Terminal = "2"
|
||||
)
|
||||
|
||||
// UIThemeEnum UI主题枚举
|
||||
type UIThemeEnum int
|
||||
|
||||
const (
|
||||
ThemeDefault UIThemeEnum = 0 // 默认主题
|
||||
ThemeNewYear UIThemeEnum = 1 // 新春主题
|
||||
)
|
||||
@@ -0,0 +1,136 @@
|
||||
package imnotifyconst
|
||||
|
||||
import "fmt"
|
||||
|
||||
//im 通知 模块
|
||||
|
||||
// Action 消息行为
|
||||
type Action string
|
||||
|
||||
// Message 消息结构体
|
||||
type Message struct {
|
||||
Title string `json:"title"` //消息标题
|
||||
MsgType Action `json:"msgType"` //消息类型
|
||||
Data string `json:"data"` //内容
|
||||
}
|
||||
|
||||
// 消息类型
|
||||
const (
|
||||
AccountCharge Action = "CHARGE" //充值消息
|
||||
VIPCharge Action = "VIP-CHARGE" //VIP充值消息
|
||||
BuyModel Action = "BUY—MODEL" //购买嫩模
|
||||
AccountWithDraw Action = "WITHDRAW" //提现消息
|
||||
VideoCheck Action = "VIDCHECK" //视频审核
|
||||
BlockUser Action = "BLOCK" //用户禁止登陆
|
||||
BannedUser Action = "BANNED" //用户禁言
|
||||
UnBannedUser Action = "UNBANNED" //解除禁言
|
||||
ForbidUpload Action = "FORBIDUPLOAD" //禁止上传文件
|
||||
UnForbidUpload Action = "UNFORBIDUPLOAD" //解除禁止上传文件
|
||||
SysOpUserCoins Action = "SysOpUserCoins" //系统操作用户金币
|
||||
OfficialRecharge Action = "OfficialRecharge" //官方充值
|
||||
OfficialSeekScore Action = "OfficialSeekScore" //官方追分
|
||||
MeetingCard Action = "MeetingCard" //购买约会卡
|
||||
UserReward Action = "UserReward" //玩家打赏通知
|
||||
OtherCard Action = "OtherCard" //购买其他卡
|
||||
UserFeedBackReply Action = "UserFeedBackReply" //用户反馈回复
|
||||
LoufengFeedBackReply Action = "LoufengFeedBackReply" //楼凤举报回复
|
||||
VerifyReport Action = "VerifyReport" //验证报告回复
|
||||
UnBlockUser Action = "UNBLOCK" //解除用户禁止登陆
|
||||
WithDrawRefuse Action = "WithDrawRefuse" //提现拒绝消息
|
||||
WithDrawUnknownErr Action = "WithDrawUnknownErr" //提现未知错误
|
||||
GrandPrize Action = "GrandPrize" //大奖通知
|
||||
ConsumerFeedbackGame Action = "ConsumerFeedbackGame" //充值有礼(游戏)
|
||||
ConsumerFeedback Action = "ConsumerFeedback" //消费回馈(楼凤)
|
||||
NudeChatFeedBackReply Action = "NudeChatFeedBackReply" //裸聊举报回复
|
||||
SysOpUserFruitCoin Action = "SysOpUserFruitCoin" //系统操作用户果币
|
||||
)
|
||||
|
||||
var imNotifyTitle = map[Action]string{
|
||||
VIPCharge: "VIP购买",
|
||||
BuyModel: "购买嫩模",
|
||||
AccountCharge: "充值到账",
|
||||
AccountWithDraw: "提现到账",
|
||||
VideoCheck: "视频审核",
|
||||
BlockUser: "禁止登陆",
|
||||
BannedUser: "禁言通知",
|
||||
UnBannedUser: "解除用户禁言",
|
||||
ForbidUpload: "禁止上传帖子",
|
||||
UnForbidUpload: "解除禁止上传帖子",
|
||||
SysOpUserCoins: "系统金币管理",
|
||||
OfficialRecharge: "官方充值",
|
||||
OfficialSeekScore: "官方追分",
|
||||
MeetingCard: "购买约会卡",
|
||||
UserReward: "玩家打赏",
|
||||
UserFeedBackReply: "官方回复",
|
||||
OtherCard: "购买其他卡",
|
||||
LoufengFeedBackReply: "楼凤举报",
|
||||
VerifyReport: "验证报告",
|
||||
UnBlockUser: "解除封禁",
|
||||
WithDrawRefuse: "提现拒绝",
|
||||
WithDrawUnknownErr: "提现未知错误",
|
||||
GrandPrize: "中奖通知",
|
||||
ConsumerFeedbackGame: "充值有礼",
|
||||
ConsumerFeedback: "消费回馈",
|
||||
NudeChatFeedBackReply: "裸聊举报",
|
||||
}
|
||||
|
||||
var imNotifyContent = map[Action]string{
|
||||
VIPCharge: "官人,恭喜您购买%s成功,小娘子在此等候您的光临哦!",
|
||||
BuyModel: "官人,恭喜您%s,小娘子在此等候您的光临哦!",
|
||||
AccountCharge: "充值成功到账%s金币,请查收!",
|
||||
AccountWithDraw: "提现成功到账%s金币,请查收!",
|
||||
VideoCheck: "您的视频%s因%s审核未通过!",
|
||||
BlockUser: "尊敬的用户,您因:%s,现做出封禁处理,如需要解除封禁,请联系客服咨询,谢谢!",
|
||||
BannedUser: "尊敬的用户,您因:%s,受到系统禁言处理,如需要解除禁言,请联系客服咨询,谢谢!",
|
||||
UnBannedUser: "尊敬的用户,恭喜您解除禁言,现在您可以畅所欲言啦!",
|
||||
ForbidUpload: "尊敬的用户,您因:%s,系统作出禁止上传文件处理,如需要解除限制,请联系客服咨询,谢谢!",
|
||||
UnForbidUpload: "尊敬的用户,恭喜您解除禁止上传帖子,现在您可以上传帖子,痛快的赚取收益啦!",
|
||||
SysOpUserCoins: "尊敬的用户,系统%s您%s金币,请注意查看,谢谢!",
|
||||
OfficialRecharge: "尊敬的用户,您已经通过官方充值到账%s金币,请注意查看,谢谢!",
|
||||
OfficialSeekScore: "尊敬的用户,官方追分%s金币,如有疑惑,请联系客服!",
|
||||
MeetingCard: "官人,恭喜您购买%s成功,小娘子在此等候您的光临哦!",
|
||||
UserReward: "玩家%s打赏%s金币,请查收!",
|
||||
OtherCard: "官人,恭喜您购买%s成功,小娘子在此等候您的光临哦!",
|
||||
UserFeedBackReply: "尊敬的用户!针对您的问题:%s,官方在此作出认真解答:%s,如果您还有疑惑,请联系客服或继续反馈,谢谢您对我们的支持!",
|
||||
LoufengFeedBackReply: "尊敬的用户!针对你举报的楼凤信息,官方已作出相应处理:%s,给您造成的不便,深感抱歉!",
|
||||
VerifyReport: "尊敬的用户!您提交的关于%s验证报告因%s审核未通过!",
|
||||
UnBlockUser: "尊敬的用户,恭喜您解除封禁.%s",
|
||||
WithDrawRefuse: "尊敬的用户,你的提现申请因-s%已失败,金币已返还.",
|
||||
WithDrawUnknownErr: "尊敬的用户,你的提现申请因网络异常原因发生异常,请联系客服确认金币是否返还",
|
||||
GrandPrize: "官人,恭喜您%s,小娘子在此等候您的光临哦!",
|
||||
ConsumerFeedbackGame: "恭喜您昨日游戏累计楼充值%s元,已满足游戏充值有礼领取条件,快点击本消息领取吧~",
|
||||
ConsumerFeedback: "恭喜您今日累计楼凤消费%s金币,已满足消费回馈领取条件,快点击本消息领取吧~",
|
||||
NudeChatFeedBackReply: "尊敬的用户!针对你举报的裸聊信息,官方已作出相应处理:%s,给您造成的不便,深感抱歉!",
|
||||
SysOpUserFruitCoin: "尊敬的用户,系统%s您%s果币币,请注意查看,谢谢!",
|
||||
}
|
||||
|
||||
func (a Action) Title() string {
|
||||
if v, ok := imNotifyTitle[a]; ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 适用于无字符串字符的fmt模版
|
||||
func (a Action) Desc() string {
|
||||
if v, ok := imNotifyContent[a]; ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 适用于单个字符串字符的fmt模版
|
||||
func (a Action) SingleStringParamData(amount string) string {
|
||||
if v, ok := imNotifyContent[a]; ok {
|
||||
return fmt.Sprintf(v, amount)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 适用于双个字符串字符的fmt模版
|
||||
func (a Action) DoubleStringParamData(p1 string, p2 string) string {
|
||||
if v, ok := imNotifyContent[a]; ok {
|
||||
return fmt.Sprintf(v, p1, p2)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package redisconst
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMostNewModuleVideoListKeyKeepsOriginalFormat(t *testing.T) {
|
||||
got := GetMostNewModuleVideoListKey(1, 1, 10)
|
||||
if want := "mostNewModuleVideoList:1:1:10"; got != want {
|
||||
t.Fatalf("cache key = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,688 @@
|
||||
package redisconst
|
||||
|
||||
import (
|
||||
"91porn-server/common/crypt"
|
||||
"91porn-server/common/timeutil"
|
||||
"91porn-server/common/timeutil/timerange"
|
||||
"91porn-server/models/s/statrecordmod"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const (
|
||||
Db0ExpiredChannel = "__keyevent@0__:expired"
|
||||
|
||||
userTokenFmt = "token:user:%d"
|
||||
UserTokenExpire = time.Hour
|
||||
|
||||
imSDKUserTokenFmt = "im:sdk:userToken:%d" // IM SDK 用户 token 缓存(按 imUserID)
|
||||
IMSDKUserTokenExpire = 12 * time.Hour
|
||||
|
||||
imSDKEnsureLockFmt = "im:sdk:ensure:lock:%d" // EnsureSDKUser 按 uid 加锁,防并发重复注册
|
||||
|
||||
IMSDKAppTokenKey = "im:sdk:appToken" // IM SDK appToken 缓存(内存 L1 + redis L2)
|
||||
|
||||
MobileCaptchaMaxCountInMinute = 1 // 1分钟内同一手机号只能发送一次验证码
|
||||
MobileCaptchaMaxCountInHour = 5 // 1小时内同一手机号只能发送五次验证码
|
||||
MobileCaptchaMaxCountInDay = 10 // 1天内同一手机号只能发送十次验证码
|
||||
mobileCaptchaFmt = "captcha:mobile:%s"
|
||||
MobileCaptchaExpire = 5 * time.Minute
|
||||
|
||||
smsCaptchaIPFmt = "sms:captcha:ip:%s"
|
||||
smsCaptchaUIDFmt = "sms:captcha:uid:%d"
|
||||
smsCaptchaPhoneNumberFmt = "sms:captcha:number:%s" // sms:captcha:number:mobile:xxx 用于统计手机号发送验证码的次数
|
||||
smsCaptchaUIDCooldownFmt = "sms:captcha:uid:cd:%d"
|
||||
SMSCaptchaUIDCooldown = 1 * time.Minute
|
||||
|
||||
emailCaptchaFmt = "captcha:email:%s"
|
||||
EmailCaptchaExpire = 5 * time.Minute
|
||||
|
||||
traderChatCacheFmt = "trader:cache:%d:%d"
|
||||
TraderChatCacheExpire = 2 * time.Minute
|
||||
|
||||
withdrawCfgFmt = "withdraw:cache:cfg"
|
||||
WithdrawCfgExpire = 8 * time.Hour
|
||||
|
||||
withdrawTariffFmt = "withdraw:tariff"
|
||||
WithdrawTariffExpire = 8 * time.Hour
|
||||
|
||||
webTokenFmt = "token:%s:%s" //token:admin:xxx, token:channel:xxx
|
||||
WebTokenExpire = 10 * time.Hour
|
||||
|
||||
visitLogsFmt = "visitlogs:%s" //visitlog:20301020
|
||||
|
||||
visitFmt = "visitFmt:%s" //统计访问频率
|
||||
VisitExpireMax = 1 * time.Hour
|
||||
|
||||
registFmt = "registFmt:%s" //用户注册频率
|
||||
RegistExpireMax = 1 * time.Hour
|
||||
|
||||
rankFmt = "rankFmt:%s"
|
||||
RankExpireMax = 20 * time.Minute
|
||||
|
||||
toneFmt = "toneFmt"
|
||||
ToneExpireMax = 20 * time.Minute
|
||||
|
||||
ipLocationFmt = "ip:city:%s" //ip:city:124.168.1.0.248 缓存ip
|
||||
IPLocationExpireMax = 24 * time.Hour
|
||||
|
||||
ipBlockFmt = "ip:block:%s" // ip:block:comment 用于存放限制发表评论的ip
|
||||
IPBlockExpire = 7 * 24 * time.Hour
|
||||
|
||||
ipAutoBlockFmt = "ip:auto:%s:%s" // ip:block:comment 用于自动限制基于IP的接口访问
|
||||
autoBlockFmt = "auto:%s:%d" // ip:block:comment 用于自动限制基于次数的接口访问
|
||||
|
||||
dataCachFmt = "data:%s:%s" //visitlog:20301020:uid
|
||||
DataCachExpire = time.Minute * 2
|
||||
|
||||
m3u8CacheFmt = "video:m3u8:%s" //video:m3u8:b0ac9b27eec64abebfc4c65cc4b89d03 m3u8键 //用于缓存添加相对的路径的m3u8文件
|
||||
m3u8PureCacheFmt = "video:m3u8:pure:%s" //video:m3u8:b0ac9b27eec64abebfc4c65cc4b89d03 m3u8键 //用于缓存原生的m3u8文件
|
||||
m3u8H5CacheFmt = "video:m3u8:h5:%s" //video:m3u8:b0ac9b27eec64abebfc4c65cc4b89d03 m3u8键 //用于缓存原生的m3u8文件
|
||||
M3u8CacheExpire = 1 * time.Hour
|
||||
|
||||
m3u8HourlyCountFmt = "m3u8:hourly:count:%d" // 每用户每小时 m3u8 请求次数
|
||||
M3u8HourlyCountExpire = 1 * time.Hour
|
||||
|
||||
// M3u8H5RefererSet H5 m3u8 请求来源计数(Redis ZSet):member=来源 scheme://host(Referer 缺省回退 Origin),
|
||||
// score=累计请求次数。ZScore 查单个来源总数,ZRevRangeWithScores 看 Top 来源;每天凌晨 5 点(Asia/Shanghai)
|
||||
// 过期清零、从零重新累计(刷回时 EXPIREAT 到下一个 5 点,见 updownctrl.FlushM3u8RefererStats)。
|
||||
M3u8H5RefererSet = "m3u8:h5:referer"
|
||||
|
||||
CdnCacheKey = "cdn:cache:url" //cdn域名缓存
|
||||
CdnCacheExpire = 2 * time.Minute
|
||||
|
||||
NewRegisterBehaFmt = "user:new:register:%d" //用户行为记录
|
||||
NewRegisterBehaCountFmt = "user:new:register:%d:%s" //用户行为次数记录
|
||||
NotifyKeyFmt = "notify:user:new:register:%d" //过期键通知
|
||||
NotifyKeyExpire = 20 * time.Minute //专门设置的通知键
|
||||
NewRegisterBehaExpire = 30 * time.Minute //用户
|
||||
NewRegisterBehaCountExpire = 30 * time.Minute //用户
|
||||
|
||||
replayNonceFmt = "replay:nonce:%s" //接口调用sign值
|
||||
|
||||
tagGroup = "taggroup"
|
||||
TagGroupExpireMax = time.Minute
|
||||
|
||||
DailyConsumeReward = "dailyConsumeReward:"
|
||||
|
||||
PayableAgentsKey = "payableAgents"
|
||||
PayableAgentsExpire = 5 * time.Minute
|
||||
|
||||
MarqueeKey = "marquee"
|
||||
|
||||
playLeaderboardKey = "leaderboard:play:%s:%d"
|
||||
playLeaderboardKeyExpire = 24 * time.Hour
|
||||
|
||||
NameSource = "name:source" //姓名池
|
||||
NameSourceExpire = 2 * 24 * time.Hour
|
||||
|
||||
AdNewsListKeyFmt = "video:news:ad:%d" //广告ID列表
|
||||
AdNewsListKeyExpire = time.Hour
|
||||
|
||||
LandDomainCacheKey = "domain:land" //落地页/推广域名 Key
|
||||
LandDomainCacheExpire = 2 * time.Hour
|
||||
|
||||
userInfo = "user:info:%d"
|
||||
userInfoExpire = 20 * time.Minute
|
||||
|
||||
tagInfo = "tag:info:%s"
|
||||
tagInfoExpire = 20 * time.Minute
|
||||
|
||||
videoInfo = "video:info:%s"
|
||||
videoInfoExpire = 30 * time.Minute
|
||||
|
||||
newestNews = "newest:news:%s"
|
||||
newestNewsExpire = 5 * time.Minute
|
||||
|
||||
userRechargeLimit = "userRechargeLimit:%d" //token:admin:xxx, token:channel:xxx
|
||||
userRechargeLimitExpire = 6 * time.Second
|
||||
|
||||
userReqLimit = "userRechargeLimit:%d:%s" //token:admin:xxx, token:channel:xxx
|
||||
userReqLimitExpire = 1 * time.Second
|
||||
|
||||
userWithdrawLimit = "userWithdrawimit:%d" //userWithdrawimit:xxx
|
||||
userWithdrawLimitExpire = 60 * time.Second
|
||||
|
||||
promotionCodeSetKey = "promotion_code_set" //推广码set key值
|
||||
|
||||
userLikeRateLimitKey = "userLike:%d"
|
||||
userLikeRateLimitExpire = 6 * time.Second
|
||||
|
||||
userBuyVidRateLimitKey = "userBuyVid:%d"
|
||||
userBuyVidRateLimitExpire = 6 * time.Second
|
||||
|
||||
initialPopKey = "vid:initialPopularity"
|
||||
|
||||
tagSortVidKey = "tagSortVid:%s:%v:%v:%d:%d"
|
||||
tagSortVidExpire = 30 * time.Minute
|
||||
|
||||
userCollectionKey = "vid:userCollection:%d:%d:%d:%s:%d"
|
||||
userCollectionExpire = 5 * time.Minute
|
||||
|
||||
vidPageViewListKey = "vidPageViewList:%d"
|
||||
vidPageViewListExpire = 30 * time.Minute
|
||||
|
||||
waliPlayerMacSet = "waliPlayerMacSet:%s"
|
||||
waliPlayerMacExpire = 24 * 7 * time.Hour
|
||||
waliPlayerGlobalID = "waliPlayerGlobalIDSet:%s"
|
||||
waliPlayerGlobalIDExpire = 24 * 7 * time.Hour
|
||||
waliPlayerIP = "waliPlayerIPSet:%s"
|
||||
waliPlayerIPExpire = 24 * time.Hour
|
||||
|
||||
WithdrawOrderKey = "withdraw:order:processing" // 提现处于审核中的用户
|
||||
WithdrawOrderDuration = 15 * time.Minute // 过滤用户窗口时间
|
||||
|
||||
RedsyncUserKey = "redsync:user:%d" // 用户维度分布式锁
|
||||
AiMateLoginLockKey = "redsync:ai-mate-login:%d"
|
||||
AiFundLockKey = "redsync:ai-fund:%d"
|
||||
RedsyncUserExpiration = 10 * time.Second // 用户维度分布式锁过期时间
|
||||
|
||||
ContentUpdateMarkersCache = "content:update-markers:v1"
|
||||
ContentUpdateMarkersCacheExpire = 30 * time.Second
|
||||
|
||||
CenterAdvertiseCache = "centerAdvertiseCache" // 数据中心广告缓存
|
||||
CenterVersionCache = "centerVersionCache:%s:%s:%s:%s" // 数据中心版本缓存
|
||||
|
||||
VidAdKey = "video:ad" //广告ID列表
|
||||
VidAdKeyExpire = time.Minute * 2
|
||||
|
||||
active2023RedisKey = "active_2023:%d"
|
||||
active2023Expired = time.Minute
|
||||
|
||||
// 幸福广场缓存
|
||||
ImageTopKey = "imageTop:filter:%d"
|
||||
ImageTopExpire = time.Minute * 5
|
||||
|
||||
userDailyTaskRewardLockKey = "userDailyTask:%d:%s"
|
||||
userDailyTaskRewardLockExpire = time.Minute
|
||||
|
||||
// 缓存用户每日完成数量. 比如点击了x次广告
|
||||
userDailyTaskCountKey = "userDailyTaskCount:%d:%d:%s"
|
||||
|
||||
// 缓存用户每日领取奖励次数.
|
||||
userDailyTaskRewardTimeKey = "userDailyTaskRewardTime:%d:%d:%s"
|
||||
|
||||
// 缓存用户一次性任务完成情况
|
||||
userOnceTaskCompleteKey = "userOnceTaskComplete:%d:%d"
|
||||
userOnceTaskCompleteExpire = time.Hour * 24
|
||||
ModulesCache = "modulesCache" // 模块缓存
|
||||
dailyTaskCacheFmt = "dailyTask:%v:%v:%v" // 每日任务缓存
|
||||
InviteCache = "inviteCache:uid:%v" // 推广数据缓存
|
||||
PublishTagInfoCache = "publishTagInfoCache" // 发布标签缓存
|
||||
ModuleVideoInfoCache = "moduleVideoInfoCache:mid:%v:%v:%v:%v:%v:%v" // 视频模块缓存
|
||||
SectionVideoInfoCache = "sectionVideoInfoCache:mid:%v:sortType:%v:pageSize:%v:pageNumber:%v" // 视频模块缓存
|
||||
AIModCache = "aiModCache" // AI模版缓存
|
||||
GetAwVipInfo = "getAwVipInfo" // AWVip信息缓存
|
||||
checkApiKeyFmt = "checkApi:ip:%s:api:%s:s:%v" // 接口调用
|
||||
SensitiveWordsCache = "sensitiveWordsCache" // 敏感词库缓存
|
||||
GameAdvanceCache = "gameAdvanceCache:%v" // 游戏预售缓存
|
||||
AdvanceCache = "hj_advanceCache:%v" // 预售缓卡存
|
||||
CheckApiKeyExpire = 1 * time.Minute
|
||||
RecommendVip = "RecommendVip" // VIP推荐展示
|
||||
RecommendVipExpire = 2 * time.Minute
|
||||
DiscountArea = "DiscountArea" // 折扣专区列表缓存
|
||||
DiscountAreaExpire = 2 * time.Minute
|
||||
shortVideoListCache = "shortDiscoverList:types:%d:tagId:%s:page:%v_%v" // 短视频发现模块数据换粗
|
||||
ShortVideoListCacheExpire = time.Minute * 10 // 短视频发现模块数据换粗
|
||||
|
||||
mediaTagInfoExpire = 20 * time.Minute
|
||||
mediaTagInfo = "mediaTag:info:%s" // 动漫标签缓存
|
||||
acgBrowseCountListKey = "acgBrowseCountList:%d"
|
||||
MediaLibraryInfo = "mediaLibraryInfo:info" // 动漫片库缓存
|
||||
ShortVideosKey = "short-videos-all-ids-list" // ShortVideosKey 短视频ID列表 - 用于推荐
|
||||
VideoLibraryCache = "videoLibraryCache" // 片库缓存
|
||||
ShortVideosRecoCacheKey = "shortVideosRecoCache" // 短视频-推荐 ID列表 - 用于短视频推荐
|
||||
followUpUsersAndSHORTKey = "follow:%d:SHORT:%d_%d"
|
||||
|
||||
mostNewModuleVideoListKey = "mostNewModuleVideoList:%v:%v:%v" // 91porn 首页最新视频列表缓存key
|
||||
MostNewModuleVideoListExpire = time.Minute * 10
|
||||
|
||||
MonitorCacheKey = "rs_monitorCache" // 监控缓存
|
||||
MonitorExpired = time.Minute * 4
|
||||
|
||||
OfficialWebsiteBasicDataCacheKey = "officialWebsiteBasicDataCache" // 官方网站基础数据缓存
|
||||
OfficialWebsiteBasicDataCacheExpire = time.Minute * 10
|
||||
OfficialWebsiteAlbumListCacheKey = "officialWebsiteAlbumListCache:%d:%d" // 官方网站专辑列表缓存
|
||||
OfficialWebsiteAlbumListCacheExpire = time.Minute * 10
|
||||
OfficialWebsiteHeroListCacheKey = "officialWebsiteHeroListCache:%d:%d:%d" // 官方网站演员列表缓存
|
||||
OfficialWebsiteHeroListCacheExpire = time.Minute * 10
|
||||
OfficialWebsiteVideoListCacheKey = "officialWebsiteVideoListCache:%d:%s:%d:%d:%d" // 官方网站视频列表缓存
|
||||
OfficialWebsiteVideoListCacheExpire = time.Minute * 10
|
||||
OfficialWebsiteNewsListCacheKey = "officialWebsiteNewsListCache:%d:%d" // 官方网站资讯列表缓存
|
||||
OfficialWebsiteNewsListCacheExpire = time.Minute * 10
|
||||
OfficialWebsitePartnerListCacheKey = "officialWebsitePartnerListCache" // 官方网站合作伙伴列表缓存
|
||||
OfficialWebsitePartnerListCacheExpire = time.Minute * 10
|
||||
)
|
||||
|
||||
const (
|
||||
NavigateRecreationCache = "navigateRecreationCache" // 导航站娱乐广告缓存
|
||||
UserPaymentStatusPopupCache = "UserPaymentStatusPopup" // 用户分层弹窗配置缓存
|
||||
)
|
||||
|
||||
const (
|
||||
aiFreeUndressTodayUseTimes = "aiFreeUndressTodayUseTimes:%v"
|
||||
)
|
||||
const (
|
||||
JanGangQuCacheKey = "jingangqu"
|
||||
JanGangQuCacheExpire = 10 * time.Minute
|
||||
)
|
||||
const (
|
||||
rankingListCacheKey = "ranking:%v:%v:%d:%d"
|
||||
hotRankingListCacheKey = "hotRanking:%v:%v:%v"
|
||||
RankingListCacheExpire = 10 * time.Minute
|
||||
)
|
||||
|
||||
// 渠道订阅的 消息体
|
||||
type RecommedPayload struct {
|
||||
UID uint64 `json:"uid"`
|
||||
City string `json:"city"`
|
||||
Page int `json:"page"`
|
||||
}
|
||||
|
||||
func GetFollowUpUsersAndSHORTKey(uid uint64, pageSize, pageNumber uint64) (string, time.Duration) {
|
||||
return fmt.Sprintf(followUpUsersAndSHORTKey, uid, pageSize, pageNumber), time.Minute * 10
|
||||
}
|
||||
|
||||
func GetMonitorCacheExpired() time.Duration {
|
||||
return MonitorExpired
|
||||
}
|
||||
|
||||
func AcgBrowseCountListKey(timestamp int64) string {
|
||||
return fmt.Sprintf(acgBrowseCountListKey, timestamp)
|
||||
}
|
||||
|
||||
func MediaTagInfoExpire() time.Duration {
|
||||
return mediaTagInfoExpire
|
||||
}
|
||||
|
||||
func MediaTagInfoKey(tid string) string {
|
||||
return fmt.Sprintf(mediaTagInfo, tid)
|
||||
}
|
||||
|
||||
func AiFreeUndressTodayUseTimesKey(uid uint64) string {
|
||||
return fmt.Sprintf(aiFreeUndressTodayUseTimes, uid)
|
||||
}
|
||||
|
||||
func UserTokenKey(uid uint64) string {
|
||||
return fmt.Sprintf(userTokenFmt, uid)
|
||||
}
|
||||
|
||||
// IMSDKUserTokenKey IM SDK 用户 token 缓存 key(按 imUserID)
|
||||
func IMSDKUserTokenKey(imUserID int64) string {
|
||||
return fmt.Sprintf(imSDKUserTokenFmt, imUserID)
|
||||
}
|
||||
|
||||
// IMSDKEnsureLockKey EnsureSDKUser 并发注册锁 key(按 uid)
|
||||
func IMSDKEnsureLockKey(uid uint64) string {
|
||||
return fmt.Sprintf(imSDKEnsureLockFmt, uid)
|
||||
}
|
||||
|
||||
func GameAdvanceKey(uid uint64) string {
|
||||
return fmt.Sprintf(GameAdvanceCache, uid)
|
||||
}
|
||||
|
||||
func AdvanceKey(uid uint64) string {
|
||||
return fmt.Sprintf(AdvanceCache, uid)
|
||||
}
|
||||
|
||||
// InviteCacheKey 推广数据
|
||||
func InviteCacheKey(uid uint64) string {
|
||||
return fmt.Sprintf(InviteCache, uid)
|
||||
}
|
||||
|
||||
func CheckApiKey(ip, apiPath string, second time.Time) string {
|
||||
return fmt.Sprintf(checkApiKeyFmt, ip, apiPath, second)
|
||||
}
|
||||
|
||||
// SectionVideoInfoCacheKey 模块视频Key
|
||||
func SectionVideoInfoCacheKey(sid string, sortType string, pageSize, pageNumber int64) string {
|
||||
return fmt.Sprintf(SectionVideoInfoCache, sid, sortType, pageSize, pageNumber)
|
||||
}
|
||||
|
||||
func DailyTaskCacheKey(uid uint64, taskType any, today time.Time) string {
|
||||
return fmt.Sprintf(dailyTaskCacheFmt, taskType, uid, today)
|
||||
}
|
||||
|
||||
func MobileCaptchaKey(mobile string) string {
|
||||
return fmt.Sprintf(mobileCaptchaFmt, mobile)
|
||||
}
|
||||
|
||||
func SMSCaptchaIPKey(ip string) string {
|
||||
return fmt.Sprintf(smsCaptchaIPFmt, ip)
|
||||
}
|
||||
|
||||
func SMSCaptchaIPExpire() time.Duration {
|
||||
year, month, day := time.Now().Date()
|
||||
return time.Until(time.Date(year, month, day+1, 0, 0, 0, 0, time.Local))
|
||||
}
|
||||
|
||||
func SMSCaptchaUIDKey(uid uint64) string {
|
||||
return fmt.Sprintf(smsCaptchaUIDFmt, uid)
|
||||
}
|
||||
|
||||
func SMSCaptchaUIDCooldownKey(uid uint64) string {
|
||||
return fmt.Sprintf(smsCaptchaUIDCooldownFmt, uid)
|
||||
}
|
||||
|
||||
func SMSCaptchaPhoneNumberKey(mobile string) string {
|
||||
return fmt.Sprintf(smsCaptchaPhoneNumberFmt, mobile)
|
||||
}
|
||||
|
||||
func WebTokenKey(typ string, act string) string {
|
||||
return fmt.Sprintf(webTokenFmt, typ, act)
|
||||
}
|
||||
|
||||
func UserAdNewsListKey(uid uint64) string {
|
||||
return fmt.Sprintf(AdNewsListKeyFmt, uid)
|
||||
}
|
||||
|
||||
func VisitKey(t time.Time) string {
|
||||
recentMinute := timerange.RecentMinute(t, statrecordmod.FiveMinuteScale) //对齐本次统计时间 Minute % frequency == 0
|
||||
return fmt.Sprintf(visitFmt, recentMinute.Format("200601021504"))
|
||||
}
|
||||
|
||||
func RegistKey(t time.Time) string {
|
||||
recentMinute := timerange.RecentMinute(t, statrecordmod.FiveMinuteScale) //对齐本次统计时间 Minute % frequency == 0
|
||||
return fmt.Sprintf(registFmt, recentMinute.Format("200601021504"))
|
||||
}
|
||||
|
||||
func NotifyKey(uid uint64) string {
|
||||
return fmt.Sprintf(NotifyKeyFmt, uid)
|
||||
}
|
||||
|
||||
func RegistBehaviorKey(uid uint64) string {
|
||||
return fmt.Sprintf(NewRegisterBehaFmt, uid)
|
||||
}
|
||||
|
||||
func RegistBehaviorCountKey(uid uint64, req string) string {
|
||||
return fmt.Sprintf(NewRegisterBehaCountFmt, uid, req)
|
||||
}
|
||||
|
||||
func RankKey(rankType string) string {
|
||||
return fmt.Sprintf(rankFmt, rankType)
|
||||
}
|
||||
|
||||
func ToneKey() string {
|
||||
return toneFmt
|
||||
}
|
||||
|
||||
func UserVisitLogsKey(t time.Time) string {
|
||||
return fmt.Sprintf(visitLogsFmt, t.Format("20060102"))
|
||||
}
|
||||
|
||||
func UserVisitLogExpire(now time.Time) time.Duration {
|
||||
return timeutil.BeginningOfTomorrow(now).Sub(now) + 5*time.Minute
|
||||
}
|
||||
|
||||
func IpLocationFmt(ip string) string {
|
||||
return fmt.Sprintf(ipLocationFmt, ip)
|
||||
}
|
||||
|
||||
func M3u8CacheFmt(m3u8 string) string {
|
||||
return fmt.Sprintf(m3u8CacheFmt, m3u8)
|
||||
}
|
||||
|
||||
func M3u8PureCacheFmt(m3u8 string) string {
|
||||
return fmt.Sprintf(m3u8PureCacheFmt, m3u8)
|
||||
}
|
||||
func M3u8H5CacheFmt(m3u8 string) string {
|
||||
return fmt.Sprintf(m3u8H5CacheFmt, m3u8)
|
||||
}
|
||||
|
||||
func M3u8HourlyCountKey(uid uint64) string {
|
||||
return fmt.Sprintf(m3u8HourlyCountFmt, uid)
|
||||
}
|
||||
func UserInfoKey() string {
|
||||
return userInfo
|
||||
}
|
||||
func TagInfoKey() string {
|
||||
return tagInfo
|
||||
}
|
||||
func VideoInfoKey() string {
|
||||
return videoInfo
|
||||
}
|
||||
|
||||
func UserInfoExpire() time.Duration {
|
||||
return userInfoExpire
|
||||
}
|
||||
func TagInfoExpire() time.Duration {
|
||||
return tagInfoExpire
|
||||
}
|
||||
func VideoInfoExpire() time.Duration {
|
||||
return videoInfoExpire
|
||||
}
|
||||
func DataCachKey(db string, m string) string {
|
||||
return fmt.Sprintf(dataCachFmt, db, m)
|
||||
}
|
||||
|
||||
func IPBlockKey(blockType string) string {
|
||||
return fmt.Sprintf(ipBlockFmt, blockType)
|
||||
}
|
||||
|
||||
func IPAutoBlockKey(blockType, ip string) string {
|
||||
return fmt.Sprintf(ipAutoBlockFmt, blockType, ip)
|
||||
}
|
||||
|
||||
func AutoBlockKey(blockType string, uid uint64) string {
|
||||
return fmt.Sprintf(autoBlockFmt, blockType, uid)
|
||||
}
|
||||
|
||||
func ReplayNonceKey(nonce string) string {
|
||||
return fmt.Sprintf(replayNonceFmt, nonce)
|
||||
}
|
||||
|
||||
func TagGroup() string {
|
||||
return tagGroup
|
||||
}
|
||||
|
||||
func TraderChatCacheKey(uid uint64, proT int) string {
|
||||
return fmt.Sprintf(traderChatCacheFmt, uid, proT)
|
||||
}
|
||||
|
||||
func WithdrawCfgCacheKey() string {
|
||||
return withdrawCfgFmt
|
||||
}
|
||||
|
||||
func WithdrawTariffCacheKey() string {
|
||||
return withdrawTariffFmt
|
||||
}
|
||||
|
||||
func NewestNewsKey(t time.Time) string {
|
||||
return fmt.Sprintf(newestNews, t.String())
|
||||
}
|
||||
|
||||
func NewestNewsKey_old() string {
|
||||
return "newest:news"
|
||||
}
|
||||
|
||||
func NewestShortVideoKey() string {
|
||||
return "newest:shortvideo"
|
||||
}
|
||||
|
||||
func NewestNewsExpire() time.Duration {
|
||||
return newestNewsExpire
|
||||
}
|
||||
|
||||
func RechargeLimtKey(uid uint64) string {
|
||||
return fmt.Sprintf(userRechargeLimit, uid)
|
||||
}
|
||||
func RechargeLimtKeyExpire() time.Duration {
|
||||
return userRechargeLimitExpire
|
||||
}
|
||||
func WithdrawLimtKey() string {
|
||||
return userWithdrawLimit
|
||||
}
|
||||
func WithdrawLimtKeyExpire() time.Duration {
|
||||
return userWithdrawLimitExpire
|
||||
}
|
||||
func PromotionCodeKey() string {
|
||||
return promotionCodeSetKey
|
||||
}
|
||||
func WaLiIPSetKey(ip string) string {
|
||||
return fmt.Sprintf(waliPlayerIP, ip)
|
||||
}
|
||||
func WaLiIPSetExpire() time.Duration {
|
||||
return waliPlayerIPExpire
|
||||
}
|
||||
func WaLiMacSetKey(mac string) string {
|
||||
return fmt.Sprintf(waliPlayerMacSet, mac)
|
||||
}
|
||||
func WaLiMacSetExpire() time.Duration {
|
||||
return waliPlayerMacExpire
|
||||
}
|
||||
func WaLiDevSetKey(dev string) string {
|
||||
return fmt.Sprintf(waliPlayerGlobalID, dev)
|
||||
}
|
||||
func WaLiDevSetExpire() time.Duration {
|
||||
return waliPlayerGlobalIDExpire
|
||||
}
|
||||
|
||||
func GetRedsyncUserKey(uid uint64) string {
|
||||
return fmt.Sprintf(RedsyncUserKey, uid)
|
||||
}
|
||||
|
||||
func GetAiMateLoginLockKey(uid uint64) string {
|
||||
return fmt.Sprintf(AiMateLoginLockKey, uid)
|
||||
}
|
||||
|
||||
func GetAiFundLockKey(uid uint64) string {
|
||||
return fmt.Sprintf(AiFundLockKey, uid)
|
||||
}
|
||||
|
||||
func GetUserLikeRateLimitKey() string {
|
||||
return userLikeRateLimitKey
|
||||
}
|
||||
|
||||
func GetUserLikeRateLimitExpire() time.Duration {
|
||||
return userLikeRateLimitExpire
|
||||
}
|
||||
|
||||
func GetEmailCaptchaKey(email string) string {
|
||||
return fmt.Sprintf(emailCaptchaFmt, email)
|
||||
}
|
||||
|
||||
func GetUserBuyVidRateLimitKey() string {
|
||||
return userBuyVidRateLimitKey
|
||||
}
|
||||
|
||||
func GetUserBuyVidRateLimitExpire() time.Duration {
|
||||
return userBuyVidRateLimitExpire
|
||||
}
|
||||
|
||||
func GetInitialVideoPopularityKey() string {
|
||||
return initialPopKey
|
||||
}
|
||||
|
||||
func GetTagSortVideoKey(tid string, newsType string, sort int, skip, limit uint64) string {
|
||||
return fmt.Sprintf(tagSortVidKey, newsType, tid, sort, skip, limit)
|
||||
}
|
||||
func GetTagSortVideoExpire() time.Duration {
|
||||
return tagSortVidExpire
|
||||
}
|
||||
func ReqLimtKey(uid uint64, m string) string {
|
||||
return fmt.Sprintf(userReqLimit, uid, m)
|
||||
}
|
||||
func ReqLimtKeyExpire() time.Duration {
|
||||
return userReqLimitExpire
|
||||
}
|
||||
func UserCollectionKey(uid uint64, pageNum, pageSize uint64, playTimeType int, sortType string) string {
|
||||
return fmt.Sprintf(userCollectionKey, uid, pageNum, pageSize, sortType, playTimeType)
|
||||
}
|
||||
func UserCollectionExpire() time.Duration {
|
||||
return userCollectionExpire
|
||||
}
|
||||
|
||||
func VidPageViewListKey(timestamp int64) string {
|
||||
return fmt.Sprintf(vidPageViewListKey, timestamp)
|
||||
}
|
||||
func VidPageViewListExpire() time.Duration {
|
||||
return vidPageViewListExpire
|
||||
}
|
||||
|
||||
func GetUserActive2023RedisKey(uid uint64) string {
|
||||
return fmt.Sprintf(active2023RedisKey, uid)
|
||||
}
|
||||
|
||||
func GetUserActive2023Expred() time.Duration {
|
||||
return active2023Expired
|
||||
}
|
||||
|
||||
func GetVersionRedisKey(a, b, c, d string) string {
|
||||
return fmt.Sprintf(CenterVersionCache, a, b, c, d)
|
||||
}
|
||||
|
||||
func PlayLeaderboardKey(rT string, timestamp int64) string {
|
||||
return fmt.Sprintf(playLeaderboardKey, rT, timestamp)
|
||||
}
|
||||
func PlayLeaderboardKeyExpire() time.Duration {
|
||||
return playLeaderboardKeyExpire
|
||||
}
|
||||
|
||||
func GetUserDailyTaskRewardLockKey(uid uint64, taskId string) string {
|
||||
return fmt.Sprintf(userDailyTaskRewardLockKey, uid, taskId)
|
||||
}
|
||||
|
||||
func GetUserDailyTaskRewardLockExpired() time.Duration {
|
||||
return userDailyTaskRewardLockExpire
|
||||
}
|
||||
|
||||
// 缓存用户每日完成数量. 比如点击了x次广告
|
||||
func GetUserDailyTaskCountKey(uid uint64, taskType int64) string {
|
||||
return fmt.Sprintf(userDailyTaskCountKey, uid, taskType, time.Now().Format("2006-01-02"))
|
||||
}
|
||||
|
||||
// 缓存用户每日领取奖励次数.
|
||||
func GetUserDailyTaskRewardTimeKey(uid uint64, taskType int64) string {
|
||||
return fmt.Sprintf(userDailyTaskRewardTimeKey, uid, taskType, time.Now().Format("2006-01-02"))
|
||||
}
|
||||
|
||||
func GetUserDailyTaskExpired() time.Duration {
|
||||
year, month, day := time.Now().Date()
|
||||
return time.Until(time.Date(year, month, day+1, 0, 1, 0, 0, time.Local)) // 多缓存1分钟
|
||||
}
|
||||
|
||||
// 用户一次性任务是否完成. value: 0 未完成; 1 已完成
|
||||
func GetUserOnceTaskCompleteKey(uid uint64, taskType int64) string {
|
||||
return fmt.Sprintf(userOnceTaskCompleteKey, uid, taskType)
|
||||
}
|
||||
|
||||
func GetUserOnceTaskCompleteExpired() time.Duration {
|
||||
return userOnceTaskCompleteExpire
|
||||
}
|
||||
|
||||
const (
|
||||
videoShareList = "videoShareList"
|
||||
videoShareExpired = time.Minute * 10
|
||||
)
|
||||
|
||||
func GetVideoShareListKey() string {
|
||||
return videoShareList
|
||||
}
|
||||
|
||||
func GetVideoShareListExpired() time.Duration {
|
||||
return videoShareExpired
|
||||
}
|
||||
|
||||
func GetShortVideoListCacheKey(types int, tid string, page, pagesize uint64) string {
|
||||
return fmt.Sprintf(shortVideoListCache, types, tid, page, pagesize)
|
||||
}
|
||||
|
||||
func GetRankingListExpired() time.Duration {
|
||||
return RankingListCacheExpire
|
||||
}
|
||||
func GetRankingListCacheKey(v ...any) string {
|
||||
return fmt.Sprintf(rankingListCacheKey, v...)
|
||||
}
|
||||
|
||||
func GetHotRankingListCacheKey(tagIds []primitive.ObjectID, pageNumber, pageSize uint64) string {
|
||||
b, _ := json.Marshal(tagIds)
|
||||
md5Str := crypt.ByteToMd5(b)
|
||||
return fmt.Sprintf(hotRankingListCacheKey, md5Str, pageNumber, pageSize)
|
||||
}
|
||||
|
||||
func GetMostNewModuleVideoListKey(sortType int, page, pageSize uint64) string {
|
||||
return fmt.Sprintf(mostNewModuleVideoListKey, sortType, page, pageSize)
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/constant"
|
||||
|
||||
"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{})
|
||||
return m, json.Unmarshal(j, &m)
|
||||
}
|
||||
|
||||
// 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)
|
||||
return m, bson.Unmarshal(data, &m)
|
||||
}
|
||||
|
||||
func MapToJsonString(m map[string]string) (string, error) {
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
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) {
|
||||
j, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]interface{})
|
||||
return m, json.Unmarshal(j, &m)
|
||||
}
|
||||
|
||||
// Map2JSONStruct 将map转换为Json struct
|
||||
func Map2JSONStruct(v interface{}, m map[string]interface{}) error {
|
||||
j, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(j, v)
|
||||
}
|
||||
|
||||
func IDArray(ids []string) ([]ID, error) {
|
||||
idArray := make([]ID, len(ids))
|
||||
for i, id := range ids {
|
||||
_id, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idArray[i] = _id
|
||||
}
|
||||
return idArray, nil
|
||||
}
|
||||
|
||||
// 拼接https://
|
||||
func BindHttpSch(path string) string {
|
||||
if strings.HasPrefix(path, "http") {
|
||||
return path
|
||||
}
|
||||
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 {
|
||||
if len(obj) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
arr := make([]string, len(obj))
|
||||
for i, o := range obj {
|
||||
arr[i] = o.Hex()
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
func String2ObjectID(obj []string) []primitive.ObjectID {
|
||||
arr := make([]primitive.ObjectID, 0, len(obj))
|
||||
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
|
||||
}
|
||||
|
||||
func HandleSysType(sysType string) string {
|
||||
if strings.Contains(strings.ToLower(sysType), constant.SysTypeIOS) {
|
||||
return constant.SysTypeIOS
|
||||
}
|
||||
if strings.Contains(strings.ToLower(sysType), constant.SysTypeH5) {
|
||||
return constant.SysTypeH5
|
||||
}
|
||||
return constant.SysTypeAndroid
|
||||
}
|
||||
|
||||
func GetJsonTags(tags *[]string, t reflect.Type) {
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if field.Type.Kind() == reflect.Struct {
|
||||
GetJsonTags(tags, field.Type)
|
||||
continue
|
||||
}
|
||||
tag := field.Tag.Get("json")
|
||||
*tags = append(*tags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeDate 将时间归一化为北京时间的当天 0 点
|
||||
func NormalizeDate(t time.Time) time.Time {
|
||||
local, _ := time.LoadLocation("Asia/Shanghai")
|
||||
t = t.In(local)
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, local)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package crypt
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/web/webg"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//const appSecret = "iPEnB74mfCZhNNnY" //(客服后台->产品列表 对应产品的产品密钥) 测试
|
||||
|
||||
func CheckSign(sign string, data interface{}, router string) bool {
|
||||
//对参数进行加密,和sign 进行对比
|
||||
dataMap, _ := StructToMap(data, router)
|
||||
params := url.Values{}
|
||||
for k, v := range dataMap {
|
||||
params.Set(k, v)
|
||||
}
|
||||
mySign, err := GenerateSign(params.Encode(), webg.Conf.Customer.Secret)
|
||||
if err != nil {
|
||||
log.Error("CheckSign Generate signature error:", log.Any("Secret", webg.Conf.Customer.Secret), log.E(err))
|
||||
return false
|
||||
}
|
||||
return mySign == sign
|
||||
}
|
||||
|
||||
// StructToMap 将结构体转换为map[string]string
|
||||
func StructToMap(data interface{}, router string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
// 使用反射获取结构体信息
|
||||
v := reflect.ValueOf(data)
|
||||
t := reflect.TypeOf(data)
|
||||
|
||||
// 如果是指针,获取指向的元素
|
||||
if t.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
// 确保是结构体类型
|
||||
if t.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("data must be a struct or pointer to struct")
|
||||
}
|
||||
|
||||
// 遍历结构体字段
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
value := v.Field(i)
|
||||
|
||||
// 获取json标签作为key
|
||||
jsonTag := field.Tag.Get("json")
|
||||
if jsonTag == "" {
|
||||
// 如果没有json标签,使用字段名
|
||||
jsonTag = strings.ToLower(field.Name)
|
||||
}
|
||||
if router != "" {
|
||||
if jsonTag == "appId" || jsonTag == "sign" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 获取字段值并转换为字符串
|
||||
var valueStr string
|
||||
switch value.Kind() {
|
||||
case reflect.String:
|
||||
valueStr = value.String()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
valueStr = fmt.Sprintf("%d", value.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
valueStr = fmt.Sprintf("%d", value.Uint())
|
||||
case reflect.Bool:
|
||||
valueStr = fmt.Sprintf("%t", value.Bool())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
valueStr = fmt.Sprintf("%f", value.Float())
|
||||
default:
|
||||
valueStr = fmt.Sprintf("%v", value.Interface())
|
||||
}
|
||||
|
||||
// 添加到结果中
|
||||
result[jsonTag] = valueStr
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PKCS7Padding PKCS7填充
|
||||
func PKCS7Padding_1(data []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(data)%blockSize
|
||||
padtext := make([]byte, padding)
|
||||
for i := range padtext {
|
||||
padtext[i] = byte(padding)
|
||||
}
|
||||
return append(data, padtext...)
|
||||
}
|
||||
|
||||
// AESEncrypt AES-CBC加密
|
||||
func AESEncrypt(origData, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 使用密钥的前16字节作为IV
|
||||
iv := key[:16]
|
||||
|
||||
// PKCS7填充
|
||||
blockSize := block.BlockSize()
|
||||
origData = PKCS7Padding_1(origData, blockSize)
|
||||
|
||||
// CBC模式加密
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
crypted := make([]byte, len(origData))
|
||||
mode.CryptBlocks(crypted, origData)
|
||||
|
||||
return crypted, nil
|
||||
}
|
||||
|
||||
// GenerateSign 生成签名
|
||||
func GenerateSign(data, appSecret string) (string, error) {
|
||||
key := []byte(appSecret)
|
||||
plaintext := []byte(data)
|
||||
|
||||
ciphertext, err := AESEncrypt(plaintext, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 转换为十六进制字符串
|
||||
hexStr := hex.EncodeToString(ciphertext)
|
||||
|
||||
return hexStr, nil
|
||||
}
|
||||
@@ -0,0 +1,755 @@
|
||||
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"
|
||||
|
||||
"91porn-server/common/crypt/ecb"
|
||||
sli "91porn-server/common/slice"
|
||||
|
||||
"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)
|
||||
return token.SignedString([]byte(secret))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if err = json.Unmarshal(bytes, &data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return MapToStr(data)
|
||||
}
|
||||
|
||||
func JsonStr2Str(str string, key ...string) string {
|
||||
if str == "" {
|
||||
return ""
|
||||
}
|
||||
mapstr, err := JSON2Map(str)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
sortStr, err := MapToStrSkipObject(mapstr, key...)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return sortStr
|
||||
}
|
||||
|
||||
func Obj2Obj(src, dest interface{}) error {
|
||||
data, err := json.Marshal(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, dest)
|
||||
}
|
||||
|
||||
// MapToStr 对map按照字典顺序排序 并返回字符串
|
||||
func MapToStr(data map[string]interface{}) (string, error) {
|
||||
if 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
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// MapToStr 对map按照字典顺序排序 并返回字符串 对value是对象 map list 类型的key 过滤掉
|
||||
func MapToStrSkipObject(data map[string]interface{}, key ...string) (string, error) {
|
||||
if 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 {
|
||||
if sli.Contains(key, k) {
|
||||
continue
|
||||
}
|
||||
t := reflect.TypeOf(data[k])
|
||||
if t != nil {
|
||||
tkind := t.Kind()
|
||||
if tkind == reflect.Map || tkind == reflect.Slice || tkind == reflect.Array || tkind == reflect.Struct {
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// JSON2Struct json转struct
|
||||
func JSON2Struct(str string, obj interface{}) error {
|
||||
return json.Unmarshal([]byte(str), &obj)
|
||||
}
|
||||
|
||||
// JSONArray2Struct json转struct
|
||||
func JSONArray2Struct(str []string, obj interface{}) []interface{} {
|
||||
data := make([]interface{}, len(str))
|
||||
for i, v := range str {
|
||||
_ = JSON2Struct(v, &obj)
|
||||
data[i] = obj
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// JSON2Map json转map
|
||||
func JSON2Map(str string) (map[string]interface{}, error) {
|
||||
var mapResult map[string]interface{}
|
||||
return mapResult, json.Unmarshal([]byte(str), &mapResult)
|
||||
}
|
||||
|
||||
// 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 len(data) == 0 {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
keys := make([]string, len(data))
|
||||
i := 0
|
||||
for k := range data {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
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 len(data) == 0 {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
keys := make([]string, len(data))
|
||||
i := 0
|
||||
for k := range data {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
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)
|
||||
continue
|
||||
}
|
||||
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 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
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// CoreAesDecryptEx 是 CoreAesEncryptEx 的逆运算。
|
||||
// 入参 crypt 前 nonceLen 字节为随机 nonce,其余为 CBC 密文;密钥派生与加密端完全一致。
|
||||
// 由于 crypt 可能来自不可信输入(如客户端伪造的票据),这里对长度和分组边界做了保护,避免切片或 CBC 解密 panic。
|
||||
func CoreAesDecryptEx(crypt []byte, nonceLen int, key string) ([]byte, error) {
|
||||
if nonceLen < 0 || len(crypt) < nonceLen {
|
||||
return nil, errors.New("CoreAesDecryptEx: ciphertext shorter than nonce")
|
||||
}
|
||||
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:]
|
||||
blockSize := block.BlockSize()
|
||||
if len(realData) == 0 || len(realData)%blockSize != 0 {
|
||||
return nil, errors.New("CoreAesDecryptEx: ciphertext is not a multiple of block size")
|
||||
}
|
||||
blockMode := cipher.NewCBCDecrypter(block, aesIV)
|
||||
origData := make([]byte, len(realData))
|
||||
blockMode.CryptBlocks(origData, realData)
|
||||
// 用带校验的 PKCS7 去填充:错误密钥/损坏数据几乎必然产生非法填充,这里返回错误而非 panic,
|
||||
// 也顺带充当一次完整性校验(共享的 PKCS7UnPadding 在非法填充时会越界 panic,故不复用)。
|
||||
return pkcs7UnpadSafe(origData, blockSize)
|
||||
}
|
||||
|
||||
// pkcs7UnpadSafe 校验并剥离 PKCS7 填充,非法填充返回错误而不 panic。
|
||||
func pkcs7UnpadSafe(data []byte, blockSize int) ([]byte, error) {
|
||||
length := len(data)
|
||||
if length == 0 || length%blockSize != 0 {
|
||||
return nil, errors.New("invalid PKCS7 padding: bad length")
|
||||
}
|
||||
pad := int(data[length-1])
|
||||
if pad <= 0 || pad > blockSize || pad > length {
|
||||
return nil, errors.New("invalid PKCS7 padding: bad size")
|
||||
}
|
||||
for _, b := range data[length-pad:] {
|
||||
if int(b) != pad {
|
||||
return nil, errors.New("invalid PKCS7 padding: inconsistent bytes")
|
||||
}
|
||||
}
|
||||
return data[:length-pad], nil
|
||||
}
|
||||
|
||||
// CoreAesDecrypt AES解密
|
||||
func CoreAesDecrypt(crypted []byte, key string) (s string, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("CoreAesDecrypt is panic. crypted:%s,key:%s", string(crypted), key)
|
||||
return
|
||||
}
|
||||
}()
|
||||
keybyte := []byte(key)
|
||||
block, err := aes.NewCipher(keybyte)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
blockSize := block.BlockSize()
|
||||
if blockSize < 0 {
|
||||
return "", errors.New("blockSize less than zero")
|
||||
}
|
||||
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]
|
||||
}
|
||||
|
||||
// ECB PKCS5 加密
|
||||
func AESECBEncrypt(src, key []byte) []byte {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
fmt.Printf("txn put fail: %v", err)
|
||||
return nil
|
||||
}
|
||||
ecbMod := ecb.NewECBEncrypter(block)
|
||||
content := PKCS5Padding(src, block.BlockSize())
|
||||
des := make([]byte, len(content))
|
||||
ecbMod.CryptBlocks(des, content)
|
||||
return des
|
||||
}
|
||||
|
||||
// ECB PKCS5 解密
|
||||
func AESECBDecrypt(encrypted, key []byte) []byte {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
fmt.Printf("decrypt fail: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
ecbMod := ecb.NewECBDecrypter(block)
|
||||
des := make([]byte, len(encrypted))
|
||||
ecbMod.CryptBlocks(des, encrypted)
|
||||
|
||||
// 去除 PKCS5 填充
|
||||
result := PKCS5UnPadding(des)
|
||||
return result
|
||||
}
|
||||
|
||||
// PKCS5UnPadding
|
||||
func PKCS5UnPadding(origData []byte) []byte {
|
||||
length := len(origData)
|
||||
// 去掉最后一个字节 unpadding 次
|
||||
unpadding := int(origData[length-1])
|
||||
if unpadding > length {
|
||||
return origData
|
||||
}
|
||||
return origData[:(length - unpadding)]
|
||||
}
|
||||
|
||||
// PKCS5Padding
|
||||
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(ciphertext)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(ciphertext, padtext...)
|
||||
}
|
||||
|
||||
// 加密
|
||||
func AESCBCPck5Encrypt(origData, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockSize := block.BlockSize()
|
||||
origData = PKCS5Padding(origData, blockSize)
|
||||
blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
|
||||
crypted := make([]byte, len(origData))
|
||||
blockMode.CryptBlocks(crypted, origData)
|
||||
return crypted, nil
|
||||
}
|
||||
|
||||
// base64补全
|
||||
func padding(origin string) string {
|
||||
missing := len(origin) % 4
|
||||
if missing != 0 {
|
||||
origin += strings.Repeat("=", 4-missing)
|
||||
}
|
||||
|
||||
return origin
|
||||
}
|
||||
|
||||
// AdDecrypt 集团广告中心-广告数据解密专用
|
||||
func AdDecrypt(encryptedData, keyBase64 string) (res string, err error) {
|
||||
// 1. Base64 解码获取密钥
|
||||
keyBytes, err := base64.StdEncoding.DecodeString(padding(keyBase64))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("密钥 Base64 解码失败: %v", err)
|
||||
}
|
||||
// 2. Base64 解码获取加密数据
|
||||
data, err := base64.StdEncoding.DecodeString(padding(encryptedData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("数据 Base64 解码失败: %v", err)
|
||||
}
|
||||
|
||||
// 3. 拆分 IV 与密文(前 12 字节为 IV)
|
||||
if len(data) < 12 {
|
||||
return "", errors.New("数据长度不足,无法提取 IV")
|
||||
}
|
||||
iv := data[:12]
|
||||
ciphertext := data[12:]
|
||||
// 4. 初始化 AES 密码块
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建 AES 密码块失败: %v", err)
|
||||
}
|
||||
// 5. 采用 GCM 模式
|
||||
aesGCM, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建 GCM 失败: %v", err)
|
||||
}
|
||||
// 6. 解密数据
|
||||
plainText, err := aesGCM.Open(nil, iv, ciphertext, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解密失败: %v", err)
|
||||
}
|
||||
|
||||
return string(plainText), nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package crypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCoreAesEncryptDecryptExRoundTrip 校验新增的 CoreAesDecryptEx 与 CoreAesEncryptEx 互逆。
|
||||
func TestCoreAesEncryptDecryptExRoundTrip(t *testing.T) {
|
||||
key := "h5-m3u8-ticket-key-roundtrip-000000000000"
|
||||
plains := [][]byte{
|
||||
[]byte(""),
|
||||
[]byte("a"),
|
||||
[]byte(`{"u":10086,"p":"v3/av/a.m3u8","e":1737000000}`),
|
||||
bytes.Repeat([]byte("x"), 512),
|
||||
}
|
||||
for _, plain := range plains {
|
||||
enc, err := CoreAesEncryptEx(plain, 12, key)
|
||||
if err != nil {
|
||||
t.Fatalf("encrypt: %v", err)
|
||||
}
|
||||
dec, err := CoreAesDecryptEx(enc, 12, key)
|
||||
if err != nil {
|
||||
t.Fatalf("decrypt: %v", err)
|
||||
}
|
||||
if !bytes.Equal(dec, plain) {
|
||||
t.Fatalf("round trip mismatch: got %q want %q", dec, plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCoreAesDecryptExRejectsShort 确保短于 nonce 的非法输入不 panic 且返回错误。
|
||||
func TestCoreAesDecryptExRejectsShort(t *testing.T) {
|
||||
if _, err := CoreAesDecryptEx([]byte{1, 2, 3}, 12, "key"); err == nil {
|
||||
t.Fatal("expected error for input shorter than nonce")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCoreAesDecryptExWrongKey 用错误密钥解密不应还原出原文。
|
||||
func TestCoreAesDecryptExWrongKey(t *testing.T) {
|
||||
enc, err := CoreAesEncryptEx([]byte("secret-payload"), 12, "key-a-000000000000000000000000000000")
|
||||
if err != nil {
|
||||
t.Fatalf("encrypt: %v", err)
|
||||
}
|
||||
dec, err := CoreAesDecryptEx(enc, 12, "key-b-111111111111111111111111111111")
|
||||
if err == nil && string(dec) == "secret-payload" {
|
||||
t.Fatal("wrong key must not recover plaintext")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package ecb
|
||||
|
||||
import "crypto/cipher"
|
||||
|
||||
type ecb struct {
|
||||
b cipher.Block
|
||||
blockSize int
|
||||
}
|
||||
|
||||
func newECB(b cipher.Block) *ecb {
|
||||
return &ecb{
|
||||
b: b,
|
||||
blockSize: b.BlockSize(),
|
||||
}
|
||||
}
|
||||
|
||||
type ecbEncrypter ecb
|
||||
|
||||
// NewECBEncrypter returns a BlockMode which encrypts in electronic code book
|
||||
// mode, using the given Block.
|
||||
func NewECBEncrypter(b cipher.Block) cipher.BlockMode {
|
||||
return (*ecbEncrypter)(newECB(b))
|
||||
}
|
||||
|
||||
func (x *ecbEncrypter) BlockSize() int { return x.blockSize }
|
||||
|
||||
func (x *ecbEncrypter) CryptBlocks(dst, src []byte) {
|
||||
if len(src)%x.blockSize != 0 {
|
||||
panic("crypto/cipher: input not full blocks")
|
||||
}
|
||||
if len(dst) < len(src) {
|
||||
panic("crypto/cipher: output smaller than input")
|
||||
}
|
||||
for len(src) > 0 {
|
||||
x.b.Encrypt(dst, src[:x.blockSize])
|
||||
src = src[x.blockSize:]
|
||||
dst = dst[x.blockSize:]
|
||||
}
|
||||
}
|
||||
|
||||
type ecbDecrypter ecb
|
||||
|
||||
// NewECBDecrypter returns a BlockMode which decrypts in electronic code book
|
||||
// mode, using the given Block.
|
||||
func NewECBDecrypter(b cipher.Block) cipher.BlockMode {
|
||||
return (*ecbDecrypter)(newECB(b))
|
||||
}
|
||||
|
||||
func (x *ecbDecrypter) BlockSize() int { return x.blockSize }
|
||||
|
||||
func (x *ecbDecrypter) CryptBlocks(dst, src []byte) {
|
||||
if len(src)%x.blockSize != 0 {
|
||||
panic("crypto/cipher: input not full blocks")
|
||||
}
|
||||
if len(dst) < len(src) {
|
||||
panic("crypto/cipher: output smaller than input")
|
||||
}
|
||||
for len(src) > 0 {
|
||||
x.b.Decrypt(dst, src[:x.blockSize])
|
||||
src = src[x.blockSize:]
|
||||
dst = dst[x.blockSize:]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package crypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
|
||||
"91porn-server/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) {
|
||||
block, _ := pem.Decode(pub)
|
||||
b := block.Bytes
|
||||
var err error
|
||||
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)
|
||||
b := block.Bytes
|
||||
var err error
|
||||
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 []byte, cipherData []byte) ([]byte, error) {
|
||||
privateKey, err := rsaBytesToPrivateKey(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
|
||||
}
|
||||
|
||||
// 公钥解密
|
||||
func RSADecryptByPrivateKey1(pemPrivateKey []byte, cipherData []byte) ([]byte, error) {
|
||||
block, _ := pem.Decode(pemPrivateKey) //将密钥解析成私钥实例
|
||||
if block == nil {
|
||||
return nil, errors.New("private key error!")
|
||||
}
|
||||
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) //解析pem.Decode()返回的Block指针实例
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rsa.DecryptPKCS1v15(rand.Reader, priv, cipherData) //RSA算法解密
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package daichong
|
||||
|
||||
import "91porn-server/common/stderr"
|
||||
|
||||
// 接口地址
|
||||
const (
|
||||
takeChat = "/api/chat/playerChat" //发起聊天
|
||||
getOrder = "/api/chat/getOrder" //获取订单信息
|
||||
orderCallBack = "/api/call/orderCallBack" //通知代充平台订单处理结果
|
||||
uploadFile = "/file" //上传图片 路径file 拼接要上传的图片名称 玩家ID加上毫秒时间戳 /file/u123456156332544.jpg
|
||||
getChatRecordsByOrderId = "/api/chat/getChatRecordsByOrderId" //根据订单ID获取聊天记录
|
||||
getTraderPayInfo = "/api/chat/getTraderPayInfo" //获取商人支付信息
|
||||
|
||||
// 接口地址
|
||||
newtakeChat = "/api/dc/daichong/takeChat" //发起聊天
|
||||
newgetOrder = "/api/dc/daichong/orderInfo" //获取订单信息
|
||||
newgetTraderPayInfo = "/api/dc/daichong/traderPayInfo" //获取商人支付信息
|
||||
|
||||
// 通知订单状态
|
||||
Success int = 4 //成功
|
||||
Failed int = 5 //失败
|
||||
)
|
||||
|
||||
// ======================================================================================================================
|
||||
// 聊天返回响应模型
|
||||
type ChatResp struct {
|
||||
Code stderr.Code `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data Chat `json:"data"`
|
||||
}
|
||||
|
||||
// 发起聊天参数模型
|
||||
type ChatReq struct {
|
||||
AppId string `json:"appId"` //代充平台提供的appId
|
||||
Data string `json:"data"` //加密结果字符串
|
||||
ProtoType int `json:"protoType"` //协议类型 1: Google Protobuf 2: Pomelo Protobuf 3: JSON
|
||||
ProductType int `json:"productType"` //0 站群 (默认) 1棋牌
|
||||
}
|
||||
|
||||
type ChatSign struct {
|
||||
UID string `json:"uid"`
|
||||
NickName string `json:"nickName"`
|
||||
Avatar string `json:"avatar"`
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
|
||||
// 聊天模型
|
||||
type Chat struct {
|
||||
IsReconnect bool `json:"isReconnect"` //当前玩家是否处于断线重连
|
||||
Traders []Trader `json:"traders"`
|
||||
URL string `json:"url"` //客户端连接webSocket的地址
|
||||
WsURL string `json:"wsUrl"` //客户端连接webSocket的地址
|
||||
PicUrl string `json:"picUrl"` //图片文件服务
|
||||
OrdUrl string `json:"ordUrl,omitempty"` //获取订单信息地址
|
||||
TraderUrl string `json:"traderUrl,omitempty"` //获取商人列表地址
|
||||
UserInfo struct {
|
||||
UID uint64 `json:"uid,omitempty"` //用户id
|
||||
Gender string `json:"gender,omitempty"` //性别
|
||||
Name string `json:"name,omitempty"` //用户名称
|
||||
Portrait string `json:"portrait,omitempty"` //头像
|
||||
} `json:"userInfo"` //用户uid
|
||||
ChargeMoney uint64 `json:"chargeMoney"` //充值金额
|
||||
Limit uint64 `json:"limit"` //限定金额 超过限定金额 使用大额扫码 ,小于等于限定金额 使用小额扫码
|
||||
UserAgent string `json:"userAgent,omitempty"` //UserAgent header
|
||||
}
|
||||
|
||||
// 商人模型
|
||||
type Trader struct {
|
||||
ImId int64 `json:"imId"` //商人在聊天系统中的ID
|
||||
UserId string `json:"userId"` //商人在代充系统中的ID
|
||||
Avatar string `json:"avatar"` //头像
|
||||
NickName string `json:"nickName"` //昵称
|
||||
PayInfos []PayInfo `json:"payInfos"` //支付方式
|
||||
WelcomeMsg string `json:"welcomeMsg"` //商人欢迎语
|
||||
}
|
||||
|
||||
// 支付方式
|
||||
type PayInfo struct {
|
||||
PayMethod int64 `json:"payMethod"` //支付方式大类别
|
||||
PayType []int64 `json:"payType"` //支付方式小类别
|
||||
}
|
||||
|
||||
// ======================================================================================================================
|
||||
// 获取订单响应模型
|
||||
type OrderResp struct {
|
||||
Code stderr.Code `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data Order `json:"data"`
|
||||
}
|
||||
|
||||
type OrderSign struct {
|
||||
TraderId int64 `form:"traderId" json:"traderId"`
|
||||
PlayerId int64 `form:"playerId" json:"playerId"`
|
||||
Proof string `form:"proof" json:"proof"` //玩家上传的图片名称
|
||||
SessionId string `form:"sessionId" json:"sessionId"` //当前会话的sessionId
|
||||
ProductInfo string `form:"productInfo" json:"productInfo"` //商品信息
|
||||
ProT int `form:"productType" json:"productType" ` //0 站群 1棋牌
|
||||
}
|
||||
|
||||
// 请求模型
|
||||
type CommonReq struct {
|
||||
AppId string `json:"appId"` //代充平台提供的appId
|
||||
Data string `json:"data"` //加密字符串
|
||||
}
|
||||
|
||||
// 订单模型
|
||||
type Order struct {
|
||||
OrderId string `json:"orderId"` //订单号
|
||||
Proof string `json:"proof"` //当前订单生成的凭证(即玩家上传的图片名称
|
||||
CreateTime string `json:"createTime"` //订单创建的时间
|
||||
}
|
||||
|
||||
// ======================================================================================================================
|
||||
// 上分通知
|
||||
type OrderCallSign struct {
|
||||
OrderId string `json:"orderId"` //订单号 代充平台订单号
|
||||
Time int64 `json:"time"` //通知时间 时间戳 毫秒
|
||||
Status int `json:"status"` //状态
|
||||
}
|
||||
|
||||
// 上传参数
|
||||
type UploadFileReq struct {
|
||||
FileName string `json:"fileName"` //文件名
|
||||
AppId string `json:"appId"` //代充平台提供的appId
|
||||
UserID string `json:"userId"` //用户ID
|
||||
SessionId string `json:"sessionId"` //会话ID
|
||||
File string `json:"file"` //base64 字符串
|
||||
}
|
||||
|
||||
// 聊天记录返回模型
|
||||
type RecordResp struct {
|
||||
Code stderr.Code `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data recordChat `json:"data"`
|
||||
}
|
||||
|
||||
// 聊天记录列表
|
||||
type recordChat struct {
|
||||
List []chat `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
// 聊天
|
||||
type chat struct {
|
||||
MessageId string `json:"messageId"`
|
||||
SessionId string `json:"sessionId"`
|
||||
SenderId int64 `json:"senderId"`
|
||||
TargetId int64 `json:"targetId"`
|
||||
SendType int `json:"sendType"`
|
||||
MessageType int `json:"messageType"`
|
||||
Text string `json:"text"`
|
||||
Photo []string `json:"photo"`
|
||||
Payload string `json:"payload"`
|
||||
CreateDate int `json:"createDate"`
|
||||
IsRead int `json:"isRead"`
|
||||
}
|
||||
|
||||
// 商人支付信息
|
||||
type PayInfoResp struct {
|
||||
Code stderr.Code `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data PayInfo `json:"data"`
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package daichong
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/crypt"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/commod"
|
||||
)
|
||||
|
||||
// 发起聊天
|
||||
func TakeChat(req ChatReq, domian string) (chatResp ChatResp, err error) {
|
||||
var code int
|
||||
param, _ := common.ToJsonM(req)
|
||||
c, cancle := context.WithTimeout(context.Background(), time.Duration(appg.Conf.HttpOptions.DcCtxTimeOut)*time.Second)
|
||||
defer cancle()
|
||||
code, err = httputil.DefaultClientPostJsonWithResp(&chatResp, common.BindUrl(domian, takeChat), nil, param)
|
||||
common.Go(func() {
|
||||
<-c.Done()
|
||||
fmt.Print("current http method takeChat", c.Err())
|
||||
})
|
||||
log.Info("http method TakeChat response code ==>", log.Any("statusCode", code), log.Any("respCode", chatResp.Code))
|
||||
if err != nil {
|
||||
log.Error("TakeChat failed", log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取订单信息
|
||||
func GetOrder(req CommonReq, domian string) (orderResp OrderResp, err error) {
|
||||
param, _ := common.ToJsonM(req)
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&orderResp, common.BindUrl(domian, getOrder), nil, param)
|
||||
log.Info("http method GetOrder response code ==>", log.Any("statusCode", code), log.Any("respCode", orderResp.Code))
|
||||
if err != nil {
|
||||
log.Error("GetOrder failed", log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 上分通知
|
||||
func OrderCallBack(ctx context.Context, req CommonReq, domian string) (resp commod.Resp, err error) {
|
||||
param, _ := common.ToJsonM(req)
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&resp, common.BindUrl(domian, orderCallBack), nil, param)
|
||||
log.InfoX(ctx, "http method OrderCallBack response code ==>", log.Any("statusCode", code),
|
||||
log.Any("respCode", resp.Code))
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, "OrderCallBack failed", log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 上传图片
|
||||
func UploadFile(req UploadFileReq, domian string) (resp commod.Resp, err error) {
|
||||
header := map[string]string{
|
||||
"Authorization": req.UserID + "&" + req.SessionId,
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"file": req.File,
|
||||
}
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&resp, common.BindUrl(domian, uploadFile, req.FileName), header, data)
|
||||
log.Info("http method UploadFile response code ==>", log.Any("statusCode", code), log.Any("respCode", resp.Code))
|
||||
if err != nil {
|
||||
log.Error("UploadFile failed", log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 根据订单ID获取聊天记录
|
||||
func GetChatRecordsByOrderId(req CommonReq, domian string) (resp RecordResp, err error) {
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&resp, common.BindUrl(domian, getChatRecordsByOrderId), nil, req)
|
||||
log.Info("http method GetChatRecordsByOrderId response code ==>", log.Any("statusCode", code), log.Any("respCode", resp.Code))
|
||||
if err != nil {
|
||||
log.Error("GetChatRecordsByOrderId failed", log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取商人支付信息
|
||||
func GetTraderPayInfo(req CommonReq, domian string) (resp PayInfoResp, err error) {
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&resp, common.BindUrl(domian, getTraderPayInfo), nil, req)
|
||||
log.Info("http method GetTraderPayInfo response code ==>", log.Any("statusCode", code), log.Any("respCode", resp.Code))
|
||||
if err != nil {
|
||||
log.Error("GetTraderPayInfo failed", log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 结构体转换为加密的16进制字符串
|
||||
func Convert2Sign(obj interface{}, appSecret string) (string, error) {
|
||||
urlParam, err := crypt.Strcut2UrlValue(obj)
|
||||
if err != nil {
|
||||
log.Warn("chat sign struct convert to url values wrong", log.Any("urlParam", urlParam), log.Any("warn", err))
|
||||
return "", err
|
||||
}
|
||||
log.Info("covert obj to urlParam ==>", log.Any("obj", obj), log.Any("urlParam", urlParam))
|
||||
sign, err := crypt.AesEncrypt(urlParam, appSecret)
|
||||
if err != nil {
|
||||
log.Warn("create sign data wrong", log.Any("warn", err))
|
||||
return "", err
|
||||
}
|
||||
cryptdata := hex.EncodeToString(sign)
|
||||
return cryptdata, nil
|
||||
}
|
||||
|
||||
// 加密的16进制字符串转为结构体
|
||||
func Sign2Struct(obj interface{}, sign, appSecret string) error {
|
||||
cryptdata, err := hex.DecodeString(sign)
|
||||
if err != nil {
|
||||
log.Warn("hex decode string occour error", log.Any("warn", err))
|
||||
return err
|
||||
}
|
||||
origin, err := crypt.CoreAesDecrypt(cryptdata, appSecret)
|
||||
if err != nil {
|
||||
log.Warn("core aes decrypt sign data wrong", log.Any("warn", err))
|
||||
return err
|
||||
}
|
||||
t := reflect.TypeOf(obj)
|
||||
m := make(map[string]interface{})
|
||||
u, err := url.ParseQuery(origin)
|
||||
if err == nil {
|
||||
for k, v := range u {
|
||||
s, _ := t.Elem().FieldByName(common.Ucfirst(k))
|
||||
typ := s.Type.Kind()
|
||||
switch typ {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
rv, _ := strconv.ParseInt(v[0], 10, 64)
|
||||
m[k] = rv
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
rv, _ := strconv.ParseUint(v[0], 10, 64)
|
||||
m[k] = rv
|
||||
default:
|
||||
m[k] = v[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(m) > 0 {
|
||||
return common.Map2JSONStruct(obj, m)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package daichong
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/e/daichongmod"
|
||||
)
|
||||
|
||||
// 发起聊天
|
||||
func NewTakeChat(ctx context.Context, req NewChatReq, domain string) (chatResp ChatResp, err error) {
|
||||
var code int
|
||||
param, _ := common.ToJsonM(req)
|
||||
c, cancle := context.WithTimeout(ctx, time.Duration(appg.Conf.HttpOptions.DcCtxTimeOut)*time.Second)
|
||||
defer cancle()
|
||||
code, err = httputil.DefaultClientPostJsonWithResp(&chatResp, common.BindUrl(domain, newtakeChat), nil, param)
|
||||
common.Go(func() {
|
||||
<-c.Done()
|
||||
fmt.Print("current http method takeChat", c.Err())
|
||||
})
|
||||
log.InfoX(c, "http method TakeChat response code ==>", log.Any("statusCode", code),
|
||||
log.Any("respCode", chatResp.Code))
|
||||
if err != nil {
|
||||
log.ErrorX(c, "TakeChat failed", log.Any("req", req), log.Any("domain", domain), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取订单信息
|
||||
func NewGetOrder(req OrderSign, domain string) (orderResp OrderResp, err error) {
|
||||
param, _ := common.ToJsonM(req)
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&orderResp, common.BindUrl(domain, newgetOrder), nil, param)
|
||||
log.Info("http method GetOrder response code ==>", log.Any("statusCode", code), log.Any("respCode", orderResp.Code))
|
||||
if err != nil {
|
||||
log.Error("GetOrder failed", log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取商人支付信息
|
||||
func NewGetTraderPayInfo(req daichongmod.PayInfoReq, domain string) (resp PayInfoResp, err error) {
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&resp, common.BindUrl(domain, newgetTraderPayInfo), nil, req)
|
||||
log.Info("http method GetTraderPayInfo response code ==>", log.Any("statusCode", code), log.Any("respCode", resp.Code))
|
||||
if err != nil {
|
||||
log.Error("GetTraderPayInfo failed", log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package daichong
|
||||
|
||||
// 发起聊天参数模型
|
||||
type NewChatReq struct {
|
||||
UID string `json:"uid"` //用户id
|
||||
Name string `json:"name"` //用户名称
|
||||
Portrait string `json:"portrait"` //用户头像
|
||||
ProductType int `json:"productType"` //0 站群 (默认) 1棋牌
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package dataReport
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/commod"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/moul/http2curl"
|
||||
)
|
||||
|
||||
var config = Config{}
|
||||
|
||||
func Init(c Config) {
|
||||
config = c
|
||||
}
|
||||
|
||||
// GenerateGlobalEventID 生成全局唯一的 event_id
|
||||
func GenerateGlobalEventID() string {
|
||||
// 生成一个新的 UUID
|
||||
return strings.ReplaceAll(uuid.New().String(), "-", "")
|
||||
}
|
||||
|
||||
func ReportAppId() string {
|
||||
if commod.KFK_APPID < 10 {
|
||||
return fmt.Sprintf("JHA-00%v", commod.KFK_APPID)
|
||||
}
|
||||
if commod.KFK_APPID < 100 {
|
||||
return fmt.Sprintf("JHA-0%v", commod.KFK_APPID)
|
||||
}
|
||||
return fmt.Sprintf("JHA-%v", commod.KFK_APPID)
|
||||
}
|
||||
|
||||
// 上报事件
|
||||
func BatchReport(data interface{}) error {
|
||||
apiUrl := config.ApiUrl + "/api/eventTracking/batchReport.json"
|
||||
reqBody, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
log.Error("Event report json.Marshal fail", log.E(err))
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("POST", apiUrl, bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
log.Error("Event report NewRequest fail", log.E(err))
|
||||
return fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
curl, _ := http2curl.GetCurlCommand(req)
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second, // 设置超时时间为 5 秒
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Error("Event report fail", log.Any("curl", curl), log.E(err))
|
||||
return fmt.Errorf("failed to send request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Error("Event report fail", log.Any("curl", curl), log.Any("http.code", resp.StatusCode))
|
||||
return fmt.Errorf("failed to report event, status code: %d", resp.StatusCode)
|
||||
}
|
||||
// 读取返回的 body
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Error("Event report fail", log.Any("curl", curl), log.E(err))
|
||||
return fmt.Errorf("failed to read response body: %v", err)
|
||||
}
|
||||
// 输出响应状态和返回的 body
|
||||
log.Info("Event report Response", log.Any("body", string(body)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func structToFormData(data interface{}) (string, error) {
|
||||
// 序列化结构体为 JSON 字符串
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 反序列化为 map
|
||||
var dataMap map[string]interface{}
|
||||
err = json.Unmarshal(jsonData, &dataMap)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var formData []string
|
||||
|
||||
// 遍历 map,将其转换为 form 数据
|
||||
for key, value := range dataMap {
|
||||
if value == nil {
|
||||
continue // 如果值为 nil,跳过
|
||||
}
|
||||
|
||||
// 将值转换为字符串
|
||||
valueStr := fmt.Sprintf("%v", value)
|
||||
|
||||
// 使用 URL 编码
|
||||
formData = append(formData, fmt.Sprintf(
|
||||
"%s=%s",
|
||||
url.QueryEscape(key),
|
||||
url.QueryEscape(valueStr),
|
||||
))
|
||||
}
|
||||
|
||||
return strings.Join(formData, "&"), nil
|
||||
}
|
||||
|
||||
func NewCommonField(eventId string, event string, uid uint64, districtCode string, ua ua.UA, ip string) CommonField {
|
||||
device := normalizeDevice(ua.SysType)
|
||||
deviceID := CutTo50(strings.TrimSpace(ua.DevID))
|
||||
deviceModel := strings.TrimSpace(ua.DeviceModel)
|
||||
if deviceModel == "" {
|
||||
deviceModel = strings.TrimSpace(ua.DevType)
|
||||
}
|
||||
systemName := strings.TrimSpace(ua.SystemName)
|
||||
if systemName == "" {
|
||||
systemName = device
|
||||
}
|
||||
return CommonField{
|
||||
UID: fmt.Sprintf("%v", uid),
|
||||
EventID: eventId,
|
||||
Event: event, // 事件类型由外部事件对象指定
|
||||
Channel: strings.TrimSpace(districtCode),
|
||||
SID: strings.TrimSpace(ua.SID),
|
||||
AppID: ReportAppId(),
|
||||
ClientTS: JsonNumber(time.Now().Unix()),
|
||||
Device: device,
|
||||
DeviceID: deviceID,
|
||||
UserAgent: strings.TrimSpace(ua.UserAgent),
|
||||
DeviceModel: deviceModel,
|
||||
DeviceBrand: strings.TrimSpace(ua.DeviceBrand),
|
||||
SystemName: systemName,
|
||||
SystemVersion: strings.TrimSpace(ua.SystemVersion),
|
||||
IP: ip,
|
||||
}
|
||||
}
|
||||
|
||||
func Device(ua ua.UA) string {
|
||||
return normalizeDevice(ua.SysType)
|
||||
}
|
||||
|
||||
func normalizeDevice(value string) string {
|
||||
lower := strings.ToLower(strings.TrimSpace(value))
|
||||
switch {
|
||||
case lower == "":
|
||||
return ""
|
||||
case strings.Contains(lower, "ios"), strings.Contains(lower, "iphone"), strings.Contains(lower, "ipad"), strings.Contains(lower, "apple"):
|
||||
return "iOS"
|
||||
case strings.Contains(lower, "android"):
|
||||
return "Android"
|
||||
case strings.Contains(lower, "pc"), strings.Contains(lower, "windows"), strings.Contains(lower, "mac"), strings.Contains(lower, "h5"):
|
||||
return "PC"
|
||||
}
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
// CutTo50 截取前 50 个“字符”(支持中文、-、_ 等特殊字符)
|
||||
func CutTo50(s string) string {
|
||||
runes := []rune(s)
|
||||
if len(runes) <= 50 {
|
||||
return s
|
||||
}
|
||||
return string(runes[:50])
|
||||
}
|
||||
|
||||
func JsonNumber(v int64) json.Number {
|
||||
return json.Number(strconv.FormatInt(v, 10))
|
||||
}
|
||||
|
||||
func JsonRawMessage(v interface{}) json.RawMessage {
|
||||
b, _ := json.Marshal(v)
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package dataReport
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReportEvent(t *testing.T) {
|
||||
reportAppId := "11"
|
||||
// 创建一个广告点击
|
||||
event := &CommonField{
|
||||
UID: "user_123",
|
||||
EventID: GenerateGlobalEventID(),
|
||||
Event: EventTypeAdClick,
|
||||
Channel: "666",
|
||||
AppID: reportAppId,
|
||||
//SID string `json:"sid"` // 会话ID
|
||||
//ClientTS int64 `json:"client_ts"` // 客户端时间戳
|
||||
Device: "Android",
|
||||
DeviceID: "device_12345",
|
||||
UserAgent: "Mozilla/5.0",
|
||||
DeviceBrand: "Huawei",
|
||||
DeviceModel: "Mate 40",
|
||||
IP: "192.168.0.1",
|
||||
}
|
||||
event.Payload = JsonRawMessage(AdClickEvent{
|
||||
AdID: "1286400000000040", // 广告ID
|
||||
})
|
||||
config.ApiUrl = "https://api.shuifeng.cc"
|
||||
list := []*CommonField{event}
|
||||
BatchReport(list)
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package dataReport
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Config struct {
|
||||
ApiUrl string `json:"apiUrl"`
|
||||
}
|
||||
|
||||
const (
|
||||
EventTypeAdClick = "ad_click"
|
||||
EventTypeUserLogin = "user_login"
|
||||
EventTypeUserRegister = "user_register"
|
||||
EventTypeOrderCreated = "order_created"
|
||||
EventTypeOrderPaid = "order_paid"
|
||||
EventTypeCoinConsume = "coin_consume"
|
||||
EventTypeVideoPurchase = "video_purchase"
|
||||
EventTypeVideoLike = "video_like"
|
||||
EventTypeVideoCollect = "video_collect"
|
||||
EventTypeVideoComment = "video_comment"
|
||||
EventTypeVideoStatusChange = "video_status_change"
|
||||
|
||||
// 漫画事件
|
||||
EventTypeComicPurchase = "comic_purchase" // 漫画购买事件
|
||||
EventTypeComicLike = "comic_like" // 漫画点赞
|
||||
EventTypeComicCollect = "comic_collect" // 漫画收藏
|
||||
EventTypeComicComment = "comic_comment" // 漫画评论
|
||||
|
||||
// 小说事件
|
||||
EventTypeNovelPurchase = "novel_purchase" // 小说购买事件
|
||||
EventTypeNovelLike = "novel_like" // 小说点赞
|
||||
EventTypeNovelCollect = "novel_collect" // 小说收藏
|
||||
EventTypeNovelComment = "novel_comment" // 小说评论
|
||||
|
||||
)
|
||||
|
||||
// 定义公共字段和事件数据结构
|
||||
type CommonField struct {
|
||||
Event string `json:"event"` // 事件类型
|
||||
Channel string `json:"channel"` // 渠道码
|
||||
EventID string `json:"event_id"` // 事件唯一标识符
|
||||
AppID string `json:"app_id"` // 应用ID
|
||||
UID string `json:"uid"` // 用户ID
|
||||
SID string `json:"sid"` // 会话ID
|
||||
ClientTS json.Number `json:"client_ts"` // 客户端时间戳
|
||||
Device string `json:"device"` // 设备类型(如:Android, iOS, PC)
|
||||
DeviceID string `json:"device_id"` // 设备ID
|
||||
UserAgent string `json:"user_agent"` // 用户代理信息
|
||||
DeviceBrand string `json:"device_brand"` // 设备品牌(如:HUAWEI)
|
||||
DeviceModel string `json:"device_model"` // 设备型号
|
||||
SystemName string `json:"system_name"` // 系统名称
|
||||
SystemVersion string `json:"system_version"` // 系统版本
|
||||
IP string `json:"ip,omitempty"` // 用户IP地址
|
||||
Payload json.RawMessage `json:"payload"` // 事件数据
|
||||
}
|
||||
|
||||
// UserRegisterEvent 注册事件
|
||||
type UserRegisterEvent struct {
|
||||
Type string `json:"type"` // 注册方式:phone, deviceid, email, username
|
||||
TraceID string `json:"trace_id"` // 唯一标识,落地页点击生成
|
||||
CreateTime json.Number `json:"create_time"` // 注册时间,10位时间戳
|
||||
}
|
||||
|
||||
// UserLoginEvent 登录事件
|
||||
type UserLoginEvent struct {
|
||||
Type string `json:"type"` // 登录方式:phone, deviceid, email, username
|
||||
}
|
||||
|
||||
// OrderCreatedEvent 订单创建
|
||||
type OrderCreatedEvent struct {
|
||||
OrderID string `json:"order_id"` // 订单号(唯一标识)
|
||||
OrderType string `json:"order_type"` // 订单类型
|
||||
ProductID string `json:"product_id"` // 商品ID
|
||||
ProductName string `json:"product_name"` // 商品名称
|
||||
Amount json.Number `json:"amount"` // 订单金额(分)
|
||||
Currency string `json:"currency"` // 货币类型
|
||||
CoinQuantity json.Number `json:"coin_quantity"` // 金币数量
|
||||
VIPDurationID string `json:"vip_duration_type"`
|
||||
VIPDurationName string `json:"vip_duration_name"`
|
||||
SourcePageKey string `json:"source_page_key"` // 来源页面标识
|
||||
SourcePageName string `json:"source_page_name"` // 来源页面名称
|
||||
CreateTime json.Number `json:"create_time"` // 订单创建时间
|
||||
}
|
||||
|
||||
// OrderPaidEvent 订单支付成功
|
||||
type OrderPaidEvent struct {
|
||||
OrderID string `json:"order_id"` // 订单号(关联创建事件)
|
||||
OrderType string `json:"order_type"` // 订单类型:coin_purchase, vip_subscription
|
||||
ProductID string `json:"product_id"` // 商品ID
|
||||
Amount json.Number `json:"amount"` // 实际支付金额(分)
|
||||
Currency string `json:"currency"` // 货币类型:CNY, USD
|
||||
CoinQuantity json.Number `json:"coin_quantity"` // 金币数量(仅金币订单)
|
||||
VIPExpirationTime json.Number `json:"vip_expiration_time"` // VIP过期时间(10位时间戳)
|
||||
PayType string `json:"pay_type"` // 支付方式:wechat, alipay, bank_card
|
||||
PayChannel string `json:"pay_channel"` // 支付渠道:银行名称
|
||||
TransactionID string `json:"transaction_id"` // 第三方支付交易号
|
||||
CreateTime json.Number `json:"create_time"` // 订单创建时间
|
||||
|
||||
}
|
||||
|
||||
// CoinConsumeEvent 金币消耗
|
||||
type CoinConsumeEvent struct {
|
||||
ProductID string `json:"product_id"` // 商品ID
|
||||
ProductName string `json:"product_name"` // 商品名称
|
||||
CoinConsumeAmount json.Number `json:"coin_consume_amount"` // 金币消耗数量
|
||||
CoinBalanceBefore json.Number `json:"coin_balance_before"` // 消耗前金币余额
|
||||
CoinBalanceAfter json.Number `json:"coin_balance_after"` // 消耗后金币余额
|
||||
ConsumeReasonKey string `json:"consume_reason_key"` // 消耗原因
|
||||
ConsumeReasonName string `json:"consume_reason_name"` // 消耗原因名称
|
||||
CreateTime json.Number `json:"create_time"` // 订单创建时间
|
||||
OrderId string `json:"order_id"` // 订单id
|
||||
|
||||
}
|
||||
|
||||
// VideoPurchaseEvent 视频购买事件
|
||||
type VideoPurchaseEvent struct {
|
||||
VideoEventCore
|
||||
CoinQuantity json.Number `json:"coin_quantity"` // 金币数量
|
||||
OrderID string `json:"order_id"` // 订单号
|
||||
}
|
||||
|
||||
type VideoEventCore struct {
|
||||
MediaID string `json:"media_id"` // 老司机媒体资源ID(老司机接口返回的id字段)
|
||||
VideoID string `json:"video_id"` // 视频ID
|
||||
VideoTitle string `json:"video_title"` // 视频标题
|
||||
VideoTypeID string `json:"video_type_id"` // 视频分类ID
|
||||
VideoTypeName string `json:"video_type_name"` // 视频分类名称
|
||||
VideoContentType string `json:"video_content_type"` // 视频类型: video(长视频)、short_video(短视频)
|
||||
RecommendTraceID string `json:"recommend_trace_id"` // 推荐引擎的trace_id。没有对接搜索推荐引擎的上报空字符串。
|
||||
}
|
||||
|
||||
type VideoLikeEvent struct {
|
||||
VideoEventCore
|
||||
Flag json.Number `json:"flag"` // 点赞状态:1(点赞), 2(取消点赞)
|
||||
}
|
||||
|
||||
type VideoCollectEvent struct {
|
||||
VideoEventCore
|
||||
Flag json.Number `json:"flag"` // 收藏状态:1(收藏), 2(取消收藏)
|
||||
}
|
||||
|
||||
type VideoCommentEvent struct {
|
||||
VideoEventCore
|
||||
CommentContent string `json:"comment_content"` // 评论内容
|
||||
}
|
||||
|
||||
type VideoStatusChangeEvent struct {
|
||||
MediaID string `json:"media_id"` // 老司机媒体资源ID(老司机接口返回的id字段)
|
||||
VideoID string `json:"video_id"` // 视频ID
|
||||
VideoTitle string `json:"video_title"` // 视频标题
|
||||
VideoTypeID string `json:"video_type_id"` // 视频分类ID
|
||||
VideoTypeName string `json:"video_type_name"` // 视频分类名称
|
||||
Flag json.Number `json:"flag"` // 1(上架), 2(下架), 3(审核通过), 4(审核拒绝)
|
||||
}
|
||||
|
||||
// AdClickEvent 广告点击
|
||||
type AdClickEvent struct {
|
||||
PageKey string `json:"page_key"` // 页面标识
|
||||
PageName string `json:"page_name"` // 页面名称
|
||||
AdSlotKey string `json:"ad_slot_key"` // 广告位标识
|
||||
AdSlotName string `json:"ad_slot_name"` // 广告位名称
|
||||
AdID string `json:"ad_id"` // 广告ID
|
||||
CreativeID string `json:"creative_id"` // 素材ID(可选)
|
||||
AdType string `json:"ad_type"` // 广告类型
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// 漫画相关上报
|
||||
//
|
||||
//
|
||||
|
||||
type (
|
||||
ComicEventPub struct {
|
||||
MediaId string `json:"media_id"` // 老司机媒体资源ID(老司机接口返回的id字段)
|
||||
ComicId string `json:"comic_id"` // 漫画 id
|
||||
ComicTitle string `json:"comic_title"` // 漫画标题
|
||||
ComicTypeId string `json:"comic_type_id"` // 分类ID
|
||||
ComicTypeName string `json:"comic_type_name"` // 分类名称
|
||||
RecommendTraceId string `json:"recommend_trace_id"` // 推荐引擎的trace_id。没有对接搜索推荐引擎的上报空字符串。
|
||||
}
|
||||
|
||||
// ComicPurchaseEvent 漫画购买
|
||||
ComicPurchaseEvent struct {
|
||||
ComicEventPub
|
||||
CoinQuantity json.Number `json:"coin_quantity"` // 金币数量
|
||||
OrderId string `json:"order_id"` // 订单号
|
||||
PageNo json.Number `json:"page_no"` // 事件发生在第几页(如果在列表页当前字段设置为0)
|
||||
}
|
||||
|
||||
// ComicCollectOrLikeEvent 漫画收藏/点赞
|
||||
ComicCollectOrLikeEvent struct {
|
||||
ComicEventPub
|
||||
Flag json.Number `json:"flag"` //收藏状态:1(收藏), 2(取消收藏)
|
||||
PageNo json.Number `json:"page_no"` // 事件发生在第几页(如果在列表页当前字段设置为0)
|
||||
}
|
||||
|
||||
// ComicCommentEvent 漫画评论
|
||||
ComicCommentEvent struct {
|
||||
ComicEventPub
|
||||
CommentContent string `json:"comment_content"` // 评论内容
|
||||
PageNo json.Number `json:"page_no"` // 事件发生在第几页(如果在列表页当前字段设置为0)
|
||||
}
|
||||
)
|
||||
|
||||
//
|
||||
//
|
||||
// 小说相关上报
|
||||
//
|
||||
//
|
||||
|
||||
type (
|
||||
NovelEventPub struct {
|
||||
MediaId string `json:"media_id"` // 老司机媒体资源ID(老司机接口返回的id字段)
|
||||
NovelId string `json:"novel_id"` // 小说 id
|
||||
NovelTitle string `json:"novel_title"` // 小说标题
|
||||
NovelTypeId string `json:"novel_type_id"` // 分类ID
|
||||
NovelTypeName string `json:"novel_type_name"` // 分类名称
|
||||
RecommendTraceId string `json:"recommend_trace_id"` // 推荐引擎的trace_id。没有对接搜索推荐引擎的上报空字符串。
|
||||
}
|
||||
|
||||
// NovelPurchaseEvent 小说购买
|
||||
NovelPurchaseEvent struct {
|
||||
NovelEventPub
|
||||
CoinQuantity json.Number `json:"coin_quantity"` // 金币数量
|
||||
OrderId string `json:"order_id"` // 订单号
|
||||
PageNo json.Number `json:"page_no"` // 事件发生在第几页(如果在列表页当前字段设置为0)
|
||||
}
|
||||
|
||||
// NovelCollectOrLikeEvent 小说收藏/点赞
|
||||
NovelCollectOrLikeEvent struct {
|
||||
NovelEventPub
|
||||
Flag json.Number `json:"flag"` // 收藏状态:1(收藏), 2(取消收藏)
|
||||
PageNo json.Number `json:"page_no"` // 事件发生在第几页(如果在列表页当前字段设置为0)
|
||||
}
|
||||
|
||||
// NovelCommentEvent 小说评论
|
||||
NovelCommentEvent struct {
|
||||
NovelEventPub
|
||||
CommentContent string `json:"comment_content"` // 评论内容
|
||||
PageNo json.Number `json:"page_no"` // 事件发生在第几页(如果在列表页当前字段设置为0)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
package datacenter
|
||||
|
||||
import (
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/log"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/sarama"
|
||||
)
|
||||
|
||||
// kafka 使用
|
||||
var (
|
||||
gSyncProducer sarama.SyncProducer
|
||||
)
|
||||
|
||||
/**********************************************客户端 *********************/
|
||||
// 初始化kafka生产者 发送消息入口
|
||||
func InitKafkaProducter(addrs []string) error {
|
||||
config := sarama.NewConfig()
|
||||
config.Version = sarama.V2_0_0_0
|
||||
config.Producer.Return.Successes = true
|
||||
config.Net.KeepAlive = 2 * time.Hour
|
||||
|
||||
cli, err := sarama.NewClient(addrs, config)
|
||||
if err != nil {
|
||||
log.Error("startUp Kafka Init Kafka error", log.E(err))
|
||||
return err
|
||||
}
|
||||
|
||||
gSyncProducer, err = SyncProducter(cli)
|
||||
if err != nil {
|
||||
log.Error("startUp Kafka new SyncProducter error", log.E(err))
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AsyncSendMessage 同步发送确保消息成功
|
||||
func AsyncSendMessage(topic TopicType, message []byte) {
|
||||
common.Go(func() {
|
||||
if gSyncProducer != nil {
|
||||
msg := sarama.ProducerMessage{
|
||||
Topic: string(topic),
|
||||
Value: sarama.ByteEncoder(message),
|
||||
}
|
||||
partition, offset, err := gSyncProducer.SendMessage(&msg)
|
||||
if err != nil {
|
||||
log.Error("gSyncProducer SendMessage Fail", log.Any("topic", topic), log.E(err))
|
||||
|
||||
return
|
||||
}
|
||||
log.Info("SyncSendMessage ", log.Any("topic", topic), log.Any("Partition", partition), log.Any("Offset", offset))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 创建同步生产者 用于对消息的顺序有严格要求的场景 性能相对较低
|
||||
func SyncProducter(client sarama.Client) (sarama.SyncProducer, error) {
|
||||
producer, err := sarama.NewSyncProducerFromClient(client)
|
||||
if err != nil {
|
||||
log.Error("create syncProducer error", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
return producer, nil
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package datacenter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type IJsonMarshal interface {
|
||||
Marshal() []byte //用户ID
|
||||
}
|
||||
type TopicType string
|
||||
|
||||
const (
|
||||
TopicUserRegister TopicType = "user_register"
|
||||
TopicUserVisit TopicType = "user_visit"
|
||||
TopicUserOrder TopicType = "user_order"
|
||||
TopicUserOrderSuccess TopicType = "user_order_success"
|
||||
TopicUserAction TopicType = "user_action"
|
||||
)
|
||||
|
||||
/* 通信协议,第一版*/
|
||||
// 新用户注册消息
|
||||
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识别
|
||||
RegisterTime time.Time `json:"registerTime" bson:"registerTime"` // 注册时间时间
|
||||
}
|
||||
|
||||
var _ IJsonMarshal = &UserRegisterMsg{}
|
||||
|
||||
func (u *UserRegisterMsg) Marshal() []byte {
|
||||
jsonData, err := json.Marshal(u)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to marshal JSON: %v", err)
|
||||
}
|
||||
return jsonData
|
||||
}
|
||||
|
||||
// UserVisitMsg 用户每日第一次访问
|
||||
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
|
||||
DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码
|
||||
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"` // 访问时间
|
||||
}
|
||||
|
||||
var _ IJsonMarshal = &UserVisitMsg{}
|
||||
|
||||
func (u *UserVisitMsg) Marshal() []byte {
|
||||
jsonData, err := json.Marshal(u)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to marshal JSON: %v", err)
|
||||
}
|
||||
return jsonData
|
||||
}
|
||||
|
||||
// UserOrderMsg 用户订单消息
|
||||
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
|
||||
DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码
|
||||
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付款成功(目前只有成功才发送)
|
||||
Rate string `json:"rate" bson:"rate"` // 渠道费率
|
||||
SuccessAt time.Time `json:"successAt" bson:"successAt"` // 成功时间
|
||||
CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
|
||||
}
|
||||
|
||||
var _ IJsonMarshal = &UserOrderMsg{}
|
||||
|
||||
func (u *UserOrderMsg) Marshal() []byte {
|
||||
jsonData, err := json.Marshal(u)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to marshal JSON: %v", err)
|
||||
}
|
||||
return jsonData
|
||||
}
|
||||
|
||||
// UserChannelEvent 用户渠道统计
|
||||
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点击
|
||||
RegisterAt time.Time `json:"registerAt,omitempty" bson:"registerAt"` // 注册时间
|
||||
RequestCount int64 `json:"requestCount,omitempty" bson:"requestCount"` // 请求次数
|
||||
}
|
||||
|
||||
var _ IJsonMarshal = &UserChannelEvent{}
|
||||
|
||||
func (u *UserChannelEvent) Marshal() []byte {
|
||||
jsonData, err := json.Marshal(u)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to marshal JSON: %v", err)
|
||||
}
|
||||
return jsonData
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/bsoncodec"
|
||||
"go.mongodb.org/mongo-driver/bson/bsonrw"
|
||||
"go.mongodb.org/mongo-driver/bson/bsontype"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
var registry = func() *bsoncodec.Registry {
|
||||
builder := bson.NewRegistryBuilder()
|
||||
builder.RegisterTypeDecoder(reflect.TypeOf(time.Time{}), &localTimeDecoder{})
|
||||
builder.RegisterTypeDecoder(reflect.TypeOf(decimal.Decimal{}), &decimalDecoder{})
|
||||
builder.RegisterTypeEncoder(reflect.TypeOf(decimal.Decimal{}), &decimalEncoder{})
|
||||
builder.RegisterDefaultDecoder(reflect.Float32, &floatDecoder{})
|
||||
builder.RegisterDefaultDecoder(reflect.Float64, &floatDecoder{})
|
||||
return builder.Build()
|
||||
}()
|
||||
|
||||
type floatDecoder struct {
|
||||
}
|
||||
|
||||
func (dvd *floatDecoder) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
|
||||
var f float64
|
||||
var err error
|
||||
switch vr.Type() {
|
||||
case bsontype.Int32:
|
||||
i32, err := vr.ReadInt32()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f = float64(i32)
|
||||
case bsontype.Int64:
|
||||
i64, err := vr.ReadInt64()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f = float64(i64)
|
||||
case bsontype.Double:
|
||||
f, err = vr.ReadDouble()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("cannot decode %v into a float32 or float64 type", vr.Type())
|
||||
}
|
||||
val.SetFloat(f)
|
||||
return nil
|
||||
}
|
||||
|
||||
type localTimeDecoder struct{}
|
||||
|
||||
func (*localTimeDecoder) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
|
||||
if err := (&bsoncodec.TimeCodec{}).DecodeValue(dc, vr, val); err != nil {
|
||||
return err
|
||||
}
|
||||
t := val.Interface().(time.Time)
|
||||
val.Set(reflect.ValueOf(t.Local()))
|
||||
return nil
|
||||
}
|
||||
|
||||
type decimalDecoder struct{}
|
||||
|
||||
func (*decimalDecoder) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
|
||||
if !val.IsValid() || val.Type() != reflect.TypeOf(decimal.Decimal{}) {
|
||||
return bsoncodec.ValueDecoderError{Name: "DecimalDecodeValue", Types: []reflect.Type{reflect.TypeOf(decimal.Decimal{})}, Received: val}
|
||||
}
|
||||
if vr.Type() == bson.TypeInt32 || vr.Type() == bson.TypeInt64 {
|
||||
_, _ = vr.ReadInt32()
|
||||
_, _ = vr.ReadInt64()
|
||||
d := decimal.NewFromFloat(0.0)
|
||||
val.Set(reflect.ValueOf(d))
|
||||
return nil
|
||||
}
|
||||
mongodecimal, err := vr.ReadDecimal128()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d, err := decimal.NewFromString(mongodecimal.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
val.Set(reflect.ValueOf(d))
|
||||
return nil
|
||||
}
|
||||
|
||||
type decimalEncoder struct{}
|
||||
|
||||
func (*decimalEncoder) EncodeValue(ctx bsoncodec.EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error {
|
||||
if !val.IsValid() || val.Type() != reflect.TypeOf(decimal.Decimal{}) {
|
||||
return bsoncodec.ValueDecoderError{Name: "DecimalEncodeValue", Types: []reflect.Type{reflect.TypeOf(decimal.Decimal{})}, Received: val}
|
||||
}
|
||||
if d, ok := val.Interface().(decimal.Decimal); ok {
|
||||
mongodecimal, err := primitive.ParseDecimal128(d.StringFixed(2))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
val = reflect.ValueOf(mongodecimal)
|
||||
}
|
||||
dve := bsoncodec.DefaultValueEncoders{}
|
||||
return dve.Decimal128EncodeValue(ctx, vw, val)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package db
|
||||
|
||||
import "go.mongodb.org/mongo-driver/mongo"
|
||||
|
||||
func IsMongoDupKey(err error) bool {
|
||||
return mongo.IsDuplicateKeyError(err)
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
var (
|
||||
enableSortMCheck = true
|
||||
bsonDType = reflect.TypeOf(bson.D{})
|
||||
bsonDPtrType = reflect.TypeOf(&bson.D{})
|
||||
bsonEType = reflect.TypeOf(bson.E{})
|
||||
bsonEPtrType = reflect.TypeOf(&bson.E{})
|
||||
bsonMType = reflect.TypeOf(bson.M{})
|
||||
bsonMPtrType = reflect.TypeOf(&bson.M{})
|
||||
)
|
||||
|
||||
var skipErrors = []error{mongo.ErrNoDocuments}
|
||||
|
||||
func handleDbError(err error) error {
|
||||
for _, e := range skipErrors {
|
||||
if err == e {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if mongo.IsDuplicateKeyError(err) {
|
||||
return stderr.InsertExistError
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func sortCheck(sort interface{}) error {
|
||||
if !enableSortMCheck {
|
||||
return nil
|
||||
}
|
||||
if sort == nil {
|
||||
return nil
|
||||
}
|
||||
typ := reflect.TypeOf(sort)
|
||||
var m bson.M
|
||||
switch typ {
|
||||
case bsonMType:
|
||||
//log.Warn("mongo sort use bson.M use bson.D instead", log.Any("sort", sort))
|
||||
m, _ = sort.(bson.M)
|
||||
case bsonMPtrType:
|
||||
//log.Warn("mongo sort use *bson.M use bson.D instead", log.Any("sort", sort))
|
||||
pm, _ := sort.(*bson.M)
|
||||
m = *pm
|
||||
case bsonDType, bsonDPtrType, bsonEType, bsonEPtrType:
|
||||
return nil
|
||||
default:
|
||||
log.Warn("sort use unknown sort type please check", log.Any("sort", sort), log.Any("typ", typ))
|
||||
return errors.New("mongo error sort type")
|
||||
}
|
||||
if len(m) > 1 {
|
||||
return errors.New("mongo error sort, use bson.M and len(sort) > 1")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateIndex 创建数据索引.options 在index里面创建
|
||||
func (u *MongoTool) CreateIndex(models []mongo.IndexModel) ([]string, error) {
|
||||
//return nil, nil
|
||||
return u.coll.Indexes().CreateMany(u.ctx, models)
|
||||
}
|
||||
|
||||
// DropIndex 删除数据索引.options 在index里面创建
|
||||
func (u *MongoTool) DropIndex(indexname string) error {
|
||||
if _, err := u.coll.Indexes().DropOne(u.ctx, indexname); err != nil {
|
||||
log.Error(fmt.Sprintf("drop indexes error %+v", err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DropIndexIfExists 删除指定索引;索引或所在集合尚未创建时均视为成功。
|
||||
func (u *MongoTool) DropIndexIfExists(indexname string) error {
|
||||
if _, err := u.coll.Indexes().DropOne(u.ctx, indexname); err != nil {
|
||||
var commandErr mongo.CommandError
|
||||
// 27=IndexNotFound(索引不存在)、26=NamespaceNotFound(集合/库尚未创建):目标索引本就不存在,视为成功。
|
||||
if errors.As(err, &commandErr) && (commandErr.Code == 27 || commandErr.Code == 26) {
|
||||
return nil
|
||||
}
|
||||
log.Error(fmt.Sprintf("drop indexes error %+v", err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertOne 插入单条信息
|
||||
func (u *MongoTool) InsertOne(document interface{}, opts ...*options.InsertOneOptions) (*mongo.InsertOneResult, error) {
|
||||
insertResult, err := u.coll.InsertOne(u.ctx, document, opts...)
|
||||
return insertResult, handleDbError(err)
|
||||
}
|
||||
|
||||
// InsertMany 批量插入信息
|
||||
func (u *MongoTool) InsertMany(documents interface{}, opts ...*options.InsertManyOptions) (*mongo.InsertManyResult, error) {
|
||||
if err := validInterfaceSlice(documents); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := setTimeForSlice(documents)
|
||||
return u.coll.InsertMany(u.ctx, res, opts...)
|
||||
}
|
||||
|
||||
// Find 查询多条数据
|
||||
func (u *MongoTool) Find(model interface{}, filter bson.M, opts ...*options.FindOptions) error {
|
||||
if err := validInterfaceSlice(model); err != nil {
|
||||
return err
|
||||
}
|
||||
cur, err := u.FindCursor(filter, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleDbError(cur.All(u.ctx, model))
|
||||
}
|
||||
|
||||
// FindCursor 查询多条数据并返回游标。
|
||||
// Cursor 不是并发安全的,调用方必须在完成或失败后关闭它。
|
||||
func (u *MongoTool) FindCursor(filter bson.M, opts ...*options.FindOptions) (*mongo.Cursor, error) {
|
||||
for _, opt := range opts {
|
||||
if opt == nil {
|
||||
continue
|
||||
}
|
||||
if opt.Limit != nil && *opt.Limit > 1000 {
|
||||
fmt.Println("limit beyond 1000 ==================>", *opt.Limit)
|
||||
}
|
||||
if opt.Sort != nil {
|
||||
if err := sortCheck(opt.Sort); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return u.coll.Find(u.ctx, filter, opts...)
|
||||
}
|
||||
|
||||
// FindOne 单条查询
|
||||
func (u *MongoTool) FindOne(model interface{}, filter bson.M, opts ...*options.FindOneOptions) error {
|
||||
for _, opt := range opts {
|
||||
if opt.Sort != nil {
|
||||
if err := sortCheck(opt.Sort); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return handleDbError(u.coll.FindOne(u.ctx, filter, opts...).Decode(model))
|
||||
}
|
||||
|
||||
// FindOneAndDelete 单条查询并删除
|
||||
func (u *MongoTool) FindOneAndDelete(model interface{}, filter bson.M, opts ...*options.FindOneAndDeleteOptions) error {
|
||||
for _, opt := range opts {
|
||||
if opt.Sort != nil {
|
||||
if err := sortCheck(opt.Sort); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return handleDbError(u.coll.FindOneAndDelete(u.ctx, filter, opts...).Decode(model))
|
||||
}
|
||||
|
||||
// FindOneAndReplace 单条查询 rd set to Before 表示返回原始数据, set to After 表示返回替换后的数据
|
||||
func (u *MongoTool) FindOneAndReplace(model interface{}, filter bson.M, replacement bson.M, opts ...*options.FindOneAndReplaceOptions) error {
|
||||
for _, opt := range opts {
|
||||
if opt.Sort != nil {
|
||||
if err := sortCheck(opt.Sort); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return handleDbError(u.coll.FindOneAndReplace(u.ctx, filter, replacement, opts...).Decode(model))
|
||||
}
|
||||
|
||||
// FindOneAndUpdate 单条查询 rd set to Before 表示返回原始数据, set to After 表示返回更新后的数据 默认为返回更新后的数据
|
||||
func (u *MongoTool) FindOneAndUpdate(model interface{}, filter bson.M, update bson.M, opts ...*options.FindOneAndUpdateOptions) error {
|
||||
for _, opt := range opts {
|
||||
if opt.Sort != nil {
|
||||
if err := sortCheck(opt.Sort); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
setReturn := false
|
||||
for _, opt := range opts {
|
||||
if opt != nil && opt.ReturnDocument != nil {
|
||||
setReturn = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !setReturn {
|
||||
after := options.After
|
||||
opts = append(opts, &options.FindOneAndUpdateOptions{ReturnDocument: &after})
|
||||
}
|
||||
return handleDbError(u.coll.FindOneAndUpdate(u.ctx, filter, update, opts...).Decode(model))
|
||||
}
|
||||
|
||||
// FindOneAndUpsert 单条查询 匹配到数据更新,未匹配到数据则upsert
|
||||
func (u *MongoTool) FindOneAndUpsert(model interface{}, filter bson.M, update bson.M, opts ...*options.FindOneAndUpdateOptions) error {
|
||||
upsert := true
|
||||
var beforeOrAfter options.ReturnDocument
|
||||
for _, opt := range opts {
|
||||
if opt.Sort != nil {
|
||||
if err := sortCheck(opt.Sort); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
setReturn := false
|
||||
for _, opt := range opts {
|
||||
if opt != nil && opt.ReturnDocument != nil {
|
||||
beforeOrAfter = *opt.ReturnDocument
|
||||
setReturn = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !setReturn {
|
||||
beforeOrAfter = options.After
|
||||
}
|
||||
opts = append(opts, &options.FindOneAndUpdateOptions{ReturnDocument: &beforeOrAfter, Upsert: &upsert})
|
||||
return handleDbError(u.coll.FindOneAndUpdate(u.ctx, filter, update, opts...).Decode(model))
|
||||
}
|
||||
|
||||
func (u *MongoTool) FindOneAndUpdateReturnTiny(bind interface{}, query bson.M, update bson.M, afterDoc bool, opts ...*options.FindOneAndUpdateOptions) error {
|
||||
if afterDoc {
|
||||
opts = append(opts, options.FindOneAndUpdate().SetReturnDocument(options.After))
|
||||
} else {
|
||||
opts = append(opts, options.FindOneAndUpdate().SetReturnDocument(options.Before))
|
||||
}
|
||||
result := handleDbError(u.coll.FindOneAndUpdate(u.ctx, query, update, opts...).Decode(bind))
|
||||
return result
|
||||
}
|
||||
|
||||
// FindOneByID 通过id查找一条数据
|
||||
func (u *MongoTool) FindOneByID(model interface{}, id primitive.ObjectID, opts ...*options.FindOneOptions) error {
|
||||
for _, opt := range opts {
|
||||
if opt.Sort != nil {
|
||||
if err := sortCheck(opt.Sort); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return handleDbError(u.coll.FindOne(u.ctx, bson.M{"_id": id}, opts...).Decode(model))
|
||||
}
|
||||
|
||||
// Aggregate 聚合查找数据
|
||||
func (u *MongoTool) Aggregate(model interface{}, pipeline []bson.M, opts ...*options.AggregateOptions) error {
|
||||
if err := validInterfaceSlice(model); err != nil {
|
||||
return err
|
||||
}
|
||||
cur, err := u.coll.Aggregate(u.ctx, pipeline, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleDbError(cur.All(u.ctx, model))
|
||||
}
|
||||
|
||||
// AggregateDecode 聚合.Decode
|
||||
func (u *MongoTool) AggregateDecode(model interface{}, pipeline []bson.M, opts ...*options.AggregateOptions) error {
|
||||
cur, err := u.coll.Aggregate(u.ctx, pipeline, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cur.Next(u.ctx) {
|
||||
return handleDbError(cur.Decode(model))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Distinct 去重查询
|
||||
func (u *MongoTool) Distinct(fieldName string, filter bson.M, opts ...*options.DistinctOptions) ([]interface{}, error) {
|
||||
return u.coll.Distinct(u.ctx, fieldName, filter, opts...)
|
||||
}
|
||||
|
||||
// DeleteOne 删除一条数据
|
||||
func (u *MongoTool) DeleteOne(filter bson.M, opt ...*options.DeleteOptions) (*mongo.DeleteResult, error) {
|
||||
return u.coll.DeleteOne(u.ctx, filter, opt...)
|
||||
}
|
||||
|
||||
// DeleteMany 删除多条数据
|
||||
func (u *MongoTool) DeleteMany(filter bson.M, opt ...*options.DeleteOptions) (*mongo.DeleteResult, error) {
|
||||
return u.coll.DeleteMany(u.ctx, filter, opt...)
|
||||
}
|
||||
|
||||
// DeleteById 根据ID删除数据单条数据
|
||||
func (u *MongoTool) DeleteById(id primitive.ObjectID) (*mongo.DeleteResult, error) {
|
||||
return u.coll.DeleteOne(u.ctx, bson.M{"_id": id})
|
||||
}
|
||||
|
||||
// UpdateOne 更新单条数据
|
||||
func (u *MongoTool) UpdateOne(filter bson.M, update interface{}) (*mongo.UpdateResult, error) {
|
||||
return u.coll.UpdateOne(u.ctx, filter, update)
|
||||
}
|
||||
|
||||
// UpdateMany 修改多条数据
|
||||
func (u *MongoTool) UpdateMany(filter bson.M, update bson.M) (*mongo.UpdateResult, error) {
|
||||
return u.coll.UpdateMany(u.ctx, filter, update)
|
||||
}
|
||||
|
||||
// UpsertMany 或者修改或者插入多条数据
|
||||
func (u *MongoTool) UpsertMany(filter bson.M, update bson.M) (*mongo.UpdateResult, error) {
|
||||
return u.coll.UpdateMany(u.ctx, filter, update, options.Update().SetUpsert(true))
|
||||
}
|
||||
|
||||
// UpsertOne 或者修改或者插入一条数据
|
||||
func (u *MongoTool) UpsertOne(filter bson.M, update bson.M) (*mongo.UpdateResult, error) {
|
||||
return u.coll.UpdateOne(u.ctx, filter, update, options.Update().SetUpsert(true))
|
||||
}
|
||||
|
||||
// UpdateOneForSet 修改一条数据 【根据修改数据中集合类型字段】
|
||||
func (u *MongoTool) UpdateOneForSet(filter bson.M, update bson.D) (*mongo.UpdateResult, error) {
|
||||
return u.coll.UpdateOne(u.ctx, filter, update)
|
||||
}
|
||||
|
||||
// Count 获取数量
|
||||
func (u *MongoTool) Count(filter interface{}, opts ...*options.CountOptions) (int64, error) {
|
||||
if reflect.TypeOf(filter).Kind() == reflect.Slice {
|
||||
cur, err := u.coll.Aggregate(u.ctx, filter)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var count int64 = 0
|
||||
for cur.Next(context.TODO()) {
|
||||
count++
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
return u.coll.CountDocuments(u.ctx, filter, opts...)
|
||||
}
|
||||
|
||||
// EstimateCount 获取数量
|
||||
func (u *MongoTool) EstimateCount(opts ...*options.EstimatedDocumentCountOptions) (int64, error) {
|
||||
return u.coll.EstimatedDocumentCount(u.ctx, opts...)
|
||||
}
|
||||
|
||||
// Bulk Bulk
|
||||
func (u *MongoTool) Bulk(models []mongo.WriteModel, opts ...*options.BulkWriteOptions) (*mongo.BulkWriteResult, error) {
|
||||
return u.coll.BulkWrite(u.ctx, models, opts...)
|
||||
}
|
||||
|
||||
// Exists 是否存在数据
|
||||
func (u *MongoTool) Exists(filter interface{}, opts ...*options.FindOneOptions) (bool, error) {
|
||||
var limit int64 = 1
|
||||
lo := &options.CountOptions{Limit: &limit}
|
||||
n, err := u.Count(filter, lo)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return n > 0, nil
|
||||
}
|
||||
|
||||
func setTimeForSlice(docs interface{}) []interface{} {
|
||||
arr := reflect.ValueOf(docs)
|
||||
if arr.Kind() == reflect.Ptr {
|
||||
arr = reflect.ValueOf(docs).Elem()
|
||||
}
|
||||
result := make([]interface{}, arr.Len())
|
||||
for i := 0; i < arr.Len(); i++ {
|
||||
ele := arr.Index(i)
|
||||
now := time.Now()
|
||||
if ma := ele.FieldByName("UpdatedAt"); ma.IsValid() {
|
||||
ma.Set(reflect.ValueOf(now))
|
||||
}
|
||||
if ca := ele.FieldByName("CreatedAt"); ca.IsValid() {
|
||||
ca.Set(reflect.ValueOf(now))
|
||||
}
|
||||
result[i] = ele.Interface()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func validInterfaceSlice(bind interface{}) error {
|
||||
t := reflect.TypeOf(bind)
|
||||
k := t.Kind()
|
||||
if t.Kind() == reflect.Ptr {
|
||||
k = t.Elem().Kind()
|
||||
}
|
||||
if k != reflect.Slice {
|
||||
return stderr.MustSliceOrSlicePtr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/log"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/x/mongo/driver/connstring"
|
||||
)
|
||||
|
||||
var dataSource = make(map[string]*MongoDB)
|
||||
var registerPool []Register
|
||||
|
||||
// DBmap
|
||||
type DBmap struct {
|
||||
Key string
|
||||
URL string
|
||||
}
|
||||
|
||||
type Register struct {
|
||||
Key string
|
||||
Table []string
|
||||
}
|
||||
|
||||
// MongoOptions 数据库配置
|
||||
type MongoOptions struct {
|
||||
URL string `json:"url"` // 服务器连接地址
|
||||
}
|
||||
|
||||
// MongoDB MongoDB
|
||||
type MongoDB struct {
|
||||
db *mongo.Database
|
||||
}
|
||||
|
||||
func (m *MongoDB) Tool() *MongoTool {
|
||||
return m.ToolCtx(context.Background())
|
||||
}
|
||||
|
||||
func (m *MongoDB) ToolCtx(ctx context.Context) *MongoTool {
|
||||
return &MongoTool{db: m.db, ctx: ctx}
|
||||
}
|
||||
|
||||
// Coll 获取表名
|
||||
func (m *MongoDB) Coll(name string) *MongoTool {
|
||||
t := m.Tool()
|
||||
return t.Coll(name)
|
||||
}
|
||||
|
||||
// CollCtx 获取表名 从外部传入ctx
|
||||
func (m *MongoDB) CollCtx(ctx context.Context, name string) *MongoTool {
|
||||
t := m.ToolCtx(ctx)
|
||||
return t.Coll(name)
|
||||
}
|
||||
|
||||
// MongoTool mongo官方库事务封装
|
||||
type MongoTool struct {
|
||||
db *mongo.Database
|
||||
ctx context.Context // 当前使用的ctx
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (m *MongoTool) Coll(name string) *MongoTool {
|
||||
opt := options.CollectionOptions{Registry: registry}
|
||||
m.coll = m.db.Collection(name, &opt)
|
||||
return m
|
||||
}
|
||||
|
||||
// Trans 开启事务处理包裹处理,里面处理的全是利用的事务的ctx
|
||||
func (m *MongoDB) Trans(fn func(*MongoTool) error, opts ...*TransOpts) error {
|
||||
return m.TransCtx(context.Background(), fn, opts...)
|
||||
}
|
||||
|
||||
// TransCtx 外部传入ctx
|
||||
func (m *MongoDB) TransCtx(ctx context.Context, fn func(*MongoTool) error, opts ...*TransOpts) error {
|
||||
t := m.ToolCtx(ctx)
|
||||
return m.db.Client().UseSession(ctx, func(sessionContext mongo.SessionContext) error {
|
||||
if err := sessionContext.StartTransaction(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Warn(fmt.Sprintf("caught panic during transaction, aborting. err: %+v, stack: %s", err, debug.Stack()))
|
||||
if err := sessionContext.AbortTransaction(sessionContext); err != nil {
|
||||
log.Warn("mongo AbortTransaction panic err", log.E(err))
|
||||
}
|
||||
}
|
||||
sessionContext.EndSession(sessionContext)
|
||||
}()
|
||||
t.ctx = sessionContext
|
||||
if err := runTransactionWithRetry(t, fn, MergeTransOpts(opts)); err != nil {
|
||||
if strings.Contains(err.Error(), "NoSuchTransaction") {
|
||||
log.Warn("NoSuchTransaction error, return")
|
||||
return err
|
||||
}
|
||||
if err := sessionContext.AbortTransaction(sessionContext); err != nil {
|
||||
log.Warn("mongo AbortTransaction err", log.E(err))
|
||||
}
|
||||
log.Warn("caught exception during transaction, aborting.", log.E(err))
|
||||
sessionContext.EndSession(sessionContext)
|
||||
return err
|
||||
}
|
||||
return commitWithRetry(sessionContext)
|
||||
})
|
||||
}
|
||||
|
||||
// runTransactionWithRetry is an example function demonstrating transaction retry logic.
|
||||
func runTransactionWithRetry(t *MongoTool, txnFn func(t *MongoTool) error, opts *TransOpts) error {
|
||||
//no set ReEntryCount is loop retry
|
||||
for {
|
||||
err := txnFn(t) // Performs transaction.
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
log.Warn("Transaction aborted. Caught exception during transaction.", log.E(err))
|
||||
// If transient error, retry the whole transaction
|
||||
if strings.Contains(err.Error(), "NoSuchTransaction") {
|
||||
log.Info("NoSuchTransaction error,return and break retry loop")
|
||||
return err
|
||||
}
|
||||
cmdErr, ok := err.(mongo.CommandError)
|
||||
if ok && cmdErr.HasErrorLabel("TransientTransactionError") {
|
||||
if opts != nil {
|
||||
if opts.ReEntryCount != nil {
|
||||
if *opts.ReEntryCount <= 0 {
|
||||
return cmdErr
|
||||
}
|
||||
*opts.ReEntryCount--
|
||||
}
|
||||
}
|
||||
log.Info("TransientTransactionError, retrying transaction...")
|
||||
continue
|
||||
}
|
||||
// else return err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// commitWithRetry is an example function demonstrating transaction retry logic.
|
||||
func commitWithRetry(sess mongo.SessionContext) error {
|
||||
for {
|
||||
err := sess.CommitTransaction(sess)
|
||||
switch e := err.(type) {
|
||||
case nil:
|
||||
log.Info("Transaction committed.")
|
||||
return nil
|
||||
case mongo.CommandError:
|
||||
// Can retry commit
|
||||
if e.HasErrorLabel("UnknownTransactionCommitResult") {
|
||||
log.Info("UnknownTransactionCommitResult, retrying commit operation...")
|
||||
continue
|
||||
}
|
||||
log.Info("Error during commit...")
|
||||
return e
|
||||
default:
|
||||
log.Info("Error during commit...")
|
||||
return e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect 关闭数据库连接
|
||||
func (m *MongoDB) Disconnect() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
log.Info("closing mongodb connections")
|
||||
defer cancel()
|
||||
if err := m.db.Client().Disconnect(ctx); err != nil {
|
||||
log.Warn(fmt.Sprintf("close mongo connections err: %+v", err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitMongo 初始化Mongo
|
||||
func InitMongo(murl string) (*MongoDB, error) {
|
||||
cs, err := connstring.Parse(murl)
|
||||
if err != nil {
|
||||
log.Error("mongo URL parse fail", log.Any("url", murl), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
db := cs.Database
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
// Connect to MongoDB
|
||||
mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(murl))
|
||||
if err != nil {
|
||||
log.Error("mongodb connect fail", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
// Check the connection
|
||||
if err = mongoClient.Ping(context.Background(), nil); err != nil {
|
||||
log.Error("mongodb connect ping is fail")
|
||||
return nil, err
|
||||
}
|
||||
mongoDataBase := mongoClient.Database(db)
|
||||
return &MongoDB{db: mongoDataBase}, nil
|
||||
}
|
||||
|
||||
func InitDS(dbmap []DBmap, register []Register) map[string]*MongoDB {
|
||||
if len(dbmap) == 0 {
|
||||
log.Error("dbmap must be not empty ")
|
||||
os.Exit(1)
|
||||
}
|
||||
for _, v := range dbmap {
|
||||
db, err := InitMongo(v.URL)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("[DB-%s] start up error", v.Key), log.E(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Info(fmt.Sprintf("[DB-%s] connect is successfully", v.Key))
|
||||
dataSource[v.Key] = db
|
||||
}
|
||||
registerPool = register
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func Init(tableName string) *MongoDB {
|
||||
if tableName == "" {
|
||||
panic(errors.New("table name must not be empty"))
|
||||
}
|
||||
return Selector(tableName)
|
||||
}
|
||||
|
||||
// 初始化只读数据
|
||||
func InitRead(tableName string) *MongoDB {
|
||||
if tableName == "" {
|
||||
panic(errors.New("table name must not be empty"))
|
||||
}
|
||||
return SelectorRead(tableName)
|
||||
}
|
||||
|
||||
func CloseDS() {
|
||||
if len(dataSource) == 0 {
|
||||
log.Warn("dataSource empty ")
|
||||
return
|
||||
}
|
||||
for k, v := range dataSource {
|
||||
if err := v.Disconnect(); err != nil {
|
||||
log.Error(fmt.Sprintf("[DB-%s]Mongo Disconnect error", k), log.E(err))
|
||||
continue
|
||||
}
|
||||
log.Info(fmt.Sprintf("[DB-%s]Mongo Disconnect OK", k))
|
||||
}
|
||||
}
|
||||
|
||||
// BaseDAO 如果ctx为nil 则表示不使用事务,如果ctx不为空则表示使用事务
|
||||
func BaseDAO(tableName string, ctx context.Context) *MongoTool {
|
||||
if tableName == "" {
|
||||
panic(errors.New("table name must not be empty"))
|
||||
}
|
||||
mongdb := Selector(tableName)
|
||||
if ctx == nil {
|
||||
return &MongoTool{db: mongdb.db, ctx: context.Background(), coll: mongdb.db.Collection(tableName)}
|
||||
}
|
||||
return &MongoTool{db: mongdb.db, ctx: ctx, coll: mongdb.db.Collection(tableName)}
|
||||
}
|
||||
|
||||
func Selector(tableName string) *MongoDB {
|
||||
var isExist = false
|
||||
var db *MongoDB
|
||||
for _, r := range registerPool {
|
||||
if strings.HasPrefix(r.Key, "Read") {
|
||||
continue
|
||||
}
|
||||
for _, v := range r.Table {
|
||||
if v == tableName {
|
||||
isExist = true
|
||||
db = dataSource[r.Key]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !isExist {
|
||||
log.Warn("current table not register,please register it first", log.Any("table", tableName))
|
||||
panic(errors.New("current table not register"))
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func SelectorRead(tableName string) *MongoDB {
|
||||
var isExist = false
|
||||
var db *MongoDB
|
||||
for _, r := range registerPool {
|
||||
if !strings.HasPrefix(r.Key, "Read") {
|
||||
continue
|
||||
}
|
||||
for _, v := range r.Table {
|
||||
if v == tableName {
|
||||
isExist = true
|
||||
db = dataSource[r.Key]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !isExist {
|
||||
log.Warn("current table not register,please register it first", log.Any("table", tableName))
|
||||
panic(errors.New("current table not register"))
|
||||
}
|
||||
return db
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package db
|
||||
|
||||
// 事务设置
|
||||
type TransOpts struct {
|
||||
ReEntryCount *int //重入次数
|
||||
//...
|
||||
}
|
||||
|
||||
func (t *TransOpts) SetReEntry(count int) *TransOpts {
|
||||
t.ReEntryCount = &count
|
||||
return t
|
||||
}
|
||||
|
||||
// MergeTransOpts 合并事务设置
|
||||
func MergeTransOpts(opts []*TransOpts) *TransOpts {
|
||||
transOpts := &TransOpts{}
|
||||
for _, opt := range opts {
|
||||
if opt == nil {
|
||||
continue
|
||||
}
|
||||
if opt.ReEntryCount != nil {
|
||||
transOpts.ReEntryCount = opt.ReEntryCount
|
||||
//...
|
||||
}
|
||||
}
|
||||
return transOpts
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package common
|
||||
|
||||
import "math/rand"
|
||||
|
||||
// CanDeductByProbability 扣量指标的未完成度和今日时间进度递增 能扣量的概率递增 [0,100]
|
||||
func CanDeductByProbability(probability float64) bool {
|
||||
if probability <= 0 {
|
||||
return false
|
||||
}
|
||||
if probability == 0 {
|
||||
return false
|
||||
}
|
||||
if probability >= 1 {
|
||||
return true //绝对扣量
|
||||
}
|
||||
canDeduct := probability > rand.Float64() //根据 probability 概率决定是否扣量
|
||||
return canDeduct
|
||||
}
|
||||
|
||||
// ConDeductionRatio 格式:%
|
||||
func ConsumeDeductionRatio(optDividendRatio, dividendRatio float64) float64 {
|
||||
if optDividendRatio == 0 {
|
||||
return dividendRatio
|
||||
}
|
||||
return (1 - dividendRatio/optDividendRatio)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package dramatopic
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/models/cache/sysconfdata"
|
||||
"91porn-server/models/v/moduleconfmod"
|
||||
"91porn-server/models/v/sysconfmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const (
|
||||
TypeSystem = "SYSTEM"
|
||||
TypeCustom = "CUSTOM"
|
||||
|
||||
IDHot = "hot_recommend"
|
||||
IDLatest = "latest"
|
||||
IDEveryone = "everyone_likes"
|
||||
|
||||
KeyHot = "HOT_RECOMMEND"
|
||||
KeyLatest = "LATEST"
|
||||
KeyEveryone = "EVERYONE_LIKES"
|
||||
)
|
||||
|
||||
type SystemTopic struct {
|
||||
ID string
|
||||
Name string
|
||||
SystemKey string
|
||||
Sort int
|
||||
VCode sysconfmod.VCode
|
||||
TieOrder int
|
||||
}
|
||||
|
||||
func SystemTopics() []SystemTopic {
|
||||
defaults := map[sysconfmod.VCode]int{
|
||||
sysconfmod.VCodeDramaTopicHotSort: 500,
|
||||
sysconfmod.VCodeDramaTopicLatestSort: 400,
|
||||
sysconfmod.VCodeDramaTopicEveryoneSort: 300,
|
||||
}
|
||||
values, err := sysconfdata.GetIntsFromSharedCache(defaults)
|
||||
if err != nil {
|
||||
values = defaults
|
||||
}
|
||||
topics := []SystemTopic{
|
||||
{ID: IDHot, Name: "热门推荐", SystemKey: KeyHot, Sort: values[sysconfmod.VCodeDramaTopicHotSort], VCode: sysconfmod.VCodeDramaTopicHotSort, TieOrder: 3},
|
||||
{ID: IDLatest, Name: "最新上架", SystemKey: KeyLatest, Sort: values[sysconfmod.VCodeDramaTopicLatestSort], VCode: sysconfmod.VCodeDramaTopicLatestSort, TieOrder: 2},
|
||||
{ID: IDEveryone, Name: "大家爱看", SystemKey: KeyEveryone, Sort: values[sysconfmod.VCodeDramaTopicEveryoneSort], VCode: sysconfmod.VCodeDramaTopicEveryoneSort, TieOrder: 1},
|
||||
}
|
||||
return topics
|
||||
}
|
||||
|
||||
func FindSystem(id string) (SystemTopic, bool) {
|
||||
for _, topic := range SystemTopics() {
|
||||
if topic.ID == id {
|
||||
return topic, true
|
||||
}
|
||||
}
|
||||
return SystemTopic{}, false
|
||||
}
|
||||
|
||||
// ModuleID returns the configured hot-drama module that owns custom topics.
|
||||
// It follows the same compatibility fallback as the short-drama channel.
|
||||
func ModuleID(now time.Time) (primitive.ObjectID, bool, error) {
|
||||
modules, err := moduleconfmod.GetModuleConfByType(moduleconfmod.Drama)
|
||||
if err != nil {
|
||||
return primitive.NilObjectID, false, err
|
||||
}
|
||||
active := make([]moduleconfmod.ModuleConf, 0, len(modules))
|
||||
for _, module := range modules {
|
||||
if module.IsActiveAt(now) {
|
||||
active = append(active, module)
|
||||
}
|
||||
}
|
||||
for _, module := range active {
|
||||
name := strings.ToLower(strings.TrimSpace(module.ModuleName + " " + module.SubModuleName))
|
||||
if strings.Contains(name, "热门") || strings.Contains(name, "hot") {
|
||||
return module.ID, true, nil
|
||||
}
|
||||
}
|
||||
if len(active) > 1 {
|
||||
return active[1].ID, true, nil
|
||||
}
|
||||
if len(active) == 1 {
|
||||
return active[0].ID, true, nil
|
||||
}
|
||||
return primitive.NilObjectID, false, nil
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package elastic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/elastic/go-elasticsearch/v8"
|
||||
)
|
||||
|
||||
var (
|
||||
AnalyzerIkSmart = "ik_smart"
|
||||
AnalyzerIkMaxWord = "ik_max_word"
|
||||
NumberOfShards = 1
|
||||
NumberOfReplicas = 1
|
||||
)
|
||||
|
||||
// Client
|
||||
type Client struct {
|
||||
Client *elasticsearch.Client
|
||||
}
|
||||
|
||||
func unmarshalAggregateBody(bind interface{}, resp *Response) error {
|
||||
var v map[string]interface{}
|
||||
if bind == nil { //传入空表示只执行 defer
|
||||
return nil
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(v) > 0 {
|
||||
if v, ok := v["aggregations"].(map[string]interface{}); ok {
|
||||
b, _ := json.Marshal(v)
|
||||
_ = json.Unmarshal(b, &bind)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalBodyWithTotal(bind interface{}, resp *Response) error {
|
||||
var v map[string]interface{}
|
||||
if bind == nil { //传入空表示只执行 defer
|
||||
return nil
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(v) > 0 {
|
||||
if h, ok := v["hits"].(map[string]interface{}); ok {
|
||||
b, err := json.Marshal(h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = json.Unmarshal(b, &bind); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalBody(bind interface{}, resp *Response) error {
|
||||
var v map[string]interface{}
|
||||
if bind == nil { //传入空表示只执行 defer
|
||||
return nil
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(v) > 0 {
|
||||
if h, ok := v["hits"].(map[string]interface{}); ok {
|
||||
if hh, ok := h["hits"].([]interface{}); ok {
|
||||
b, _ := json.Marshal(hh)
|
||||
_ = json.Unmarshal(b, &bind)
|
||||
}
|
||||
} else if _, ok := v["docs"].([]interface{}); ok {
|
||||
b, _ := json.Marshal(v["docs"])
|
||||
_ = json.Unmarshal(b, &bind)
|
||||
} else if _, ok := v["_source"].(map[string]interface{}); ok {
|
||||
b, _ := json.Marshal(v)
|
||||
_ = json.Unmarshal(b, &bind)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalSearchM(query M) io.Reader {
|
||||
var buf bytes.Buffer
|
||||
if len(query) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := json.NewEncoder(&buf).Encode(query); err != nil {
|
||||
return nil
|
||||
}
|
||||
return bytes.NewReader(buf.Bytes())
|
||||
}
|
||||
|
||||
func marshalBulkM(source M) io.Reader {
|
||||
var buf bytes.Buffer
|
||||
for k, v := range source {
|
||||
var meta = []byte(fmt.Sprintf(`{ "index" : { "_id" : "%s" } }%s`, k, "\n"))
|
||||
var data, _ = json.Marshal(v)
|
||||
data = append(data, "\n"...)
|
||||
buf.Grow(len(meta) + len(data))
|
||||
buf.Write(meta)
|
||||
buf.Write(data)
|
||||
}
|
||||
return bytes.NewReader(buf.Bytes())
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package elastic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/log"
|
||||
|
||||
"github.com/elastic/go-elasticsearch/v8/esapi"
|
||||
)
|
||||
|
||||
type M map[string]interface{}
|
||||
|
||||
type A []map[string]interface{}
|
||||
|
||||
type MGetBody struct {
|
||||
ID string `json:"_id"`
|
||||
}
|
||||
|
||||
// ElasticResp 响应
|
||||
type Response = esapi.Response
|
||||
|
||||
type Options struct {
|
||||
Address []string
|
||||
MaxIdleConnsPerHost int //每个client z最多允许的空闲连接数
|
||||
IdleConnTimeout time.Duration //空闲连接超时时间
|
||||
UserName string
|
||||
PassWord string
|
||||
}
|
||||
|
||||
// 查看集群信息
|
||||
func (c *Client) Info() (*Response, error) {
|
||||
resp, err := c.Client.Info(c.Client.Info.WithContext(context.Background()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// 初始化索引
|
||||
func (c *Client) CreateIndices(index string, setting M) error {
|
||||
resp, err := c.Client.Indices.Create(index, func(request *esapi.IndicesCreateRequest) {
|
||||
request.Body = marshalSearchM(setting)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// search 搜索
|
||||
func (c *Client) Search(index string, bind interface{}, query M, o ...func(*esapi.SearchRequest)) error {
|
||||
o = append(o,
|
||||
c.Client.Search.WithContext(context.Background()),
|
||||
c.Client.Search.WithIndex(index),
|
||||
c.Client.Search.WithBody(marshalSearchM(query)),
|
||||
c.Client.Search.WithTrackTotalHits(true),
|
||||
c.Client.Search.WithPretty(),
|
||||
)
|
||||
resp, err := c.Client.Search(o...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return unmarshalBody(bind, resp)
|
||||
}
|
||||
|
||||
func (c *Client) SearchWithTotal(index string, bind interface{}, query M, o ...func(*esapi.SearchRequest)) error {
|
||||
o = append(o,
|
||||
c.Client.Search.WithContext(context.Background()),
|
||||
c.Client.Search.WithIndex(index),
|
||||
c.Client.Search.WithBody(marshalSearchM(query)),
|
||||
c.Client.Search.WithTrackTotalHits(true),
|
||||
c.Client.Search.WithPretty(),
|
||||
)
|
||||
resp, err := c.Client.Search(o...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return unmarshalBodyWithTotal(bind, resp)
|
||||
}
|
||||
|
||||
// Aggregate Aggregate 聚合
|
||||
func (c *Client) Aggregate(index string, bind interface{}, query M, o ...func(*esapi.SearchRequest)) (int, error) {
|
||||
o = append(o,
|
||||
c.Client.Search.WithContext(context.Background()),
|
||||
c.Client.Search.WithIndex(index),
|
||||
c.Client.Search.WithBody(marshalSearchM(query)),
|
||||
c.Client.Search.WithTrackTotalHits(true),
|
||||
c.Client.Search.WithPretty(),
|
||||
)
|
||||
resp, err := c.Client.Search(o...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.StatusCode, unmarshalAggregateBody(bind, resp)
|
||||
}
|
||||
|
||||
// Get 根据ID搜索
|
||||
func (c *Client) Get(index string, bind interface{}, id string, o ...func(*esapi.GetRequest)) (int, error) {
|
||||
o = append(o,
|
||||
c.Client.Get.WithContext(context.Background()),
|
||||
c.Client.Get.WithPretty(),
|
||||
)
|
||||
resp, err := c.Client.Get(index, id, o...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.StatusCode, unmarshalBody(bind, resp)
|
||||
}
|
||||
|
||||
// MGet MGet 批量查询
|
||||
func (c *Client) MGet(index string, bind interface{}, ids []string, o ...func(*esapi.MgetRequest)) error {
|
||||
m := make([]MGetBody, len(ids))
|
||||
for i, v := range ids {
|
||||
m[i] = MGetBody{ID: v}
|
||||
}
|
||||
o = append(o,
|
||||
c.Client.Mget.WithIndex(index),
|
||||
c.Client.Mget.WithContext(context.Background()),
|
||||
)
|
||||
resp, err := c.Client.Mget(marshalSearchM(M{"docs": m}), o...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%+v", resp)
|
||||
}
|
||||
return unmarshalBody(bind, resp)
|
||||
}
|
||||
|
||||
// BulkDelete 函数
|
||||
func (c *Client) BulkDelete(index string, source M, o ...func(*esapi.BulkRequest)) error {
|
||||
o = append(o,
|
||||
c.Client.Bulk.WithIndex(index),
|
||||
c.Client.Bulk.WithContext(context.Background()),
|
||||
c.Client.Bulk.WithRefresh("true"),
|
||||
)
|
||||
resp, err := c.Client.Bulk(marshalDeleteBulkM(source), o...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.IsError() {
|
||||
return fmt.Errorf("error response: %s", resp.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalDeleteBulkM(source M) io.Reader {
|
||||
var buf bytes.Buffer
|
||||
for k, _ := range source {
|
||||
var meta = []byte(fmt.Sprintf(`{ "delete" : { "_id" : "%s" } }%s`, k, "\n"))
|
||||
buf.Grow(len(meta))
|
||||
buf.Write(meta)
|
||||
}
|
||||
return bytes.NewReader(buf.Bytes())
|
||||
}
|
||||
|
||||
// 批量插入
|
||||
func (c *Client) Bulk(index string, source M, o ...func(*esapi.BulkRequest)) error {
|
||||
o = append(o,
|
||||
c.Client.Bulk.WithIndex(index),
|
||||
c.Client.Bulk.WithContext(context.Background()),
|
||||
)
|
||||
resp, err := c.Client.Bulk(marshalBulkM(source), o...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%+v", resp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BulkChecked 在 HTTP 成功后继续检查每条写入结果,供需要可靠推进同步进度的任务使用。
|
||||
func (c *Client) BulkChecked(index string, source M) error {
|
||||
if len(source) == 0 {
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
resp, err := c.Client.Bulk(marshalBulkM(source),
|
||||
c.Client.Bulk.WithIndex(index), c.Client.Bulk.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bulk %s: HTTP %d", index, resp.StatusCode)
|
||||
}
|
||||
var result struct {
|
||||
Errors bool `json:"errors"`
|
||||
Items []map[string]struct {
|
||||
ID string `json:"_id"`
|
||||
Status int `json:"status"`
|
||||
Error json.RawMessage `json:"error"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return fmt.Errorf("decode bulk %s response: %w", index, err)
|
||||
}
|
||||
if len(result.Items) != len(source) {
|
||||
return fmt.Errorf("bulk %s: expected %d results, got %d", index, len(source), len(result.Items))
|
||||
}
|
||||
for _, item := range result.Items {
|
||||
entry, ok := item["index"]
|
||||
if !ok || len(item) != 1 {
|
||||
return fmt.Errorf("bulk %s: missing index result", index)
|
||||
}
|
||||
if entry.Status < 200 || entry.Status >= 300 || (len(entry.Error) > 0 && string(entry.Error) != "null") {
|
||||
return fmt.Errorf("bulk %s: document %s failed, status %d", index, entry.ID, entry.Status)
|
||||
}
|
||||
}
|
||||
if result.Errors {
|
||||
return fmt.Errorf("bulk %s: response contains errors", index)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Count 获取数量
|
||||
func (c *Client) Count(index string, query M, o ...func(*esapi.CountRequest)) (cnt int, err error) {
|
||||
o = append(o,
|
||||
c.Client.Count.WithIndex(index),
|
||||
c.Client.Count.WithBody(marshalSearchM(query)),
|
||||
c.Client.Count.WithContext(context.Background()),
|
||||
)
|
||||
resp, err := c.Client.Count(o...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return 0, fmt.Errorf("%+v", resp)
|
||||
}
|
||||
var bind struct {
|
||||
Count int `json:"count"`
|
||||
}
|
||||
return bind.Count, unmarshalBody(&bind, resp)
|
||||
}
|
||||
|
||||
func (c *Client) Ping() error {
|
||||
resp, err := c.Client.Ping(c.Client.Ping.WithContext(context.Background()))
|
||||
if err != nil {
|
||||
log.Warn("elasticSarch ping error ", log.E(err))
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Warn("elasticSarch ping failed ", log.Any("status code", resp.StatusCode))
|
||||
return errors.New("elasticSarch ping failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package elastic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"91porn-server/common/log"
|
||||
|
||||
"github.com/elastic/go-elasticsearch/v8"
|
||||
)
|
||||
|
||||
var esClient *Client
|
||||
|
||||
// Client 获取elastic客户端
|
||||
func InitElastic(opt Options) (*Client, error) {
|
||||
if len(opt.Address) == 0 {
|
||||
return nil, errors.New("elasticSearch address is empty")
|
||||
}
|
||||
cfg := elasticsearch.Config{
|
||||
Addresses: opt.Address,
|
||||
Username: opt.UserName,
|
||||
Password: opt.PassWord,
|
||||
}
|
||||
es, err := elasticsearch.NewClient(cfg)
|
||||
if err != nil {
|
||||
log.Error("create elasticSearch client occour error", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
esClient = &Client{Client: es}
|
||||
if err = esClient.Ping(); err != nil {
|
||||
log.Warn("elasticSarch ping failed", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
log.Info("init elasticSearch successful")
|
||||
return esClient, nil
|
||||
}
|
||||
|
||||
func Init() *Client {
|
||||
return esClient
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ses"
|
||||
"github.com/pkg/errors"
|
||||
gomail "gopkg.in/gomail.v2"
|
||||
)
|
||||
|
||||
// Send sends email without attachments.
|
||||
func (c *EmailClient) Send(ctx context.Context, sender string, mailList []*string, title string, body string) error {
|
||||
if err := check(sender, mailList); err != nil {
|
||||
return err
|
||||
}
|
||||
sesEmailInput := &ses.SendEmailInput{
|
||||
Destination: &ses.Destination{
|
||||
ToAddresses: mailList,
|
||||
},
|
||||
Message: &ses.Message{
|
||||
Body: &ses.Body{
|
||||
Html: &ses.Content{
|
||||
Data: aws.String(body)},
|
||||
},
|
||||
Subject: &ses.Content{
|
||||
Data: aws.String(title),
|
||||
},
|
||||
},
|
||||
Source: aws.String(sender),
|
||||
}
|
||||
if _, err := c.ses.SendEmail(sesEmailInput); err != nil {
|
||||
return errors.Wrap(err, "send email failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendRaw send email that supports attachments.
|
||||
func (c *EmailClient) SendRaw(ctx context.Context, sender string, mailList []*string, title string, body string, attachments []string) error {
|
||||
if err := check(sender, mailList); err != nil {
|
||||
return err
|
||||
}
|
||||
msg := gomail.NewMessage(gomail.SetCharset("UTF-8"))
|
||||
msg.SetHeader("From", sender)
|
||||
toList := make([]string, len(mailList))
|
||||
for i, l := range mailList {
|
||||
toList[i] = *l
|
||||
}
|
||||
msg.SetHeader("To", toList...)
|
||||
msg.SetHeader("Subject", title)
|
||||
msg.SetBody("text/html", body)
|
||||
for _, a := range attachments {
|
||||
msg.Attach(a)
|
||||
}
|
||||
var emailRaw bytes.Buffer
|
||||
_, _ = msg.WriteTo(&emailRaw)
|
||||
if _, err := c.ses.SendRawEmail(&ses.SendRawEmailInput{
|
||||
RawMessage: &ses.RawMessage{Data: emailRaw.Bytes()},
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "send raw email failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func check(sender string, mailList []*string) error {
|
||||
if sender == "" {
|
||||
return errors.New("no sender")
|
||||
}
|
||||
if len(mailList) == 0 {
|
||||
return errors.New("no recipient")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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/ses"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Region string `json:"region"` // aws region
|
||||
AccessKeyID string `json:"accessKeyID"` // aws accessKeyID
|
||||
SecretAccessKey string `json:"secretAccessKey"` // aws secretAccessKey
|
||||
VerifiedDomain string `json:"verifiedDomain"` // aws verifiedDomain. The sender's email must in this domain.
|
||||
}
|
||||
|
||||
type EmailClient struct {
|
||||
ses *ses.SES
|
||||
}
|
||||
|
||||
var Client *EmailClient
|
||||
|
||||
// MustInit 初始化ses连接
|
||||
func MustInit(ctx context.Context, cfg Config) error {
|
||||
session, err := session.NewSession(&aws.Config{
|
||||
Region: &cfg.Region,
|
||||
Credentials: credentials.NewStaticCredentials(cfg.AccessKeyID, cfg.SecretAccessKey, ""),
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Client = &EmailClient{}
|
||||
Client.ses = ses.New(session)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package imad
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
ListBanner = "IM_LIST_BANNER"
|
||||
ListFeedNative = "IM_LIST_FEED_NATIVE"
|
||||
ChatNotificationBar = "IM_CHAT_NOTIFICATION_BAR"
|
||||
ChatFloatingGifBall = "IM_CHAT_FLOATING_GIF_BALL"
|
||||
)
|
||||
|
||||
// IsPositionCode reports whether code is an IM ad-center location identifier.
|
||||
func IsPositionCode(code string) bool {
|
||||
return strings.HasPrefix(strings.TrimSpace(code), "IM_")
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/tealeg/xlsx"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func ExportFilePath(fileName string) string {
|
||||
var path, _ = filepath.Abs("temp")
|
||||
return path + "/" + fileName
|
||||
}
|
||||
|
||||
func ExportExcel(sheeters []ExcelSheeter, filePath string) error {
|
||||
f := xlsx.NewFile()
|
||||
for _, sheeter := range sheeters {
|
||||
sheet, _ := f.AddSheet(sheeter.GetSheetName())
|
||||
for _, rower := range sheeter.GetRows() {
|
||||
row := sheet.AddRow()
|
||||
for _, cell := range rower.GetCells() {
|
||||
c := row.AddCell()
|
||||
c.Value = getValue(reflect.TypeOf(cell).Kind(), reflect.ValueOf(cell))
|
||||
}
|
||||
}
|
||||
}
|
||||
return f.Save(filePath)
|
||||
}
|
||||
|
||||
func getValue(kind reflect.Kind, value reflect.Value) string {
|
||||
switch kind {
|
||||
case reflect.String:
|
||||
return value.String()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return strconv.FormatInt(value.Int(), 10)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return strconv.FormatUint(value.Uint(), 10)
|
||||
case reflect.Bool:
|
||||
return strconv.FormatBool(value.Bool())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return strconv.FormatFloat(value.Float(), 'f', 2, 64)
|
||||
case reflect.Struct:
|
||||
if timeV, ok := value.Interface().(time.Time); ok {
|
||||
return timeV.String()
|
||||
}
|
||||
if oid, ok := value.Interface().(primitive.ObjectID); ok {
|
||||
return oid.Hex()
|
||||
}
|
||||
return ""
|
||||
case reflect.Interface:
|
||||
if timeV, ok := value.Interface().(time.Time); ok {
|
||||
return timeV.String()
|
||||
}
|
||||
return ""
|
||||
case reflect.Ptr:
|
||||
if value.IsNil() {
|
||||
return ""
|
||||
}
|
||||
value = value.Elem()
|
||||
if value.Interface() != nil {
|
||||
if timeV, ok := value.Interface().(time.Time); ok {
|
||||
return timeV.String()
|
||||
}
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
case reflect.Array:
|
||||
if id, ok := value.Interface().(primitive.ObjectID); ok {
|
||||
if id.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return id.Hex()
|
||||
}
|
||||
}
|
||||
return "未知"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package export
|
||||
|
||||
type Rower interface {
|
||||
GetCells() []interface{}
|
||||
}
|
||||
|
||||
type ExcelSheeter interface {
|
||||
GetSheetName() string
|
||||
GetRows() []Rower
|
||||
}
|
||||
|
||||
type TitleSlice []string
|
||||
|
||||
func (t TitleSlice) GetCells() []interface{} {
|
||||
list := []interface{}{}
|
||||
for _, v := range t {
|
||||
list = append(list, v)
|
||||
}
|
||||
return list
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// 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 MakeDir(name string) error {
|
||||
if err := os.MkdirAll(name, 0755); err != nil {
|
||||
return errors.New("make dir wrong")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteFile(data []byte, filename string) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = os.Remove(filename)
|
||||
}
|
||||
}()
|
||||
pi := GetPathInfo(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
|
||||
}
|
||||
|
||||
// WriteStruct2Xlsx 将struct切片写入Excel sheet
|
||||
func WriteStruct2Xlsx(sheet string, records interface{}) *excelize.File {
|
||||
xlsx := excelize.NewFile() // new file
|
||||
index, _ := xlsx.NewSheet(sheet) // new sheet
|
||||
xlsx.SetActiveSheet(index) // set active (default) sheet
|
||||
t := reflect.TypeOf(records)
|
||||
if t.Kind() != reflect.Slice {
|
||||
panic("records must be slice")
|
||||
}
|
||||
s := reflect.ValueOf(records)
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
elem := s.Index(i).Interface()
|
||||
elemType := reflect.TypeOf(elem)
|
||||
elemValue := reflect.ValueOf(elem)
|
||||
if elemType.Kind() == reflect.Ptr {
|
||||
elemType = elemType.Elem()
|
||||
elemValue = elemValue.Elem()
|
||||
}
|
||||
if elemType.Kind() != reflect.Struct {
|
||||
panic("record in slice must be a struct")
|
||||
}
|
||||
k := 0
|
||||
for j := 0; j < elemType.NumField(); j++ {
|
||||
field := elemType.Field(j)
|
||||
tag := field.Tag.Get("xlsx")
|
||||
if tag == "-" {
|
||||
continue
|
||||
}
|
||||
if tag == "" {
|
||||
tag = field.Name
|
||||
}
|
||||
column, _ := excelize.ColumnNumberToName(k + 1)
|
||||
k++
|
||||
name := tag
|
||||
// 设置表头
|
||||
if i == 0 {
|
||||
_ = xlsx.SetCellValue(sheet, fmt.Sprintf("%s%d", column, i+1), name)
|
||||
}
|
||||
// 设置内容
|
||||
_ = xlsx.SetCellValue(sheet, fmt.Sprintf("%s%d", column, i+2), elemValue.Field(j).Interface())
|
||||
}
|
||||
}
|
||||
return xlsx
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PathInfo 路径信息
|
||||
type PathInfo struct {
|
||||
Dir string // 目录
|
||||
Ext string // 扩展名
|
||||
FileName string // 没有扩展名的文件名名称
|
||||
FullFileName string // 文件全名
|
||||
}
|
||||
|
||||
// GetPathInfo 解析路径
|
||||
// 返回文件,路径,文件名等信息
|
||||
func GetPathInfo(path string) (pi PathInfo) {
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
if !filepath.IsAbs(path) {
|
||||
path, _ = filepath.Abs(path)
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"path"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
id = "AKIAJM3XLDNUBXUY36EQ"
|
||||
secret = "DZ1Y1ETQlCEc/6JlyW2mxdE8SAfPsmSSrpxGzcTi"
|
||||
region = "ap-east-1"
|
||||
bucket = "tknk.zahokc.cn"
|
||||
URL = "https://tknk.zahokc.cn.s3.ap-east-1.amazonaws.com/"
|
||||
APPFlag = "ys-7527/"
|
||||
)
|
||||
|
||||
var (
|
||||
endpoint = ""
|
||||
disableSSL = true
|
||||
AwsSession *session.Session
|
||||
)
|
||||
|
||||
func init() {
|
||||
NewSession()
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建session
|
||||
*/
|
||||
func NewSession() {
|
||||
creds := credentials.NewStaticCredentials(id, secret, "")
|
||||
config := &aws.Config{
|
||||
Region: aws.String(region),
|
||||
Endpoint: &endpoint,
|
||||
S3ForcePathStyle: aws.Bool(true),
|
||||
Credentials: creds,
|
||||
DisableSSL: &disableSSL,
|
||||
}
|
||||
se, err := session.NewSession(config)
|
||||
AwsSession = se
|
||||
if err != nil {
|
||||
fmt.Printf("create session fail %+v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func ListAllBucket() {
|
||||
svc := s3.New(AwsSession)
|
||||
resp, err := svc.ListBuckets(&s3.ListBucketsInput{})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
fmt.Println(resp.Buckets)
|
||||
}
|
||||
}
|
||||
|
||||
/**上传文件*/
|
||||
func PutObject(key string, content []byte, ttl int64) error {
|
||||
svc := s3.New(AwsSession)
|
||||
params := &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket), // Required
|
||||
Key: aws.String(key), // Required
|
||||
ACL: aws.String("public-read"), //设置成公共读。
|
||||
Body: bytes.NewReader(content),
|
||||
}
|
||||
duration := time.Duration(ttl * 1000)
|
||||
ext := path.Ext(key)
|
||||
if ttl > 0 {
|
||||
params.SetExpires(time.Now().Add(duration))
|
||||
}
|
||||
if ext != "" {
|
||||
contentType := mime.TypeByExtension(ext)
|
||||
if contentType != "" {
|
||||
params.SetContentType(contentType)
|
||||
}
|
||||
}
|
||||
_, err := svc.PutObject(params)
|
||||
//svc.PutObjectLegalHold
|
||||
return err
|
||||
}
|
||||
|
||||
func GetObject(key string) ([]byte, error) {
|
||||
svc := s3.New(AwsSession)
|
||||
params := &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucket), // Required
|
||||
Key: aws.String(key), // Require
|
||||
}
|
||||
getObjectOutput, err := svc.GetObject(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := io.ReadAll(getObjectOutput.Body)
|
||||
getObjectOutput.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
Executable
+291
@@ -0,0 +1,291 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Filter 提供敏感词过滤接口
|
||||
type Filter interface {
|
||||
// Filter 文本过滤函数
|
||||
// excludes 表示排除指定的字符
|
||||
// 返回文本中出现的敏感词,如果敏感词不存在则返回nil
|
||||
// 如果出现异常,则返回error
|
||||
Filter(text string, excludes ...rune) ([]string, error)
|
||||
|
||||
// FilterResult 文本过滤函数
|
||||
// excludes 表示排除指定的字符
|
||||
// 返回文本中出现的敏感词及出现次数,如果敏感词不存在则返回nil
|
||||
// 如果出现异常,则返回error
|
||||
FilterResult(text string, excludes ...rune) (map[string]int, error)
|
||||
|
||||
// FilterReader 从可读流中过滤敏感词
|
||||
// excludes 表示排除指定的字符
|
||||
// 返回可读流中出现的敏感词,如果敏感词不存在则返回nil
|
||||
// 如果出现异常,则返回error
|
||||
FilterReader(reader io.Reader, excludes ...rune) ([]string, error)
|
||||
|
||||
// FilterReaderResult 从可读流中过滤敏感词
|
||||
// excludes 表示排除指定的字符
|
||||
// 返回可读流中出现的敏感词及出现次数,如果敏感词不存在则返回nil
|
||||
// 如果出现异常,则返回error
|
||||
FilterReaderResult(reader io.Reader, excludes ...rune) (map[string]int, error)
|
||||
|
||||
// Replace 使用字符替换文本中的敏感词
|
||||
// delim 替换的字符
|
||||
// 如果出现异常,则返回error
|
||||
Replace(text string, delim rune) (string, error)
|
||||
}
|
||||
|
||||
// NewNodeReaderFilter 创建节点过滤器,实现敏感词的过滤
|
||||
// 从可读流中读取敏感词数据(以指定的分隔符读取数据)
|
||||
func NewNodeReaderFilter(rd io.Reader, delim byte) Filter {
|
||||
nf := &nodeFilter{
|
||||
root: newNode(),
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
_, _ = io.Copy(buf, rd)
|
||||
buf.WriteByte(delim)
|
||||
for {
|
||||
line, err := buf.ReadString(delim)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
nf.addFilterWords(line)
|
||||
}
|
||||
buf.Reset()
|
||||
return nf
|
||||
}
|
||||
|
||||
// NewNodeChanFilter 创建节点过滤器,实现敏感词的过滤
|
||||
// 从通道中读取敏感词数据
|
||||
func NewNodeChanFilter(text <-chan string) Filter {
|
||||
nf := &nodeFilter{
|
||||
root: newNode(),
|
||||
}
|
||||
for v := range text {
|
||||
nf.addFilterWords(v)
|
||||
}
|
||||
return nf
|
||||
}
|
||||
|
||||
// NewNodeFilter 创建节点过滤器,实现敏感词的过滤
|
||||
// 从切片中读取敏感词数据
|
||||
func NewNodeFilter(text []string) Filter {
|
||||
nf := &nodeFilter{
|
||||
root: newNode(),
|
||||
}
|
||||
for i, l := 0, len(text); i < l; i++ {
|
||||
nf.addFilterWords(text[i])
|
||||
}
|
||||
return nf
|
||||
}
|
||||
|
||||
func newNode() *node {
|
||||
return &node{
|
||||
child: make(map[rune]*node),
|
||||
}
|
||||
}
|
||||
|
||||
type node struct {
|
||||
end bool
|
||||
child map[rune]*node
|
||||
}
|
||||
|
||||
type nodeFilter struct {
|
||||
root *node
|
||||
}
|
||||
|
||||
func (nf *nodeFilter) addFilterWords(text string) {
|
||||
n := nf.root
|
||||
uChars := []rune(text)
|
||||
for i, l := 0, len(uChars); i < l; i++ {
|
||||
if unicode.IsSpace(uChars[i]) {
|
||||
continue
|
||||
}
|
||||
if _, ok := n.child[uChars[i]]; !ok {
|
||||
n.child[uChars[i]] = newNode()
|
||||
}
|
||||
n = n.child[uChars[i]]
|
||||
}
|
||||
n.end = true
|
||||
}
|
||||
|
||||
func (nf *nodeFilter) Filter(text string, excludes ...rune) ([]string, error) {
|
||||
buf := bytes.NewBufferString(text)
|
||||
defer buf.Reset()
|
||||
return nf.FilterReader(buf, excludes...)
|
||||
}
|
||||
|
||||
func (nf *nodeFilter) FilterResult(text string, excludes ...rune) (map[string]int, error) {
|
||||
buf := bytes.NewBufferString(text)
|
||||
defer buf.Reset()
|
||||
return nf.FilterReaderResult(buf, excludes...)
|
||||
}
|
||||
|
||||
func (nf *nodeFilter) FilterReader(reader io.Reader, excludes ...rune) ([]string, error) {
|
||||
data, err := nf.FilterReaderResult(reader, excludes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]string, len(data))
|
||||
i := 0
|
||||
for k := range data {
|
||||
result[i] = k
|
||||
i++
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (nf *nodeFilter) FilterReaderResult(reader io.Reader, excludes ...rune) (map[string]int, error) {
|
||||
var uChars []rune
|
||||
data := make(map[string]int)
|
||||
bi := bufio.NewReader(reader)
|
||||
for {
|
||||
ur, _, err := bi.ReadRune()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
break
|
||||
}
|
||||
if nf.checkExclude(ur, excludes...) {
|
||||
continue
|
||||
}
|
||||
if (unicode.IsSpace(ur) || unicode.IsPunct(ur)) && len(uChars) > 0 {
|
||||
nf.doFilter(uChars[:], data)
|
||||
uChars = nil
|
||||
continue
|
||||
}
|
||||
uChars = append(uChars, ur)
|
||||
}
|
||||
if len(uChars) > 0 {
|
||||
nf.doFilter(uChars, data)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (nf *nodeFilter) Replace(text string, delimiter rune) (string, error) {
|
||||
newNF, err := CheckUpdate()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if newNF != nil {
|
||||
nf = newNF
|
||||
}
|
||||
uChars := []rune(text)
|
||||
indexes := nf.doIndexes(uChars)
|
||||
if len(indexes) == 0 {
|
||||
return text, nil
|
||||
}
|
||||
for i := 0; i < len(indexes); i++ {
|
||||
uChars[indexes[i]] = delimiter
|
||||
}
|
||||
return string(uChars), nil
|
||||
}
|
||||
|
||||
func (nf *nodeFilter) checkExclude(u rune, excludes ...rune) bool {
|
||||
if len(excludes) == 0 {
|
||||
return false
|
||||
}
|
||||
var exist bool
|
||||
for i, l := 0, len(excludes); i < l; i++ {
|
||||
if u == excludes[i] {
|
||||
exist = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return exist
|
||||
}
|
||||
|
||||
func (nf *nodeFilter) doFilter(uChars []rune, data map[string]int) {
|
||||
var result []string
|
||||
ul := len(uChars)
|
||||
buf := new(bytes.Buffer)
|
||||
n := nf.root
|
||||
for i := 0; i < ul; i++ {
|
||||
if _, ok := n.child[uChars[i]]; !ok {
|
||||
continue
|
||||
}
|
||||
n = n.child[uChars[i]]
|
||||
buf.WriteRune(uChars[i])
|
||||
if n.end {
|
||||
result = append(result, buf.String())
|
||||
}
|
||||
for j := i + 1; j < ul; j++ {
|
||||
if _, ok := n.child[uChars[j]]; !ok {
|
||||
break
|
||||
}
|
||||
n = n.child[uChars[j]]
|
||||
buf.WriteRune(uChars[j])
|
||||
if n.end {
|
||||
result = append(result, buf.String())
|
||||
}
|
||||
}
|
||||
buf.Reset()
|
||||
n = nf.root
|
||||
}
|
||||
for i, l := 0, len(result); i < l; i++ {
|
||||
var c int
|
||||
if v, ok := data[result[i]]; ok {
|
||||
c = v
|
||||
}
|
||||
data[result[i]] = c + 1
|
||||
}
|
||||
}
|
||||
|
||||
func (nf *nodeFilter) doIndexes(uChars []rune) (indexes []int) {
|
||||
var (
|
||||
tIndexes []int
|
||||
ul = len(uChars)
|
||||
n = nf.root
|
||||
)
|
||||
for i := 0; i < ul; i++ {
|
||||
if _, ok := n.child[uChars[i]]; !ok {
|
||||
continue
|
||||
}
|
||||
n = n.child[uChars[i]]
|
||||
tIndexes = append(tIndexes, i)
|
||||
if n.end {
|
||||
indexes = nf.appendTo(indexes, tIndexes)
|
||||
tIndexes = nil
|
||||
}
|
||||
for j := i + 1; j < ul; j++ {
|
||||
if _, ok := n.child[uChars[j]]; !ok {
|
||||
break
|
||||
}
|
||||
n = n.child[uChars[j]]
|
||||
tIndexes = append(tIndexes, j)
|
||||
if n.end {
|
||||
indexes = nf.appendTo(indexes, tIndexes)
|
||||
}
|
||||
}
|
||||
if tIndexes != nil {
|
||||
tIndexes = nil
|
||||
}
|
||||
n = nf.root
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (nf *nodeFilter) appendTo(dst, src []int) []int {
|
||||
var t []int
|
||||
for i, il := 0, len(src); i < il; i++ {
|
||||
var exist bool
|
||||
for j, jl := 0, len(dst); j < jl; j++ {
|
||||
if src[i] == dst[j] {
|
||||
exist = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !exist {
|
||||
t = append(t, src[i])
|
||||
}
|
||||
}
|
||||
return append(dst, t...)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/cache"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/filtermod"
|
||||
"91porn-server/web/webg"
|
||||
)
|
||||
|
||||
var (
|
||||
WordCache = &cache.Cache{}
|
||||
WordFilter Filter
|
||||
TagFilter Filter
|
||||
|
||||
WordsFilterKey = "FILTER-WORDS"
|
||||
WordsReadStsKey = "isRead"
|
||||
|
||||
Read = "read"
|
||||
UnRead = "unread"
|
||||
|
||||
ReplaceMark = '*'
|
||||
)
|
||||
|
||||
// FilterStart 启动文本过滤器-(评论)
|
||||
func Start() {
|
||||
Init()
|
||||
InitTagFilter()
|
||||
}
|
||||
|
||||
func InitTagFilter() {
|
||||
log.Info("Init Tag Filter start...")
|
||||
TagFilter = NewNodeFilter(appg.Static.TagFilter)
|
||||
}
|
||||
|
||||
// Init Init
|
||||
func Init() {
|
||||
log.Info("Init WordsFilter start...")
|
||||
WordCache.New()
|
||||
// 从数据库获取过滤词
|
||||
words, err := GetFilterWordsFromDB()
|
||||
if err != nil {
|
||||
log.Error("WordFilter Init GetFilterWords error", log.E(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
// 添加过滤词到缓存
|
||||
if err = WordCache.Add(WordsFilterKey, words, 0); err != nil {
|
||||
log.Error("WordFilter Init WordCache Add FILTER-WORD error", log.E(err), log.Any("words", words))
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(words) > 0 {
|
||||
WordFilter = NewNodeFilter(words)
|
||||
// 添加过滤词到Redis
|
||||
if err = appg.Redis.Lpush(WordsFilterKey, words); err != nil {
|
||||
log.Error("WordFilter Init GetNewestFilterWord error", log.E(err), log.Any("words", words))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
log.Info("Init WordsFilter end")
|
||||
}
|
||||
|
||||
// GetFilterWords 从Redis获取过滤词
|
||||
func GetFilterWordsFromDB() ([]string, error) {
|
||||
filterWords, err := filtermod.GetAllFilterWords()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
words := make([]string, len(filterWords))
|
||||
for i, v := range filterWords {
|
||||
words[i] = v.Word
|
||||
}
|
||||
return words, nil
|
||||
}
|
||||
|
||||
// GetFilterWords 从Redis获取过滤词
|
||||
func GetFilterWordsFromRedis() ([]string, error) {
|
||||
count, err := appg.Redis.LCount(WordsFilterKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return appg.Redis.LRange(WordsFilterKey, 0, count)
|
||||
}
|
||||
|
||||
// 检查是否有跟新
|
||||
func CheckUpdate() (*nodeFilter, error) {
|
||||
isRead, err := appg.Redis.Get(WordsReadStsKey)
|
||||
if err != nil {
|
||||
log.Error("CheckUpdate Redis Get error")
|
||||
return nil, err
|
||||
}
|
||||
if isRead != nil && *isRead == UnRead {
|
||||
words, err := GetFilterWordsFromRedis()
|
||||
if err != nil {
|
||||
log.Error("WordFilter CheckUpdate GetFilterWords error")
|
||||
return nil, err
|
||||
}
|
||||
WordCache.New()
|
||||
if err = WordCache.Add(WordsFilterKey, words, 0); err != nil {
|
||||
log.Error("WordFilter CheckUpdate WordCache Add error")
|
||||
return nil, err
|
||||
}
|
||||
WordFilter = NewNodeFilter(words)
|
||||
if err = appg.Redis.Set(WordsReadStsKey, Read, 0); err != nil {
|
||||
log.Error("WordFilter Redis Del error")
|
||||
return nil, err
|
||||
}
|
||||
var tmp interface{} = WordFilter
|
||||
var nf = tmp.(*nodeFilter)
|
||||
return nf, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UpdateFilterWords web更新Redis
|
||||
func UpdateFilterWords() error {
|
||||
var (
|
||||
sensitiveWords []string
|
||||
)
|
||||
// 更新Redis
|
||||
if _, err := webg.Redis.Del(redisconst.SensitiveWordsCache); err != nil {
|
||||
log.Error("WordsFilter UpdateFilterWords Del error", log.E(err))
|
||||
return err
|
||||
}
|
||||
words, err := filtermod.GetAllFilterWords()
|
||||
if err != nil || len(words) <= 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, w := range words {
|
||||
sensitiveWords = append(sensitiveWords, w.Word)
|
||||
}
|
||||
|
||||
if len(sensitiveWords) > 0 {
|
||||
// 放入缓存
|
||||
bytes, _ := json.Marshal(sensitiveWords)
|
||||
err := webg.Redis.Set(redisconst.SensitiveWordsCache, bytes, 10*time.Minute)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("redis set SensitiveWords err:%v", err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/web/webg"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
GainGameCodeUrl = "/api/jt/product/getscode"
|
||||
QueryGameCodeUrl = "/api/jt/product/getsclist"
|
||||
)
|
||||
|
||||
type GameCodeReq struct {
|
||||
Level int `json:"level" bson:"level"` // 商品等级
|
||||
Sign string `json:"sign" bson:"sign"` // md5加密
|
||||
}
|
||||
|
||||
type GameCodeResp struct {
|
||||
Code int `json:"code" bson:"code"` // code编码
|
||||
Data Data `json:"data" bson:"data"` // 游戏码响应内容
|
||||
Msg string `json:"msg" bson:"msg"` // 返回内容
|
||||
Hash bool `json:"hash" bson:"hash"` // hash
|
||||
Time string `json:"time" bson:"time"` // 时间
|
||||
Tip string `json:"tip" bson:"tip"` // 提示内容
|
||||
}
|
||||
|
||||
type Data struct {
|
||||
Code int `json:"code" bson:"code"` // code编码
|
||||
Msg string `json:"msg" bson:"msg"` // 返回内容
|
||||
SubscriptionCode string `json:"subscriptionCode" bson:"subscriptionCode"` // 订单订阅码(无用)
|
||||
SerialCode string `json:"serialCode" bson:"serialCode"` // 序列号 (玩家游戏使用)
|
||||
}
|
||||
|
||||
type QueryGameCodeReq struct {
|
||||
PageNum int `json:"pageNum" bson:"pageNum"` // 当前页
|
||||
PageSize int `json:"pageSize" bson:"pageSize"` // 页码
|
||||
}
|
||||
|
||||
type QueryGameCodeResp struct {
|
||||
Code int `json:"code" bson:"code"` // code编码
|
||||
Data QueryData `json:"data" bson:"data"` // 游戏码响应内容
|
||||
Msg string `json:"msg" bson:"msg"` // 返回内容
|
||||
Hash bool `json:"hash" bson:"hash"` // hash
|
||||
Time string `json:"time" bson:"time"` // 时间
|
||||
Tip string `json:"tip" bson:"tip"` // 提示内容
|
||||
}
|
||||
|
||||
type QueryData struct {
|
||||
Total int `json:"total" bson:"total"` // 总数
|
||||
List []Detail `json:"list" bson:"list"` // 返回内容
|
||||
}
|
||||
|
||||
type Detail struct {
|
||||
SubscriptionCode string `json:"subscriptionCode" bson:"subscriptionCode"` // 订单订阅码(无用)
|
||||
SerialCode string `json:"serialCode" bson:"serialCode"` // 序列号 (玩家游戏使用)
|
||||
UserId string `json:"userId" bson:"userId"` // 用户ID
|
||||
MerchantId int `json:"merchantId" bson:"merchantId"` // 商户号
|
||||
ProductPrice int `json:"productPrice" bson:"productPrice"` // 商品价格
|
||||
ProductLevel int `json:"productLevel" bson:"productLevel"` // 商品等级
|
||||
CreatedAt string `json:"createdAt" bson:"createdAt"` // 创建时间
|
||||
}
|
||||
|
||||
func GainTripartiteGameCode(uid uint64) (string, error) {
|
||||
var gameCode string
|
||||
userId := fmt.Sprintf("LLD_%v", uid)
|
||||
sprintf := fmt.Sprintf("userid=%v&merchantid=%v", userId, appg.Conf.Game.MercId)
|
||||
reqHeader := base64.StdEncoding.EncodeToString([]byte(sprintf))
|
||||
|
||||
md5Str := fmt.Sprintf("level=%d&userid=%v&merchantid=%d", 1, userId, appg.Conf.Game.MercId)
|
||||
hash := md5.Sum([]byte(md5Str))
|
||||
// 将 MD5 转换为十六进制字符串
|
||||
md5String := hex.EncodeToString(hash[:])
|
||||
|
||||
var (
|
||||
url = appg.Conf.Game.URL + GainGameCodeUrl
|
||||
params = GameCodeReq{Level: 1, Sign: md5String}
|
||||
out GameCodeResp
|
||||
)
|
||||
h := map[string]string{"m-api-key": reqHeader}
|
||||
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&out, url, h, params)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("GainTripartiteGameCode http request err:[%v],uid:[%v],url:[%v],params:[%v]", err, uid, url, params))
|
||||
return gameCode, err
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
return gameCode, errors.New("status code is err")
|
||||
}
|
||||
|
||||
if out.Data.Code == http.StatusOK {
|
||||
gameCode = out.Data.SerialCode
|
||||
}
|
||||
return gameCode, nil
|
||||
}
|
||||
|
||||
func QueryTripartiteGameCode(uid uint64) (*QueryData, error) {
|
||||
userId := fmt.Sprintf("LLD_%v", uid)
|
||||
sprintf := fmt.Sprintf("userid=%v&merchantid=%v", userId, webg.Conf.Game.MercId)
|
||||
reqHeader := base64.StdEncoding.EncodeToString([]byte(sprintf))
|
||||
|
||||
var (
|
||||
url = webg.Conf.Game.URL + QueryGameCodeUrl
|
||||
params = QueryGameCodeReq{PageNum: 1, PageSize: 10}
|
||||
out QueryGameCodeResp
|
||||
)
|
||||
h := map[string]string{"m-api-key": reqHeader}
|
||||
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&out, url, h, params)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("QueryTripartiteGameCode http request err:[%v],uid:[%v],url:[%v],params:[%v]", err, uid, url, params))
|
||||
return nil, err
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
return nil, errors.New("status code is err")
|
||||
}
|
||||
|
||||
if out.Code == http.StatusOK {
|
||||
return &out.Data, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"91porn-server/common/constant"
|
||||
"91porn-server/common/crypt"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/common/version"
|
||||
"91porn-server/middleware/ua"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var (
|
||||
env string
|
||||
pid = os.Getgid()
|
||||
cryptSecret360 string
|
||||
ErrUserNotExist = errors.New("userId not exist")
|
||||
|
||||
FirstRealUserID uint64
|
||||
)
|
||||
|
||||
type FileType string
|
||||
|
||||
const (
|
||||
FileCSV FileType = "csv"
|
||||
FileExcel FileType = "xlsx"
|
||||
|
||||
firstRealUserIDTest = 300001 // 首个真实的用户id(测试环境)
|
||||
firstRealUserIDProd = 300001 // 首个真实的用户id(正式环境)
|
||||
)
|
||||
|
||||
// ServeFile serve request with an file by attachment.
|
||||
func ServeFile(c *gin.Context, fileName string, fileType FileType, fileBuffer *bytes.Buffer) {
|
||||
c.DataFromReader(http.StatusOK, int64(fileBuffer.Len()), "application/octet-stream", fileBuffer, map[string]string{
|
||||
"Content-Disposition": fmt.Sprintf("attachment;filename=%s-%s.%s",
|
||||
fileName, time.Now().Local().Format("2006-01-02"), fileType),
|
||||
})
|
||||
}
|
||||
|
||||
// ServeJSON 返回数据并处理多语言
|
||||
func ServeJSON(c *gin.Context, code stderr.Code, data interface{}) {
|
||||
ServeJsonWithExtra(c, code, data, nil)
|
||||
}
|
||||
|
||||
func ServeJsonWithExtra(c *gin.Context, code stderr.Code, data interface{}, extra map[string]interface{}) {
|
||||
var hash bool
|
||||
if code != stderr.Success && code != stderr.ErrVersionUpdate { //发生错误,记录日志
|
||||
var version, sysType, devType string
|
||||
id, _ := GetUID(c)
|
||||
ua, _ := GetUA(c)
|
||||
if ua.Ver != "" {
|
||||
version = ua.Ver
|
||||
}
|
||||
if ua.SysType != "" {
|
||||
sysType = ua.SysType
|
||||
}
|
||||
if devType != "" {
|
||||
devType = ua.DevType
|
||||
}
|
||||
// 预防打印出现空指针异常 PANIC=runtime error: invalid memory address or nil pointer dereference
|
||||
if data == nil {
|
||||
data = "nil"
|
||||
}
|
||||
log.WarnX(c, "Error:",
|
||||
log.Any("UID", strconv.FormatUint(id, 10)),
|
||||
log.Any("IP", c.ClientIP()),
|
||||
log.Any("Version", version),
|
||||
log.Any("SysType", sysType),
|
||||
log.Any("DevType", devType),
|
||||
log.Any("Router", c.Request.RequestURI),
|
||||
log.Any("PID", pid),
|
||||
log.Any("Data", data),
|
||||
log.Any("Code", code))
|
||||
}
|
||||
if !IsNilOrEmpty(data) && code == stderr.Success {
|
||||
t := reflect.TypeOf(data)
|
||||
if !(t.Kind() == reflect.Map || t.Kind() == reflect.Struct || t.Kind() == reflect.Slice) {
|
||||
log.WarnX(c, "[===TypeError===] Return Data Type error is not struct or slice",
|
||||
log.Any("path", c.Request.URL.Path))
|
||||
}
|
||||
}
|
||||
// 在 data 加密前抽取一次 msg:敏感词命中等场景需要把 data 的明文详情同步到 msg
|
||||
msg := resolveMsg(code, data)
|
||||
if env == constant.ProdEnv {
|
||||
//返给前端是否加密
|
||||
hash = true
|
||||
dataByte, _ := json.Marshal(data)
|
||||
cipher, _ := crypt.CoreAesEncryptEx(dataByte, 12, cryptSecret360)
|
||||
data = base64.StdEncoding.EncodeToString(cipher)
|
||||
}
|
||||
if IsNilOrEmpty(data) {
|
||||
data = ""
|
||||
}
|
||||
h := gin.H{
|
||||
"code": code,
|
||||
"hash": hash,
|
||||
"msg": msg,
|
||||
"tip": code.Tip(),
|
||||
"data": data,
|
||||
"time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
|
||||
}
|
||||
if len(extra) > 0 {
|
||||
for k, v := range extra {
|
||||
h[k] = v
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, h)
|
||||
}
|
||||
|
||||
// resolveMsg 按 code 选择 msg:默认走 code.Msg();对于内容敏感词命中这类
|
||||
// 详情完全包含在 data 里的错误码,把 data 的字符串明文同步覆盖到 msg,
|
||||
// 便于前端直接用 msg 弹窗,无需再额外读取 data 字段。
|
||||
func resolveMsg(code stderr.Code, data interface{}) string {
|
||||
if code == stderr.ContentSensitiveHit {
|
||||
if s, ok := data.(string); ok && s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return code.Msg()
|
||||
}
|
||||
|
||||
// ServeToJSON 返回数据并处理多语言
|
||||
func ServeToJSON(c *gin.Context, code stderr.Code, data interface{}) {
|
||||
var hash bool
|
||||
if code != stderr.Success && code != stderr.ErrVersionUpdate { //发生错误,记录日志
|
||||
var version, sysType, devType string
|
||||
id, _ := GetUID(c)
|
||||
ua, _ := GetUA(c)
|
||||
if ua.Ver != "" {
|
||||
version = ua.Ver
|
||||
}
|
||||
if ua.SysType != "" {
|
||||
sysType = ua.SysType
|
||||
}
|
||||
if devType != "" {
|
||||
devType = ua.DevType
|
||||
}
|
||||
log.WarnX(c, "Error:",
|
||||
log.Any("UID", strconv.FormatUint(id, 10)),
|
||||
log.Any("IP", c.ClientIP()),
|
||||
log.Any("Version", version),
|
||||
log.Any("SysType", sysType),
|
||||
log.Any("DevType", devType),
|
||||
log.Any("Router", c.Request.RequestURI),
|
||||
log.Any("PID", pid),
|
||||
log.Any("Error", data),
|
||||
log.Any("Code", code))
|
||||
}
|
||||
if !IsNilOrEmpty(data) && code == stderr.Success {
|
||||
t := reflect.TypeOf(data)
|
||||
if !(t.Kind() == reflect.Map || t.Kind() == reflect.Struct || t.Kind() == reflect.Slice || t.Kind() == reflect.Ptr) {
|
||||
log.WarnX(c, "[===TypeError===] Return Data Type error is not struct or slice",
|
||||
log.Any("kind", t.Kind()),
|
||||
log.Any("path", c.Request.URL.Path))
|
||||
}
|
||||
}
|
||||
if IsNilOrEmpty(data) {
|
||||
data = gin.H{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": code,
|
||||
"hash": hash,
|
||||
"msg": code.Msg(),
|
||||
"tip": code.Tip(),
|
||||
"data": data,
|
||||
"time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
|
||||
})
|
||||
}
|
||||
|
||||
func ServeError(c *gin.Context, err error) {
|
||||
cErr, ok := err.(*stderr.CustomErr)
|
||||
if ok {
|
||||
serveJsonLogic(c, cErr.Code, cErr.Msg, nil, cErr.Msg, cErr.Msg)
|
||||
} else {
|
||||
log.ErrorX(c, "ServeError", log.E(err))
|
||||
var errMsg string
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
}
|
||||
serveJsonLogic(c, stderr.Failure, errMsg, nil, stderr.Failure.Error(), stderr.Failure.Tip())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func serveJsonLogic(c *gin.Context, code stderr.Code, data interface{}, extra map[string]interface{}, msg, tip string) {
|
||||
var hash bool
|
||||
if code != stderr.Success && code != stderr.ErrVersionUpdate { //发生错误,记录日志
|
||||
var version, sysType, devType string
|
||||
id, _ := GetUID(c)
|
||||
ua, _ := GetUA(c)
|
||||
if ua.Ver != "" {
|
||||
version = ua.Ver
|
||||
}
|
||||
if ua.SysType != "" {
|
||||
sysType = ua.SysType
|
||||
}
|
||||
if devType != "" {
|
||||
devType = ua.DevType
|
||||
}
|
||||
// 预防打印出现空指针异常 PANIC=runtime error: invalid memory address or nil pointer dereference
|
||||
if data == nil {
|
||||
data = "nil"
|
||||
}
|
||||
log.WarnX(c, "Error:",
|
||||
log.Any("UID", strconv.FormatUint(id, 10)),
|
||||
log.Any("IP", c.ClientIP()),
|
||||
log.Any("Version", version),
|
||||
log.Any("SysType", sysType),
|
||||
log.Any("DevType", devType),
|
||||
log.Any("Router", c.Request.RequestURI),
|
||||
log.Any("PID", pid),
|
||||
log.Any("Data", data),
|
||||
log.Any("Code", code))
|
||||
}
|
||||
if !IsNilOrEmpty(data) && code == stderr.Success {
|
||||
t := reflect.TypeOf(data)
|
||||
if !(t.Kind() == reflect.Map || t.Kind() == reflect.Struct || t.Kind() == reflect.Slice) {
|
||||
log.WarnX(c, "[===TypeError===] Return Data Type error is not struct or slice",
|
||||
log.Any("path", c.Request.URL.Path))
|
||||
}
|
||||
}
|
||||
if env == constant.ProdEnv {
|
||||
//返给前端是否加密
|
||||
hash = true
|
||||
dataByte, _ := json.Marshal(data)
|
||||
cipher, _ := crypt.CoreAesEncryptEx(dataByte, 12, cryptSecret360)
|
||||
data = base64.StdEncoding.EncodeToString(cipher)
|
||||
}
|
||||
if IsNilOrEmpty(data) {
|
||||
data = ""
|
||||
}
|
||||
h := gin.H{
|
||||
"code": code,
|
||||
"hash": hash,
|
||||
"msg": msg,
|
||||
"tip": tip,
|
||||
"data": data,
|
||||
"time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
|
||||
}
|
||||
if len(extra) > 0 {
|
||||
for k, v := range extra {
|
||||
h[k] = v
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, h)
|
||||
}
|
||||
|
||||
// GetUID 获取用户uid
|
||||
// oauth.Auth执行后uid有效
|
||||
func GetUID(ctx *gin.Context) (uid uint64, err error) {
|
||||
val, exists := ctx.Get(constant.CtxUserID)
|
||||
if !exists {
|
||||
err = ErrUserNotExist
|
||||
return
|
||||
}
|
||||
uid, ok := val.(uint64)
|
||||
if !ok {
|
||||
return 0, errors.New("userId type error")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TryGetUID 尝试获取用户ID,用户未登录的情况下uid为0
|
||||
func TryGetUID(ctx *gin.Context) (uid uint64) {
|
||||
val, exists := ctx.Get(constant.CtxUserID)
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
uid, _ = val.(uint64)
|
||||
return
|
||||
}
|
||||
|
||||
// GetUA
|
||||
func GetUA(ctx *gin.Context) (u ua.UA, err error) {
|
||||
val, exists := ctx.Get(constant.CtxUA)
|
||||
if !exists {
|
||||
err = errors.New("user-agent not exists")
|
||||
return
|
||||
}
|
||||
u, ok := val.(ua.UA)
|
||||
if !ok {
|
||||
err = errors.New("user-agent type error")
|
||||
return
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前App 是否是制定的版本号
|
||||
* 历史判断:
|
||||
1. 是否是flutter版本 ver=2.0.0 主要更新列表,使用hasNext 替换total
|
||||
2. 是否2.0.1版本 ver=2.0.1 用于获取账单更改
|
||||
3. 是否2.0.9版本 ver=2.0.9 用户用户手机登陆流程
|
||||
4. 是否2.1.0版本 ver=2.1.0 用于接口防重放
|
||||
*/
|
||||
|
||||
func IsGTESpecifyVer(ctx *gin.Context, specVer string) bool {
|
||||
val, exists := ctx.Get(constant.CtxUA)
|
||||
if !exists {
|
||||
log.Warn("user-agent not exists")
|
||||
return false
|
||||
}
|
||||
u, ok := val.(ua.UA)
|
||||
if !ok {
|
||||
log.Warn("user-agent type error")
|
||||
return false
|
||||
}
|
||||
if u.Ver == "" {
|
||||
return false
|
||||
}
|
||||
v1, err := version.New(u.Ver)
|
||||
if v1 == nil || err != nil {
|
||||
return false
|
||||
}
|
||||
v2, err := version.New(specVer)
|
||||
if v2 == nil || err != nil {
|
||||
return false
|
||||
}
|
||||
if v1.GTE(v2) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsSpecifyVerBaseOnUa(u ua.UA, specVer string) bool {
|
||||
if u.Ver == "" {
|
||||
return false
|
||||
}
|
||||
v1, err := version.New(u.Ver)
|
||||
if v1 == nil || err != nil {
|
||||
return false
|
||||
}
|
||||
v2, err := version.New(specVer)
|
||||
if v2 == nil || err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if v1.GTE(v2) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetAdminAct 获取管理员账号
|
||||
// oauth.Auth执行后账号有效
|
||||
func GetAdminAct(ctx *gin.Context) (string, error) {
|
||||
t1 := ctx.Request.Header.Get("mod")
|
||||
if t1 == "debug" {
|
||||
return "debug", nil
|
||||
}
|
||||
val, exists := ctx.Get(constant.CtxAdminAct)
|
||||
if !exists {
|
||||
return "", errors.New("Admin not exist")
|
||||
}
|
||||
v, ok := val.(string)
|
||||
if !ok {
|
||||
return "", errors.New("Admin type error")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// GetAdminRole 获取管理员账号
|
||||
// oauth.Auth执行后账号有效
|
||||
func GetAdminRole(ctx *gin.Context) (string, error) {
|
||||
val, exists := ctx.Get(constant.CtxAdminRole)
|
||||
if !exists {
|
||||
return "", errors.New("AdminRole not exist")
|
||||
}
|
||||
v, ok := val.(string)
|
||||
if !ok {
|
||||
return "", errors.New("AdminRole type error")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// GetJuShangId 获取管理员账号
|
||||
// oauth.Auth执行后账号有效
|
||||
func GetJuShangID(ctx *gin.Context) (string, error) {
|
||||
val, exists := ctx.Get(constant.CtxJuShangCID)
|
||||
if !exists {
|
||||
return "", errors.New("JuShangID not exist")
|
||||
}
|
||||
v, ok := val.(string)
|
||||
if !ok {
|
||||
return "", errors.New("JuShangID type error")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// GetUIDAct 获取管理员账号
|
||||
// oauth.Auth执行后uid有效
|
||||
func GetDistrictAct(ctx *gin.Context) (string, error) {
|
||||
val, exists := ctx.Get(constant.CtxDistrictName)
|
||||
if !exists {
|
||||
return "", errors.New("DistrictUserID not exist")
|
||||
}
|
||||
act, ok := val.(string)
|
||||
if !ok {
|
||||
return "", errors.New("DistrictAct type error")
|
||||
}
|
||||
return act, nil
|
||||
}
|
||||
|
||||
// InitGinSecret InitResponseParam
|
||||
func InitGinSecret(secret, secret360, env_ string) {
|
||||
cryptSecret360 = secret360
|
||||
env = env_
|
||||
FirstRealUserID = firstRealUserIDTest
|
||||
if env == constant.ProdEnv {
|
||||
FirstRealUserID = firstRealUserIDProd
|
||||
}
|
||||
}
|
||||
|
||||
func IsNilOrEmpty(in interface{}) bool {
|
||||
return in == nil || in == ""
|
||||
}
|
||||
|
||||
// GetIP 获取真实IP
|
||||
func GetIP(ctx *gin.Context) string {
|
||||
relIP, exists := ctx.Get(constant.CtxIP)
|
||||
v, _ := relIP.(string)
|
||||
// CtxIP 未设置(或存的是空串)时回落到 ClientIP;
|
||||
// 此前写成 if !exists { if relIP == "" ... } —— relIP 为 nil 接口,永不等于 "",回落是死代码,会返回空串
|
||||
if !exists || v == "" {
|
||||
v = ctx.ClientIP()
|
||||
}
|
||||
//log.Info(fmt.Sprintf("[IP-ROUTER] %s,PID %d", ctx.GetHeader("X-Forwarded-For"), os.Getpid()))
|
||||
return v
|
||||
}
|
||||
|
||||
// 判断是否是ip4
|
||||
func IsIP4(ip string) (bool, string) {
|
||||
ipAddr := net.ParseIP(ip).To4()
|
||||
if ipAddr == nil {
|
||||
return false, ""
|
||||
}
|
||||
return true, ipAddr.String()
|
||||
}
|
||||
|
||||
// 判断是否是正常的设备id
|
||||
func IsNormalDevId(devId string) bool {
|
||||
for _, r := range devId {
|
||||
//判断是否包含中文汉子
|
||||
if unicode.Is(unicode.Scripts["Han"], r) {
|
||||
return false
|
||||
}
|
||||
//判断是否包含空格
|
||||
if unicode.IsSpace(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func ServeJSONNoEncrypt(c *gin.Context, code stderr.Code, data interface{}) {
|
||||
ServeJsonWithExtraNoEncrypt(c, code, data, nil)
|
||||
}
|
||||
|
||||
func ServeJsonWithExtraNoEncrypt(c *gin.Context, code stderr.Code, data interface{}, extra map[string]interface{}) {
|
||||
var hash bool
|
||||
if code != stderr.Success && code != stderr.ErrVersionUpdate { //发生错误,记录日志
|
||||
var version, sysType, devType string
|
||||
id, _ := GetUID(c)
|
||||
ua, _ := GetUA(c)
|
||||
if ua.Ver != "" {
|
||||
version = ua.Ver
|
||||
}
|
||||
if ua.SysType != "" {
|
||||
sysType = ua.SysType
|
||||
}
|
||||
if devType != "" {
|
||||
devType = ua.DevType
|
||||
}
|
||||
// 预防打印出现空指针异常 PANIC=runtime error: invalid memory address or nil pointer dereference
|
||||
if data == nil {
|
||||
data = "nil"
|
||||
}
|
||||
log.WarnX(c, "Error:",
|
||||
log.Any("UID", strconv.FormatUint(id, 10)),
|
||||
log.Any("IP", c.ClientIP()),
|
||||
log.Any("Version", version),
|
||||
log.Any("SysType", sysType),
|
||||
log.Any("DevType", devType),
|
||||
log.Any("Router", c.Request.RequestURI),
|
||||
log.Any("PID", pid),
|
||||
log.Any("Data", data),
|
||||
log.Any("Code", code))
|
||||
}
|
||||
if !IsNilOrEmpty(data) && code == stderr.Success {
|
||||
t := reflect.TypeOf(data)
|
||||
if !(t.Kind() == reflect.Map || t.Kind() == reflect.Struct || t.Kind() == reflect.Slice) {
|
||||
log.WarnX(c, "[===TypeError===] Return Data Type error is not struct or slice",
|
||||
log.Any("path", c.Request.URL.Path))
|
||||
}
|
||||
}
|
||||
|
||||
msg := resolveMsg(code, data)
|
||||
if IsNilOrEmpty(data) {
|
||||
data = ""
|
||||
}
|
||||
h := gin.H{
|
||||
"code": code,
|
||||
"hash": hash,
|
||||
"msg": msg,
|
||||
"tip": code.Tip(),
|
||||
"data": data,
|
||||
"time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
|
||||
}
|
||||
if len(extra) > 0 {
|
||||
for k, v := range extra {
|
||||
h[k] = v
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, h)
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
)
|
||||
|
||||
var (
|
||||
routinePanicHandler func(string)
|
||||
routineCount int64
|
||||
)
|
||||
|
||||
func SetPanicHandler(handler func(string)) {
|
||||
routinePanicHandler = handler
|
||||
}
|
||||
|
||||
func Go(f func()) {
|
||||
atomic.AddInt64(&routineCount, 1)
|
||||
go func() {
|
||||
defer func() {
|
||||
atomic.AddInt64(&routineCount, -1)
|
||||
if r := recover(); r != nil {
|
||||
log.Error("【Warning】serevr occour error,please attention", log.Any("Detail", r))
|
||||
if routinePanicHandler != nil {
|
||||
var stack string
|
||||
if !stderr.IsContain(r, stderr.WriteBrokenError) {
|
||||
stack = string(debug.Stack())
|
||||
}
|
||||
routinePanicHandler(fmt.Sprintf("[Panic] routineCount:%d \n err: %+v\n", routineCount, r) + stack)
|
||||
}
|
||||
}
|
||||
}()
|
||||
f()
|
||||
}()
|
||||
}
|
||||
|
||||
func GoParam(i int, f func(i int)) {
|
||||
atomic.AddInt64(&routineCount, 1)
|
||||
go func(i int) {
|
||||
defer func() {
|
||||
atomic.AddInt64(&routineCount, -1)
|
||||
if r := recover(); r != nil {
|
||||
log.Error("【Warning】serevr occour error,please attention", log.Any("Detail", r))
|
||||
if routinePanicHandler != nil {
|
||||
var stack string
|
||||
if !stderr.IsContain(r, stderr.WriteBrokenError) {
|
||||
stack = string(debug.Stack())
|
||||
}
|
||||
routinePanicHandler(fmt.Sprintf("[Panic] routineCount:%d \n err: %+v\n", routineCount, r) + stack)
|
||||
}
|
||||
}
|
||||
}()
|
||||
f(i)
|
||||
}(i)
|
||||
}
|
||||
|
||||
// WaitGoQuit 等待go退出,timeoutSec 超时秒数,<=0表示不超时
|
||||
func WaitGoQuit(timeoutSec int32) {
|
||||
for {
|
||||
count := atomic.LoadInt64(&routineCount)
|
||||
if count == 0 {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
if timeoutSec > 0 {
|
||||
timeoutSec--
|
||||
if timeoutSec == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GoAndWait(funcs ...func() error) (err error) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(funcs))
|
||||
for i := range funcs {
|
||||
go func(f func() error) {
|
||||
atomic.AddInt64(&routineCount, 1)
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
atomic.AddInt64(&routineCount, -1)
|
||||
if r := recover(); r != nil {
|
||||
log.Error("【Warning】serevr occour error,please attention", log.Any("Detail", r))
|
||||
if routinePanicHandler != nil {
|
||||
var stack string
|
||||
if !stderr.IsContain(r, stderr.WriteBrokenError) {
|
||||
stack = string(debug.Stack())
|
||||
}
|
||||
routinePanicHandler(fmt.Sprintf("[Panic] routineCount:%d \n err: %+v\n", routineCount, r) + stack)
|
||||
}
|
||||
}
|
||||
}()
|
||||
if routineErr := f(); routineErr != nil {
|
||||
err = routineErr
|
||||
|
||||
}
|
||||
}(funcs[i])
|
||||
}
|
||||
wg.Wait()
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package googauth
|
||||
|
||||
import "github.com/dgryski/dgoogauth"
|
||||
|
||||
const windowSize = 30
|
||||
const hotpWindowSize = 5
|
||||
|
||||
var HotpCfg = dgoogauth.OTPConfig{
|
||||
Secret: "MRSHM6LTMFYHAMJSGM2DKNQ=",
|
||||
HotpCounter: 1,
|
||||
WindowSize: hotpWindowSize,
|
||||
}
|
||||
|
||||
func New(secret string, name string) string {
|
||||
cfg := &dgoogauth.OTPConfig{
|
||||
Secret: secret,
|
||||
WindowSize: windowSize,
|
||||
}
|
||||
return cfg.ProvisionURI(name)
|
||||
}
|
||||
|
||||
func Verify(secret string, pwd string) (bool, error) {
|
||||
cfg := &dgoogauth.OTPConfig{
|
||||
Secret: secret,
|
||||
WindowSize: windowSize,
|
||||
}
|
||||
return cfg.Authenticate(pwd)
|
||||
}
|
||||
|
||||
func VerifyHotp(cfg *dgoogauth.OTPConfig, authCode string, counter int) (bool, error) {
|
||||
return cfg.Authenticate(authCode)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
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)
|
||||
return realCode == code
|
||||
}
|
||||
|
||||
// Verify verify OTP code
|
||||
func (h HOTP) GenerateCode(counter uint64) string {
|
||||
return h.At(counter)
|
||||
}
|
||||
|
||||
// NewHOTP generate new HOTP instance
|
||||
func NewHOTP(secret []byte, digits int) (h *HOTP) {
|
||||
h = new(HOTP)
|
||||
h.secret = secret
|
||||
h.digits = digits
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package common
|
||||
|
||||
import "errors"
|
||||
|
||||
// FsOption 读取文件服务器配置
|
||||
type FsOption struct {
|
||||
BucketName string `json:"bucketName"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
Secret string `json:"secret"`
|
||||
TTL string `json:"ttl"`
|
||||
}
|
||||
|
||||
// SelectFsOption 从配置文件中读取Bucket对应的相关值
|
||||
func SelectFsOption(bucket string, FsOptions []FsOption) (FsOption, error) {
|
||||
option := FsOption{}
|
||||
if bucket == "" || len(FsOptions) == 0 {
|
||||
return option, errors.New("bucket or FsOptions must nor be empty")
|
||||
}
|
||||
for _, option := range FsOptions {
|
||||
if option.BucketName == bucket {
|
||||
return option, nil
|
||||
}
|
||||
}
|
||||
return option, nil
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package hevcpull
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ExpiresParam = "hevc_exp"
|
||||
SignatureParam = "hevc_sig"
|
||||
|
||||
minSecretBytes = 32
|
||||
maxSignatureTTL = 480*time.Hour + 5*time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidSecret = errors.New("HEVC pull secret must contain at least 32 bytes")
|
||||
ErrInvalidSignature = errors.New("invalid HEVC pull signature")
|
||||
ErrExpired = errors.New("expired HEVC pull signature")
|
||||
|
||||
signatureTextPattern = regexp.MustCompile(`(?i)(hevc_sig(?:=|%3D))[0-9a-f]{64}`)
|
||||
)
|
||||
|
||||
// NormalizeSource accepts only the canonical relative m3u8 paths stored by
|
||||
// the video service. Absolute URLs, query strings, fragments, backslashes,
|
||||
// and dot traversal are rejected so the signed path and cloud task identity
|
||||
// always refer to the same source.
|
||||
func NormalizeSource(source string) (string, error) {
|
||||
source = strings.TrimSpace(source)
|
||||
if source == "" || strings.Contains(source, "\\") {
|
||||
return "", errors.New("invalid HEVC pull source")
|
||||
}
|
||||
parsed, err := url.Parse(source)
|
||||
if err != nil ||
|
||||
parsed.IsAbs() ||
|
||||
parsed.Host != "" ||
|
||||
parsed.User != nil ||
|
||||
parsed.RawQuery != "" ||
|
||||
parsed.Fragment != "" {
|
||||
return "", errors.New("invalid HEVC pull source")
|
||||
}
|
||||
sourcePath := strings.TrimLeft(parsed.Path, "/")
|
||||
if sourcePath == "" || strings.Contains(sourcePath, "\\") {
|
||||
return "", errors.New("invalid HEVC pull source")
|
||||
}
|
||||
for _, char := range sourcePath {
|
||||
if char < 0x20 || char == 0x7f {
|
||||
return "", errors.New("invalid HEVC pull source")
|
||||
}
|
||||
}
|
||||
for _, segment := range strings.Split(sourcePath, "/") {
|
||||
if segment == "." || segment == ".." {
|
||||
return "", errors.New("invalid HEVC pull source traversal")
|
||||
}
|
||||
}
|
||||
normalized := strings.TrimLeft(path.Clean("/"+sourcePath), "/")
|
||||
if normalized == "" ||
|
||||
normalized == "." ||
|
||||
path.Ext(normalized) != ".m3u8" {
|
||||
return "", errors.New("invalid HEVC pull source")
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
// ResolveChildSource resolves a child playlist URI from a signed master
|
||||
// playlist against the parent source path. Only local m3u8 paths are accepted;
|
||||
// absolute URLs and query-bearing variants are rejected.
|
||||
func ResolveChildSource(parentSource, childURI string) (string, error) {
|
||||
parentSource, err := NormalizeSource(parentSource)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
childURI = strings.TrimSpace(childURI)
|
||||
if childURI == "" || strings.Contains(childURI, "\\") {
|
||||
return "", errors.New("invalid HEVC child playlist")
|
||||
}
|
||||
child, err := url.Parse(childURI)
|
||||
if err != nil ||
|
||||
child.IsAbs() ||
|
||||
child.Host != "" ||
|
||||
child.User != nil ||
|
||||
child.RawQuery != "" ||
|
||||
child.Fragment != "" ||
|
||||
child.Path == "" {
|
||||
return "", errors.New("invalid HEVC child playlist")
|
||||
}
|
||||
|
||||
parentNamespace := knownSourceNamespace(parentSource)
|
||||
var resolved string
|
||||
if strings.HasPrefix(child.Path, "/") {
|
||||
resolved = strings.TrimLeft(child.Path, "/")
|
||||
childNamespace := knownSourceNamespace(resolved)
|
||||
switch parentNamespace {
|
||||
case "", "sp":
|
||||
// A path without a reserved prefix still resolves to the default
|
||||
// SP origin. Explicitly switching to another origin is forbidden.
|
||||
if childNamespace != "" && childNamespace != "sp" {
|
||||
return "", errors.New("HEVC child playlist changes source namespace")
|
||||
}
|
||||
default:
|
||||
// For non-default origins the source namespace is encoded in the
|
||||
// route path. A plain root-relative URI cannot preserve that origin,
|
||||
// so require the playlist to name the same namespace explicitly.
|
||||
if childNamespace != parentNamespace {
|
||||
return "", errors.New("ambiguous HEVC root-relative child playlist")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if parentNamespace != "" && childEscapesSourceNamespace(parentSource, child.Path) {
|
||||
return "", errors.New("HEVC child playlist escapes source namespace")
|
||||
}
|
||||
resolved = path.Join("/", path.Dir(parentSource), child.Path)
|
||||
}
|
||||
resolved, err = NormalizeSource(strings.TrimLeft(resolved, "/"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !strings.HasPrefix(child.Path, "/") &&
|
||||
parentNamespace != "" &&
|
||||
knownSourceNamespace(resolved) != parentNamespace {
|
||||
return "", errors.New("HEVC child playlist escapes source namespace")
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
func childEscapesSourceNamespace(parentSource, childPath string) bool {
|
||||
parentDir := strings.Trim(path.Dir(parentSource), "/")
|
||||
depth := 0
|
||||
if parentDir != "" && parentDir != "." {
|
||||
depth = len(strings.Split(parentDir, "/"))
|
||||
}
|
||||
for _, segment := range strings.Split(childPath, "/") {
|
||||
switch segment {
|
||||
case "", ".":
|
||||
continue
|
||||
case "..":
|
||||
if depth <= 1 {
|
||||
return true
|
||||
}
|
||||
depth--
|
||||
default:
|
||||
depth++
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func knownSourceNamespace(source string) string {
|
||||
first, _, _ := strings.Cut(strings.TrimLeft(source, "/"), "/")
|
||||
switch first {
|
||||
case "sp", "pms", "laosiji", "v1", "v2", "v3":
|
||||
return first
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// SignURL signs an exact source path for the cloud transcoder. The returned
|
||||
// URL contains only the expiry and signature query parameters.
|
||||
func SignURL(rawURL, secret string, expiresAt time.Time) (string, error) {
|
||||
secret = strings.TrimSpace(secret)
|
||||
if len([]byte(secret)) < minSecretBytes {
|
||||
return "", ErrInvalidSecret
|
||||
}
|
||||
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if parsed.Scheme == "" || parsed.Host == "" || parsed.EscapedPath() == "" {
|
||||
return "", errors.New("invalid HEVC pull URL")
|
||||
}
|
||||
if parsed.RawQuery != "" || parsed.Fragment != "" {
|
||||
return "", errors.New("HEVC pull URL must not contain query or fragment")
|
||||
}
|
||||
expires := expiresAt.UTC().Unix()
|
||||
if expires <= 0 {
|
||||
return "", errors.New("invalid HEVC pull expiry")
|
||||
}
|
||||
query := parsed.Query()
|
||||
query.Set(ExpiresParam, strconv.FormatInt(expires, 10))
|
||||
query.Set(SignatureParam, signature(parsed.EscapedPath(), expires, secret))
|
||||
parsed.RawQuery = query.Encode()
|
||||
return parsed.String(), nil
|
||||
}
|
||||
|
||||
// VerifyURL validates expiry, exact path binding, and the HMAC signature.
|
||||
func VerifyURL(parsed *url.URL, secret string, now time.Time) error {
|
||||
secret = strings.TrimSpace(secret)
|
||||
if len([]byte(secret)) < minSecretBytes {
|
||||
log.Error("VerifyURL fail 1")
|
||||
return ErrInvalidSecret
|
||||
}
|
||||
if parsed == nil || parsed.EscapedPath() == "" {
|
||||
log.Error("VerifyURL fail 2", log.Any("parsed", parsed))
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
query := parsed.Query()
|
||||
if len(query) != 2 ||
|
||||
len(query[ExpiresParam]) != 1 ||
|
||||
len(query[SignatureParam]) != 1 {
|
||||
log.Error("VerifyURL fail 3", log.Any("query", query))
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
expires, err := strconv.ParseInt(query.Get(ExpiresParam), 10, 64)
|
||||
if err != nil {
|
||||
log.Error("VerifyURL fail 4", log.E(err))
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
now = now.UTC()
|
||||
expiresAt := time.Unix(expires, 0).UTC()
|
||||
if !expiresAt.After(now) {
|
||||
log.Error("VerifyURL fail 5", log.Any("expires", expires))
|
||||
return ErrExpired
|
||||
}
|
||||
if expiresAt.After(now.Add(maxSignatureTTL)) {
|
||||
log.Error("VerifyURL fail 6", log.Any("expires", expires))
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
|
||||
provided, err := hex.DecodeString(query.Get(SignatureParam))
|
||||
if err != nil {
|
||||
log.Error("VerifyURL fail 7", log.E(err))
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
expected, err := hex.DecodeString(signature(parsed.EscapedPath(), expires, secret))
|
||||
if err != nil || !hmac.Equal(provided, expected) {
|
||||
log.Error("VerifyURL fail 8", log.Any("expires", expires))
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RedactText removes signed pull bearer values from logs and diagnostics,
|
||||
// including when the nested URL has been query-escaped by another API.
|
||||
func RedactText(text string) string {
|
||||
return signatureTextPattern.ReplaceAllString(text, `${1}[REDACTED]`)
|
||||
}
|
||||
|
||||
// RedactURL returns a diagnostic form of a signed pull URL that is safe to
|
||||
// persist. It is not a usable pull URL.
|
||||
func RedactURL(rawURL string) string {
|
||||
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
||||
if err != nil {
|
||||
return RedactText(rawURL)
|
||||
}
|
||||
query := parsed.Query()
|
||||
if query.Has(SignatureParam) {
|
||||
query.Set(SignatureParam, "[REDACTED]")
|
||||
parsed.RawQuery = query.Encode()
|
||||
}
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
func signature(escapedPath string, expires int64, secret string) string {
|
||||
canonical := fmt.Sprintf("%s\n%d", escapedPath, expires)
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
_, _ = mac.Write([]byte(canonical))
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package hevcpull
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSignAndVerifyURL(t *testing.T) {
|
||||
now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
|
||||
signed, err := SignURL(
|
||||
"https://app.example.com/api/app/vid/transcode/m3u8/laosiji/m3m/demo.m3u8",
|
||||
"test-pull-secret-strong-32-bytes!!",
|
||||
now.Add(480*time.Hour),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("SignURL failed: %v", err)
|
||||
}
|
||||
parsed, err := url.Parse(signed)
|
||||
if err != nil {
|
||||
t.Fatalf("parse signed URL: %v", err)
|
||||
}
|
||||
if err = VerifyURL(parsed, "test-pull-secret-strong-32-bytes!!", now); err != nil {
|
||||
t.Fatalf("VerifyURL failed: %v", err)
|
||||
}
|
||||
|
||||
parsed.Path += ".tampered"
|
||||
if err = VerifyURL(parsed, "test-pull-secret-strong-32-bytes!!", now); !errors.Is(err, ErrInvalidSignature) {
|
||||
t.Fatalf("tampered path returned %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyURLRejectsExpiredOrExtraQuery(t *testing.T) {
|
||||
now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
|
||||
signed, err := SignURL(
|
||||
"https://app.example.com/api/app/vid/transcode/m3u8/source.m3u8",
|
||||
"test-pull-secret-strong-32-bytes!!",
|
||||
now.Add(time.Minute),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("SignURL failed: %v", err)
|
||||
}
|
||||
parsed, _ := url.Parse(signed)
|
||||
if err = VerifyURL(parsed, "test-pull-secret-strong-32-bytes!!", now.Add(time.Minute)); !errors.Is(err, ErrExpired) {
|
||||
t.Fatalf("expired URL returned %v", err)
|
||||
}
|
||||
|
||||
parsed, _ = url.Parse(signed)
|
||||
query := parsed.Query()
|
||||
query.Set("c", "unbound-cdn")
|
||||
parsed.RawQuery = query.Encode()
|
||||
if err = VerifyURL(parsed, "test-pull-secret-strong-32-bytes!!", now); !errors.Is(err, ErrInvalidSignature) {
|
||||
t.Fatalf("extra query returned %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignURLRejectsWeakSecret(t *testing.T) {
|
||||
_, err := SignURL("https://app.example.com/source.m3u8", "short", time.Now().Add(time.Hour))
|
||||
if !errors.Is(err, ErrInvalidSecret) {
|
||||
t.Fatalf("weak secret returned %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactSignedPullURL(t *testing.T) {
|
||||
const signature = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
raw := "https://app.example/source.m3u8?hevc_exp=1&hevc_sig=" + signature
|
||||
redacted := RedactURL(raw)
|
||||
if redacted == raw || RedactText(redacted) != redacted {
|
||||
t.Fatalf("URL was not redacted: %s", redacted)
|
||||
}
|
||||
if got := RedactText("file_url=" + url.QueryEscape(raw)); got == "file_url="+url.QueryEscape(raw) {
|
||||
t.Fatalf("escaped nested URL was not redacted: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSource(t *testing.T) {
|
||||
tests := []struct {
|
||||
source string
|
||||
want string
|
||||
}{
|
||||
{source: " /laosiji/m3m/demo.m3u8 ", want: "laosiji/m3m/demo.m3u8"},
|
||||
{source: "sp/movie/index.m3u8", want: "sp/movie/index.m3u8"},
|
||||
{source: "sp/movie/../index.m3u8"},
|
||||
{source: `sp\movie\index.m3u8`},
|
||||
{source: `sp/movie%5Cindex.m3u8`},
|
||||
{source: `sp/movie%0Aindex.m3u8`},
|
||||
{source: "https://cdn.example.com/index.m3u8"},
|
||||
{source: "sp/movie/index.m3u8?token=x"},
|
||||
{source: "sp/movie/index.mp4"},
|
||||
{source: "sp/movie/index.M3U8"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, err := NormalizeSource(tt.source)
|
||||
if tt.want == "" {
|
||||
if err == nil {
|
||||
t.Errorf("NormalizeSource(%q) = %q, want error", tt.source, got)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil || got != tt.want {
|
||||
t.Errorf("NormalizeSource(%q) = %q, %v; want %q", tt.source, got, err, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveChildSource(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
parent string
|
||||
child string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "relative child",
|
||||
parent: "sp/movie/master.m3u8",
|
||||
child: "720/index.m3u8",
|
||||
want: "sp/movie/720/index.m3u8",
|
||||
},
|
||||
{
|
||||
name: "parent traversal",
|
||||
parent: "sp/movie/master.m3u8",
|
||||
child: "../audio/index.m3u8",
|
||||
want: "sp/audio/index.m3u8",
|
||||
},
|
||||
{
|
||||
name: "root child remains on default SP origin",
|
||||
parent: "sp/movie/master.m3u8",
|
||||
child: "/shared/index.m3u8",
|
||||
want: "shared/index.m3u8",
|
||||
},
|
||||
{
|
||||
name: "explicit PMS root namespace is preserved",
|
||||
parent: "pms/movie/master.m3u8",
|
||||
child: "/pms/shared/index.m3u8",
|
||||
want: "pms/shared/index.m3u8",
|
||||
},
|
||||
{
|
||||
name: "explicit laosiji root namespace is preserved",
|
||||
parent: "laosiji/m3m/movie/master.m3u8",
|
||||
child: "/laosiji/shared/index.m3u8",
|
||||
want: "laosiji/shared/index.m3u8",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ResolveChildSource(tt.parent, tt.child)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveChildSource error: %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("ResolveChildSource = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
parent string
|
||||
child string
|
||||
}{
|
||||
{parent: "sp/movie/master.m3u8", child: "https://cdn.example.com/index.m3u8"},
|
||||
{parent: "sp/movie/master.m3u8", child: "index.m3u8?token=secret"},
|
||||
{parent: "sp/movie/master.m3u8", child: "index.ts"},
|
||||
{parent: "sp/movie/master.m3u8", child: `..\index.m3u8`},
|
||||
{parent: "sp/movie/master.m3u8", child: "../../outside/index.m3u8"},
|
||||
{parent: "sp/movie/master.m3u8", child: "../../../sp/outside/index.m3u8"},
|
||||
{parent: "sp/movie/master.m3u8", child: "/pms/outside/index.m3u8"},
|
||||
{parent: "pms/movie/master.m3u8", child: "/shared/index.m3u8"},
|
||||
{parent: "pms/movie/master.m3u8", child: "/sp/outside/index.m3u8"},
|
||||
{parent: "laosiji/movie/master.m3u8", child: "/shared/index.m3u8"},
|
||||
} {
|
||||
if _, err := ResolveChildSource(tt.parent, tt.child); err == nil {
|
||||
t.Fatalf("unsafe child accepted: parent=%q child=%q", tt.parent, tt.child)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultTimeOut = 10
|
||||
|
||||
var (
|
||||
defaultClient *http.Client
|
||||
httpClientMap sync.Map
|
||||
errProxyNil = errors.New("proxy is nil")
|
||||
)
|
||||
|
||||
func init() {
|
||||
defaultClient = &http.Client{
|
||||
Timeout: time.Second * time.Duration(defaultTimeOut),
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: time.Second * 30,
|
||||
KeepAlive: time.Second * 30,
|
||||
}).DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
IdleConnTimeout: time.Second * 90,
|
||||
TLSHandshakeTimeout: time.Second * 10,
|
||||
ExpectContinueTimeout: time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 传入参数单位秒
|
||||
func getClientByTimeoutSet(connTimeout int) *http.Client {
|
||||
if connTimeout == defaultTimeOut {
|
||||
return defaultClient
|
||||
}
|
||||
client, _ := httpClientMap.LoadOrStore(connTimeout, &http.Client{
|
||||
Timeout: time.Duration(connTimeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: time.Second * 30,
|
||||
KeepAlive: time.Second * 30,
|
||||
}).DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
IdleConnTimeout: time.Second * 90,
|
||||
TLSHandshakeTimeout: time.Second * 10,
|
||||
ExpectContinueTimeout: time.Second,
|
||||
},
|
||||
})
|
||||
return client.(*http.Client)
|
||||
}
|
||||
|
||||
type ProxyCfg struct {
|
||||
Host string
|
||||
Timeout int //秒
|
||||
Source string
|
||||
IsActive bool //是否开启代理
|
||||
}
|
||||
|
||||
func (p *ProxyCfg) Build(rawurl string, rawHeaders map[string]string) (string, map[string]string, error) {
|
||||
if !p.IsActive {
|
||||
return rawurl, rawHeaders, nil
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 提供关闭函数以防止内存泄露
|
||||
func IgnoreResp(resp *http.Response) {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ClientGet(connTimeout int, url string, headers map[string]string, params ...any) (*http.Response, error) {
|
||||
for _, p := range params {
|
||||
url = addParams(url, toUrlValues(p))
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
cl := getClientByTimeoutSet(connTimeout)
|
||||
return cl.Do(req)
|
||||
}
|
||||
|
||||
func DefaultClientGet(url string, headers map[string]string, params ...any) (*http.Response, error) {
|
||||
return ClientGet(defaultTimeOut, url, headers, params...)
|
||||
}
|
||||
|
||||
func ClientGetBytes(connTimeout int, url string, headers map[string]string, params ...any) (int, []byte, error) {
|
||||
resp, err := ClientGet(connTimeout, url, headers, params...)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
ct, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return resp.StatusCode, nil, err
|
||||
}
|
||||
return resp.StatusCode, ct, nil
|
||||
}
|
||||
|
||||
func DefaultClientGetBytes(url string, headers map[string]string, params ...any) (int, []byte, error) {
|
||||
return ClientGetBytes(defaultTimeOut, url, headers, params...)
|
||||
}
|
||||
|
||||
func ClientGetBytesWithProxy(connTimeout int, p *ProxyCfg, url string, headers map[string]string, params ...any) (int, []byte, error) {
|
||||
if p == nil {
|
||||
return 0, nil, errProxyNil
|
||||
}
|
||||
pUrl, pHeaders, err := p.Build(url, headers)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return ClientGetBytes(connTimeout, pUrl, pHeaders, params...)
|
||||
}
|
||||
|
||||
func ClientGetWithRespWithProxy(connTimeout int, p *ProxyCfg, bind any, url string, headers map[string]string, params ...any) (int, error) {
|
||||
code, bts, err := ClientGetBytesWithProxy(connTimeout, p, url, headers, params...)
|
||||
if err != nil {
|
||||
return code, err
|
||||
}
|
||||
return code, json.Unmarshal(bts, &bind)
|
||||
}
|
||||
|
||||
func DefaultClientGetWithRespWithProxy(p *ProxyCfg, bind any, url string, headers map[string]string, params ...any) (int, error) {
|
||||
return ClientGetWithRespWithProxy(defaultTimeOut, p, bind, url, headers, params...)
|
||||
}
|
||||
|
||||
func ClientGetWithResp(bind any, connTimeout int, url string, headers map[string]string, params ...any) (int, error) {
|
||||
code, ct, err := ClientGetBytes(connTimeout, url, headers, params...)
|
||||
if err != nil {
|
||||
return code, err
|
||||
}
|
||||
return code, json.Unmarshal(ct, &bind)
|
||||
}
|
||||
|
||||
func DefaultClientGetWithResp(bind any, url string, headers map[string]string, params ...any) (int, error) {
|
||||
return ClientGetWithResp(bind, defaultTimeOut, url, headers, params...)
|
||||
}
|
||||
|
||||
func ClientGetWithRespWithCtx(ctx context.Context, bind any, connTimeout int, url string, headers map[string]string, params ...any) (int, error) {
|
||||
code, ct, err := ClientGetBytesWithCtx(ctx, connTimeout, url, headers, params...)
|
||||
if err != nil {
|
||||
return code, err
|
||||
}
|
||||
return code, json.Unmarshal(ct, &bind)
|
||||
}
|
||||
|
||||
func DefaultClientGetWithRespWithCtx(ctx context.Context, bind any, url string, headers map[string]string, params ...any) (int, error) {
|
||||
code, ct, err := ClientGetBytesWithCtx(ctx, defaultTimeOut, url, headers, params...)
|
||||
if err != nil {
|
||||
return code, err
|
||||
}
|
||||
return code, json.Unmarshal(ct, &bind)
|
||||
}
|
||||
|
||||
func ClientGetWithCtx(ctx context.Context, connTimeout int, url string, headers map[string]string, params ...any) (*http.Response, error) {
|
||||
for _, p := range params {
|
||||
url = addParams(url, toUrlValues(p))
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
cl := getClientByTimeoutSet(connTimeout)
|
||||
return cl.Do(req)
|
||||
}
|
||||
|
||||
func DefaultClientGetWithCtx(ctx context.Context, url string, headers map[string]string, params ...any) (*http.Response, error) {
|
||||
return ClientGetWithCtx(ctx, defaultTimeOut, url, headers, params...)
|
||||
}
|
||||
|
||||
func ClientGetBytesWithCtx(ctx context.Context, connTimeout int, url string, headers map[string]string, params ...any) (int, []byte, error) {
|
||||
resp, err := ClientGetWithCtx(ctx, connTimeout, url, headers, params...)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
ct, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return resp.StatusCode, nil, err
|
||||
}
|
||||
return resp.StatusCode, ct, nil
|
||||
}
|
||||
|
||||
func DefaultClientGetBytesWithCtx(ctx context.Context, url string, headers map[string]string, params ...any) (int, []byte, error) {
|
||||
return ClientGetBytesWithCtx(ctx, defaultTimeOut, url, headers, params...)
|
||||
}
|
||||
|
||||
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 map[string]interface{}:
|
||||
rst := make(url.Values)
|
||||
for k, v := range t {
|
||||
rst.Add(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
return rst
|
||||
case nil:
|
||||
return make(url.Values)
|
||||
default:
|
||||
panic("Invalid value")
|
||||
}
|
||||
}
|
||||
|
||||
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_
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ClientPost(connTimeout int, url string, headers map[string]string, data any) (*http.Response, error) {
|
||||
req, err := getPostRequest(url, headers, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cl := getClientByTimeoutSet(connTimeout)
|
||||
return cl.Do(req)
|
||||
}
|
||||
|
||||
func DefaultClientPost(url string, headers map[string]string, data any) (*http.Response, error) {
|
||||
return ClientPost(defaultTimeOut, url, headers, data)
|
||||
}
|
||||
|
||||
func ClientPostWithCtx(ctx context.Context, connTimeout int, url string, headers map[string]string, data any) (*http.Response, error) {
|
||||
req, err := getPostRequest(url, headers, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
cl := getClientByTimeoutSet(connTimeout)
|
||||
return cl.Do(req)
|
||||
}
|
||||
|
||||
func DefaultClientPostWithCtx(ctx context.Context, url string, headers map[string]string, data any) (*http.Response, error) {
|
||||
return ClientPostWithCtx(ctx, defaultTimeOut, url, headers, data)
|
||||
}
|
||||
|
||||
func ClientPostWithProxy(connTimeout int, p *ProxyCfg, url string, headers map[string]string, data any) (*http.Response, error) {
|
||||
if p == nil {
|
||||
return nil, errProxyNil
|
||||
}
|
||||
pUrl, pHeaders, err := p.Build(url, headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ClientPost(connTimeout, pUrl, pHeaders, data)
|
||||
}
|
||||
|
||||
func ClientPostWithResp(connTimeout int, bind any, url string, headers map[string]string, data any) (int, error) {
|
||||
resp, err := ClientPost(connTimeout, url, headers, data)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ct, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
return resp.StatusCode, json.Unmarshal(ct, &bind)
|
||||
}
|
||||
|
||||
func DefaultClientPostWithResp(bind any, url string, headers map[string]string, data any) (int, error) {
|
||||
return ClientPostWithResp(defaultTimeOut, bind, url, headers, data)
|
||||
}
|
||||
|
||||
func ClientPostWithRespWithCtx(ctx context.Context, connTimeout int, bind any, url string, headers map[string]string, data any) (int, error) {
|
||||
resp, err := ClientPostWithCtx(ctx, connTimeout, url, headers, data)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ct, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
return resp.StatusCode, json.Unmarshal(ct, &bind)
|
||||
}
|
||||
|
||||
func DefaultClientPostWithRespWithCtx(ctx context.Context, bind any, url string, headers map[string]string, data any) (int, error) {
|
||||
return ClientPostWithRespWithCtx(ctx, defaultTimeOut, bind, url, headers, data)
|
||||
}
|
||||
|
||||
func ClientPostWithRespWithProxy(connTimeout int, p *ProxyCfg, bind any, url string, headers map[string]string, data any) (int, error) {
|
||||
resp, err := ClientPostWithProxy(connTimeout, p, url, headers, data)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ct, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
return resp.StatusCode, json.Unmarshal(ct, &bind)
|
||||
}
|
||||
|
||||
func DefaultClientPostWithRespWithProxy(p *ProxyCfg, bind any, url string, headers map[string]string, data any) (int, error) {
|
||||
return ClientPostWithRespWithProxy(defaultTimeOut, p, &bind, url, headers, data)
|
||||
}
|
||||
|
||||
func getPostRequest(url string, headers map[string]string, data any) (*http.Request, error) {
|
||||
switch data.(type) {
|
||||
case []byte, string, *bytes.Reader, *bytes.Buffer:
|
||||
req, err := http.NewRequest(http.MethodPost, url, toReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
return req, nil
|
||||
default:
|
||||
}
|
||||
paramsValues := toUrlValues(data)
|
||||
if checkParamFile(paramsValues) {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
for k, v := range paramsValues {
|
||||
for _, vv := range v {
|
||||
// is file
|
||||
if k[0] == '@' {
|
||||
if err := addFormFile(writer, k[1:], vv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
_ = writer.WriteField(k, vv)
|
||||
}
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
if err = writer.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(paramsValues.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func toReader(v interface{}) *bytes.Reader {
|
||||
switch t := v.(type) {
|
||||
case []byte:
|
||||
return bytes.NewReader(t)
|
||||
case string:
|
||||
return bytes.NewReader([]byte(t))
|
||||
case *bytes.Buffer:
|
||||
return bytes.NewReader(t.Bytes())
|
||||
case *bytes.Reader:
|
||||
return t
|
||||
case nil:
|
||||
return bytes.NewReader(nil)
|
||||
default:
|
||||
panic("Invalid value")
|
||||
}
|
||||
}
|
||||
|
||||
// Does the params contain a file?
|
||||
func checkParamFile(params url.Values) bool {
|
||||
for k := range params {
|
||||
if k[0] == '@' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Add a file to a multipart writer.
|
||||
func addFormFile(writer *multipart.Writer, name, path string) error {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
part, err := writer.CreateFormFile(name, filepath.Base(path))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(part, file)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func ClientPostJson(connTimeout int, url string, headers map[string]string, data any) (*http.Response, error) {
|
||||
body, err := dataToJsonReader(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
cl := getClientByTimeoutSet(connTimeout)
|
||||
return cl.Do(req)
|
||||
}
|
||||
|
||||
func DefaultClientPostJson(url string, headers map[string]string, data any) (*http.Response, error) {
|
||||
return ClientPostJson(defaultTimeOut, url, headers, data)
|
||||
}
|
||||
|
||||
func ClientPostJsonWithProxy(connTimeout int, p *ProxyCfg, url string, headers map[string]string, params any) (*http.Response, error) {
|
||||
if p == nil {
|
||||
return nil, errProxyNil
|
||||
}
|
||||
pUrl, pHeaders, err := p.Build(url, headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ClientPostJson(connTimeout, pUrl, pHeaders, params)
|
||||
}
|
||||
|
||||
func DefaultClientPostJsonWithProxy(p *ProxyCfg, url string, headers map[string]string, params any) (*http.Response, error) {
|
||||
return ClientPostJsonWithProxy(defaultTimeOut, p, url, headers, params)
|
||||
}
|
||||
|
||||
func ClientPostJsonWithRespWithProxy(connTimeout int, p *ProxyCfg, bind any, url string, headers map[string]string, data any) (int, error) {
|
||||
resp, err := ClientPostJsonWithProxy(connTimeout, p, url, headers, data)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ct, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
return resp.StatusCode, json.Unmarshal(ct, &bind)
|
||||
}
|
||||
|
||||
func DefaultClientPostJsonWithRespWithProxy(p *ProxyCfg, bind any, url string, headers map[string]string, data any) (int, error) {
|
||||
return ClientPostJsonWithRespWithProxy(defaultTimeOut, p, bind, url, headers, data)
|
||||
}
|
||||
|
||||
func ClientPostJsonWithResp(connTimeout int, bind any, url string, headers map[string]string, data any) (int, error) {
|
||||
resp, err := ClientPostJson(connTimeout, url, headers, data)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ct, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
return resp.StatusCode, json.Unmarshal(ct, &bind)
|
||||
}
|
||||
|
||||
func DefaultClientPostJsonWithResp(bind any, url string, headers map[string]string, data any) (int, error) {
|
||||
return ClientPostJsonWithResp(defaultTimeOut, bind, url, headers, data)
|
||||
}
|
||||
|
||||
func ClientPostJsonWithCtx(ctx context.Context, connTimeout int, url string, headers map[string]string, data any) (*http.Response, error) {
|
||||
cl := getClientByTimeoutSet(connTimeout)
|
||||
body, err := dataToJsonReader(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
return cl.Do(req)
|
||||
}
|
||||
|
||||
func DefaultClientPostJsonWithCtx(ctx context.Context, url string, headers map[string]string, data any) (*http.Response, error) {
|
||||
return ClientPostJsonWithCtx(ctx, defaultTimeOut, url, headers, data)
|
||||
}
|
||||
|
||||
func ClientPostJsonWithRespWithCtx(ctx context.Context, bind any, connTimeout int, url string, headers map[string]string, data any) (int, error) {
|
||||
resp, err := ClientPostJsonWithCtx(ctx, connTimeout, url, headers, data)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ct, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
return resp.StatusCode, json.Unmarshal(ct, &bind)
|
||||
}
|
||||
|
||||
func DefaultClientPostJsonWithRespWithCtx(ctx context.Context, bind any, url string, headers map[string]string, data any) (int, error) {
|
||||
return ClientPostJsonWithRespWithCtx(ctx, bind, defaultTimeOut, url, headers, data)
|
||||
}
|
||||
|
||||
func dataToJsonReader(data any) (*bytes.Reader, error) {
|
||||
var body []byte
|
||||
switch t := data.(type) {
|
||||
case []byte:
|
||||
body = t
|
||||
case string:
|
||||
body = []byte(t)
|
||||
default:
|
||||
var err error
|
||||
body, err = json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return bytes.NewReader(body), nil
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/middleware/requestid"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ddliu/go-httpclient"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// HTTPClient htttp客户端
|
||||
type HTTPClient struct {
|
||||
client *httpclient.HttpClient
|
||||
}
|
||||
|
||||
func NewHTTPClient(client *httpclient.HttpClient) *HTTPClient {
|
||||
return &HTTPClient{client}
|
||||
}
|
||||
|
||||
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().WithOptions(httpclient.Map{
|
||||
httpclient.OPT_CONNECTTIMEOUT: 5,
|
||||
httpclient.OPT_TIMEOUT: 10,
|
||||
})
|
||||
return &HTTPClient{client: h}
|
||||
}
|
||||
|
||||
func NewCtx(ctx context.Context) *HTTPClient {
|
||||
reqID, _ := ctx.Value(requestid.ContextKey).(string)
|
||||
h := httpclient.NewHttpClient().WithOptions(httpclient.Map{
|
||||
httpclient.OPT_CONNECTTIMEOUT: 5,
|
||||
httpclient.OPT_TIMEOUT: 10,
|
||||
httpclient.OPT_CONTEXT: ctx,
|
||||
}).WithHeader(requestid.HeaderKey, reqID)
|
||||
|
||||
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: 5,
|
||||
})
|
||||
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_CONNECTTIMEOUT, 10)
|
||||
httpCliPool.cliPool[cliID] = h
|
||||
return &HTTPClient{client: h}
|
||||
}
|
||||
return &HTTPClient{client: h}
|
||||
}
|
||||
|
||||
func jsonUnmarshalResp(bind interface{}, resp *HTTPResponse) error {
|
||||
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
|
||||
}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
log.Error("http client Get method status code not ok", log.Any("url", url), log.Any("headers", headers), log.Any("params", params), log.Any("res", response))
|
||||
return nil, errors.New("http response satus code not ok statusCoe:" + response.Status)
|
||||
}
|
||||
data, err := response.ReadAll()
|
||||
if err != nil {
|
||||
log.ZapLog.Error("resp readAll errror", log.Any("Error", err))
|
||||
return data, err
|
||||
}
|
||||
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
|
||||
}
|
||||
defer func() { _ = resp.Body.Body.Close() }()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
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...)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
defer resp.Body.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
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
|
||||
}
|
||||
defer resp.Body.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
err = jsonUnmarshalResp(bind, resp)
|
||||
}
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
|
||||
// PPOSTWithJResp 结果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
|
||||
}
|
||||
defer resp.Body.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
err = jsonUnmarshalResp(bind, resp)
|
||||
}
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
@@ -0,0 +1,879 @@
|
||||
package imclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/crypt/ecb"
|
||||
"91porn-server/common/log"
|
||||
|
||||
"github.com/moul/http2curl"
|
||||
)
|
||||
|
||||
const (
|
||||
MessageTypeText = 0
|
||||
MessageTypeImage = 2
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Enable bool
|
||||
BaseURL string
|
||||
MerchantCode string
|
||||
TenantCode string
|
||||
AppKey string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
SignKey string
|
||||
AESKey string
|
||||
EnableSign bool
|
||||
EncryptTimestamp bool
|
||||
OS string
|
||||
OSType string
|
||||
BusinessType string
|
||||
Language string
|
||||
TokenTTL int64
|
||||
RequestTimeoutSeconds int
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
cfg Config
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Msg string `json:"msg"`
|
||||
Message string `json:"message"`
|
||||
Result string `json:"result"`
|
||||
}
|
||||
|
||||
type Attachment struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
ThumbURL string `json:"thumbUrl,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
Width int `json:"width,omitempty"`
|
||||
Height int `json:"height,omitempty"`
|
||||
Length int64 `json:"length,omitempty"`
|
||||
FileName string `json:"fileName,omitempty"`
|
||||
}
|
||||
|
||||
type AppTokenRequest struct {
|
||||
ClientID string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
OSType string `json:"osType"`
|
||||
TTL int64 `json:"ttl,omitempty"`
|
||||
}
|
||||
|
||||
type UserTokenRequest struct {
|
||||
ClientID string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
UserID int64 `json:"userId"`
|
||||
OSType string `json:"osType"`
|
||||
TTL int64 `json:"ttl,omitempty"`
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
ThirdPartyID string `json:"thirdPartyId"`
|
||||
Password string `json:"password"`
|
||||
Nickname string `json:"nickname,omitempty"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateBaseInfoRequest struct {
|
||||
UserID int64 `json:"userId"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ImgURL string `json:"imgUrl,omitempty"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
}
|
||||
|
||||
type UserBaseInfo struct {
|
||||
ID int64 `json:"id"`
|
||||
UserPhone string `json:"userPhone"`
|
||||
Name string `json:"name"`
|
||||
Sex int `json:"sex"`
|
||||
ImgURL string `json:"imgUrl"`
|
||||
Enable int `json:"enable"`
|
||||
CreateTime int64 `json:"createTime"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
type UpdatePasswordRequest struct {
|
||||
UserID int64 `json:"userId"`
|
||||
Pwd string `json:"pwd"`
|
||||
Code string `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateHeadImgRequest struct {
|
||||
UserID int64 `json:"userId"`
|
||||
ImgURL string `json:"imgUrl"`
|
||||
}
|
||||
|
||||
type SetOnlineStatusRequest struct {
|
||||
UserID int64 `json:"userId"`
|
||||
ShowOnlineStatus bool `json:"showOnlineStatus"`
|
||||
}
|
||||
|
||||
type BatchOnlineStatusRequest struct {
|
||||
UserIDs []int64 `json:"userIds"`
|
||||
}
|
||||
|
||||
type OnlineStatus struct {
|
||||
UserID int64 `json:"userId"`
|
||||
UserName string `json:"userName"`
|
||||
Visible bool `json:"visible"`
|
||||
Online bool `json:"online"`
|
||||
}
|
||||
|
||||
type Friend struct {
|
||||
FriendID int64 `json:"friendId"`
|
||||
Remark string `json:"remark"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
type AddFriendRequest struct {
|
||||
UserID int64 `json:"userId"`
|
||||
FriendID int64 `json:"friendId"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type DirectAddFriendRequest struct {
|
||||
UserID int64 `json:"userId"`
|
||||
FriendID int64 `json:"friendId"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Archive bool `json:"archive,omitempty"`
|
||||
}
|
||||
|
||||
type HandleFriendRequest struct {
|
||||
RequestID int64 `json:"requestId"`
|
||||
UserID int64 `json:"userId"`
|
||||
}
|
||||
|
||||
type PendingFriendRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
FriendID int64 `json:"friendId"`
|
||||
Message string `json:"message"`
|
||||
Status int `json:"status"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
HandleTime int64 `json:"handleTime"`
|
||||
ExpireTime int64 `json:"expireTime"`
|
||||
}
|
||||
|
||||
type AvailableFriend struct {
|
||||
UserID int64 `json:"userId"`
|
||||
FriendID int64 `json:"friendId"`
|
||||
ConvID string `json:"convId"`
|
||||
Remark string `json:"remark"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
DisturbStatus int `json:"disturbStatus"`
|
||||
Archive int `json:"archive"`
|
||||
}
|
||||
|
||||
type SetFriendArchiveRequest struct {
|
||||
UserID int64 `json:"userId"`
|
||||
FriendID int64 `json:"friendId"`
|
||||
Archive bool `json:"archive"`
|
||||
}
|
||||
|
||||
type SendMessageRequest struct {
|
||||
SenderID int64 `json:"senderId"`
|
||||
ReceiverID int64 `json:"receiverId"`
|
||||
Content string `json:"content"`
|
||||
MessageType int `json:"messageType"`
|
||||
ExtInfo string `json:"extInfo,omitempty"`
|
||||
ChannelType string `json:"channelType,omitempty"`
|
||||
Attachment *Attachment `json:"attachment,omitempty"`
|
||||
}
|
||||
|
||||
type BatchSendMessageRequest struct {
|
||||
SenderID int64 `json:"senderId"`
|
||||
ReceiverIDSet []int64 `json:"receiverIdSet"`
|
||||
Content string `json:"content"`
|
||||
MessageType int `json:"messageType"`
|
||||
ExtInfo string `json:"extInfo,omitempty"`
|
||||
ChannelType string `json:"channelType,omitempty"`
|
||||
Attachment *Attachment `json:"attachment,omitempty"`
|
||||
}
|
||||
|
||||
type OnlinePassthroughRequest struct {
|
||||
SenderID int64 `json:"senderId,omitempty"`
|
||||
ReceiverIDSet []int64 `json:"receiverIdSet"`
|
||||
PassthroughType string `json:"passthroughType,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ExtInfo string `json:"extInfo,omitempty"`
|
||||
ChannelType string `json:"channelType,omitempty"`
|
||||
}
|
||||
|
||||
type AppPassthroughRequest struct {
|
||||
SenderID int64 `json:"senderId,omitempty"`
|
||||
PassthroughType string `json:"passthroughType,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ExtInfo string `json:"extInfo,omitempty"`
|
||||
ChannelType string `json:"channelType,omitempty"`
|
||||
}
|
||||
|
||||
type PassthroughResult struct {
|
||||
MessageID string `json:"messageId"`
|
||||
MessageType int `json:"messageType"`
|
||||
ReceiverCount int `json:"receiverCount"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
type MessageSendResult struct {
|
||||
MessageID string `json:"messageId"`
|
||||
ConvID string `json:"convId"`
|
||||
Seq int64 `json:"seq"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
type HistoryMessageRequest struct {
|
||||
UserID1 int64 `json:"userId1"`
|
||||
UserID2 int64 `json:"userId2"`
|
||||
StartTime int64 `json:"startTime,omitempty"`
|
||||
EndTime int64 `json:"endTime,omitempty"`
|
||||
StartSeq int64 `json:"startSeq,omitempty"`
|
||||
EndSeq int64 `json:"endSeq,omitempty"`
|
||||
Direction string `json:"direction,omitempty"`
|
||||
Size int `json:"size,omitempty"`
|
||||
}
|
||||
|
||||
type MessageDetail struct {
|
||||
MessageID string `json:"messageId"`
|
||||
SenderID int64 `json:"senderId"`
|
||||
ReceiverID int64 `json:"receiverId"`
|
||||
Content string `json:"content"`
|
||||
MessageType string `json:"messageType"`
|
||||
Seq int64 `json:"seq"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
type ModifyMessageRequest struct {
|
||||
MessageID string `json:"messageId"`
|
||||
ConvID string `json:"convId"`
|
||||
SenderID int64 `json:"senderId"`
|
||||
ReceiverID int64 `json:"receiverId"`
|
||||
ModifierID int64 `json:"modifierId"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ExtInfo string `json:"extInfo,omitempty"`
|
||||
}
|
||||
|
||||
type HideInboxRequest struct {
|
||||
UserID int64 `json:"userId"`
|
||||
ConvID string `json:"convId"`
|
||||
StartSeq int64 `json:"startSeq"`
|
||||
EndSeq int64 `json:"endSeq"`
|
||||
}
|
||||
|
||||
func New(cfg Config) *Client {
|
||||
timeout := cfg.RequestTimeoutSeconds
|
||||
if timeout <= 0 {
|
||||
timeout = 10
|
||||
}
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
httpClient: &http.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Enabled() bool {
|
||||
return c.DisabledReason() == ""
|
||||
}
|
||||
|
||||
// DisabledReason 返回 SDK 不可用的具体原因(未开启 / 缺失的配置项);可用时返回 ""。
|
||||
func (c *Client) DisabledReason() string {
|
||||
if c == nil {
|
||||
return "im client is nil"
|
||||
}
|
||||
if !c.cfg.Enable {
|
||||
return "config Enable=false"
|
||||
}
|
||||
var missing []string
|
||||
if c.cfg.BaseURL == "" {
|
||||
missing = append(missing, "BaseURL")
|
||||
}
|
||||
if c.cfg.MerchantCode == "" {
|
||||
missing = append(missing, "MerchantCode")
|
||||
}
|
||||
if c.tenantCode() == "" {
|
||||
missing = append(missing, "TenantCode/AppKey")
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
missingStr := "missing config: " + strings.Join(missing, ", ")
|
||||
log.Error(missingStr)
|
||||
return missingStr
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Client) AppToken() (string, error) {
|
||||
req := AppTokenRequest{
|
||||
ClientID: c.cfg.ClientID,
|
||||
ClientSecret: c.cfg.ClientSecret,
|
||||
OSType: c.osType(),
|
||||
TTL: c.tokenTTL(),
|
||||
}
|
||||
return c.postString("/authenticate/token.e", "", req)
|
||||
}
|
||||
|
||||
func (c *Client) UserToken(userID int64) (string, error) {
|
||||
req := UserTokenRequest{
|
||||
ClientID: c.cfg.ClientID,
|
||||
ClientSecret: c.cfg.ClientSecret,
|
||||
UserID: userID,
|
||||
OSType: c.osType(),
|
||||
TTL: c.tokenTTL(),
|
||||
}
|
||||
return c.postString("/authenticate/user/token.e", "", req)
|
||||
}
|
||||
|
||||
func (c *Client) Register(req RegisterRequest, token string) (int64, error) {
|
||||
var userID int64
|
||||
if err := c.postData("/user/register", token, req, &userID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
func (c *Client) UpdateBaseInfo(req UpdateBaseInfoRequest, token string) error {
|
||||
var ok bool
|
||||
return c.postData("/user/updateBaseInfo", token, req, &ok)
|
||||
}
|
||||
|
||||
func (c *Client) UserBaseInfo(userID int64, token string) (*UserBaseInfo, error) {
|
||||
query := url.Values{}
|
||||
query.Set("userId", strconv.FormatInt(userID, 10))
|
||||
var info UserBaseInfo
|
||||
if err := c.getData("/user/baseUserInfo", token, query, nil, &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func (c *Client) UpdatePassword(req UpdatePasswordRequest, token string) error {
|
||||
return c.postData("/user/updatePwd", token, req, nil)
|
||||
}
|
||||
|
||||
func (c *Client) UpdateHeadImg(req UpdateHeadImgRequest, token string) error {
|
||||
var ok bool
|
||||
return c.postData("/user/updateHeadImg", token, req, &ok)
|
||||
}
|
||||
|
||||
func (c *Client) SetOnlineStatus(req SetOnlineStatusRequest, token string) error {
|
||||
var ok bool
|
||||
return c.postData("/user/onlineStatus/set", token, req, &ok)
|
||||
}
|
||||
|
||||
func (c *Client) BatchOnlineStatus(req BatchOnlineStatusRequest, token string) ([]OnlineStatus, error) {
|
||||
var statuses []OnlineStatus
|
||||
if err := c.postData("/user/onlineStatus/batch", token, req, &statuses); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func (c *Client) FriendList(userID, now int64, token string) ([]Friend, error) {
|
||||
query := url.Values{}
|
||||
query.Set("userId", strconv.FormatInt(userID, 10))
|
||||
if now > 0 {
|
||||
query.Set("now", strconv.FormatInt(now, 10))
|
||||
}
|
||||
headers := map[string]string{
|
||||
"userId": strconv.FormatInt(userID, 10),
|
||||
}
|
||||
var friends []Friend
|
||||
if err := c.getData("/friend/list", token, query, headers, &friends); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return friends, nil
|
||||
}
|
||||
|
||||
func (c *Client) AddFriend(req AddFriendRequest, token string) (int64, error) {
|
||||
var requestID int64
|
||||
if err := c.postData("/friend/add", token, req, &requestID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return requestID, nil
|
||||
}
|
||||
|
||||
func (c *Client) DirectAddFriend(req DirectAddFriendRequest, token string) error {
|
||||
var ok bool
|
||||
return c.postData("/friend/add/direct", token, req, &ok)
|
||||
}
|
||||
|
||||
func (c *Client) ConfirmFriend(req HandleFriendRequest, token string) error {
|
||||
var ok bool
|
||||
return c.postData("/friend/confirm", token, req, &ok)
|
||||
}
|
||||
|
||||
func (c *Client) RejectFriend(req HandleFriendRequest, token string) error {
|
||||
var ok bool
|
||||
return c.postData("/friend/reject", token, req, &ok)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteFriend(userID, friendID int64, token string) error {
|
||||
query := url.Values{}
|
||||
query.Set("userId", strconv.FormatInt(userID, 10))
|
||||
query.Set("friendId", strconv.FormatInt(friendID, 10))
|
||||
var ok bool
|
||||
return c.getData("/friend/delete", token, query, nil, &ok)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteAllFriends(userID int64, token string) error {
|
||||
query := url.Values{}
|
||||
query.Set("userId", strconv.FormatInt(userID, 10))
|
||||
var ok bool
|
||||
return c.getData("/allFriend/delete", token, query, nil, &ok)
|
||||
}
|
||||
|
||||
func (c *Client) PendingFriendRequests(userID, now int64, token string) ([]PendingFriendRequest, error) {
|
||||
query := url.Values{}
|
||||
query.Set("userId", strconv.FormatInt(userID, 10))
|
||||
if now > 0 {
|
||||
query.Set("now", strconv.FormatInt(now, 10))
|
||||
}
|
||||
var requests []PendingFriendRequest
|
||||
if err := c.getData("/friend/pending", token, query, nil, &requests); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return requests, nil
|
||||
}
|
||||
|
||||
func (c *Client) AvailableFriends(userID int64, token string) ([]AvailableFriend, error) {
|
||||
query := url.Values{}
|
||||
query.Set("userId", strconv.FormatInt(userID, 10))
|
||||
var friends []AvailableFriend
|
||||
if err := c.getData("/friend/available/list", token, query, nil, &friends); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return friends, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetFriendArchive(req SetFriendArchiveRequest, token string) error {
|
||||
var ok bool
|
||||
return c.postData("/friend/archive", token, req, &ok)
|
||||
}
|
||||
|
||||
func (c *Client) SendMessage(req SendMessageRequest, token string) error {
|
||||
_, err := c.SendMessageWithResult(req, token)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) SendMessageWithResult(req SendMessageRequest, token string) (*MessageSendResult, error) {
|
||||
if req.ExtInfo == "" {
|
||||
req.ExtInfo = "{}"
|
||||
}
|
||||
if req.ChannelType == "" {
|
||||
req.ChannelType = "UNKNOW"
|
||||
}
|
||||
var result MessageSendResult
|
||||
if err := c.postData("/message/send", token, req, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *Client) BatchSendMessage(req BatchSendMessageRequest, token string) error {
|
||||
_, err := c.BatchSendMessageWithResult(req, token)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) BatchSendMessageWithResult(req BatchSendMessageRequest, token string) (json.RawMessage, error) {
|
||||
if req.ExtInfo == "" {
|
||||
req.ExtInfo = "{}"
|
||||
}
|
||||
if req.ChannelType == "" {
|
||||
req.ChannelType = "UNKNOW"
|
||||
}
|
||||
var data json.RawMessage
|
||||
if err := c.postData("/message/send/batch", token, req, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *Client) HistoryMessages(req HistoryMessageRequest, token string) ([]MessageDetail, error) {
|
||||
var messages []MessageDetail
|
||||
if err := c.postData("/message/history", token, req, &messages); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func (c *Client) MessageDetail(messageID, convID, token string) (*MessageDetail, error) {
|
||||
query := url.Values{}
|
||||
query.Set("messageId", messageID)
|
||||
query.Set("convId", convID)
|
||||
var detail MessageDetail
|
||||
if err := c.getData("/message/detail", token, query, nil, &detail); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &detail, nil
|
||||
}
|
||||
|
||||
func (c *Client) ModifyMessage(req ModifyMessageRequest, token string) (string, error) {
|
||||
var messageID string
|
||||
if err := c.postData("/message/modify", token, req, &messageID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return messageID, nil
|
||||
}
|
||||
|
||||
func (c *Client) HideInbox(req HideInboxRequest, token string) (int64, error) {
|
||||
var count int64
|
||||
if err := c.postData("/message/inbox/hide", token, req, &count); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (c *Client) SendOnlinePassthrough(req OnlinePassthroughRequest, token string) (*PassthroughResult, error) {
|
||||
if len(req.ReceiverIDSet) == 0 {
|
||||
return nil, errors.New("receiverIdSet is empty")
|
||||
}
|
||||
normalizePassthrough(&req.PassthroughType, &req.ExtInfo, &req.ChannelType)
|
||||
var result PassthroughResult
|
||||
if err := c.postData("/message/passthrough/send", token, req, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *Client) SendAppPassthrough(req AppPassthroughRequest, token string) (*PassthroughResult, error) {
|
||||
normalizePassthrough(&req.PassthroughType, &req.ExtInfo, &req.ChannelType)
|
||||
var result PassthroughResult
|
||||
if err := c.postData("/message/passthrough/send/batch/all", token, req, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func normalizePassthrough(passthroughType, extInfo, channelType *string) {
|
||||
if *passthroughType == "" {
|
||||
*passthroughType = "AD_NOTIFY"
|
||||
}
|
||||
if *extInfo == "" {
|
||||
*extInfo = "{}"
|
||||
}
|
||||
if *channelType == "" {
|
||||
*channelType = "UNKNOW"
|
||||
}
|
||||
}
|
||||
|
||||
func marshalRequestBody(data any) ([]byte, error) {
|
||||
switch req := data.(type) {
|
||||
case AppTokenRequest:
|
||||
return marshalOrderedJSON(map[string]any{
|
||||
"clientId": req.ClientID,
|
||||
"clientSecret": req.ClientSecret,
|
||||
"osType": req.OSType,
|
||||
"ttl": req.TTL,
|
||||
})
|
||||
case UserTokenRequest:
|
||||
return marshalOrderedJSON(map[string]any{
|
||||
"clientId": req.ClientID,
|
||||
"clientSecret": req.ClientSecret,
|
||||
"osType": req.OSType,
|
||||
"ttl": req.TTL,
|
||||
"userId": req.UserID,
|
||||
})
|
||||
case RegisterRequest:
|
||||
fields := map[string]any{
|
||||
"password": req.Password,
|
||||
"thirdPartyId": req.ThirdPartyID,
|
||||
}
|
||||
if req.Nickname != "" {
|
||||
fields["nickname"] = req.Nickname
|
||||
}
|
||||
if req.Avatar != "" {
|
||||
fields["avatar"] = req.Avatar
|
||||
}
|
||||
return marshalOrderedJSON(fields)
|
||||
default:
|
||||
return json.Marshal(data)
|
||||
}
|
||||
}
|
||||
|
||||
func marshalOrderedJSON(fields map[string]any) ([]byte, error) {
|
||||
keys := make([]string, 0, len(fields))
|
||||
for key, value := range fields {
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
if s, ok := value.(string); ok && s == "" {
|
||||
continue
|
||||
}
|
||||
if n, ok := value.(int64); ok && n <= 0 {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte('{')
|
||||
for i, key := range keys {
|
||||
if i > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
keyJSON, err := json.Marshal(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valueJSON, err := json.Marshal(fields[key])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf.Write(keyJSON)
|
||||
buf.WriteByte(':')
|
||||
buf.Write(valueJSON)
|
||||
}
|
||||
buf.WriteByte('}')
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (c *Client) postString(path, token string, data any) (string, error) {
|
||||
var out string
|
||||
if err := c.postData(path, token, data, &out); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Client) postData(path, token string, data any, out any) error {
|
||||
if !c.Enabled() {
|
||||
return errors.New("im client is disabled")
|
||||
}
|
||||
body, err := marshalRequestBody(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.do(http.MethodPost, path, token, nil, nil, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return decodeResponseData(resp, out)
|
||||
}
|
||||
|
||||
func (c *Client) getData(path, token string, query url.Values, headers map[string]string, out any) error {
|
||||
if !c.Enabled() {
|
||||
return errors.New("im client is disabled")
|
||||
}
|
||||
resp, err := c.do(http.MethodGet, path, token, query, headers, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return decodeResponseData(resp, out)
|
||||
}
|
||||
|
||||
func decodeResponseData(resp *Response, out any) error {
|
||||
if out == nil || len(resp.Data) == 0 || bytes.Equal(resp.Data, []byte("null")) {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(resp.Data, out)
|
||||
}
|
||||
|
||||
func (c *Client) do(method, path, token string, query url.Values, headers map[string]string, body []byte) (*Response, error) {
|
||||
fullURL, queryString := c.endpoint(path, query)
|
||||
req, err := http.NewRequest(method, fullURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("X-Merchant-Code", c.cfg.MerchantCode)
|
||||
req.Header.Set("X-App-Key", c.appKey())
|
||||
req.Header.Set("X-Client-Id", c.cfg.ClientID)
|
||||
if token != "" {
|
||||
req.Header.Set("token", token)
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
for k, v := range c.baseHeaders(queryString, body) {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
start := time.Now()
|
||||
httpResp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
log.Warn("im sdk request error", log.Any("method", method), log.Any("path", path), log.Any("costMs", time.Since(start).Milliseconds()), log.Any("curl", requestCurl(req, body)), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
respBody, err := io.ReadAll(httpResp.Body)
|
||||
cost := time.Since(start)
|
||||
if err != nil {
|
||||
log.Warn("im sdk read body error", log.Any("method", method), log.Any("path", path), log.Any("costMs", cost.Milliseconds()), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
log.Info("im sdk request", log.Any("method", method), log.Any("path", path), log.Any("status", httpResp.StatusCode), log.Any("costMs", cost.Milliseconds()))
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
log.Warn("im sdk http status error", log.Any("method", method), log.Any("path", path), log.Any("status", httpResp.StatusCode), log.Any("curl", requestCurl(req, body)), log.Any("resp", string(respBody)))
|
||||
return nil, apiErrorFromResponse(httpResp.StatusCode, respBody)
|
||||
}
|
||||
var resp Response
|
||||
if err = json.Unmarshal(respBody, &resp); err != nil {
|
||||
log.Warn("im sdk resp unmarshal error", log.Any("method", method), log.Any("path", path), log.Any("curl", requestCurl(req, body)), log.Any("resp", string(respBody)), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
if resp.Code != 0 {
|
||||
log.Warn("im sdk api error", log.Any("method", method), log.Any("path", path), log.Any("code", resp.Code), log.Any("curl", requestCurl(req, body)), log.Any("resp", string(respBody)))
|
||||
return nil, &APIError{
|
||||
StatusCode: httpResp.StatusCode,
|
||||
Code: resp.Code,
|
||||
Message: firstNonEmpty(resp.Message, resp.Msg, resp.Result),
|
||||
Body: respBody,
|
||||
}
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// requestCurl 把请求还原成 curl 命令字符串,用于报错时打印复现。
|
||||
// body 为请求体原始字节:Do 已消费 req.Body,这里用 body 重新填充再生成。
|
||||
func requestCurl(req *http.Request, body []byte) string {
|
||||
req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
cmd, err := http2curl.GetCurlCommand(req)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return cmd.String()
|
||||
}
|
||||
|
||||
func apiErrorFromResponse(statusCode int, body []byte) *APIError {
|
||||
resp := Response{}
|
||||
_ = json.Unmarshal(body, &resp)
|
||||
return &APIError{
|
||||
StatusCode: statusCode,
|
||||
Code: resp.Code,
|
||||
Message: firstNonEmpty(resp.Message, resp.Msg, resp.Result, string(body)),
|
||||
Body: body,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) endpoint(path string, query url.Values) (string, string) {
|
||||
base := strings.TrimRight(c.cfg.BaseURL, "/")
|
||||
path = strings.TrimLeft(path, "/")
|
||||
u := fmt.Sprintf("%s/api/endpoint/%s/%s/%s", base, url.PathEscape(c.cfg.MerchantCode), url.PathEscape(c.tenantCode()), path)
|
||||
if len(query) == 0 {
|
||||
return u, ""
|
||||
}
|
||||
queryString := query.Encode()
|
||||
return u + "?" + queryString, queryString
|
||||
}
|
||||
|
||||
func (c *Client) baseHeaders(queryString string, body []byte) map[string]string {
|
||||
headers := map[string]string{
|
||||
"os": c.os(),
|
||||
"osType": c.osType(),
|
||||
"businessType": c.businessType(),
|
||||
"language": c.language(),
|
||||
}
|
||||
if !c.cfg.EnableSign || c.cfg.SignKey == "" {
|
||||
return headers
|
||||
}
|
||||
timestamps := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
headers["timestamps"] = timestamps
|
||||
signTimestamp := timestamps
|
||||
if c.cfg.EncryptTimestamp && c.cfg.AESKey != "" {
|
||||
if encrypted := encryptTimestamp(timestamps, c.cfg.AESKey); encrypted != "" {
|
||||
headers["envTimestamps"] = encrypted
|
||||
}
|
||||
}
|
||||
signBody := queryString
|
||||
if len(body) > 0 {
|
||||
signBody += string(body)
|
||||
}
|
||||
sum := md5.Sum([]byte(signBody + signTimestamp + c.cfg.SignKey))
|
||||
headers["sign"] = hex.EncodeToString(sum[:])
|
||||
return headers
|
||||
}
|
||||
|
||||
func (c *Client) os() string {
|
||||
if c.cfg.OS != "" {
|
||||
return c.cfg.OS
|
||||
}
|
||||
return "web"
|
||||
}
|
||||
|
||||
func (c *Client) osType() string {
|
||||
if c.cfg.OSType != "" {
|
||||
return c.cfg.OSType
|
||||
}
|
||||
return "web"
|
||||
}
|
||||
|
||||
func (c *Client) businessType() string {
|
||||
if c.cfg.BusinessType != "" {
|
||||
return c.cfg.BusinessType
|
||||
}
|
||||
return "im_sdk_customer"
|
||||
}
|
||||
|
||||
func (c *Client) language() string {
|
||||
if c.cfg.Language != "" {
|
||||
return c.cfg.Language
|
||||
}
|
||||
return "zh-cn"
|
||||
}
|
||||
|
||||
func (c *Client) tokenTTL() int64 {
|
||||
if c.cfg.TokenTTL > 0 {
|
||||
return c.cfg.TokenTTL
|
||||
}
|
||||
return 86400
|
||||
}
|
||||
|
||||
func (c *Client) appKey() string {
|
||||
if c.cfg.AppKey != "" {
|
||||
return c.cfg.AppKey
|
||||
}
|
||||
if c.cfg.MerchantCode == "" || c.cfg.TenantCode == "" {
|
||||
return ""
|
||||
}
|
||||
return c.cfg.MerchantCode + "#" + c.cfg.TenantCode
|
||||
}
|
||||
|
||||
func (c *Client) tenantCode() string {
|
||||
if c.cfg.TenantCode != "" {
|
||||
return c.cfg.TenantCode
|
||||
}
|
||||
return c.cfg.AppKey
|
||||
}
|
||||
|
||||
func encryptTimestamp(timestamps, key string) string {
|
||||
block, err := aes.NewCipher([]byte(key))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
src := zeroPadding([]byte(timestamps), block.BlockSize())
|
||||
dst := make([]byte, len(src))
|
||||
ecb.NewECBEncrypter(block).CryptBlocks(dst, src)
|
||||
return base64.StdEncoding.EncodeToString(dst)
|
||||
}
|
||||
|
||||
func zeroPadding(src []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(src)%blockSize
|
||||
if padding == blockSize {
|
||||
padding = blockSize
|
||||
}
|
||||
return append(src, bytes.Repeat([]byte{0}, padding)...)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package imclient
|
||||
|
||||
const (
|
||||
DefaultSignKey = "shDOUArrDhpeAMw9FGY79Zmy3MLWwNWy"
|
||||
DefaultAESKey = "4d5bc50346c22dde12be2c3b1b89ada6"
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
package imclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const sessionExpiredCode = 401
|
||||
|
||||
type APIError struct {
|
||||
StatusCode int
|
||||
Code int
|
||||
Message string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
if e.Code != 0 {
|
||||
return fmt.Sprintf("im api error: status=%d code=%d message=%s", e.StatusCode, e.Code, e.Message)
|
||||
}
|
||||
return fmt.Sprintf("im api error: status=%d message=%s", e.StatusCode, e.Message)
|
||||
}
|
||||
|
||||
func IsSessionExpired(err error) bool {
|
||||
apiErr, ok := err.(*APIError)
|
||||
if !ok || apiErr == nil {
|
||||
return false
|
||||
}
|
||||
if apiErr.Code == sessionExpiredCode || apiErr.StatusCode == http.StatusUnauthorized {
|
||||
return true
|
||||
}
|
||||
msg := strings.ToLower(apiErr.Message)
|
||||
return strings.Contains(msg, "session has expired") ||
|
||||
strings.Contains(msg, "log in again") ||
|
||||
strings.Contains(msg, "token expired") ||
|
||||
strings.Contains(msg, "token invalid")
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
package imclient
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protowire"
|
||||
)
|
||||
|
||||
type historyMessageWire struct {
|
||||
MessageID json.RawMessage `json:"messageId"`
|
||||
ID json.RawMessage `json:"id"`
|
||||
MsgID json.RawMessage `json:"msgId"`
|
||||
MID json.RawMessage `json:"mid"`
|
||||
SenderID int64 `json:"senderId"`
|
||||
ReceiverID int64 `json:"receiverId"`
|
||||
Content string `json:"content"`
|
||||
Text string `json:"text"`
|
||||
Body string `json:"body"`
|
||||
MsgContent string `json:"msgContent"`
|
||||
MessageContent string `json:"messageContent"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
ExtInfo json.RawMessage `json:"extInfo"`
|
||||
Attachment json.RawMessage `json:"attachment"`
|
||||
Message json.RawMessage `json:"message"`
|
||||
MessageType json.RawMessage `json:"messageType"`
|
||||
Seq int64 `json:"seq"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (m *MessageDetail) UnmarshalJSON(data []byte) error {
|
||||
var wire historyMessageWire
|
||||
if err := json.Unmarshal(data, &wire); err != nil {
|
||||
return err
|
||||
}
|
||||
m.SenderID = wire.SenderID
|
||||
m.ReceiverID = wire.ReceiverID
|
||||
m.Seq = wire.Seq
|
||||
m.CreatedAt = wire.CreatedAt
|
||||
m.MessageID = firstNonEmpty(
|
||||
jsonScalarString(wire.MessageID),
|
||||
jsonScalarString(wire.MsgID),
|
||||
jsonScalarString(wire.MID),
|
||||
jsonScalarString(wire.ID),
|
||||
)
|
||||
m.MessageType = normalizeHistoryMessageType(wire.MessageType)
|
||||
if m.MessageType == "" {
|
||||
m.MessageType = extractPayloadMessageType(wire.Payload)
|
||||
}
|
||||
m.Content = firstNonEmpty(
|
||||
strings.TrimSpace(wire.Content),
|
||||
strings.TrimSpace(wire.Text),
|
||||
strings.TrimSpace(wire.Body),
|
||||
strings.TrimSpace(wire.MsgContent),
|
||||
strings.TrimSpace(wire.MessageContent),
|
||||
extractPayloadContent(wire.Payload, m.MessageID, strconv.FormatInt(m.SenderID, 10), strconv.FormatInt(m.ReceiverID, 10)),
|
||||
extractPayloadContent(wire.ExtInfo, m.MessageID, strconv.FormatInt(m.SenderID, 10), strconv.FormatInt(m.ReceiverID, 10)),
|
||||
extractAttachmentContent(wire.Attachment),
|
||||
)
|
||||
if nested := decodeHistoryMessageWire(wire.Message); nested != nil {
|
||||
if m.MessageID == "" {
|
||||
m.MessageID = nested.messageID
|
||||
}
|
||||
if m.Content == "" {
|
||||
m.Content = nested.content
|
||||
}
|
||||
if m.MessageType == "" {
|
||||
m.MessageType = nested.messageType
|
||||
}
|
||||
}
|
||||
if m.MessageID == "" && m.Seq > 0 {
|
||||
m.MessageID = strconv.FormatInt(m.Seq, 10)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type decodedHistoryMessage struct {
|
||||
messageID string
|
||||
content string
|
||||
messageType string
|
||||
}
|
||||
|
||||
func decodeHistoryMessageWire(raw json.RawMessage) *decodedHistoryMessage {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return nil
|
||||
}
|
||||
var nested historyMessageWire
|
||||
if err := json.Unmarshal(raw, &nested); err != nil {
|
||||
return nil
|
||||
}
|
||||
msg := &decodedHistoryMessage{
|
||||
messageID: firstNonEmpty(
|
||||
jsonScalarString(nested.MessageID),
|
||||
jsonScalarString(nested.MsgID),
|
||||
jsonScalarString(nested.MID),
|
||||
jsonScalarString(nested.ID),
|
||||
),
|
||||
messageType: normalizeHistoryMessageType(nested.MessageType),
|
||||
}
|
||||
if msg.messageType == "" {
|
||||
msg.messageType = extractPayloadMessageType(nested.Payload)
|
||||
}
|
||||
msg.content = firstNonEmpty(
|
||||
strings.TrimSpace(nested.Content),
|
||||
strings.TrimSpace(nested.Text),
|
||||
strings.TrimSpace(nested.Body),
|
||||
strings.TrimSpace(nested.MsgContent),
|
||||
strings.TrimSpace(nested.MessageContent),
|
||||
extractPayloadContent(nested.Payload, msg.messageID, strconv.FormatInt(nested.SenderID, 10), strconv.FormatInt(nested.ReceiverID, 10)),
|
||||
extractPayloadContent(nested.ExtInfo, msg.messageID, strconv.FormatInt(nested.SenderID, 10), strconv.FormatInt(nested.ReceiverID, 10)),
|
||||
extractAttachmentContent(nested.Attachment),
|
||||
)
|
||||
if msg.messageID == "" && msg.content == "" && msg.messageType == "" {
|
||||
return nil
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func jsonScalarString(raw json.RawMessage) string {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return ""
|
||||
}
|
||||
var s string
|
||||
if err := json.Unmarshal(raw, &s); err == nil {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
var n json.Number
|
||||
if err := json.Unmarshal(raw, &n); err == nil {
|
||||
return n.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractPayloadContent(raw json.RawMessage, excludes ...string) string {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return ""
|
||||
}
|
||||
var s string
|
||||
if err := json.Unmarshal(raw, &s); err == nil {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
var payload map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, key := range []string{"text", "content", "body"} {
|
||||
if value := jsonScalarString(payload[key]); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
if value := jsonScalarString(payload["data"]); value != "" {
|
||||
if content := extractProtobufPayloadText(value, excludes...); content != "" {
|
||||
return content
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractPayloadMessageType(raw json.RawMessage) string {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return ""
|
||||
}
|
||||
var payload map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return ""
|
||||
}
|
||||
return normalizeHistoryMessageType(payload["type"])
|
||||
}
|
||||
|
||||
func extractAttachmentContent(raw json.RawMessage) string {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return ""
|
||||
}
|
||||
var attachment map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &attachment); err != nil {
|
||||
return ""
|
||||
}
|
||||
if fileName := jsonScalarString(attachment["fileName"]); fileName != "" {
|
||||
return fileName
|
||||
}
|
||||
if url := jsonScalarString(attachment["url"]); url != "" {
|
||||
return url
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeHistoryMessageType(raw json.RawMessage) string {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return ""
|
||||
}
|
||||
var s string
|
||||
if err := json.Unmarshal(raw, &s); err == nil {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
var n int
|
||||
if err := json.Unmarshal(raw, &n); err == nil {
|
||||
return historyMessageTypeName(n)
|
||||
}
|
||||
return strings.TrimSpace(string(raw))
|
||||
}
|
||||
|
||||
func historyMessageTypeName(messageType int) string {
|
||||
switch messageType {
|
||||
case MessageTypeText:
|
||||
return "TEXT"
|
||||
case 1:
|
||||
return "AUDIO"
|
||||
case MessageTypeImage:
|
||||
return "IMAGE"
|
||||
case 3:
|
||||
return "VIDEO"
|
||||
case 4:
|
||||
return "FILE"
|
||||
case 5:
|
||||
return "EMOJI"
|
||||
case 100:
|
||||
return "CUSTOMIZED"
|
||||
default:
|
||||
return strconv.Itoa(messageType)
|
||||
}
|
||||
}
|
||||
|
||||
func extractProtobufPayloadText(encoded string, excludes ...string) string {
|
||||
raw, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
excludeSet := make(map[string]struct{}, len(excludes))
|
||||
for _, item := range excludes {
|
||||
if item = strings.TrimSpace(item); item != "" {
|
||||
excludeSet[item] = struct{}{}
|
||||
}
|
||||
}
|
||||
candidates := make([]string, 0, 4)
|
||||
collectProtoTextCandidates(raw, 0, excludeSet, &candidates)
|
||||
if len(candidates) == 0 {
|
||||
return ""
|
||||
}
|
||||
return candidates[len(candidates)-1]
|
||||
}
|
||||
|
||||
func collectProtoTextCandidates(raw []byte, depth int, excludes map[string]struct{}, candidates *[]string) {
|
||||
if len(raw) == 0 || depth > 8 {
|
||||
return
|
||||
}
|
||||
for len(raw) > 0 {
|
||||
_, typ, n := protowire.ConsumeTag(raw)
|
||||
if n < 0 {
|
||||
return
|
||||
}
|
||||
raw = raw[n:]
|
||||
switch typ {
|
||||
case protowire.BytesType:
|
||||
value, m := protowire.ConsumeBytes(raw)
|
||||
if m < 0 {
|
||||
return
|
||||
}
|
||||
if candidate := protoStringCandidate(value, excludes); candidate != "" {
|
||||
*candidates = append(*candidates, candidate)
|
||||
}
|
||||
collectProtoTextCandidates(value, depth+1, excludes, candidates)
|
||||
raw = raw[m:]
|
||||
default:
|
||||
m := protowire.ConsumeFieldValue(0, typ, raw)
|
||||
if m < 0 {
|
||||
return
|
||||
}
|
||||
raw = raw[m:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func protoStringCandidate(raw []byte, excludes map[string]struct{}) string {
|
||||
if len(raw) == 0 || !utf8.Valid(raw) {
|
||||
return ""
|
||||
}
|
||||
value := strings.TrimSpace(string(raw))
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
if _, ok := excludes[value]; ok {
|
||||
return ""
|
||||
}
|
||||
if isLongNumber(value) {
|
||||
return ""
|
||||
}
|
||||
for _, r := range value {
|
||||
if unicode.IsControl(r) && r != '\n' && r != '\r' && r != '\t' {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func isLongNumber(value string) bool {
|
||||
if len(value) < 9 {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultCity = "广州"
|
||||
defaultProvince = "广东"
|
||||
ipNewURL = "https://loc.ztgba.com/self/getLocationByIp"
|
||||
)
|
||||
|
||||
// 老接口
|
||||
type LocationResp struct {
|
||||
RegionName string `json:"region_name"`
|
||||
CityName string `json:"city_name"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
func GetLocationByIP(ip string) (string, string) {
|
||||
|
||||
return "-", "-"
|
||||
|
||||
// var params = map[string]string{"ip": ip}
|
||||
// loc := LocationResp{}
|
||||
// c, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
// defer cancel()
|
||||
// if _, err := httputil.DefaultClientPostJsonWithRespWithCtx(c, &loc, ipNewURL, nil, params); err != nil {
|
||||
// log.Error("GetLocationByIP error", log.Any("ip", ip), log.E(err))
|
||||
// return defaultCity, defaultProvince
|
||||
// }
|
||||
// if (len(loc.RegionName) == 0 && len(loc.CityName) == 0) || loc.RegionName == "局域网" || loc.RegionName == "柬埔寨" {
|
||||
// return defaultCity, defaultProvince
|
||||
// }
|
||||
// if len(loc.CityName) == 0 {
|
||||
// return loc.RegionName, loc.RegionName
|
||||
// }
|
||||
// return loc.CityName, loc.RegionName
|
||||
}
|
||||
|
||||
type CountryResp struct {
|
||||
Country string `json:"country_name"`
|
||||
RegionName string `json:"region_name"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
// InChina
|
||||
// 香港返回false
|
||||
func InChina(ip string) bool {
|
||||
var params = map[string]string{"ip": ip}
|
||||
resp := CountryResp{}
|
||||
if _, err := httputil.DefaultClientPostJsonWithResp(&resp, ipNewURL, nil, params); err != nil {
|
||||
log.Error("InChina error", log.Any("ip", ip), log.E(err))
|
||||
return true
|
||||
}
|
||||
if resp.RegionName == "香港" {
|
||||
return false
|
||||
}
|
||||
return resp.Country == "中国"
|
||||
}
|
||||
|
||||
func GetLocalIP() string {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil && ipnet.IP.IsGlobalUnicast() {
|
||||
return ipnet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package kafka
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/log"
|
||||
|
||||
"github.com/Shopify/sarama"
|
||||
)
|
||||
|
||||
const ClientID string = "pf_sp_kafka"
|
||||
|
||||
// kafka 使用
|
||||
var gSyncProducer sarama.SyncProducer
|
||||
|
||||
/**********************************************客户端 *********************/
|
||||
// 初始化kafka生产者 发送消息入口
|
||||
func InitKafkaProducter(addrs []string) error {
|
||||
config := sarama.NewConfig()
|
||||
config.Version = sarama.V2_0_0_0
|
||||
config.Producer.Return.Successes = true
|
||||
config.Net.KeepAlive = 2 * time.Hour
|
||||
cli, err := sarama.NewClient(addrs, config)
|
||||
if err != nil {
|
||||
log.Error("startUp Kafka Init Kafka error", log.E(err))
|
||||
return err
|
||||
}
|
||||
gSyncProducer, err = SyncProducter(cli)
|
||||
if err != nil {
|
||||
log.Error("startUp Kafka new SyncProducter error", log.E(err))
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncSendMessage 同步发送确保消息成功
|
||||
func SyncSendMessage(topic string, message []byte) {
|
||||
if gSyncProducer != nil {
|
||||
msg := sarama.ProducerMessage{
|
||||
Topic: topic,
|
||||
Value: sarama.ByteEncoder(message),
|
||||
}
|
||||
if _, _, err := gSyncProducer.SendMessage(&msg); err != nil {
|
||||
log.Error("gSyncProducer SendMessage Fail", log.Any("topic", topic), log.E(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建同步生产者 用于对消息的顺序有严格要求的场景 性能相对较低
|
||||
func SyncProducter(client sarama.Client) (sarama.SyncProducer, error) {
|
||||
producer, err := sarama.NewSyncProducerFromClient(client)
|
||||
if err != nil {
|
||||
log.Error("create syncProducer error", log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
return producer, nil
|
||||
}
|
||||
|
||||
/**********************************************客户端 end*********************/
|
||||
|
||||
/**********************************************服务端 *********************/
|
||||
func InitKafkaConsumerGroup(addrs []string, groupName string) sarama.ConsumerGroup {
|
||||
config := sarama.NewConfig()
|
||||
config.Version = sarama.V2_0_0_0
|
||||
config.Consumer.Return.Errors = true
|
||||
fmt.Println("addrs:", addrs)
|
||||
group, err := sarama.NewConsumerGroup(addrs, groupName, config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return group
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getComicsSearchUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/comics/search", APIUrl)
|
||||
}
|
||||
|
||||
func getComicsDetailUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/comics/detail", APIUrl)
|
||||
}
|
||||
|
||||
func ComicsSearch(ctx context.Context, req ComicsSearchListReq) (resp ComicsSearchListResp, err error) {
|
||||
endpoint := getComicsSearchUrl()
|
||||
req.Need_total_info = "y"
|
||||
var data map[string]interface{}
|
||||
data, err = StructToMapViaJSON(req)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = comicsPost(ctx, endpoint, data, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ComicsDetail(ctx context.Context, req ComicsDetailReq) (resp ComicsDetailResp, err error) {
|
||||
endpoint := getComicsDetailUrl()
|
||||
|
||||
var data map[string]interface{}
|
||||
data, err = StructToMapViaJSON(req)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = comicsPost(ctx, endpoint, data, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func comicsPost(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) (err error) {
|
||||
jsonData, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress", log.E(err))
|
||||
return
|
||||
}
|
||||
encryptedData, err := encryptBase64(string(jsonData), APIKey)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress encryptBase64", log.E(err))
|
||||
return
|
||||
}
|
||||
// 创建 POST 请求
|
||||
httpReq, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer([]byte(encryptedData)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Content-Length", fmt.Sprintf("%d", len(encryptedData)))
|
||||
httpReq.Header.Set("appid", Appid)
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应体
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return
|
||||
}
|
||||
// 解析 JSON
|
||||
var res Response
|
||||
if err = json.Unmarshal(body, &res); err != nil {
|
||||
return
|
||||
}
|
||||
if res.Status != "y" {
|
||||
err = fmt.Errorf("errorCode:%v error:%v", res.ErrorCode, res.Error)
|
||||
return
|
||||
}
|
||||
|
||||
// 进行解密
|
||||
dataStr, err := decryptBase64(res.Data, APIKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(dataStr), response)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package laosiji
|
||||
|
||||
type Tags struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ContentInfo struct {
|
||||
W string `json:"w"`
|
||||
H string `json:"h"`
|
||||
F string `json:"f"`
|
||||
}
|
||||
|
||||
type ComicsSearchInfo struct {
|
||||
Id string `json:"id"` // id
|
||||
Name string `json:"name"` // 名称
|
||||
Alias_name string `json:"alias_name"` // 别名
|
||||
Type string `json:"type"` // 类型
|
||||
Img string `json:"img"` // 封面
|
||||
Description string `json:"description"` // 描述
|
||||
Money string `json:"money"` //
|
||||
Category string `json:"category"` // 分类
|
||||
Update_status string `json:"update_status"` // 更新状态 0更新中 1更新完成
|
||||
Update_date string `json:"update_date"` // 更新时间
|
||||
Chapter_count string `json:"chapter_count"` // 章节数量
|
||||
Is_adult string `json:"is_adult"` // 是否有声漫
|
||||
Tags []Tags `json:"tags"` // 标签
|
||||
Sub_title string `json:"sub_title"` // 子标题
|
||||
Chapter []ChapterInfo `json:"chapter"` // 漫画章节
|
||||
IsAdd bool `json:"isAdd"`
|
||||
}
|
||||
|
||||
type ComicsSearchListReq struct {
|
||||
Page string `json:"page"` //
|
||||
Page_size string `json:"page_size"` //
|
||||
Cat_id string `json:"cat_id"` //
|
||||
Start_time string `json:"start_time"` // 更新开始时间
|
||||
End_time string `json:"end_time"` // 更新结束时间
|
||||
Is_end string `json:"is_end"` // 是否完结 y | n
|
||||
Source_site string `json:"source_site"` // 源站 如 www.toptoon.net
|
||||
Source_url string `json:"source_url"` // 源链接 如 https://www.toptoon.net/comic/epList/81181
|
||||
Need_total_info string `json:"need_total_info"` // 分页信息 默认y
|
||||
Keywords string `json:"keywords"` // 关键字
|
||||
}
|
||||
|
||||
type ChapterInfo struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Img string `json:"img"`
|
||||
Content []ContentInfo `json:"content"`
|
||||
}
|
||||
type ComicsSearchListResp struct {
|
||||
Data []ComicsSearchInfo `json:"data"` //
|
||||
Total string `json:"total"` //
|
||||
Current_page string `json:"current_page"` //
|
||||
Page_size string `json:"page_size"` //
|
||||
Last_page string `json:"last_page"` //
|
||||
}
|
||||
|
||||
type ComicsDetailReq struct {
|
||||
Id string `json:"id"` // id
|
||||
}
|
||||
|
||||
type ComicsDetailResp struct {
|
||||
ComicsSearchInfo
|
||||
}
|
||||
|
||||
type ComicsSyncReq struct {
|
||||
Ids []string `json:"ids"`
|
||||
}
|
||||
|
||||
type ComicsSyncResp struct {
|
||||
List []ComicsSearchInfo `json:"list"`
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/web/webg"
|
||||
"bytes"
|
||||
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
Name string
|
||||
Appid string
|
||||
APIKey string
|
||||
APIUrl string
|
||||
IMAGEYUAN string
|
||||
NoticeURL string
|
||||
)
|
||||
|
||||
// Config 是老司机接口配置。所有凭证必须由各服务自己的配置文件注入。
|
||||
type Config struct {
|
||||
AppID string
|
||||
APIKey string
|
||||
APIUrl string
|
||||
ImageYuan string
|
||||
NoticeURL string
|
||||
}
|
||||
|
||||
func Init(lsjCfg webg.GlobalConfig) {
|
||||
InitConfig(Config{
|
||||
AppID: lsjCfg.LSJ.AppID,
|
||||
APIKey: lsjCfg.LSJ.APIKey,
|
||||
APIUrl: lsjCfg.LSJ.APIUrl,
|
||||
ImageYuan: lsjCfg.LSJ.ImageYuan,
|
||||
})
|
||||
}
|
||||
|
||||
// InitConfig 供不依赖 web 配置结构的服务(例如 skd)初始化老司机客户端。
|
||||
func InitConfig(cfg Config) {
|
||||
Name = "老司机"
|
||||
Appid = strings.TrimSpace(cfg.AppID)
|
||||
APIKey = strings.TrimSpace(cfg.APIKey)
|
||||
APIUrl = strings.TrimRight(strings.TrimSpace(cfg.APIUrl), "/")
|
||||
IMAGEYUAN = strings.TrimRight(strings.TrimSpace(cfg.ImageYuan), "/")
|
||||
NoticeURL = strings.TrimSpace(cfg.NoticeURL)
|
||||
}
|
||||
|
||||
// Configured 判断调用老司机接口所需的配置是否完整。
|
||||
func Configured() bool {
|
||||
return Appid != "" && APIKey != "" && APIUrl != ""
|
||||
}
|
||||
|
||||
func ensureConfigured() error {
|
||||
if Configured() {
|
||||
return nil
|
||||
}
|
||||
missing := make([]string, 0, 3)
|
||||
if Appid == "" {
|
||||
missing = append(missing, "appId")
|
||||
}
|
||||
if APIKey == "" {
|
||||
missing = append(missing, "apiKey")
|
||||
}
|
||||
if APIUrl == "" {
|
||||
missing = append(missing, "apiUrl")
|
||||
}
|
||||
return fmt.Errorf("laosiji configuration missing: %s", strings.Join(missing, ","))
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Status string `json:"status"`
|
||||
Data string `json:"data"` // 如果成功,返回的这个数据是加密的
|
||||
Time string `json:"time"`
|
||||
Error string `json:"error"`
|
||||
ErrorCode int `json:"errorCode"`
|
||||
}
|
||||
|
||||
func QueryUndress(ctx context.Context, taskID string) (resp QueryUndressResponse, err error) {
|
||||
endpoint := getQueryUrl()
|
||||
err = post(ctx, endpoint, map[string]interface{}{
|
||||
"task_id": taskID,
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("taskID", taskID), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GenerateUndress(ctx context.Context, req map[string]interface{}) (resp GenerateUndressResponse, err error) {
|
||||
endpoint := getAiUndressGenerateUrl()
|
||||
err = post(ctx, endpoint, req, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("req", req), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func QueryTextToImage(ctx context.Context, taskID string) (resp QueryTextToImageResponse, err error) {
|
||||
endpoint := getQueryUrl()
|
||||
err = post(ctx, endpoint, map[string]interface{}{
|
||||
"task_id": taskID,
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
log.Error("QueryTextToImage post fail", log.Any("taskID", taskID), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GenerateTextToImage(ctx context.Context, req map[string]interface{}) (resp GenerateTextToImageResponse, err error) {
|
||||
endpoint := getTextToImageGenerateUrl()
|
||||
err = post(ctx, endpoint, req, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateTextToImage post fail", log.Any("req", req), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getQueryUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/ai/detail", APIUrl)
|
||||
}
|
||||
|
||||
func getTextToImageGenerateUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/ai/generate", APIUrl)
|
||||
}
|
||||
|
||||
func getAiUndressGenerateUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/ai/undress", APIUrl)
|
||||
}
|
||||
|
||||
func post(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) (err error) {
|
||||
if err = ensureConfigured(); err != nil {
|
||||
return err
|
||||
}
|
||||
jsonData, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress", log.E(err))
|
||||
return
|
||||
}
|
||||
encryptedData, err := encryptBase64(string(jsonData), APIKey)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress encryptBase64", log.E(err))
|
||||
return
|
||||
}
|
||||
// 创建 POST 请求
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer([]byte(encryptedData)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Content-Length", fmt.Sprintf("%d", len(encryptedData)))
|
||||
httpReq.Header.Set("appid", Appid)
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应体
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return fmt.Errorf("laosiji api http status:%d body:%s", resp.StatusCode, truncateToolBody(body, APIKey, Appid))
|
||||
}
|
||||
|
||||
// 解析 JSON
|
||||
var res Response
|
||||
if err = json.Unmarshal(body, &res); err != nil {
|
||||
return
|
||||
}
|
||||
if res.Status != "y" {
|
||||
err = fmt.Errorf("errorCode:%v error:%s", res.ErrorCode, truncateToolText(res.Error, APIKey, Appid))
|
||||
return
|
||||
}
|
||||
|
||||
// 进行解密
|
||||
dataStr, err := decryptBase64(res.Data, APIKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(dataStr), response)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AES-128-ECB encrypt
|
||||
func encryptBase64(input, key string) (string, error) {
|
||||
if len(key) > 16 {
|
||||
key = key[:16]
|
||||
}
|
||||
plainText := []byte(input)
|
||||
keyBytes := []byte(key)
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
blockSize := block.BlockSize()
|
||||
plainText = pkcs7Padding(plainText, blockSize)
|
||||
|
||||
encrypted := make([]byte, len(plainText))
|
||||
for bs, be := 0, blockSize; bs < len(plainText); bs, be = bs+blockSize, be+blockSize {
|
||||
block.Encrypt(encrypted[bs:be], plainText[bs:be])
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
||||
}
|
||||
|
||||
// AES-128-ECB decrypt
|
||||
func decryptBase64(cipherText, key string) (string, error) {
|
||||
if len(key) > 16 {
|
||||
key = key[:16]
|
||||
}
|
||||
data, err := base64.StdEncoding.DecodeString(cipherText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher([]byte(key))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
blockSize := block.BlockSize()
|
||||
decrypted := make([]byte, len(data))
|
||||
for bs, be := 0, blockSize; bs < len(data); bs, be = bs+blockSize, be+blockSize {
|
||||
block.Decrypt(decrypted[bs:be], data[bs:be])
|
||||
}
|
||||
|
||||
decrypted = pkcs7UnPadding(decrypted)
|
||||
return string(decrypted), nil
|
||||
}
|
||||
|
||||
// PKCS7Padding pads plaintext for AES ECB
|
||||
func pkcs7Padding(src []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(src)%blockSize
|
||||
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(src, padText...)
|
||||
}
|
||||
|
||||
// PKCS7UnPadding removes padding
|
||||
func pkcs7UnPadding(src []byte) []byte {
|
||||
length := len(src)
|
||||
unpadding := int(src[length-1])
|
||||
return src[:(length - unpadding)]
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getMovieSearchUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/movie/search", APIUrl)
|
||||
}
|
||||
|
||||
func getMovieDetailUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/movie/detail", APIUrl)
|
||||
}
|
||||
|
||||
func getMoviedDetailByMidUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/movie/detailByMid", APIUrl)
|
||||
}
|
||||
|
||||
// StructToMapViaJSON 通过 JSON 转换结构体到 map
|
||||
func StructToMapViaJSON(obj interface{}) (map[string]interface{}, error) {
|
||||
// 将结构体转换为 JSON
|
||||
jsonData, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 将 JSON 解析为 map
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(jsonData, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// MovieSearch 视频查询
|
||||
func MovieSearch(ctx context.Context, req MovieSearchReq) (resp MovieSearchResp, err error) {
|
||||
endpoint := getMovieSearchUrl()
|
||||
req.Need_total_info = "y"
|
||||
|
||||
var data map[string]interface{}
|
||||
data, err = StructToMapViaJSON(req)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = moviePost(ctx, endpoint, data, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MovieDetail 获取视频列表
|
||||
func MovieDetail(ctx context.Context, req MovieDetailReq) (resp MovieDetailResp, err error) {
|
||||
endpoint := getMovieDetailUrl()
|
||||
|
||||
var data map[string]interface{}
|
||||
data, err = StructToMapViaJSON(req)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = moviePost(ctx, endpoint, data, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func moviePost(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) (err error) {
|
||||
if err = ensureConfigured(); err != nil {
|
||||
return err
|
||||
}
|
||||
jsonData, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress", log.E(err))
|
||||
return
|
||||
}
|
||||
encryptedData, err := encryptBase64(string(jsonData), APIKey)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress encryptBase64", log.E(err))
|
||||
return
|
||||
}
|
||||
// 创建 POST 请求
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer([]byte(encryptedData)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Content-Length", fmt.Sprintf("%d", len(encryptedData)))
|
||||
httpReq.Header.Set("appid", Appid)
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应体
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return fmt.Errorf("laosiji movie api http status:%d body:%s", resp.StatusCode, truncateToolBody(body, APIKey, Appid))
|
||||
}
|
||||
// 解析 JSON
|
||||
var res Response
|
||||
if err = json.Unmarshal(body, &res); err != nil {
|
||||
return
|
||||
}
|
||||
if res.Status != "y" {
|
||||
err = fmt.Errorf("errorCode:%v error:%s", res.ErrorCode, truncateToolText(res.Error, APIKey, Appid))
|
||||
return
|
||||
}
|
||||
|
||||
// 进行解密
|
||||
dataStr, err := decryptBase64(res.Data, APIKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(dataStr), response)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package laosiji
|
||||
|
||||
type TagInfo struct {
|
||||
Id string `json:"id"` // 视频编号
|
||||
Name string `json:"name"` // 视频名字
|
||||
}
|
||||
|
||||
type LinkInfo struct {
|
||||
Id string `json:"id"` // 视频编号
|
||||
Name string `json:"name"` // 视频名字
|
||||
Preview_m3u8_url string `json:"preview_m3u8_url"` // 预览地址(部分视频无预览地址)
|
||||
M3u8_url string `json:"m3u8_url"` // 播放地址
|
||||
Hevc_m3u8_url string `json:"hevc_m3u8_url"` // H.265 播放地址
|
||||
}
|
||||
|
||||
type MovieInfo struct {
|
||||
Id string `json:"id"` // 视频编号
|
||||
Name string `json:"name"` // 视频名字
|
||||
Img_x string `json:"img_x"` // 横图封面
|
||||
Img_y string `json:"img_y"` // 竖图封面
|
||||
Img_type string `json:"img_type"` // 封面类型 long 横图 short竖图
|
||||
Cat_id string `json:"cat_id"` // 分类编号
|
||||
Cat_name string `json:"cat_name"` // 分类名字
|
||||
Status string `json:"status"` // 上架状态
|
||||
Status_text string `json:"status_text"` //
|
||||
Show_at string `json:"show_at"` // 上架时间
|
||||
Tags []TagInfo `json:"tags"` // 标签
|
||||
Update_status string `json:"update_status"` // 更新状态0 更新重 1已完结
|
||||
Description string `json:"description"` // 视频描述
|
||||
Language string `json:"language"` // 视频语言
|
||||
Director string `json:"director"` // 视频导演
|
||||
Issue_date string `json:"issue_date"` // 视频上架时间
|
||||
Actor string `json:"actor"` // 演员
|
||||
Up_user string `json:"up_user"` //
|
||||
IsAdd bool `json:"isAdd"`
|
||||
}
|
||||
|
||||
type NovelSearchInfo struct {
|
||||
Id string `json:"id"` // id
|
||||
Name string `json:"name"` // 名称
|
||||
Alias_name string `json:"alias_name"` // 别名
|
||||
Author string `json:"author"` // 作者
|
||||
Type string `json:"type"` // 类型
|
||||
Img string `json:"img"` // 封面
|
||||
Description string `json:"description"` // 描述
|
||||
Money string `json:"money"` //
|
||||
Sub_title string `json:"sub_title"` // 子标题
|
||||
Category string `json:"category"` // 分类
|
||||
Category_name string `json:"category_name"` // 分类名称
|
||||
Update_status string `json:"update_status"` // 更新状态 0更新中 1更新完成
|
||||
Update_date string `json:"update_date"` // 更新时间
|
||||
Chapter_count string `json:"chapter_count"` // 章节数量
|
||||
Is_adult string `json:"is_adult"` // 是否是有声
|
||||
Tags []Tags `json:"tags"` // 标签
|
||||
Last_update string `json:"last_update"` // 最后更新时间
|
||||
Created_at string `json:"created_at"` // 创建时间
|
||||
Updated_at string `json:"updated_at"` // 更新时间
|
||||
Chapter []NovelChapterInfo `json:"chapter"` // 小说章节
|
||||
IsAdd bool `json:"isAdd"`
|
||||
}
|
||||
|
||||
type NovelChapterInfo struct {
|
||||
Id string `json:"id"` // id
|
||||
Name string `json:"name"` // 标题
|
||||
Img string `json:"img"` // 图片
|
||||
Content string `json:"content"` // 内容
|
||||
Is_audio string `json:"is_audio"` // 是否有声
|
||||
}
|
||||
|
||||
type MovieDetailInfo struct {
|
||||
Id string `json:"id"` // 视频编号
|
||||
Name string `json:"name"` // 视频名字
|
||||
Img_x string `json:"img_x"` // 横图封面
|
||||
Img_y string `json:"img_y"` // 竖图封面
|
||||
Img_type string `json:"img_type"` // 封面类型 long 横图 short竖图
|
||||
Cat_id string `json:"cat_id"` // 分类编号
|
||||
Cat_name string `json:"cat_name"` // 分类名字
|
||||
Status string `json:"status"` // 上架状态
|
||||
Status_text string `json:"status_text"` //
|
||||
Show_at string `json:"show_at"` // 上架时间
|
||||
Tags []TagInfo `json:"tags"` // 标签
|
||||
Update_status string `json:"update_status"` // 更新状态0 更新重 1已完结
|
||||
Description string `json:"description"` // 视频描述
|
||||
Language string `json:"language"` // 视频语言
|
||||
Director string `json:"director"` // 视频导演
|
||||
Issue_date string `json:"issue_date"` // 视频上架时间
|
||||
Duration string `json:"duration"` // 视频时长
|
||||
Actor string `json:"actor"` // 演员
|
||||
|
||||
Is_more_link string `json:"is_more_link"` // 是多集还是单集
|
||||
Preview_images []string `json:"preview_images"` // 预览图片
|
||||
Links []LinkInfo `json:"links"` // 链接
|
||||
Series string `json:"series"` // 系列 主要是av
|
||||
Source_tags string `json:"source_tags"` // 采集网站的标签
|
||||
// Source_actor string `json:"source_actor"` // 采集网站的演员 主要是av使用
|
||||
}
|
||||
|
||||
/*
|
||||
| 分类编号(cat_id) | 名称 | 分区(position) |
|
||||
| 13 | 成人短视频 | guochan |
|
||||
| 12 | VR | av |
|
||||
| 11 | 电影解说 | movie |
|
||||
| 10 | 音乐 | movie |
|
||||
| 9 | 短剧 | movie |
|
||||
| 8 | 纪录片 | movie |
|
||||
| 7 | 动漫 | movie |
|
||||
| 6 | 电影 | movie |
|
||||
| 5 | 连续剧 | movie |
|
||||
| 4 | 综艺 | movie |
|
||||
| 3 | GC | guochan |
|
||||
| 2 | DM | guochan |
|
||||
| 1 | AV | av |
|
||||
| | |
|
||||
| position 说明 guochan 是国产成人视频 av 主要是日本和欧美成人视频 movie 是正规影视资源 bl 男同 douyin 短视频 cartoon 动漫 dark 暗网资源 所有资源需要用户具备权限才能获取 全部传递all
|
||||
*/
|
||||
type MovieSearchReq struct {
|
||||
Position string `json:"position"` // position 说明 guochan 是国产成人视频 av 主要是日本和欧美成人视频 movie 是正规影视资源 bl 男同 douyin 短视频 cartoon 动漫 dark 暗网资源
|
||||
Keywords string `json:"keywords"` // 关键字
|
||||
Ids string `json:"ids"` // 视频ID
|
||||
Cat_id string `json:"cat_id"` // 分类ID
|
||||
Update_status string `json:"update_status"` // 1表示已经完结 0表示未完结
|
||||
Hevc_status int `json:"hevc_status"` // 5表示只查询已有 H.265 资源的视频
|
||||
Mid string `json:"mid"` // 一般无需使用 多个用,分开
|
||||
Page string `json:"page"` // 分页
|
||||
Page_size string `json:"page_size"` // 每页数据 建议不要超过500
|
||||
Start_time string `json:"start_time"` // 更新日期开始日期 格式 2022-01-01 12:00:00
|
||||
End_time string `json:"end_time"` // 更新日期结束日期 格式 2022-01-01 12:00:00
|
||||
Home_id string `json:"home_id"` // up主id
|
||||
Need_total_info string `json:"need_total_info"` // 分页信息 默认y
|
||||
}
|
||||
|
||||
// MovieSearchResp 视频查询返回接口
|
||||
type MovieSearchResp struct {
|
||||
// Status string `json:"status"` // 状态 0待处理 -1处理失败 1处理中 2处理成功
|
||||
// Time string `json:"time"` //
|
||||
Data []MovieInfo `json:"data"` //
|
||||
Total string `json:"total"` //
|
||||
Current_page string `json:"current_page"` //
|
||||
Page_size string `json:"page_size"` //
|
||||
Last_page string `json:"last_page"` //
|
||||
}
|
||||
|
||||
// MovieDetailReq 视频详情
|
||||
type MovieDetailReq struct {
|
||||
Id string `json:"id"` // id
|
||||
}
|
||||
|
||||
// MovieSearchResp 视频详情返回接口
|
||||
type MovieDetailResp struct {
|
||||
MovieDetailInfo
|
||||
}
|
||||
|
||||
type MovieAddListReq struct {
|
||||
Ids []string `json:"ids"`
|
||||
Position string `json:"position"`
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getNovelSearchUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/novel/search", APIUrl)
|
||||
}
|
||||
|
||||
func getNovelDetailUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/novel/detail", APIUrl)
|
||||
}
|
||||
|
||||
func NovelSearch(ctx context.Context, req NovelSearchListReq) (resp NovelSearchListResp, err error) {
|
||||
endpoint := getNovelSearchUrl()
|
||||
req.Need_total_info = "y"
|
||||
|
||||
var data map[string]interface{}
|
||||
data, err = StructToMapViaJSON(req)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = novelPost(ctx, endpoint, data, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func NovelDetail(ctx context.Context, req NovelDetailReq) (resp NovelDetailResp, err error) {
|
||||
endpoint := getNovelDetailUrl()
|
||||
|
||||
var data map[string]interface{}
|
||||
data, err = StructToMapViaJSON(req)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = novelPost(ctx, endpoint, data, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func novelPost(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) (err error) {
|
||||
jsonData, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress", log.E(err))
|
||||
return
|
||||
}
|
||||
encryptedData, err := encryptBase64(string(jsonData), APIKey)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress encryptBase64", log.E(err))
|
||||
return
|
||||
}
|
||||
// 创建 POST 请求
|
||||
httpReq, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer([]byte(encryptedData)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Content-Length", fmt.Sprintf("%d", len(encryptedData)))
|
||||
httpReq.Header.Set("appid", Appid)
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应体
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return
|
||||
}
|
||||
// 解析 JSON
|
||||
var res Response
|
||||
if err = json.Unmarshal(body, &res); err != nil {
|
||||
return
|
||||
}
|
||||
if res.Status != "y" {
|
||||
err = fmt.Errorf("errorCode:%v error:%v", res.ErrorCode, res.Error)
|
||||
return
|
||||
}
|
||||
|
||||
// 进行解密
|
||||
dataStr, err := decryptBase64(res.Data, APIKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(dataStr), response)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package laosiji
|
||||
|
||||
type NovelSearchListReq struct {
|
||||
Page string `json:"page"` //
|
||||
Page_size string `json:"page_size"` //
|
||||
Cat_id string `json:"cat_id"` // 分类 audio 18R normal
|
||||
Start_time string `json:"start_time"` // 更新开始时间
|
||||
End_time string `json:"end_time"` // 更新结束时间
|
||||
Is_end string `json:"is_end"` // 是否完结 y | n
|
||||
Need_total_info string `json:"need_total_info"` // 分页信息 默认y
|
||||
Keywords string `json:"keywords"` // 关键字
|
||||
}
|
||||
|
||||
type NovelSearchListResp struct {
|
||||
Data []NovelSearchInfo `json:"data"` //
|
||||
Total string `json:"total"` // 总数
|
||||
Current_page string `json:"current_page"` // 当前页
|
||||
Page_size string `json:"page_size"` // 当前页数
|
||||
Last_page string `json:"last_page"` //
|
||||
}
|
||||
|
||||
type NovelDetailReq struct {
|
||||
Id string `json:"id"` // id
|
||||
}
|
||||
|
||||
type NovelDetailResp struct {
|
||||
NovelSearchInfo
|
||||
}
|
||||
|
||||
type NovelAddListReq struct {
|
||||
Ids []string `json:"ids"`
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getPostSearchUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/post/search", APIUrl)
|
||||
}
|
||||
|
||||
func getPostDetailUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/post/detail", APIUrl)
|
||||
}
|
||||
|
||||
func PostSearch(ctx context.Context, req PostSearchListReq) (resp PostSearchListResp, err error) {
|
||||
endpoint := getPostSearchUrl()
|
||||
req.Need_total_info = "y"
|
||||
|
||||
var data map[string]interface{}
|
||||
data, err = StructToMapViaJSON(req)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = postPost(ctx, endpoint, data, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func PostDetail(ctx context.Context, req PostDetailReq) (resp PostDetailResp, err error) {
|
||||
endpoint := getPostDetailUrl()
|
||||
|
||||
var data map[string]interface{}
|
||||
data, err = StructToMapViaJSON(req)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = postPost(ctx, endpoint, data, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func postPost(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) (err error) {
|
||||
jsonData, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress", log.E(err))
|
||||
return
|
||||
}
|
||||
encryptedData, err := encryptBase64(string(jsonData), APIKey)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress encryptBase64", log.E(err))
|
||||
return
|
||||
}
|
||||
// 创建 POST 请求
|
||||
httpReq, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer([]byte(encryptedData)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Content-Length", fmt.Sprintf("%d", len(encryptedData)))
|
||||
httpReq.Header.Set("appid", Appid)
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应体
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return
|
||||
}
|
||||
// 解析 JSON
|
||||
var res Response
|
||||
if err = json.Unmarshal(body, &res); err != nil {
|
||||
return
|
||||
}
|
||||
if res.Status != "y" {
|
||||
err = fmt.Errorf("errorCode:%v error:%v", res.ErrorCode, res.Error)
|
||||
return
|
||||
}
|
||||
|
||||
// 进行解密
|
||||
dataStr, err := decryptBase64(res.Data, APIKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(dataStr), response)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package laosiji
|
||||
|
||||
type Categories struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type PostFiles struct {
|
||||
Image string `json:"image"`
|
||||
Type string `json:"type"`
|
||||
Ico string `json:"ico"`
|
||||
Tips string `json:"tips"`
|
||||
Video_link string `json:"video_link"`
|
||||
}
|
||||
|
||||
type UpUserInfo struct {
|
||||
Id string `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Img string `json:"img"`
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
|
||||
type PostSearchListReq struct {
|
||||
Home_id string `json:"home_id"`
|
||||
Page string `json:"page"`
|
||||
Page_size string `json:"page_size"`
|
||||
Pay_type string `json:"pay_type"`
|
||||
Start_time int `json:"start_time"`
|
||||
End_time int `json:"end_time"`
|
||||
Need_total_info string `json:"need_total_info"`
|
||||
Keywords string `json:"keywords"` // 关键字
|
||||
}
|
||||
|
||||
type PostSearchInfo struct {
|
||||
Id string `json:"id"` // id
|
||||
Title string `json:"title"` // 标题
|
||||
Time string `json:"time"` // 更新时间
|
||||
Money string `json:"money"` //
|
||||
User_id string `json:"user_id"` //
|
||||
Content string `json:"content"` // 文本内容
|
||||
Img string `json:"img"` // 封面
|
||||
Hide_files string `json:"hide_files"` //
|
||||
Position string `json:"position"` // 类型
|
||||
Categories []Categories `json:"categories"` // 分类
|
||||
Img_count string `json:"img_count"` // 图片数量
|
||||
Rich_content []string `json:"up_user"` // 富文本
|
||||
Files []PostFiles `json:"files"` // 内容节点
|
||||
Up_user UpUserInfo `json:"up_user"` // 发布者
|
||||
IsAdd bool `json:"isAdd"`
|
||||
}
|
||||
|
||||
type PostSearchListResp struct {
|
||||
Data []PostSearchInfo `json:"data"` //
|
||||
Total string `json:"total"` //
|
||||
Current_page string `json:"current_page"` //
|
||||
Page_size string `json:"page_size"` //
|
||||
Last_page string `json:"last_page"` //
|
||||
}
|
||||
|
||||
type PostDetailReq struct {
|
||||
Id string `json:"id"` // id
|
||||
}
|
||||
|
||||
type PostDetailResp struct {
|
||||
PostSearchInfo
|
||||
}
|
||||
|
||||
type PostAddListReq struct {
|
||||
Ids []string `json:"ids"`
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package laosiji
|
||||
|
||||
type GenerateTextToImageResponse struct {
|
||||
TaskID string `json:"task_id"` // 任务 ID
|
||||
}
|
||||
|
||||
type QueryTextToImageResponse struct {
|
||||
TaskID string `json:"task_id"` // 任务 ID
|
||||
Bid string `json:"bid"`
|
||||
Fee string `json:"fee"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"` // 状态 0待处理 -1处理失败 1处理中 2处理成功
|
||||
OutData string `json:"out_data"` // 输出数据 不同的ai 处理的数据不一样 查看文档的描述
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/hevcpull"
|
||||
"91porn-server/common/log"
|
||||
)
|
||||
|
||||
const (
|
||||
videoTranscodingVersion = "2.0"
|
||||
transcodeNoNeedStatus = 4
|
||||
transcodeSuccessStatus = 5
|
||||
transcodeFailedStatus = -1
|
||||
|
||||
sourceM3u8CheckMaxBytes = 8 << 20
|
||||
)
|
||||
|
||||
var (
|
||||
errTranscodeTaskNotFound = errors.New("laosiji transcode task not found")
|
||||
// ErrTranscodeQueueFull 表示云端 H.265 待处理队列已经达到限制。
|
||||
ErrTranscodeQueueFull = errors.New("laosiji transcode queue full")
|
||||
sourceM3u8HTTPClient = &http.Client{Timeout: 15 * time.Second}
|
||||
uploadHTTPClient = &http.Client{Timeout: 30 * time.Second}
|
||||
)
|
||||
|
||||
// SystemDomainsResp 是老司机 system/domains 接口返回的资源域名和上传配置。
|
||||
type SystemDomainsResp struct {
|
||||
ImgFreeCDN string `json:"img_free_cdn"`
|
||||
MovieFreeCDN string `json:"movie_free_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 transcodeAPIResp 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"`
|
||||
}
|
||||
|
||||
// TranscodeQueueInfo 是云端转码队列概览。
|
||||
type TranscodeQueueInfo struct {
|
||||
Waiting int `json:"waiting"`
|
||||
Done int `json:"done"`
|
||||
Error int `json:"error"`
|
||||
}
|
||||
|
||||
// TranscodeResult 是本地异步任务使用的统一转码结果。
|
||||
type TranscodeResult struct {
|
||||
Status int
|
||||
Done bool
|
||||
NoNeed bool
|
||||
Failed bool
|
||||
HevcURL string
|
||||
FileURL string
|
||||
FileID string
|
||||
ErrorMsg string
|
||||
}
|
||||
|
||||
func getSystemDomainsURL() string {
|
||||
return strings.TrimRight(APIUrl, "/") + "/lsjapi/system/domains"
|
||||
}
|
||||
|
||||
// SystemDomains 获取老司机临时上传、转码配置。
|
||||
func SystemDomains(ctx context.Context) (resp SystemDomainsResp, err error) {
|
||||
if err = ensureConfigured(); err != nil {
|
||||
return resp, err
|
||||
}
|
||||
if err = moviePost(ctx, getSystemDomainsURL(), map[string]interface{}{}, &resp); err != nil {
|
||||
return resp, err
|
||||
}
|
||||
missing := make([]string, 0, 3)
|
||||
if strings.TrimSpace(resp.UploadURL) == "" {
|
||||
missing = append(missing, "upload_url")
|
||||
}
|
||||
if strings.TrimSpace(resp.UploadKey) == "" {
|
||||
missing = append(missing, "upload_key")
|
||||
}
|
||||
if strings.TrimSpace(resp.MovieSourceCDN) == "" {
|
||||
missing = append(missing, "movie_source_cdn")
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return resp, fmt.Errorf("laosiji system/domains missing: %s", strings.Join(missing, ","))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// SubmitH264ToH265ByFileURL 使用完整 file_url 异步提交转码任务。
|
||||
func SubmitH264ToH265ByFileURL(ctx context.Context, fileURL string, domains SystemDomainsResp, maxRunning int) (TranscodeResult, error) {
|
||||
fileURL = strings.TrimSpace(fileURL)
|
||||
if fileURL == "" {
|
||||
return TranscodeResult{}, errors.New("empty file url")
|
||||
}
|
||||
return submitByTask(ctx, md5Hex(fileURL), fileURL, domains, maxRunning)
|
||||
}
|
||||
|
||||
// SubmitH264ToH265Task decouples the stable cloud task ID from the temporary
|
||||
// signed fetch URL. This keeps polling stable across App domain or signing-key
|
||||
// changes while a task is pending.
|
||||
func SubmitH264ToH265Task(ctx context.Context, fileID, fileURL string, domains SystemDomainsResp, maxRunning int) (TranscodeResult, error) {
|
||||
fileID = strings.TrimSpace(fileID)
|
||||
fileURL = strings.TrimSpace(fileURL)
|
||||
if fileID == "" {
|
||||
return TranscodeResult{}, errors.New("empty file id")
|
||||
}
|
||||
if fileURL == "" {
|
||||
return TranscodeResult{}, errors.New("empty file url")
|
||||
}
|
||||
return submitByTask(ctx, fileID, fileURL, domains, maxRunning)
|
||||
}
|
||||
|
||||
func submitByTask(ctx context.Context, fileID, fileURL string, domains SystemDomainsResp, maxRunning int) (TranscodeResult, error) {
|
||||
task, err := queryVideoTranscoding(ctx, domains, fileID)
|
||||
if err == nil {
|
||||
return transcodeTaskToResult(domains, task, fileURL, fileID), nil
|
||||
}
|
||||
if !errors.Is(err, errTranscodeTaskNotFound) {
|
||||
return TranscodeResult{}, err
|
||||
}
|
||||
if err = checkSourceM3u8(ctx, fileURL); err != nil {
|
||||
return TranscodeResult{}, err
|
||||
}
|
||||
task, err = createVideoTranscoding(ctx, domains, fileID, fileURL, maxRunning)
|
||||
if err != nil {
|
||||
return TranscodeResult{}, err
|
||||
}
|
||||
return transcodeTaskToResult(domains, task, fileURL, fileID), nil
|
||||
}
|
||||
|
||||
func checkSourceM3u8(ctx context.Context, fileURL string) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build source m3u8 request: %s", hevcpull.RedactText(err.Error()))
|
||||
}
|
||||
resp, err := sourceM3u8HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("source m3u8 request failed: %s", hevcpull.RedactText(err.Error()))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return fmt.Errorf("source m3u8 http status:%d url:%s", resp.StatusCode, hevcpull.RedactURL(fileURL))
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, sourceM3u8CheckMaxBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !looksLikeM3u8(body) {
|
||||
return fmt.Errorf("source m3u8 invalid content url:%s", hevcpull.RedactURL(fileURL))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func looksLikeM3u8(body []byte) bool {
|
||||
body = bytes.TrimPrefix(body, []byte{0xEF, 0xBB, 0xBF})
|
||||
body = bytes.TrimLeft(body, " \t\r\n")
|
||||
return bytes.HasPrefix(body, []byte("#EXTM3U"))
|
||||
}
|
||||
|
||||
// QueryH264ToH265 查询老司机源视频对应的 H.265 云转码结果。
|
||||
func QueryH264ToH265(ctx context.Context, h264URL string) (TranscodeResult, bool, error) {
|
||||
domains, err := SystemDomains(ctx)
|
||||
if err != nil {
|
||||
return TranscodeResult{}, false, err
|
||||
}
|
||||
return QueryH264ToH265WithDomains(ctx, h264URL, domains)
|
||||
}
|
||||
|
||||
// QueryH264ToH265WithDomains 使用已经获取的域名配置查询转码结果。
|
||||
func QueryH264ToH265WithDomains(ctx context.Context, h264URL string, domains SystemDomainsResp) (TranscodeResult, bool, error) {
|
||||
h264URL = strings.TrimSpace(h264URL)
|
||||
if h264URL == "" {
|
||||
return TranscodeResult{}, false, errors.New("empty h264 m3u8 url")
|
||||
}
|
||||
return queryByFileURL(ctx, transcodeSourceURL(h264URL, domains.MovieSourceCDN), domains)
|
||||
}
|
||||
|
||||
// QueryH264ToH265ByFileURL 与 SubmitH264ToH265ByFileURL 使用相同的完整 file_url 查询结果。
|
||||
func QueryH264ToH265ByFileURL(ctx context.Context, fileURL string, domains SystemDomainsResp) (TranscodeResult, bool, error) {
|
||||
fileURL = strings.TrimSpace(fileURL)
|
||||
if fileURL == "" {
|
||||
return TranscodeResult{}, false, errors.New("empty file url")
|
||||
}
|
||||
return queryByTask(ctx, md5Hex(fileURL), fileURL, domains)
|
||||
}
|
||||
|
||||
func queryByFileURL(ctx context.Context, fileURL string, domains SystemDomainsResp) (TranscodeResult, bool, error) {
|
||||
return queryByTask(ctx, md5Hex(fileURL), fileURL, domains)
|
||||
}
|
||||
|
||||
// QueryH264ToH265Task queries by the stable task ID used during submission.
|
||||
// fileURL is diagnostic metadata only and is not sent to the cloud query API.
|
||||
func QueryH264ToH265Task(ctx context.Context, fileID, fileURL string, domains SystemDomainsResp) (TranscodeResult, bool, error) {
|
||||
fileID = strings.TrimSpace(fileID)
|
||||
fileURL = strings.TrimSpace(fileURL)
|
||||
if fileID == "" {
|
||||
return TranscodeResult{}, false, errors.New("empty file id")
|
||||
}
|
||||
return queryByTask(ctx, fileID, fileURL, domains)
|
||||
}
|
||||
|
||||
func queryByTask(ctx context.Context, fileID, fileURL string, domains SystemDomainsResp) (TranscodeResult, bool, error) {
|
||||
task, err := queryVideoTranscoding(ctx, domains, fileID)
|
||||
if err != nil {
|
||||
if errors.Is(err, errTranscodeTaskNotFound) {
|
||||
return TranscodeResult{}, false, nil
|
||||
}
|
||||
return TranscodeResult{}, false, err
|
||||
}
|
||||
return transcodeTaskToResult(domains, task, fileURL, fileID), true, nil
|
||||
}
|
||||
|
||||
func transcodeSourceURL(rawURL, movieSourceCDN string) string {
|
||||
rawURL = strings.TrimSpace(rawURL)
|
||||
movieSourceCDN = strings.TrimSpace(movieSourceCDN)
|
||||
if rawURL == "" || movieSourceCDN == "" {
|
||||
return rawURL
|
||||
}
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err == nil && parsed.Scheme != "" {
|
||||
name := path.Base(parsed.Path)
|
||||
if name != "" && name != "." && strings.Contains(name, ".m3u8") {
|
||||
return joinURLPath(movieSourceCDN, name)
|
||||
}
|
||||
return rawURL
|
||||
}
|
||||
rawURL = strings.TrimPrefix(rawURL, "/")
|
||||
rawURL = strings.TrimPrefix(rawURL, "laosiji/")
|
||||
if idx := strings.Index(rawURL, "m3m/"); idx >= 0 {
|
||||
rawURL = rawURL[idx+len("m3m/"):]
|
||||
}
|
||||
return joinURLPath(movieSourceCDN, rawURL)
|
||||
}
|
||||
|
||||
// TranscodeSourceURL 把本地资源 path 或播放 URL 转换成云转码使用的源站 URL。
|
||||
func TranscodeSourceURL(rawURL, movieSourceCDN string) string {
|
||||
return transcodeSourceURL(rawURL, movieSourceCDN)
|
||||
}
|
||||
|
||||
func createVideoTranscoding(ctx context.Context, domains SystemDomainsResp, fileID, fileURL string, maxRunning int) (transcodeTask, error) {
|
||||
if q, err := getVideoTranscodingQueue(ctx, domains); err != nil {
|
||||
log.Warn("getVideoTranscodingQueue failed", log.E(err))
|
||||
} else if maxRunning > 0 && q.Waiting >= maxRunning {
|
||||
return transcodeTask{}, fmt.Errorf("%w waiting:%d limit:%d", ErrTranscodeQueueFull, q.Waiting, maxRunning)
|
||||
}
|
||||
params := map[string]string{
|
||||
"v": videoTranscodingVersion,
|
||||
"key": domains.UploadKey,
|
||||
"file_id": fileID,
|
||||
"file_url": fileURL,
|
||||
"ext_data": `{"project":"91porn","source":"laosiji","type":"full"}`,
|
||||
}
|
||||
if NoticeURL != "" {
|
||||
params["notice_url"] = NoticeURL
|
||||
}
|
||||
endpoint := joinURLPath(domains.UploadURL, "upload/videoTranscoding")
|
||||
return uploadAPIGet(ctx, endpoint, params)
|
||||
}
|
||||
|
||||
func getVideoTranscodingQueue(ctx context.Context, domains SystemDomainsResp) (TranscodeQueueInfo, error) {
|
||||
endpoint := joinURLPath(domains.UploadURL, "upload/getVideoTranscodingQueueInfo")
|
||||
data, err := uploadAPICall(ctx, endpoint, map[string]string{
|
||||
"v": videoTranscodingVersion,
|
||||
"key": domains.UploadKey,
|
||||
})
|
||||
if err != nil {
|
||||
return TranscodeQueueInfo{}, err
|
||||
}
|
||||
var queue TranscodeQueueInfo
|
||||
if len(data) > 0 {
|
||||
if err = json.Unmarshal(data, &queue); err != nil {
|
||||
return TranscodeQueueInfo{}, err
|
||||
}
|
||||
}
|
||||
return queue, nil
|
||||
}
|
||||
|
||||
func queryVideoTranscoding(ctx context.Context, domains SystemDomainsResp, fileID string) (transcodeTask, error) {
|
||||
endpoint := joinURLPath(domains.UploadURL, "upload/queryVideoTranscoding")
|
||||
return uploadAPIGet(ctx, endpoint, map[string]string{
|
||||
"v": videoTranscodingVersion,
|
||||
"key": domains.UploadKey,
|
||||
"file_id": fileID,
|
||||
})
|
||||
}
|
||||
|
||||
func transcodeTaskToResult(domains SystemDomainsResp, task transcodeTask, fileURL, fileID string) TranscodeResult {
|
||||
result := TranscodeResult{Status: task.Status, FileURL: fileURL, FileID: fileID}
|
||||
switch {
|
||||
case task.Status == transcodeNoNeedStatus:
|
||||
result.Done = true
|
||||
result.NoNeed = true
|
||||
result.HevcURL = task.FileURL
|
||||
case task.Status == transcodeSuccessStatus:
|
||||
if transcodeFile := strings.TrimSpace(task.TranscodeFile); transcodeFile != "" {
|
||||
result.Done = true
|
||||
result.HevcURL = joinURLPath(domains.MovieSourceCDN, transcodeFile)
|
||||
} else {
|
||||
result.Failed = true
|
||||
result.ErrorMsg = "laosiji transcode succeeded without transcode_file"
|
||||
}
|
||||
case task.Status <= transcodeFailedStatus:
|
||||
result.Failed = true
|
||||
result.ErrorMsg = truncateToolText(task.TranscodeError, domains.UploadKey)
|
||||
if result.ErrorMsg == "" {
|
||||
result.ErrorMsg = fmt.Sprintf("laosiji transcode failed status:%d", task.Status)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func uploadAPIGet(ctx context.Context, endpoint string, params map[string]string) (transcodeTask, error) {
|
||||
data, err := 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 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 key, value := range params {
|
||||
query.Set(key, value)
|
||||
}
|
||||
parsed.RawQuery = query.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := uploadHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("upload api request failed: %s", truncateToolText(err.Error(), params["key"]))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
safeBody := truncateToolBody(body, params["key"])
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return nil, fmt.Errorf("upload api http status:%d body:%s", resp.StatusCode, safeBody)
|
||||
}
|
||||
var apiResp transcodeAPIResp
|
||||
if err = json.Unmarshal(body, &apiResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if apiResp.Status != "y" {
|
||||
errMsg := truncateToolText(apiResp.Error, params["key"])
|
||||
if strings.Contains(errMsg, "最多允许待处理") || strings.Contains(errMsg, "排队处理") {
|
||||
return nil, fmt.Errorf("%w errorCode:%d error:%s", ErrTranscodeQueueFull, apiResp.ErrorCode, errMsg)
|
||||
}
|
||||
if apiResp.ErrorCode == 2000 {
|
||||
return nil, fmt.Errorf("%w errorCode:%d error:%s", errTranscodeTaskNotFound, apiResp.ErrorCode, errMsg)
|
||||
}
|
||||
return nil, fmt.Errorf("upload api errorCode:%d error:%s", apiResp.ErrorCode, errMsg)
|
||||
}
|
||||
return apiResp.Data, nil
|
||||
}
|
||||
|
||||
func md5Hex(value string) string {
|
||||
sum := md5.Sum([]byte(value))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// TranscodeFileID 返回源视频 URL 对应的云端幂等键。
|
||||
func TranscodeFileID(fileURL string) string {
|
||||
fileURL = strings.TrimSpace(fileURL)
|
||||
if fileURL == "" {
|
||||
return ""
|
||||
}
|
||||
return md5Hex(fileURL)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// MovieM3u8SourcePath 把完整 URL 或相对路径规范成本地保存的老司机资源 path。
|
||||
func MovieM3u8SourcePath(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
return joinURLPath("laosiji", movieM3u8RelativePath(raw))
|
||||
}
|
||||
|
||||
// MovieM3u8OriginURL 把本地老司机资源 path 还原成源站完整 URL。
|
||||
func MovieM3u8OriginURL(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
if parsed, err := url.Parse(raw); err == nil && parsed.IsAbs() {
|
||||
return raw
|
||||
}
|
||||
return joinURLPath(APIUrl, movieM3u8RelativePath(raw))
|
||||
}
|
||||
|
||||
func movieM3u8RelativePath(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
if parsed, err := url.Parse(raw); err == nil {
|
||||
if parsed.IsAbs() {
|
||||
raw = parsed.Path
|
||||
} else if parsed.Path != "" {
|
||||
raw = parsed.Path
|
||||
}
|
||||
}
|
||||
raw = strings.TrimPrefix(raw, "/")
|
||||
raw = strings.TrimPrefix(raw, "laosiji/")
|
||||
if idx := strings.Index(raw, "m3m/"); idx >= 0 {
|
||||
return raw[idx:]
|
||||
}
|
||||
if strings.Contains(raw, "/") {
|
||||
return raw
|
||||
}
|
||||
return joinURLPath("m3m", raw)
|
||||
}
|
||||
|
||||
func truncateToolBody(body []byte, secrets ...string) string {
|
||||
return truncateToolText(string(body), secrets...)
|
||||
}
|
||||
|
||||
func truncateToolText(text string, secrets ...string) string {
|
||||
const limit = 300
|
||||
text = hevcpull.RedactText(strings.TrimSpace(text))
|
||||
secrets = append(secrets, APIKey, Appid)
|
||||
for _, secret := range secrets {
|
||||
if secret = strings.TrimSpace(secret); secret != "" {
|
||||
text = strings.ReplaceAll(text, secret, "[REDACTED]")
|
||||
text = strings.ReplaceAll(text, url.QueryEscape(secret), "[REDACTED]")
|
||||
}
|
||||
}
|
||||
if len(text) <= limit {
|
||||
return text
|
||||
}
|
||||
return text[:limit] + "..."
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req)
|
||||
}
|
||||
|
||||
func preserveConfig(t *testing.T) {
|
||||
t.Helper()
|
||||
old := Config{
|
||||
AppID: Appid,
|
||||
APIKey: APIKey,
|
||||
APIUrl: APIUrl,
|
||||
ImageYuan: IMAGEYUAN,
|
||||
NoticeURL: NoticeURL,
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
InitConfig(old)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfiguredRejectsMissingCredentials(t *testing.T) {
|
||||
preserveConfig(t)
|
||||
InitConfig(Config{})
|
||||
|
||||
if Configured() {
|
||||
t.Fatal("empty configuration must not be ready")
|
||||
}
|
||||
_, err := SystemDomains(context.Background())
|
||||
if err == nil || !strings.Contains(err.Error(), "appId,apiKey,apiUrl") {
|
||||
t.Fatalf("expected a diagnostic configuration error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMovieM3u8PathNormalization(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"": "",
|
||||
"demo.m3u8": "laosiji/m3m/demo.m3u8",
|
||||
"m3m/demo.m3u8": "laosiji/m3m/demo.m3u8",
|
||||
"laosiji/m3m/demo.m3u8": "laosiji/m3m/demo.m3u8",
|
||||
"https://cdn.example.com/m3m/demo.m3u8?token=ignored": "laosiji/m3m/demo.m3u8",
|
||||
"https://cdn.example.com/rk130/hevc/demo/index.m3u8": "laosiji/rk130/hevc/demo/index.m3u8",
|
||||
"laosiji/rk130/hevc/demo/index.m3u8?token=also-ignored": "laosiji/rk130/hevc/demo/index.m3u8",
|
||||
}
|
||||
for input, want := range tests {
|
||||
if got := MovieM3u8SourcePath(input); got != want {
|
||||
t.Errorf("MovieM3u8SourcePath(%q) = %q, want %q", input, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranscodeTaskNoNeedIsExplicit(t *testing.T) {
|
||||
sourceURL := "https://app.example/api/app/vid/h5/light/m3u8/source.m3u8"
|
||||
result := transcodeTaskToResult(SystemDomainsResp{MovieSourceCDN: "https://cdn.example"}, transcodeTask{
|
||||
Status: transcodeNoNeedStatus,
|
||||
FileURL: sourceURL,
|
||||
}, sourceURL, TranscodeFileID(sourceURL))
|
||||
|
||||
if !result.Done || !result.NoNeed {
|
||||
t.Fatalf("status=4 must be done and no-need: %+v", result)
|
||||
}
|
||||
if result.HevcURL != sourceURL {
|
||||
t.Fatalf("no-need diagnostic URL = %q, want %q", result.HevcURL, sourceURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranscodeSuccessWithoutFileIsFailed(t *testing.T) {
|
||||
result := transcodeTaskToResult(SystemDomainsResp{}, transcodeTask{
|
||||
Status: transcodeSuccessStatus,
|
||||
}, "https://app.example/source.m3u8", "file-id")
|
||||
|
||||
if result.Done || !result.Failed {
|
||||
t.Fatalf("empty successful task must fail locally instead of staying pending: %+v", result)
|
||||
}
|
||||
if !strings.Contains(result.ErrorMsg, "without transcode_file") {
|
||||
t.Fatalf("missing diagnostic error: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranscodeFailureRedactsCredentials(t *testing.T) {
|
||||
preserveConfig(t)
|
||||
const (
|
||||
appID = "app-id-secret"
|
||||
apiKey = "api-key-secret"
|
||||
uploadKey = "upload key+secret"
|
||||
)
|
||||
InitConfig(Config{
|
||||
AppID: appID,
|
||||
APIKey: apiKey,
|
||||
APIUrl: "https://api.example",
|
||||
})
|
||||
|
||||
result := transcodeTaskToResult(SystemDomainsResp{UploadKey: uploadKey}, transcodeTask{
|
||||
Status: transcodeFailedStatus,
|
||||
TranscodeError: fmt.Sprintf(
|
||||
"failure app=%s api=%s upload=%s encoded=%s",
|
||||
appID,
|
||||
apiKey,
|
||||
uploadKey,
|
||||
url.QueryEscape(uploadKey),
|
||||
),
|
||||
}, "https://app.example/source.m3u8", "file-id")
|
||||
|
||||
if !result.Failed {
|
||||
t.Fatalf("negative cloud status must be failed: %+v", result)
|
||||
}
|
||||
for _, secret := range []string{appID, apiKey, uploadKey, url.QueryEscape(uploadKey)} {
|
||||
if strings.Contains(result.ErrorMsg, secret) {
|
||||
t.Fatalf("transcode error leaked credential %q: %s", secret, result.ErrorMsg)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(result.ErrorMsg, "[REDACTED]") {
|
||||
t.Fatalf("transcode error did not show redaction marker: %s", result.ErrorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadNetworkErrorRedactsKey(t *testing.T) {
|
||||
preserveConfig(t)
|
||||
InitConfig(Config{
|
||||
AppID: "app-id-secret",
|
||||
APIKey: "1234567890abcdef",
|
||||
APIUrl: "https://api.example",
|
||||
})
|
||||
uploadKey := "upload key+secret"
|
||||
oldClient := uploadHTTPClient
|
||||
uploadHTTPClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
return nil, fmt.Errorf("dial failed for %s", req.URL.String())
|
||||
})}
|
||||
t.Cleanup(func() {
|
||||
uploadHTTPClient = oldClient
|
||||
})
|
||||
|
||||
_, err := uploadAPICall(context.Background(), "https://upload.example/query", map[string]string{
|
||||
"key": uploadKey,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected network error")
|
||||
}
|
||||
errText := err.Error()
|
||||
if strings.Contains(errText, uploadKey) || strings.Contains(errText, url.QueryEscape(uploadKey)) {
|
||||
t.Fatalf("network error leaked upload key: %s", errText)
|
||||
}
|
||||
if !strings.Contains(errText, "[REDACTED]") {
|
||||
t.Fatalf("network error did not show redaction marker: %s", errText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoviePostNon2xxIsDiagnosticAndRedacted(t *testing.T) {
|
||||
preserveConfig(t)
|
||||
const (
|
||||
appID = "app-id-secret"
|
||||
apiKey = "1234567890abcdef"
|
||||
)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
_, _ = fmt.Fprintf(w, "failure app=%s key=%s %s", appID, apiKey, strings.Repeat("x", 500))
|
||||
}))
|
||||
defer server.Close()
|
||||
InitConfig(Config{AppID: appID, APIKey: apiKey, APIUrl: server.URL})
|
||||
|
||||
err := moviePost(context.Background(), server.URL, map[string]interface{}{}, &struct{}{})
|
||||
if err == nil || !strings.Contains(err.Error(), "status:502") {
|
||||
t.Fatalf("expected HTTP status diagnostic, got %v", err)
|
||||
}
|
||||
if strings.Contains(err.Error(), appID) || strings.Contains(err.Error(), apiKey) {
|
||||
t.Fatalf("HTTP error leaked credentials: %s", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "[REDACTED]") || !strings.HasSuffix(err.Error(), "...") {
|
||||
t.Fatalf("HTTP error was not safely redacted/truncated: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryH264ToH265TaskUsesExplicitStableFileID(t *testing.T) {
|
||||
const fileID = "stable-file-id"
|
||||
oldClient := uploadHTTPClient
|
||||
uploadHTTPClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
if got := req.URL.Query().Get("file_id"); got != fileID {
|
||||
t.Fatalf("query file_id = %q, want %q", got, fileID)
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(
|
||||
`{"status":"y","data":{"file_id":"stable-file-id","status":1}}`,
|
||||
)),
|
||||
Request: req,
|
||||
}, nil
|
||||
})}
|
||||
t.Cleanup(func() {
|
||||
uploadHTTPClient = oldClient
|
||||
})
|
||||
|
||||
result, found, err := QueryH264ToH265Task(
|
||||
context.Background(),
|
||||
fileID,
|
||||
"https://app.example/source.m3u8?hevc_exp=1&hevc_sig=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
SystemDomainsResp{UploadURL: "https://upload.example", UploadKey: "upload-key"},
|
||||
)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("explicit task query failed: found=%v err=%v", found, err)
|
||||
}
|
||||
if result.FileID != fileID {
|
||||
t.Fatalf("result file ID = %q, want %q", result.FileID, fileID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package laosiji
|
||||
|
||||
type GenerateUndressResponse struct {
|
||||
TaskID string `json:"task_id"` // 任务 ID
|
||||
}
|
||||
|
||||
type QueryUndressResponse struct {
|
||||
TaskID string `json:"task_id"` // 任务 ID
|
||||
Bid string `json:"bid"`
|
||||
Fee string `json:"fee"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"` // 状态 0待处理 -1处理失败 1处理中 2处理成功
|
||||
OutData string `json:"out_data"` // 输出数据 不同的ai 处理的数据不一样 查看文档的描述
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package laosiji_app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetUserName(env string, appID int32, uid uint64) string {
|
||||
if env != "prod" {
|
||||
return fmt.Sprintf("TEST-%d_%d", appID, uid)
|
||||
}
|
||||
return fmt.Sprintf("JHA-%d_%d", appID, uid)
|
||||
}
|
||||
|
||||
type GetAiMateURLReq struct {
|
||||
Username string
|
||||
Nickname string
|
||||
Asset string
|
||||
Currency string
|
||||
Theme string
|
||||
UserAvatar string
|
||||
LogoURL string
|
||||
}
|
||||
|
||||
type GetAiMateURLResp struct {
|
||||
AuthURL string `json:"auth_url"`
|
||||
}
|
||||
|
||||
func GetAiMateURL(ctx context.Context, req GetAiMateURLReq) (resp GetAiMateURLResp, err error) {
|
||||
err = post(ctx, cfg.APIURL+"/lsjapi/aiGirlFriend/auth", map[string]interface{}{
|
||||
"username": req.Username,
|
||||
"nickname": req.Nickname,
|
||||
"asset": req.Asset,
|
||||
"currency": req.Currency,
|
||||
"theme": req.Theme,
|
||||
"user_avatar": req.UserAvatar,
|
||||
"logo_url": req.LogoURL,
|
||||
}, &resp)
|
||||
return
|
||||
}
|
||||
|
||||
type AiMateBringOutReq struct {
|
||||
Username string
|
||||
}
|
||||
|
||||
type AiMateBringOutResp struct {
|
||||
Currency string `json:"currency"`
|
||||
Balance string `json:"balance"`
|
||||
}
|
||||
|
||||
func AiMateBringOut(ctx context.Context, req AiMateBringOutReq) (resp AiMateBringOutResp, err error) {
|
||||
err = post(ctx, cfg.APIURL+"/lsjapi/aiGirlFriend/bringOutAssets", map[string]interface{}{
|
||||
"username": req.Username,
|
||||
}, &resp)
|
||||
return
|
||||
}
|
||||
|
||||
type AiMateOrderLogsReq struct {
|
||||
AppID int32
|
||||
UID uint64
|
||||
Page int
|
||||
PageSize int
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
}
|
||||
|
||||
type AiMateOrderLogsResponse struct {
|
||||
Page string `json:"page"`
|
||||
PageSize string `json:"page_size"`
|
||||
Total string `json:"total"`
|
||||
TotalPage string `json:"total_page"`
|
||||
Items []AiMateOrderLog `json:"items"`
|
||||
}
|
||||
|
||||
type AiMateOrderLog struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Type string `json:"type"`
|
||||
TypeStr string `json:"type_str"`
|
||||
Amount string `json:"amount"`
|
||||
Balance string `json:"balance"`
|
||||
Currency string `json:"currency"`
|
||||
Remark string `json:"remark"`
|
||||
TypeName string `json:"type_name"`
|
||||
RoleID string `json:"role_id"`
|
||||
RoleName string `json:"role_name"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
func GetAiMateOrderLogs(ctx context.Context, env string, req AiMateOrderLogsReq) (resp AiMateOrderLogsResponse, err error) {
|
||||
err = post(ctx, cfg.APIURL+"/lsjapi/aiGirlFriend/orderLogs", map[string]interface{}{
|
||||
"username": GetUserName(env, req.AppID, req.UID),
|
||||
"page": req.Page,
|
||||
"page_size": req.PageSize,
|
||||
"start_time": req.StartTime.Unix(),
|
||||
"end_time": req.EndTime.Unix(),
|
||||
}, &resp)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package laosiji_app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/log"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
AppID string `json:"appId"`
|
||||
APIKey string `json:"apiKey"`
|
||||
APIURL string `json:"apiUrl"`
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
|
||||
func Init(c Config) {
|
||||
c.APIURL = strings.TrimRight(strings.TrimSpace(c.APIURL), "/")
|
||||
cfg = c
|
||||
}
|
||||
|
||||
func Configured() bool {
|
||||
return cfg.AppID != "" && cfg.APIKey != "" && cfg.APIURL != ""
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Status string `json:"status"`
|
||||
Data string `json:"data"`
|
||||
Time string `json:"time"`
|
||||
Error string `json:"error"`
|
||||
ErrorCode int `json:"errorCode"`
|
||||
}
|
||||
|
||||
func post(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) error {
|
||||
if !Configured() {
|
||||
return errors.New("laosiji app config is incomplete")
|
||||
}
|
||||
jsonData, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encryptedData, err := encryptBase64(string(jsonData), cfg.APIKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBufferString(encryptedData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("appid", cfg.AppID)
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("laosiji app http status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result Response
|
||||
if err = json.Unmarshal(body, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
if result.Status != "y" {
|
||||
return fmt.Errorf("laosiji app errorCode:%d error:%s", result.ErrorCode, result.Error)
|
||||
}
|
||||
data, err := decryptBase64(result.Data, cfg.APIKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = json.Unmarshal([]byte(data), response); err != nil {
|
||||
log.Warn("laosiji_app unmarshal response failed", log.E(err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func encryptBase64(input, key string) (string, error) {
|
||||
keyBytes, err := aesKey(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
plainText := pkcs7Padding([]byte(input), block.BlockSize())
|
||||
encrypted := make([]byte, len(plainText))
|
||||
for start, end := 0, block.BlockSize(); start < len(plainText); start, end = start+block.BlockSize(), end+block.BlockSize() {
|
||||
block.Encrypt(encrypted[start:end], plainText[start:end])
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
||||
}
|
||||
|
||||
func decryptBase64(cipherText, key string) (string, error) {
|
||||
keyBytes, err := aesKey(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err := base64.StdEncoding.DecodeString(cipherText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(data) == 0 || len(data)%block.BlockSize() != 0 {
|
||||
return "", errors.New("invalid encrypted payload length")
|
||||
}
|
||||
decrypted := make([]byte, len(data))
|
||||
for start, end := 0, block.BlockSize(); start < len(data); start, end = start+block.BlockSize(), end+block.BlockSize() {
|
||||
block.Decrypt(decrypted[start:end], data[start:end])
|
||||
}
|
||||
decrypted, err = pkcs7UnPadding(decrypted, block.BlockSize())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(decrypted), nil
|
||||
}
|
||||
|
||||
func aesKey(key string) ([]byte, error) {
|
||||
if len(key) > aes.BlockSize {
|
||||
key = key[:aes.BlockSize]
|
||||
}
|
||||
if len(key) != aes.BlockSize {
|
||||
return nil, fmt.Errorf("invalid AES key length %d", len(key))
|
||||
}
|
||||
return []byte(key), nil
|
||||
}
|
||||
|
||||
func pkcs7Padding(src []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(src)%blockSize
|
||||
return append(src, bytes.Repeat([]byte{byte(padding)}, padding)...)
|
||||
}
|
||||
|
||||
func pkcs7UnPadding(src []byte, blockSize int) ([]byte, error) {
|
||||
if len(src) == 0 {
|
||||
return nil, errors.New("empty padded payload")
|
||||
}
|
||||
padding := int(src[len(src)-1])
|
||||
if padding == 0 || padding > blockSize || padding > len(src) {
|
||||
return nil, errors.New("invalid PKCS7 padding")
|
||||
}
|
||||
for _, value := range src[len(src)-padding:] {
|
||||
if int(value) != padding {
|
||||
return nil, errors.New("invalid PKCS7 padding")
|
||||
}
|
||||
}
|
||||
return src[:len(src)-padding], nil
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package laosiji_app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncryptDecryptRoundTrip(t *testing.T) {
|
||||
const (
|
||||
key = "1234567890abcdef"
|
||||
input = `{"uid":204,"name":"测试用户"}`
|
||||
)
|
||||
encrypted, err := encryptBase64(input, key)
|
||||
if err != nil {
|
||||
t.Fatalf("encryptBase64() error = %v", err)
|
||||
}
|
||||
decrypted, err := decryptBase64(encrypted, key)
|
||||
if err != nil {
|
||||
t.Fatalf("decryptBase64() error = %v", err)
|
||||
}
|
||||
if decrypted != input {
|
||||
t.Fatalf("decryptBase64() = %q, want %q", decrypted, input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserName(t *testing.T) {
|
||||
if got := GetUserName("test", 204, 123); got != "TEST-204_123" {
|
||||
t.Fatalf("test username = %q", got)
|
||||
}
|
||||
if got := GetUserName("prod", 204, 123); got != "JHA-204_123" {
|
||||
t.Fatalf("prod username = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigured(t *testing.T) {
|
||||
original := cfg
|
||||
t.Cleanup(func() { cfg = original })
|
||||
|
||||
Init(Config{})
|
||||
if Configured() {
|
||||
t.Fatal("empty config must not be configured")
|
||||
}
|
||||
Init(Config{AppID: "app", APIKey: "1234567890abcdef", APIURL: "https://example.com/"})
|
||||
if !Configured() {
|
||||
t.Fatal("complete config must be configured")
|
||||
}
|
||||
if cfg.APIURL != "https://example.com" {
|
||||
t.Fatalf("APIURL = %q", cfg.APIURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAiMateURLRequestAndResponse(t *testing.T) {
|
||||
const key = "1234567890abcdef"
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/lsjapi/aiGirlFriend/auth" {
|
||||
t.Errorf("path = %q", r.URL.Path)
|
||||
}
|
||||
if r.Header.Get("appid") != "test-app" {
|
||||
t.Errorf("appid = %q", r.Header.Get("appid"))
|
||||
}
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Errorf("read request: %v", err)
|
||||
return
|
||||
}
|
||||
plainText, err := decryptBase64(string(body), key)
|
||||
if err != nil {
|
||||
t.Errorf("decrypt request: %v", err)
|
||||
return
|
||||
}
|
||||
var request map[string]interface{}
|
||||
if err = json.Unmarshal([]byte(plainText), &request); err != nil {
|
||||
t.Errorf("unmarshal request: %v", err)
|
||||
return
|
||||
}
|
||||
if request["username"] != "TEST-204_99" || request["asset"] != "12.30" {
|
||||
t.Errorf("request = %#v", request)
|
||||
}
|
||||
|
||||
data, err := encryptBase64(`{"auth_url":"https://example.com/ai"}`, key)
|
||||
if err != nil {
|
||||
t.Errorf("encrypt response: %v", err)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(Response{Status: "y", Data: data})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
original := cfg
|
||||
t.Cleanup(func() { cfg = original })
|
||||
Init(Config{AppID: "test-app", APIKey: key, APIURL: server.URL})
|
||||
|
||||
response, err := GetAiMateURL(context.Background(), GetAiMateURLReq{
|
||||
Username: "TEST-204_99",
|
||||
Asset: "12.30",
|
||||
Currency: "CNY",
|
||||
Theme: "dark",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GetAiMateURL() error = %v", err)
|
||||
}
|
||||
if response.AuthURL != "https://example.com/ai" {
|
||||
t.Fatalf("AuthURL = %q", response.AuthURL)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Package localcache 提供进程内一级缓存(基于 go-cache)。
|
||||
//
|
||||
// C 为包级变量,导入即就绪、永不为 nil,可被任意分层(common/models/app/web/skd)
|
||||
// 与任意二进制安全使用。此前各服务各自维护 appg.Cache / skdg.Cache,公共代码一旦
|
||||
// 依赖某个具体服务的缓存全局变量,就会在未初始化该变量的进程(如 skd 调用 appg.Cache)
|
||||
// 中触发 nil panic。统一到本包后从根上消除该耦合。
|
||||
package localcache
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
// C 进程内一级缓存实例。默认 5 分钟过期、10 分钟清理一次(与原 appg/skdg 缓存参数保持一致)。
|
||||
var C = cache.New(5*time.Minute, 10*time.Minute)
|
||||
@@ -0,0 +1,113 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/middleware/requestid"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultZap, _ = zap.NewProductionConfig().Build()
|
||||
|
||||
zapLogger *zap.Logger
|
||||
ZapLog *zap.Logger
|
||||
Debug = defaultZap.Debug
|
||||
Info = defaultZap.Info
|
||||
Warn = defaultZap.Warn
|
||||
Error = defaultZap.Error
|
||||
Fatal = defaultZap.Fatal
|
||||
)
|
||||
|
||||
// Options
|
||||
type Options struct {
|
||||
Level string // debug, warn, info, error fatal //default info
|
||||
DisableStack bool `json:"disableStack"`
|
||||
}
|
||||
|
||||
func Init(opts Options) {
|
||||
cfg := zap.NewProductionConfig()
|
||||
cfg.EncoderConfig.EncodeTime = timeEncoder
|
||||
cfg.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
|
||||
cfg.Level = zap.NewAtomicLevelAt(zapLevel(opts.Level))
|
||||
cfg.DisableStacktrace = opts.DisableStack
|
||||
//cfg.EncoderConfig.EncodeCaller = zapcore.FullCallerEncoder
|
||||
zapLogger, _ = cfg.Build()
|
||||
ZapLog = zapLogger
|
||||
Debug = zapLogger.Debug
|
||||
Info = zapLogger.Info
|
||||
Warn = zapLogger.Warn
|
||||
Error = zapLogger.Error
|
||||
Fatal = zapLogger.Fatal
|
||||
}
|
||||
|
||||
func DebugX(ctx context.Context, msg string, fs ...Field) {
|
||||
Debug(msg, appendContext(ctx, fs...)...)
|
||||
}
|
||||
|
||||
func InfoX(ctx context.Context, msg string, fs ...Field) {
|
||||
Info(msg, appendContext(ctx, fs...)...)
|
||||
}
|
||||
|
||||
func WarnX(ctx context.Context, msg string, fs ...Field) {
|
||||
Warn(msg, appendContext(ctx, fs...)...)
|
||||
}
|
||||
|
||||
func ErrorX(ctx context.Context, msg string, fs ...Field) {
|
||||
Error(msg, appendContext(ctx, fs...)...)
|
||||
}
|
||||
|
||||
func FatalX(ctx context.Context, msg string, fs ...Field) {
|
||||
Fatal(msg, appendContext(ctx, fs...)...)
|
||||
}
|
||||
|
||||
func timeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
|
||||
enc.AppendString(t.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
func zapLevel(level string) zapcore.Level {
|
||||
level = strings.ToLower(level)
|
||||
l := zapcore.InfoLevel
|
||||
switch level {
|
||||
case "debug":
|
||||
l = zapcore.DebugLevel
|
||||
case "info":
|
||||
l = zapcore.InfoLevel
|
||||
case "warn":
|
||||
l = zapcore.WarnLevel
|
||||
case "error":
|
||||
l = zapcore.ErrorLevel
|
||||
case "fatal":
|
||||
l = zapcore.FatalLevel
|
||||
default:
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
type Field = zap.Field
|
||||
|
||||
// Any
|
||||
func Any(key string, value interface{}) Field {
|
||||
return zap.Any(key, value)
|
||||
}
|
||||
|
||||
// E shortcut for Any("err", err)
|
||||
func E(err error) Field {
|
||||
return Any("err", err)
|
||||
}
|
||||
|
||||
// R is a conveinient way to log request id.
|
||||
func R(ctx context.Context) Field {
|
||||
rid := ctx.Value(requestid.ContextKey)
|
||||
return Any("request-id", rid)
|
||||
}
|
||||
|
||||
func appendContext(ctx context.Context, fs ...Field) (fields []Field) {
|
||||
fields = append(fields, R(ctx))
|
||||
fields = append(fields, fs...)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
package m3u8
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/web/webg"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafov/m3u8"
|
||||
)
|
||||
|
||||
func GetAPPM3u8(source, fileName, _ string, cdn string, fsIo func(source, mds string) (data []byte, err error)) (*bytes.Buffer, error) {
|
||||
mds := GetMediaResouce(source)
|
||||
key := m3u8PureCacheKey(source, mds)
|
||||
m3u8redis, err := appg.Redis.Get(key)
|
||||
var m3u8Byte []byte
|
||||
if m3u8redis != nil && err == nil {
|
||||
//判断是否是m3u8文件
|
||||
if IsM3u8([]byte(*m3u8redis)) {
|
||||
m3u8Byte = []byte(*m3u8redis)
|
||||
} else {
|
||||
_, _ = appg.Redis.Del(key)
|
||||
}
|
||||
}
|
||||
if mds == constant.MediaSourceLaoSiJi {
|
||||
source = normalizeLaosijiM3u8Source(source)
|
||||
}
|
||||
if m3u8Byte == nil || len(m3u8Byte) == 0 {
|
||||
m3u8Byte, err = fsIo(source, mds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = appg.Redis.Set(key, string(m3u8Byte), redisconst.M3u8CacheExpire)
|
||||
}
|
||||
// TS 分片鉴权签名密钥:优先取配置(appg.Conf.Base.TsAuth),未配置时回退内置默认(版本 default + 内置常量)
|
||||
keyVersion, authKey := appg.Conf.Base.TsAuth.Resolve()
|
||||
var bytebuff *bytes.Buffer
|
||||
if mds == constant.MediaSourcePMS {
|
||||
bytebuff = DecodeFromReader(m3u8Byte, "", "/api/app/vid", "", mds, authKey, keyVersion, source)
|
||||
} else if mds == constant.MediaSourceSP {
|
||||
bytebuff = DecodeFromReader(m3u8Byte, cdn+strings.TrimSuffix(source, fileName), "/api/app/vid/sec", "", mds, authKey, keyVersion, source)
|
||||
} else if mds == constant.MediaSourceLaoSiJi {
|
||||
bytebuff = DecodeFromReader(m3u8Byte, cdn, "/api/app/vid/lsjsec", "", mds, authKey, keyVersion, source)
|
||||
} else if mds == constant.MediaSourceJH1B {
|
||||
bytebuff = DecodeFromReader(m3u8Byte, cdn+strings.TrimSuffix(source, fileName), "/api/app/vid/m3u8sec", "", mds, authKey, keyVersion, source)
|
||||
}
|
||||
if bytebuff == nil {
|
||||
log.Warn("can't create m3u8 file", log.Any("source", source))
|
||||
return nil, errors.New(stderr.CodeEmptyData.Msg())
|
||||
}
|
||||
return bytebuff, nil
|
||||
}
|
||||
|
||||
func normalizeLaosijiM3u8Source(source string) string {
|
||||
source = strings.TrimLeft(strings.TrimSpace(source), "/")
|
||||
return strings.TrimPrefix(source, "laosiji/")
|
||||
}
|
||||
|
||||
// m3u8PureCacheKey isolates raw playlists by media source and their complete
|
||||
// normalized source path. Hashing keeps the Redis key bounded while avoiding
|
||||
// collisions between common basenames such as index.m3u8.
|
||||
func m3u8PureCacheKey(source, mds string) string {
|
||||
normalizedSource := normalizeM3u8CacheSource(source)
|
||||
sum := sha256.Sum256([]byte(mds + "\x00" + normalizedSource))
|
||||
return redisconst.M3u8PureCacheFmt(mds + ":" + hex.EncodeToString(sum[:]))
|
||||
}
|
||||
|
||||
func normalizeM3u8CacheSource(source string) string {
|
||||
source = strings.TrimSpace(source)
|
||||
parsed, err := url.Parse(source)
|
||||
if err != nil {
|
||||
return path.Clean("/" + strings.TrimLeft(strings.ReplaceAll(source, "\\", "/"), "/"))
|
||||
}
|
||||
|
||||
parsed.Scheme = strings.ToLower(parsed.Scheme)
|
||||
parsed.Host = strings.ToLower(parsed.Host)
|
||||
parsed.Fragment = ""
|
||||
parsed.Path = path.Clean("/" + strings.TrimLeft(strings.ReplaceAll(parsed.Path, "\\", "/"), "/"))
|
||||
parsed.RawPath = ""
|
||||
if parsed.RawQuery != "" {
|
||||
if query, queryErr := url.ParseQuery(parsed.RawQuery); queryErr == nil {
|
||||
parsed.RawQuery = query.Encode()
|
||||
}
|
||||
}
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
func DecodeFromReader(reader []byte, cdnUrl, serUrl, key, mds, authKey, keyVersion, source string) *bytes.Buffer {
|
||||
p, listType, err := m3u8.DecodeFrom(bytes.NewReader(reader), false)
|
||||
if err != nil {
|
||||
log.Error("m3u8 decodeFrom error", log.E(err))
|
||||
return nil
|
||||
}
|
||||
switch listType {
|
||||
case m3u8.MEDIA:
|
||||
return Create(p.(*m3u8.MediaPlaylist), cdnUrl, serUrl, key, mds, authKey, keyVersion, source)
|
||||
case m3u8.MASTER:
|
||||
return p.(*m3u8.MasterPlaylist).Encode()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RewriteMasterPlaylist rewrites every child playlist URI in a master HLS
|
||||
// playlist. Media playlists are returned unchanged. The signed H.265 pull
|
||||
// endpoint uses this so variants and alternate renditions do not lose their
|
||||
// HMAC when the cloud transcoder follows a relative child URI.
|
||||
func RewriteMasterPlaylist(reader []byte, rewrite func(string) (string, error)) (*bytes.Buffer, error) {
|
||||
if rewrite == nil {
|
||||
return bytes.NewBuffer(reader), nil
|
||||
}
|
||||
playlist, listType, err := m3u8.DecodeFrom(bytes.NewReader(reader), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if listType != m3u8.MASTER {
|
||||
return bytes.NewBuffer(reader), nil
|
||||
}
|
||||
|
||||
master := playlist.(*m3u8.MasterPlaylist)
|
||||
rewritten := make(map[string]string)
|
||||
rewriteURI := func(rawURI string) (string, error) {
|
||||
rawURI = strings.TrimSpace(rawURI)
|
||||
if rawURI == "" {
|
||||
return "", nil
|
||||
}
|
||||
if value, ok := rewritten[rawURI]; ok {
|
||||
return value, nil
|
||||
}
|
||||
value, rewriteErr := rewrite(rawURI)
|
||||
if rewriteErr != nil {
|
||||
return "", rewriteErr
|
||||
}
|
||||
rewritten[rawURI] = value
|
||||
return value, nil
|
||||
}
|
||||
|
||||
seenAlternatives := make(map[*m3u8.Alternative]struct{})
|
||||
for _, variant := range master.Variants {
|
||||
if variant == nil {
|
||||
continue
|
||||
}
|
||||
variant.URI, err = rewriteURI(variant.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rewrite HLS variant: %w", err)
|
||||
}
|
||||
for _, alternative := range variant.Alternatives {
|
||||
if alternative == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := seenAlternatives[alternative]; ok {
|
||||
continue
|
||||
}
|
||||
seenAlternatives[alternative] = struct{}{}
|
||||
alternative.URI, err = rewriteURI(alternative.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rewrite HLS alternative: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return master.Encode(), nil
|
||||
}
|
||||
|
||||
func Create(src *m3u8.MediaPlaylist, cdnUrl, serUrl, key, mds, authKey, keyVersion, source string) *bytes.Buffer {
|
||||
p, e := m3u8.NewMediaPlaylist(src.WinSize(), src.Count())
|
||||
if e != nil {
|
||||
log.Error(fmt.Sprintf("Creating of media playlist failed: %s", e))
|
||||
return nil
|
||||
}
|
||||
p.SetVersion(src.Version())
|
||||
p.SeqNo = src.SeqNo
|
||||
p.DiscontinuitySeq = src.DiscontinuitySeq
|
||||
p.StartTime = src.StartTime
|
||||
p.StartTimePrecise = src.StartTimePrecise
|
||||
p.MediaType = src.MediaType
|
||||
p.Iframe = src.Iframe
|
||||
p.Args = src.Args
|
||||
p.WV = src.WV
|
||||
for _, customTag := range src.Custom {
|
||||
p.SetCustomTag(customTag)
|
||||
}
|
||||
now := time.Now()
|
||||
var activeMap *m3u8.Map
|
||||
if src.Map != nil {
|
||||
activeMap = src.Map
|
||||
p.SetDefaultMap(
|
||||
rewritePlaylistMediaURI(src.Map.URI, cdnUrl, authKey, keyVersion, mds, source, now),
|
||||
src.Map.Limit,
|
||||
src.Map.Offset,
|
||||
)
|
||||
}
|
||||
activeKey := src.Key
|
||||
for _, v := range src.Segments {
|
||||
if v != nil {
|
||||
currentMapURI := ""
|
||||
if activeMap != nil {
|
||||
currentMapURI = activeMap.URI
|
||||
}
|
||||
if v.Map != nil {
|
||||
currentMapURI = v.Map.URI
|
||||
}
|
||||
tsurl := resolvePlaylistMediaURI(v.URI, currentMapURI)
|
||||
tsurl = rewritePlaylistMediaURI(tsurl, cdnUrl, authKey, keyVersion, mds, source, now)
|
||||
if err := p.Append(tsurl, v.Duration, v.Title); err != nil {
|
||||
log.Error(fmt.Sprintf("Appending of media playlist failed: %s", err))
|
||||
return nil
|
||||
}
|
||||
if v.Limit > 0 {
|
||||
if err := p.SetRange(v.Limit, v.Offset); err != nil {
|
||||
log.Error(fmt.Sprintf("Setting media byte range failed: %s", err))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if v.Discontinuity {
|
||||
if err := p.SetDiscontinuity(); err != nil {
|
||||
log.Error(fmt.Sprintf("Setting media discontinuity failed: %s", err))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if !v.ProgramDateTime.IsZero() {
|
||||
if err := p.SetProgramDateTime(v.ProgramDateTime); err != nil {
|
||||
log.Error(fmt.Sprintf("Setting media program date failed: %s", err))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if v.SCTE != nil {
|
||||
if err := p.SetSCTE35(v.SCTE); err != nil {
|
||||
log.Error(fmt.Sprintf("Setting media SCTE tag failed: %s", err))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
for _, customTag := range v.Custom {
|
||||
if err := p.SetCustomSegmentTag(customTag); err != nil {
|
||||
log.Error(fmt.Sprintf("Setting media custom tag failed: %s", err))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if v.Map != nil && !playlistMapsEqual(v.Map, activeMap) {
|
||||
if err := p.SetMap(
|
||||
rewritePlaylistMediaURI(v.Map.URI, cdnUrl, authKey, keyVersion, mds, source, now),
|
||||
v.Map.Limit,
|
||||
v.Map.Offset,
|
||||
); err != nil {
|
||||
log.Error(fmt.Sprintf("Setting map of media playlist failed: %s", err))
|
||||
return nil
|
||||
}
|
||||
activeMap = v.Map
|
||||
}
|
||||
if v.Key != nil && !playlistKeysEqual(v.Key, activeKey) {
|
||||
if err := p.SetKey(
|
||||
v.Key.Method,
|
||||
playlistKeyURI(v.Key.URI, serUrl, mds),
|
||||
playlistKeyIV(v.Key.IV, key),
|
||||
v.Key.Keyformat,
|
||||
v.Key.Keyformatversions,
|
||||
); err != nil {
|
||||
log.Error(fmt.Sprintf("Setting segment key failed: %s", err))
|
||||
return nil
|
||||
}
|
||||
activeKey = v.Key
|
||||
}
|
||||
}
|
||||
}
|
||||
if src.Key != nil {
|
||||
_ = p.SetDefaultKey(
|
||||
src.Key.Method,
|
||||
playlistKeyURI(src.Key.URI, serUrl, mds),
|
||||
playlistKeyIV(src.Key.IV, key),
|
||||
src.Key.Keyformat,
|
||||
src.Key.Keyformatversions,
|
||||
)
|
||||
}
|
||||
if src.TargetDuration > p.TargetDuration {
|
||||
p.TargetDuration = src.TargetDuration
|
||||
}
|
||||
p.Close()
|
||||
return p.Encode()
|
||||
}
|
||||
|
||||
func playlistMapsEqual(a, b *m3u8.Map) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == b
|
||||
}
|
||||
return a.URI == b.URI && a.Limit == b.Limit && a.Offset == b.Offset
|
||||
}
|
||||
|
||||
func playlistKeysEqual(a, b *m3u8.Key) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == b
|
||||
}
|
||||
return a.Method == b.Method &&
|
||||
a.URI == b.URI &&
|
||||
a.IV == b.IV &&
|
||||
a.Keyformat == b.Keyformat &&
|
||||
a.Keyformatversions == b.Keyformatversions
|
||||
}
|
||||
|
||||
func playlistKeyURI(sourceKeyURI, serverURL, mds string) string {
|
||||
if mds != constant.MediaSourcePMS {
|
||||
return serverURL
|
||||
}
|
||||
if strings.Contains(sourceKeyURI, "/mt/enkeymt") {
|
||||
return serverURL + "/pms/mt_sec"
|
||||
}
|
||||
return serverURL + "/pms/sec"
|
||||
}
|
||||
|
||||
func playlistKeyIV(sourceIV, override string) string {
|
||||
if override != "" {
|
||||
return override
|
||||
}
|
||||
return sourceIV
|
||||
}
|
||||
|
||||
// resolvePlaylistMediaURI uses an absolute EXT-X-MAP URI as the base for
|
||||
// relative fMP4/CMAF segment paths.
|
||||
func resolvePlaylistMediaURI(mediaURI, mapURI string) string {
|
||||
if strings.TrimSpace(mediaURI) == "" || strings.TrimSpace(mapURI) == "" {
|
||||
return mediaURI
|
||||
}
|
||||
parsedMediaURI, err := url.Parse(mediaURI)
|
||||
if err == nil && parsedMediaURI.IsAbs() {
|
||||
return mediaURI
|
||||
}
|
||||
parsedMapURI, err := url.Parse(mapURI)
|
||||
if err != nil || !parsedMapURI.IsAbs() {
|
||||
return mediaURI
|
||||
}
|
||||
parsedMapURI.RawQuery = ""
|
||||
parsedMapURI.Fragment = ""
|
||||
ref, err := url.Parse(mediaURI)
|
||||
if err != nil {
|
||||
return mediaURI
|
||||
}
|
||||
return parsedMapURI.ResolveReference(ref).String()
|
||||
}
|
||||
|
||||
// rewritePlaylistMediaURI applies the same source, auth and CDN rewriting to
|
||||
// regular media segments and EXT-X-MAP initialization segments.
|
||||
func rewritePlaylistMediaURI(mediaURI, cdnUrl, authKey, keyVersion, mds, source string, now time.Time) string {
|
||||
mediaURL := mediaURI
|
||||
if mds == constant.MediaSourceLaoSiJi {
|
||||
mediaURL = removeDomainPrefix(mediaURL)
|
||||
mediaURL = filepath.Join("laosiji", mediaURL)
|
||||
}
|
||||
if len(authKey) > 0 {
|
||||
uri := filepath.Join(filepath.Dir(source), mediaURL)
|
||||
if mds == constant.MediaSourceLaoSiJi {
|
||||
uri = "/" + mediaURL
|
||||
}
|
||||
urlAuth := generateUrlAuth(now, uri, authKey, keyVersion)
|
||||
mediaURL = fmt.Sprintf("%s%s", mediaURL, urlAuth)
|
||||
}
|
||||
if cdnUrl != "" {
|
||||
mediaURL = common.BindUrl(cdnUrl, mediaURL)
|
||||
}
|
||||
return mediaURL
|
||||
}
|
||||
|
||||
func GetTsUrlFromReader(reader []byte, preUrl, cdnUrl string) []string {
|
||||
p, listType, err := m3u8.DecodeFrom(bytes.NewReader(reader), false)
|
||||
if err != nil {
|
||||
log.Error("m3u8 decodeFrom error", log.E(err))
|
||||
return []string{}
|
||||
}
|
||||
var tsUrl []string
|
||||
switch listType {
|
||||
case m3u8.MEDIA:
|
||||
src := p.(*m3u8.MediaPlaylist)
|
||||
tsUrl = make([]string, 0, len(src.Segments))
|
||||
for _, v := range src.Segments {
|
||||
if v != nil {
|
||||
tsUrl = append(tsUrl, common.BindUrl(cdnUrl, preUrl, v.URI))
|
||||
}
|
||||
}
|
||||
}
|
||||
return tsUrl
|
||||
}
|
||||
|
||||
func IsM3u8(reader []byte) bool {
|
||||
_, _, err := m3u8.DecodeFrom(bytes.NewReader(reader), false)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
const timeout = 5 * time.Second
|
||||
|
||||
// UploadSuccess 上传成功回调, 通知文件服务, 文件上传完成
|
||||
func UploadSuccess(id string) (code stderr.Code) {
|
||||
var params = map[string]interface{}{
|
||||
"id": id,
|
||||
}
|
||||
c, cancle := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancle()
|
||||
respBody := commod.Resp{}
|
||||
//请求
|
||||
httpStatus, err := httputil.DefaultClientPostJsonWithRespWithCtx(c, &respBody, webg.Conf.URL.FileInfoUrl, nil, params)
|
||||
log.Info("http method UploadSuccess response code ==>", log.Any("httpStatus", httpStatus), log.Any("respCode", respBody.Code))
|
||||
if err != nil {
|
||||
log.Error("UploadSuccess POSTJsonWithJResp error", log.E(err))
|
||||
return stderr.ErrConnectToFs
|
||||
}
|
||||
if respBody.Code != http.StatusOK {
|
||||
log.Error("UploadSuccess status error", log.Any("respBody.Code", respBody.Code), log.E(err))
|
||||
return stderr.ErrFsServerFile
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
// 媒体资源库选择
|
||||
func GetMediaResouce(source string) string {
|
||||
if strings.HasPrefix(source, "v1/") || strings.HasPrefix(source, "/v1/") ||
|
||||
strings.HasPrefix(source, "v2/") || strings.HasPrefix(source, "/v2/") ||
|
||||
strings.HasPrefix(source, "v3/") || strings.HasPrefix(source, "/v3/") {
|
||||
return constant.MediaSourceJH1B
|
||||
}
|
||||
|
||||
if strings.HasPrefix(source, constant.MediaSourcePMSPrefixPath) {
|
||||
return constant.MediaSourcePMS
|
||||
}
|
||||
if strings.HasPrefix(source, constant.MediaSourceSPPrefixPath) {
|
||||
return constant.MediaSourceSP
|
||||
}
|
||||
if strings.Contains(source, "laosiji") {
|
||||
return constant.MediaSourceLaoSiJi
|
||||
}
|
||||
return constant.MediaSourceSP
|
||||
}
|
||||
|
||||
// 生成鉴权url
|
||||
func generateUrlAuth(now time.Time, path, authKey, keyVersion string) string {
|
||||
timestamp := now.Unix()
|
||||
// randId := 0
|
||||
//signStr := fmt.Sprintf("%s%s%d", authKey, path, timestamp)
|
||||
//md5Str := getMD5Sign(signStr)
|
||||
//urlAuth := fmt.Sprintf("?t=%d&k=%s", timestamp, md5Str)
|
||||
signStr := fmt.Sprintf("%s-%d-0-0-%s", path, timestamp, authKey)
|
||||
md5Str := getMD5Sign(signStr)
|
||||
// c={appid}(commod.KFK_APPID) 供 CDN 按应用区分统计/路由;v={keyVersion} 供 CDN 按版本选择校验密钥
|
||||
urlAuth := fmt.Sprintf("?md=%d-0-0-%s&c=%d&v=%s", timestamp, md5Str, commod.KFK_APPID, keyVersion)
|
||||
return urlAuth
|
||||
}
|
||||
|
||||
// getMD5Sign 得到签名
|
||||
func getMD5Sign(buf string) string {
|
||||
md5Ctx := md5.New()
|
||||
md5Ctx.Write([]byte(buf))
|
||||
cipherStr := md5Ctx.Sum(nil)
|
||||
nsign := hex.EncodeToString(cipherStr)
|
||||
return nsign
|
||||
}
|
||||
|
||||
func replaceDomainAndPath(originalURL, newBase string) string {
|
||||
// 解析原始 URL
|
||||
parsedURL, err := url.Parse(originalURL)
|
||||
if err != nil {
|
||||
return originalURL
|
||||
}
|
||||
|
||||
// 解析新基础 URL
|
||||
newBaseURL, err := url.Parse(newBase)
|
||||
if err != nil {
|
||||
return originalURL
|
||||
}
|
||||
|
||||
// 替换协议、主机和基础路径
|
||||
parsedURL.Scheme = newBaseURL.Scheme
|
||||
parsedURL.Host = newBaseURL.Host
|
||||
|
||||
// 构建新路径:/laosiji + 原始路径(去掉旧域名部分)
|
||||
oldBasePath := ""
|
||||
parsedURL.Path = path.Join(newBaseURL.Path, strings.TrimPrefix(parsedURL.Path, oldBasePath))
|
||||
|
||||
// 移除查询参数
|
||||
parsedURL.RawQuery = ""
|
||||
|
||||
return parsedURL.String()
|
||||
}
|
||||
func removeDomainPrefix(originalURL string) string {
|
||||
// 解析原始 URL
|
||||
parsedURL, err := url.Parse(originalURL)
|
||||
if err != nil {
|
||||
return originalURL
|
||||
}
|
||||
oldBasePath := ""
|
||||
return strings.TrimPrefix(parsedURL.Path, oldBasePath)
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
package m3u8
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"91porn-server/common/constant"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
|
||||
grafovm3u8 "github.com/grafov/m3u8"
|
||||
)
|
||||
|
||||
func TestM3u8PureCacheKeyUsesFullSource(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sourceA string
|
||||
sourceB string
|
||||
}{
|
||||
{
|
||||
name: "same basename in different directories",
|
||||
sourceA: "/sp/movie-a/index.m3u8",
|
||||
sourceB: "/sp/movie-b/index.m3u8",
|
||||
},
|
||||
{
|
||||
name: "filenames previously collapsed by strings.Trim",
|
||||
sourceA: "/sp/movie/movie.m3u8",
|
||||
sourceB: "/sp/movie/ovie.m3u8",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
keyA := m3u8PureCacheKey(tt.sourceA, constant.MediaSourceSP)
|
||||
keyB := m3u8PureCacheKey(tt.sourceB, constant.MediaSourceSP)
|
||||
if keyA == keyB {
|
||||
t.Fatalf("different playlist sources share cache key %q", keyA)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLaosijiM3u8Source(t *testing.T) {
|
||||
for _, source := range []string{
|
||||
"laosiji/m3m/movie/index.m3u8",
|
||||
"/laosiji/m3m/movie/index.m3u8",
|
||||
} {
|
||||
if got := normalizeLaosijiM3u8Source(source); got != "m3m/movie/index.m3u8" {
|
||||
t.Fatalf("normalizeLaosijiM3u8Source(%q) = %q", source, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestM3u8PureCacheKeyNormalizesSourceAndIncludesMediaSource(t *testing.T) {
|
||||
const source = "/sp/movie/season/../index.m3u8?b=2&a=1"
|
||||
normalizedVariant := " /sp/movie/index.m3u8?a=1&b=2#ignored "
|
||||
|
||||
spKey := m3u8PureCacheKey(source, constant.MediaSourceSP)
|
||||
if got := m3u8PureCacheKey(normalizedVariant, constant.MediaSourceSP); got != spKey {
|
||||
t.Fatalf("equivalent sources should share a cache key: %q != %q", got, spKey)
|
||||
}
|
||||
if got := m3u8PureCacheKey(source, constant.MediaSourcePMS); got == spKey {
|
||||
t.Fatalf("different media sources should not share a cache key: %q", got)
|
||||
}
|
||||
if !strings.HasPrefix(spKey, redisconst.M3u8PureCacheFmt(constant.MediaSourceSP+":")) {
|
||||
t.Fatalf("cache key should include media source prefix, got %q", spKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeFromReaderKeepsHevcMap(t *testing.T) {
|
||||
data := []byte(`#EXTM3U
|
||||
#EXT-X-TARGETDURATION:6
|
||||
#EXT-X-VERSION:6
|
||||
#EXT-X-MEDIA-SEQUENCE:1
|
||||
#EXT-X-INDEPENDENT-SEGMENTS
|
||||
#EXT-X-MAP:URI="https://cdn.g3ejjm8m.com/rk130/m3u8-v5/f98/f98495114eca4ae66714b0d0249d8980/683a_0.m4s"
|
||||
#EXTINF:6.006,
|
||||
683a_1.m4s
|
||||
#EXTINF:6.006,
|
||||
683a_2.m4s
|
||||
#EXT-X-ENDLIST
|
||||
`)
|
||||
buf := DecodeFromReader(
|
||||
data,
|
||||
"https://rs.hyxrp.cn",
|
||||
"/api/app/vid/lsjsec",
|
||||
"",
|
||||
constant.MediaSourceLaoSiJi,
|
||||
constant.MediaSourceAuthKey,
|
||||
constant.DefaultTsAuthKeyVersion,
|
||||
"m3m/demo.m3u8",
|
||||
)
|
||||
if buf == nil {
|
||||
t.Fatal("DecodeFromReader returned nil")
|
||||
}
|
||||
|
||||
got := buf.String()
|
||||
for _, want := range []string{
|
||||
`#EXT-X-MAP:URI="https://rs.hyxrp.cn/laosiji/rk130/m3u8-v5/f98/f98495114eca4ae66714b0d0249d8980/683a_0.m4s?md=`,
|
||||
`https://rs.hyxrp.cn/laosiji/rk130/m3u8-v5/f98/f98495114eca4ae66714b0d0249d8980/683a_1.m4s?md=`,
|
||||
`https://rs.hyxrp.cn/laosiji/rk130/m3u8-v5/f98/f98495114eca4ae66714b0d0249d8980/683a_2.m4s?md=`,
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("rewritten playlist missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
if strings.Contains(got, "#EXT-X-KEY") {
|
||||
t.Fatalf("unencrypted HEVC playlist should not add EXT-X-KEY:\n%s", got)
|
||||
}
|
||||
if strings.Contains(got, "/laosiji/683a_") {
|
||||
t.Fatalf("relative HEVC segments were not resolved against EXT-X-MAP:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeFromReaderKeepsEncryptedH264Key(t *testing.T) {
|
||||
data := []byte(`#EXTM3U
|
||||
#EXT-X-TARGETDURATION:6
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-KEY:METHOD=AES-128,URI="source.key",IV=0x00000000000000000000000000000001
|
||||
#EXTINF:6,
|
||||
segment.ts
|
||||
#EXT-X-ENDLIST
|
||||
`)
|
||||
buf := DecodeFromReader(
|
||||
data,
|
||||
"https://cdn.example.com/video",
|
||||
"/api/app/vid/sec",
|
||||
"",
|
||||
constant.MediaSourceSP,
|
||||
"",
|
||||
constant.DefaultTsAuthKeyVersion,
|
||||
"sp/demo.m3u8",
|
||||
)
|
||||
if buf == nil {
|
||||
t.Fatal("DecodeFromReader returned nil")
|
||||
}
|
||||
|
||||
got := buf.String()
|
||||
for _, want := range []string{
|
||||
`#EXT-X-KEY:METHOD=AES-128,URI="/api/app/vid/sec",IV=0x00000000000000000000000000000001`,
|
||||
"https://cdn.example.com/video/segment.ts",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("rewritten playlist missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateResolvesFirstSegmentAgainstSegmentMap(t *testing.T) {
|
||||
src, err := grafovm3u8.NewMediaPlaylist(1, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("create source playlist: %v", err)
|
||||
}
|
||||
if err = src.Append("first.m4s", 6, ""); err != nil {
|
||||
t.Fatalf("append source segment: %v", err)
|
||||
}
|
||||
mapURI := "https://cdn.g3ejjm8m.com/hevc/movie/init.m4s"
|
||||
if err = src.SetMap(mapURI, 0, 0); err != nil {
|
||||
t.Fatalf("set source segment map: %v", err)
|
||||
}
|
||||
if src.Map != nil {
|
||||
t.Fatal("test setup must use a segment-level map")
|
||||
}
|
||||
|
||||
buf := Create(
|
||||
src,
|
||||
"https://rs.hyxrp.cn",
|
||||
"/api/app/vid/lsjsec",
|
||||
"",
|
||||
constant.MediaSourceLaoSiJi,
|
||||
constant.MediaSourceAuthKey,
|
||||
constant.DefaultTsAuthKeyVersion,
|
||||
"m3m/demo.m3u8",
|
||||
)
|
||||
if buf == nil {
|
||||
t.Fatal("Create returned nil")
|
||||
}
|
||||
|
||||
got := buf.String()
|
||||
for _, want := range []string{
|
||||
`#EXT-X-MAP:URI="https://rs.hyxrp.cn/laosiji/hevc/movie/init.m4s?md=`,
|
||||
"https://rs.hyxrp.cn/laosiji/hevc/movie/first.m4s?md=",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("rewritten playlist missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
if strings.Contains(got, "/laosiji/first.m4s") {
|
||||
t.Fatalf("first relative segment was not resolved against its map:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeFromReaderPreservesCMAFSegmentMetadata(t *testing.T) {
|
||||
data := []byte(`#EXTM3U
|
||||
#EXT-X-VERSION:6
|
||||
#EXT-X-TARGETDURATION:6
|
||||
#EXT-X-KEY:METHOD=AES-128,URI="first.key",IV=0x00000000000000000000000000000001
|
||||
#EXT-X-MAP:URI="https://cdn.example.com/cmaf/init.mp4"
|
||||
#EXT-X-BYTERANGE:100@0
|
||||
#EXTINF:6,
|
||||
chunk.mp4
|
||||
#EXT-X-DISCONTINUITY
|
||||
#EXT-X-KEY:METHOD=AES-128,URI="second.key",IV=0x00000000000000000000000000000002
|
||||
#EXT-X-BYTERANGE:120@100
|
||||
#EXTINF:6,
|
||||
chunk.mp4
|
||||
#EXT-X-ENDLIST
|
||||
`)
|
||||
buf := DecodeFromReader(
|
||||
data,
|
||||
"https://play.example.com",
|
||||
"/api/app/vid/sec",
|
||||
"",
|
||||
constant.MediaSourceSP,
|
||||
"",
|
||||
constant.DefaultTsAuthKeyVersion,
|
||||
"sp/cmaf/index.m3u8",
|
||||
)
|
||||
if buf == nil {
|
||||
t.Fatal("DecodeFromReader returned nil")
|
||||
}
|
||||
|
||||
got := buf.String()
|
||||
for _, want := range []string{
|
||||
"#EXT-X-BYTERANGE:100@0",
|
||||
"#EXT-X-BYTERANGE:120@100",
|
||||
"#EXT-X-DISCONTINUITY",
|
||||
"IV=0x00000000000000000000000000000001",
|
||||
"IV=0x00000000000000000000000000000002",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("rewritten playlist missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
if count := strings.Count(got, "#EXT-X-KEY:"); count != 2 {
|
||||
t.Fatalf("rewritten playlist key count = %d, want 2:\n%s", count, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteMasterPlaylist(t *testing.T) {
|
||||
input := []byte(`#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="main",DEFAULT=YES,AUTOSELECT=YES,URI="audio/index.m3u8"
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=1500000,RESOLUTION=1280x720,AUDIO="audio"
|
||||
video/720/index.m3u8
|
||||
`)
|
||||
output, err := RewriteMasterPlaylist(input, func(rawURI string) (string, error) {
|
||||
return "/signed/" + rawURI, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RewriteMasterPlaylist failed: %v", err)
|
||||
}
|
||||
playlist, listType, err := grafovm3u8.DecodeFrom(bytes.NewReader(output.Bytes()), false)
|
||||
if err != nil {
|
||||
t.Fatalf("decode rewritten master: %v", err)
|
||||
}
|
||||
if listType != grafovm3u8.MASTER {
|
||||
t.Fatalf("list type = %v, want master", listType)
|
||||
}
|
||||
master := playlist.(*grafovm3u8.MasterPlaylist)
|
||||
if len(master.Variants) != 1 || master.Variants[0].URI != "/signed/video/720/index.m3u8" {
|
||||
t.Fatalf("variant was not rewritten: %+v", master.Variants)
|
||||
}
|
||||
if len(master.Variants[0].Alternatives) != 1 ||
|
||||
master.Variants[0].Alternatives[0].URI != "/signed/audio/index.m3u8" {
|
||||
t.Fatalf("alternative was not rewritten: %+v", master.Variants[0].Alternatives)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteMasterPlaylistLeavesMediaPlaylistUnchanged(t *testing.T) {
|
||||
input := []byte("#EXTM3U\n#EXT-X-TARGETDURATION:4\n#EXTINF:4,\nsegment.ts\n#EXT-X-ENDLIST\n")
|
||||
output, err := RewriteMasterPlaylist(input, func(string) (string, error) {
|
||||
t.Fatal("media playlist URI rewriter must not be called")
|
||||
return "", nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RewriteMasterPlaylist failed: %v", err)
|
||||
}
|
||||
if !bytes.Equal(output.Bytes(), input) {
|
||||
t.Fatalf("media playlist changed:\n%s", output.String())
|
||||
}
|
||||
}
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
package maths
|
||||
|
||||
import "math"
|
||||
|
||||
func Correlation(data1, data2 Float64Data) (float64, error) {
|
||||
l1 := data1.Len()
|
||||
l2 := data2.Len()
|
||||
if l1 == 0 || l2 == 0 {
|
||||
return math.NaN(), EmptyInputErr
|
||||
}
|
||||
if l1 != l2 {
|
||||
return math.NaN(), SizeErr
|
||||
}
|
||||
sdev1, _ := StandardDeviationPopulation(data1)
|
||||
sdev2, _ := StandardDeviationPopulation(data2)
|
||||
if sdev1 == 0 || sdev2 == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
covp, _ := CovariancePopulation(data1, data2)
|
||||
return covp / (sdev1 * sdev2), nil
|
||||
}
|
||||
|
||||
func Pearson(data1, data2 Float64Data) (float64, error) {
|
||||
return Correlation(data1, data2)
|
||||
}
|
||||
|
||||
func AutoCorrelation(data Float64Data, lags int) (float64, error) {
|
||||
if len(data) < 1 {
|
||||
return 0, EmptyInputErr
|
||||
}
|
||||
mean, _ := Mean(data)
|
||||
var result, q float64
|
||||
for i := 0; i < lags; i++ {
|
||||
v := (data[0] - mean) * (data[0] - mean)
|
||||
for i := 1; i < len(data); i++ {
|
||||
delta0 := data[i-1] - mean
|
||||
delta1 := data[i] - mean
|
||||
q += (delta0*delta1 - q) / float64(i+1)
|
||||
v += (delta1*delta1 - v) / float64(i+1)
|
||||
}
|
||||
result = q / v
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
package maths
|
||||
|
||||
type Float64Data []float64
|
||||
|
||||
func (f Float64Data) Get(i int) float64 { return f[i] }
|
||||
|
||||
func (f Float64Data) Len() int { return len(f) }
|
||||
|
||||
func (f Float64Data) Sum() (float64, error) { return Sum(f) }
|
||||
|
||||
func (f Float64Data) Mean() (float64, error) { return Mean(f) }
|
||||
|
||||
func (f Float64Data) Correlation(d Float64Data) (float64, error) {
|
||||
return Correlation(f, d)
|
||||
}
|
||||
|
||||
func (f Float64Data) AutoCorrelation(lags int) (float64, error) {
|
||||
return AutoCorrelation(f, lags)
|
||||
}
|
||||
|
||||
func (f Float64Data) Pearson(d Float64Data) (float64, error) {
|
||||
return Pearson(f, d)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package maths
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// 保留2位小数
|
||||
func Decimal2Bit(value float64) float64 {
|
||||
data, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", value), 64)
|
||||
return data
|
||||
}
|
||||
|
||||
// 保留4位小数
|
||||
func Decimal4Bit(value float64) float64 {
|
||||
data, _ := strconv.ParseFloat(fmt.Sprintf("%.4f", value), 64)
|
||||
return data
|
||||
}
|
||||
|
||||
// 保留6位小数
|
||||
func Decimal6Bit(value float64) float64 {
|
||||
data, _ := strconv.ParseFloat(fmt.Sprintf("%.6f", value), 64)
|
||||
return data
|
||||
}
|
||||
|
||||
// 相乘
|
||||
func DecimalMul(dec1, dec2 string) (string, error) {
|
||||
n1, err := decimal.NewFromString(dec1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
n2, err := decimal.NewFromString(dec2)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return n1.Mul(n2).String(), nil
|
||||
}
|
||||
|
||||
// 相加
|
||||
func DecimalAdd(dec1, dec2 string) (string, error) {
|
||||
n1, err := decimal.NewFromString(dec1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
n2, err := decimal.NewFromString(dec2)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return n1.Add(n2).String(), nil
|
||||
}
|
||||
|
||||
// 相除
|
||||
func DecimalDiv(dec1, dec2 string) (string, error) {
|
||||
n1, err := decimal.NewFromString(dec1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
n2, err := decimal.NewFromString(dec2)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return n1.Div(n2).String(), nil
|
||||
}
|
||||
|
||||
// 相减
|
||||
func DecimalSub(dec1, dec2 string) (string, error) {
|
||||
n1, err := decimal.NewFromString(dec1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
n2, err := decimal.NewFromString("-" + dec2)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return n1.Add(n2).String(), nil
|
||||
}
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
package maths
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func StandardDeviationPopulation(input Float64Data) (sdev float64, err error) {
|
||||
if input.Len() == 0 {
|
||||
return math.NaN(), EmptyInputErr
|
||||
}
|
||||
vp, _ := PopulationVariance(input)
|
||||
return math.Pow(vp, 0.5), nil
|
||||
}
|
||||
|
||||
func DivideInt64(a, b int64) float64 {
|
||||
if b == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(a) / float64(b)
|
||||
}
|
||||
|
||||
func DivideFloat64(a, b float64) float64 {
|
||||
if b == 0 {
|
||||
return 0
|
||||
}
|
||||
return a / b
|
||||
}
|
||||
|
||||
func ToFloat64_b2(v float64) float64 {
|
||||
f, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", v), 64)
|
||||
return f
|
||||
}
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
package maths
|
||||
|
||||
type statsError struct {
|
||||
err string
|
||||
}
|
||||
|
||||
func (s statsError) Error() string {
|
||||
return s.err
|
||||
}
|
||||
|
||||
func (s statsError) String() string {
|
||||
return s.err
|
||||
}
|
||||
|
||||
var (
|
||||
ErrEmptyInput = statsError{"Input must not be empty."}
|
||||
|
||||
ErrNaN = statsError{"Not a number."}
|
||||
|
||||
ErrNegative = statsError{"Must not contain negative values."}
|
||||
|
||||
ErrZero = statsError{"Must not contain zero values."}
|
||||
|
||||
ErrBounds = statsError{"Input is outside of range."}
|
||||
|
||||
ErrSize = statsError{"Must be the same length."}
|
||||
|
||||
ErrInfValue = statsError{"Value is infinite."}
|
||||
|
||||
ErrYCoord = statsError{"Y Value must be greater than zero."}
|
||||
)
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
package maths
|
||||
|
||||
func VarP(input Float64Data) (sdev float64, err error) {
|
||||
return PopulationVariance(input)
|
||||
}
|
||||
|
||||
var (
|
||||
EmptyInputErr = ErrEmptyInput
|
||||
NaNErr = ErrNaN
|
||||
NegativeErr = ErrNegative
|
||||
ZeroErr = ErrZero
|
||||
BoundsErr = ErrBounds
|
||||
SizeErr = ErrSize
|
||||
InfValue = ErrInfValue
|
||||
YCoordErr = ErrYCoord
|
||||
EmptyInput = ErrEmptyInput
|
||||
)
|
||||
Executable
+181
@@ -0,0 +1,181 @@
|
||||
package maths
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func LoadRawData(raw interface{}) (f Float64Data) {
|
||||
var r []interface{}
|
||||
var s Float64Data
|
||||
switch t := raw.(type) {
|
||||
case []interface{}:
|
||||
r = t
|
||||
case []uint:
|
||||
for _, v := range t {
|
||||
s = append(s, float64(v))
|
||||
}
|
||||
return s
|
||||
case []uint8:
|
||||
for _, v := range t {
|
||||
s = append(s, float64(v))
|
||||
}
|
||||
return s
|
||||
case []uint16:
|
||||
for _, v := range t {
|
||||
s = append(s, float64(v))
|
||||
}
|
||||
return s
|
||||
case []uint32:
|
||||
for _, v := range t {
|
||||
s = append(s, float64(v))
|
||||
}
|
||||
return s
|
||||
case []uint64:
|
||||
for _, v := range t {
|
||||
s = append(s, float64(v))
|
||||
}
|
||||
return s
|
||||
case []bool:
|
||||
for _, v := range t {
|
||||
if v {
|
||||
s = append(s, 1.0)
|
||||
} else {
|
||||
s = append(s, 0.0)
|
||||
}
|
||||
}
|
||||
return s
|
||||
case []float64:
|
||||
return Float64Data(t)
|
||||
case []int:
|
||||
for _, v := range t {
|
||||
s = append(s, float64(v))
|
||||
}
|
||||
return s
|
||||
case []int8:
|
||||
for _, v := range t {
|
||||
s = append(s, float64(v))
|
||||
}
|
||||
return s
|
||||
case []int16:
|
||||
for _, v := range t {
|
||||
s = append(s, float64(v))
|
||||
}
|
||||
return s
|
||||
case []int32:
|
||||
for _, v := range t {
|
||||
s = append(s, float64(v))
|
||||
}
|
||||
return s
|
||||
case []int64:
|
||||
for _, v := range t {
|
||||
s = append(s, float64(v))
|
||||
}
|
||||
return s
|
||||
case []string:
|
||||
for _, v := range t {
|
||||
r = append(r, v)
|
||||
}
|
||||
case []time.Duration:
|
||||
for _, v := range t {
|
||||
r = append(r, v)
|
||||
}
|
||||
case map[int]int:
|
||||
for i := 0; i < len(t); i++ {
|
||||
s = append(s, float64(t[i]))
|
||||
}
|
||||
return s
|
||||
case map[int]int8:
|
||||
for i := 0; i < len(t); i++ {
|
||||
s = append(s, float64(t[i]))
|
||||
}
|
||||
return s
|
||||
case map[int]int16:
|
||||
for i := 0; i < len(t); i++ {
|
||||
s = append(s, float64(t[i]))
|
||||
}
|
||||
return s
|
||||
case map[int]int32:
|
||||
for i := 0; i < len(t); i++ {
|
||||
s = append(s, float64(t[i]))
|
||||
}
|
||||
return s
|
||||
case map[int]int64:
|
||||
for i := 0; i < len(t); i++ {
|
||||
s = append(s, float64(t[i]))
|
||||
}
|
||||
return s
|
||||
case map[int]string:
|
||||
for i := 0; i < len(t); i++ {
|
||||
r = append(r, t[i])
|
||||
}
|
||||
case map[int]uint:
|
||||
for i := 0; i < len(t); i++ {
|
||||
s = append(s, float64(t[i]))
|
||||
}
|
||||
return s
|
||||
case map[int]uint8:
|
||||
for i := 0; i < len(t); i++ {
|
||||
s = append(s, float64(t[i]))
|
||||
}
|
||||
return s
|
||||
case map[int]uint16:
|
||||
for i := 0; i < len(t); i++ {
|
||||
s = append(s, float64(t[i]))
|
||||
}
|
||||
return s
|
||||
case map[int]uint32:
|
||||
for i := 0; i < len(t); i++ {
|
||||
s = append(s, float64(t[i]))
|
||||
}
|
||||
return s
|
||||
case map[int]uint64:
|
||||
for i := 0; i < len(t); i++ {
|
||||
s = append(s, float64(t[i]))
|
||||
}
|
||||
return s
|
||||
case map[int]bool:
|
||||
for i := 0; i < len(t); i++ {
|
||||
if t[i] {
|
||||
s = append(s, 1.0)
|
||||
} else {
|
||||
s = append(s, 0.0)
|
||||
}
|
||||
}
|
||||
return s
|
||||
case map[int]float64:
|
||||
for i := 0; i < len(t); i++ {
|
||||
s = append(s, t[i])
|
||||
}
|
||||
return s
|
||||
case map[int]time.Duration:
|
||||
for i := 0; i < len(t); i++ {
|
||||
r = append(r, t[i])
|
||||
}
|
||||
}
|
||||
for _, v := range r {
|
||||
switch t := v.(type) {
|
||||
case int:
|
||||
a := float64(t)
|
||||
f = append(f, a)
|
||||
case uint:
|
||||
f = append(f, float64(t))
|
||||
case float64:
|
||||
f = append(f, t)
|
||||
case string:
|
||||
fl, err := strconv.ParseFloat(t, 64)
|
||||
if err == nil {
|
||||
f = append(f, fl)
|
||||
}
|
||||
case bool:
|
||||
if t {
|
||||
f = append(f, 1.0)
|
||||
} else {
|
||||
f = append(f, 0.0)
|
||||
}
|
||||
case time.Duration:
|
||||
f = append(f, float64(t))
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
package maths
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func copyslice(input Float64Data) Float64Data {
|
||||
s := make(Float64Data, input.Len())
|
||||
copy(s, input)
|
||||
return s
|
||||
}
|
||||
|
||||
func sortedCopyDif(input Float64Data) (copy Float64Data) {
|
||||
if sort.Float64sAreSorted(input) {
|
||||
return input
|
||||
}
|
||||
copy = copyslice(input)
|
||||
sort.Float64s(copy)
|
||||
return
|
||||
}
|
||||
|
||||
func RandDigits(n uint) string {
|
||||
s := ""
|
||||
for n != 0 {
|
||||
s += fmt.Sprintf("%d", rand.Intn(10))
|
||||
n--
|
||||
}
|
||||
return s
|
||||
}
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
package maths
|
||||
|
||||
import "math"
|
||||
|
||||
func Max(input Float64Data) (max float64, err error) {
|
||||
if input.Len() == 0 {
|
||||
return math.NaN(), EmptyInputErr
|
||||
}
|
||||
max = input.Get(0)
|
||||
for i := 1; i < input.Len(); i++ {
|
||||
if input.Get(i) > max {
|
||||
max = input.Get(i)
|
||||
}
|
||||
}
|
||||
return max, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user