Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+22
View File
@@ -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
}
+258
View File
@@ -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"` // 用户唯一iduuidv4
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"` // 用户唯一iduuidv4
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"` // 用户唯一iduuidv4
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"` // 用户唯一iduuidv4
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
}
+396
View File
@@ -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
}
+20
View File
@@ -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)
}
+46
View File
@@ -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"`
}
+96
View File
@@ -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() {
}
+34
View File
@@ -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)
}
+445
View File
@@ -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
}
+14
View File
@@ -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
}
+76
View File
@@ -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
}
+65
View File
@@ -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()
}
+23
View File
@@ -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
}
+3
View File
@@ -0,0 +1,3 @@
package cacheconst
const LocalCdnCacheKey = "cdn:cache" //本地缓存cdn域名列表
+375
View File
@@ -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)
}
}
+688
View File
@@ -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)
}
+187
View File
@@ -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)
}
+137
View File
@@ -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
}
+755
View File
@@ -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
}
+49
View File
@@ -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")
}
}
+63
View File
@@ -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:]
}
}
+140
View File
@@ -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算法解密
}
+163
View File
@@ -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"`
}
+149
View File
@@ -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
}
+53
View File
@@ -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
}
+9
View File
@@ -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棋牌
}
+184
View File
@@ -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
}
+31
View File
@@ -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)
}
+242
View File
@@ -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)
}
)
+65
View File
@@ -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
}
+127
View File
@@ -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
}
+108
View File
@@ -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)
}
+7
View File
@@ -0,0 +1,7 @@
package db
import "go.mongodb.org/mongo-driver/mongo"
func IsMongoDupKey(err error) bool {
return mongo.IsDuplicateKeyError(err)
}
+392
View File
@@ -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
}
+307
View File
@@ -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
}
+27
View File
@@ -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
}
+26
View File
@@ -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)
}
+89
View File
@@ -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
}
+110
View File
@@ -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())
}
+267
View File
@@ -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
}
+39
View File
@@ -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
}
+74
View File
@@ -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
}
+37
View File
@@ -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
}
+15
View File
@@ -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_")
}
+79
View File
@@ -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 "未知"
}
+20
View File
@@ -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
}
+103
View File
@@ -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
}
+32
View File
@@ -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
}
+107
View File
@@ -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 = "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"
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
}
+291
View File
@@ -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...)
}
+148
View File
@@ -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
}
+130
View File
@@ -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
}
+529
View File
@@ -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
View File
@@ -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
}
+32
View File
@@ -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)
}
+64
View File
@@ -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
}
+25
View File
@@ -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
}
+269
View File
@@ -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))
}
+176
View File
@@ -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)
}
}
}
+101
View File
@@ -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()
}
+175
View File
@@ -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_
}
+198
View File
@@ -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
}
+135
View File
@@ -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
}
+282
View File
@@ -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
}
+879
View File
@@ -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)...)
}
+6
View File
@@ -0,0 +1,6 @@
package imclient
const (
DefaultSignKey = "shDOUArrDhpeAMw9FGY79Zmy3MLWwNWy"
DefaultAESKey = "4d5bc50346c22dde12be2c3b1b89ada6"
)
+41
View File
@@ -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")
}
+318
View File
@@ -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
}
+78
View File
@@ -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 ""
}
+74
View File
@@ -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
}
+114
View File
@@ -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
}
+74
View File
@@ -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"`
}
+262
View File
@@ -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)]
}
+142
View File
@@ -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
}
+156
View File
@@ -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"`
}
+115
View File
@@ -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
}
+32
View File
@@ -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"`
}
+115
View File
@@ -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
}
+69
View File
@@ -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"`
}
+14
View File
@@ -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 处理的数据不一样 查看文档的描述
}
+487
View File
@@ -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] + "..."
}
+217
View File
@@ -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)
}
}
+14
View File
@@ -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 处理的数据不一样 查看文档的描述
}
+100
View File
@@ -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
}
+169
View File
@@ -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
}
+109
View File
@@ -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)
}
}
+16
View File
@@ -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)
+113
View File
@@ -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
}
+503
View File
@@ -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)
}
+281
View File
@@ -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())
}
}
+44
View File
@@ -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
}
+23
View File
@@ -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)
}
+78
View File
@@ -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
}
+34
View File
@@ -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
}
+31
View File
@@ -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."}
)
+17
View File
@@ -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
)
+181
View File
@@ -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
}
+31
View File
@@ -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
}
+16
View File
@@ -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