Merge branch 'master' of git.u8t.cn:open/gosdk

This commit is contained in:
jiangyong 2026-04-22 10:57:17 +08:00
commit e02ab6405d
18 changed files with 590 additions and 143 deletions

1
.gitignore vendored
View File

@ -10,3 +10,4 @@ test
go.sum go.sum
pkg pkg
test.go test.go
.claude

23
cache/redis.go vendored Normal file
View File

@ -0,0 +1,23 @@
package cache
import "github.com/go-redis/redis"
var (
redisClient *redis.Client = nil
)
func NewRedis() {
addr := "127.0.0.1:6379"
redis.NewClient(&redis.Options{})
redisClient = redis.NewClient(&redis.Options{
Addr: addr,
})
}
func GetRedis() *redis.Client {
if redisClient == nil {
NewRedis()
}
return redisClient
}

View File

@ -24,6 +24,7 @@ type Form struct {
Name string `json:"name"` //表单名称 Name string `json:"name"` //表单名称
Key string `json:"key"` //表单KEY Key string `json:"key"` //表单KEY
Value interface{} `json:"value"` //表单值 Value interface{} `json:"value"` //表单值
Default interface{} `json:"default"` //默认值
Disable bool `json:"disable,omitempty"` //是否禁用 Disable bool `json:"disable,omitempty"` //是否禁用
Tips string `json:"tips"` //表单提示 Tips string `json:"tips"` //表单提示
Option []*FormOption `json:"option"` //表单选项radio和checkbox需要 Option []*FormOption `json:"option"` //表单选项radio和checkbox需要
@ -50,6 +51,8 @@ func NewFroms(tplConfig, saveConfig string) ([]*Form, error) {
for _, form := range forms { for _, form := range forms {
if _, ok := cfg[form.Key]; ok { if _, ok := cfg[form.Key]; ok {
form.Value = cfg[form.Key] form.Value = cfg[form.Key]
} else {
form.Value = form.Default
} }
} }
return forms, err return forms, err

19
enterprise/enterprise.go Normal file
View File

@ -0,0 +1,19 @@
package enterprise
type Enterprise struct {
token string
baseUrl string
}
func (e *Enterprise) GetHeader() map[string]string {
return map[string]string{
"x-token": e.token,
}
}
func (e *Enterprise) GetBaseUrl() string {
if e.baseUrl != "" {
return e.baseUrl
}
return "http://e.batiao8.com"
}

75
enterprise/staff_user.go Normal file
View File

@ -0,0 +1,75 @@
package enterprise
import (
"errors"
"fmt"
"git.u8t.cn/open/goutil"
"github.com/tidwall/gjson"
)
var (
corpId = 1000
)
type VerifyCode struct {
Username string `json:"username"`
Timestamp string `json:"timestamp"`
}
type StaffInfo struct {
Username string `json:"username"`
Realname string `json:"realname"`
Phone string `json:"phone"`
}
type StaffUser struct {
Enterprise
}
func NewStaffUser(baseUrl, token string) *StaffUser {
return &StaffUser{
Enterprise: Enterprise{
baseUrl: baseUrl,
token: token,
},
}
}
func (e *StaffUser) SendVerifyCode(username, scene string) (*VerifyCode, error) {
var reqBody string
reqBody = fmt.Sprintf(`{"username":"%s","scene":"%s"}`, username, scene)
reqUrl := e.GetBaseUrl() + "/api/staff/verify/code"
body, err := goutil.HttpPost(reqUrl, e.GetHeader(), []byte(reqBody))
if err != nil {
return nil, err
}
g := gjson.ParseBytes(body)
if g.Get("code").Int() != 0 {
return nil, errors.New(string(body))
}
r := new(VerifyCode)
r.Timestamp = g.Get("data.timestamp").String()
r.Username = g.Get("data.username").String()
return r, nil
}
func (e *StaffUser) Login(username, timestamp, code string) (*StaffInfo, error) {
reqBody := fmt.Sprintf(`{"username":"%s","timestamp":"%s","code":"%s"}`, username, timestamp, code)
reqUrl := e.GetBaseUrl() + "/api/staff/login"
body, err := goutil.HttpPost(reqUrl, e.GetHeader(), []byte(reqBody))
if err != nil {
return nil, err
}
g := gjson.ParseBytes(body)
if g.Get("code").Int() != 0 {
return nil, errors.New(string(body))
}
r := new(StaffInfo)
r.Realname = g.Get("data.realname").String()
r.Username = g.Get("data.username").String()
r.Phone = g.Get("data.phone").String()
return r, nil
}

18
go.mod
View File

@ -7,7 +7,9 @@ require (
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf
github.com/eclipse/paho.mqtt.golang v1.5.1 github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.11.0
github.com/go-redis/redis v6.15.9+incompatible
github.com/gomodule/redigo v1.9.2 github.com/gomodule/redigo v1.9.2
github.com/google/uuid v1.6.0
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c
github.com/minio/minio-go v6.0.14+incompatible github.com/minio/minio-go v6.0.14+incompatible
github.com/qiniu/go-sdk/v7 v7.25.4 github.com/qiniu/go-sdk/v7 v7.25.4
@ -15,7 +17,7 @@ require (
github.com/spf13/cast v1.10.0 github.com/spf13/cast v1.10.0
github.com/tidwall/gjson v1.18.0 github.com/tidwall/gjson v1.18.0
github.com/wechatpay-apiv3/wechatpay-go v0.2.21 github.com/wechatpay-apiv3/wechatpay-go v0.2.21
golang.org/x/crypto v0.42.0 golang.org/x/crypto v0.47.0
gorm.io/gorm v1.31.0 gorm.io/gorm v1.31.0
) )
@ -47,6 +49,8 @@ require (
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.39.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect github.com/quic-go/quic-go v0.54.0 // indirect
@ -58,12 +62,12 @@ require (
go.uber.org/mock v0.5.0 // indirect go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect golang.org/x/arch v0.20.0 // indirect
golang.org/x/image v0.31.0 // indirect golang.org/x/image v0.31.0 // indirect
golang.org/x/mod v0.27.0 // indirect golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.44.0 // indirect golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.36.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.29.0 // indirect golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.36.0 // indirect golang.org/x/tools v0.41.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect google.golang.org/protobuf v1.36.9 // indirect
modernc.org/fileutil v1.0.0 // indirect modernc.org/fileutil v1.0.0 // indirect
) )

View File

@ -316,6 +316,11 @@ func (a *App) Upload(path, kind string) (string, error) {
return cast.ToString(result["media_id"]), nil return cast.ToString(result["media_id"]), nil
} }
func (a *App) MediaUrl(mediaId string) string {
mUrl := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s", a.GetToken(), mediaId)
return mUrl
}
func (q *App) GetJsapiConfig(url string) (*JsapiConfig, error) { func (q *App) GetJsapiConfig(url string) (*JsapiConfig, error) {
ticket, err := q.getJsapiTicket() ticket, err := q.getJsapiTicket()
if err != nil { if err != nil {

View File

@ -3,11 +3,12 @@ package qyweixin
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"git.u8t.cn/open/gosdk/util" "git.u8t.cn/open/gosdk/util"
"git.u8t.cn/open/goutil" "git.u8t.cn/open/goutil"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cast" "github.com/spf13/cast"
"strings"
) )
type Applyer struct { type Applyer struct {
@ -90,57 +91,8 @@ type AppApprove struct {
} }
func (d *ApproveDetail) GetValue(title string) string { func (d *ApproveDetail) GetValue(title string) string {
data := d.GetData()
for _, content := range d.ApplyData.Contents { return data[title]
isEqual := false
for _, ti := range content.Title {
if ti.Text == title {
isEqual = true
}
}
if !isEqual {
continue
}
var value string
if content.Control == "Selector" {
for _, v := range content.Value.Selector.Options[0].Value {
if v.Text != "" {
value = v.Text
}
}
} else if content.Control == "Text" || content.Control == "Textarea" {
value = content.Value.Text
} else if content.Control == "Date" {
value = content.Value.Date.Timestamp
} else if content.Control == "Money" {
value = content.Value.NewMoney
} else if content.Control == "File" {
value = content.Value.Files[0].FileId
} else if content.Control == "Vacation" { //请假 请假类型,开始时间,结束时间,请假时长
tp := content.Value.Vacation.Selector.Options[0].Value[0].Text
value = tp + "," + cast.ToString(content.Value.Vacation.Attendance.DateRange.NewBegin) +
"," + cast.ToString(content.Value.Vacation.Attendance.DateRange.NewEnd) +
"," + cast.ToString(content.Value.Vacation.Attendance.DateRange.NewDuration)
} else if content.Control == "PunchCorrection" { //补卡:日期,时间,状态
mp := cast.ToStringMap(content.Value.PunchCorrection)
ddate := cast.ToString(mp["daymonthyear"])
dtime := cast.ToString(mp["time"])
if ddate == "" {
ddate = dtime
}
value = ddate + "," + dtime + "," + cast.ToString(mp["state"])
} else if content.Control == "BankAccount" {
mp := cast.ToStringMap(content.Value.BankAccount)
value = cast.ToString(mp["account_type"]) + "," + cast.ToString(mp["account_name"]) + "," + cast.ToString(mp["account_number"])
} else if content.Control == "Number" {
value = content.Value.NewNumber
}
return value
}
return ""
} }
func (d *ApproveDetail) GetData() map[string]string { func (d *ApproveDetail) GetData() map[string]string {
@ -165,9 +117,11 @@ func (d *ApproveDetail) GetData() map[string]string {
} else if content.Control == "Money" { } else if content.Control == "Money" {
value = content.Value.NewMoney value = content.Value.NewMoney
} else if content.Control == "File" { } else if content.Control == "File" {
if len(content.Value.Files) > 0 { fileIds := make([]string, 0)
value = content.Value.Files[0].FileId for _, v := range content.Value.Files {
fileIds = append(fileIds, v.FileId)
} }
value = strings.Join(fileIds, ",")
} else if content.Control == "Vacation" { //请假 请假类型,开始时间,结束时间,请假时长 } else if content.Control == "Vacation" { //请假 请假类型,开始时间,结束时间,请假时长
tp := content.Value.Vacation.Selector.Options[0].Value[0].Text tp := content.Value.Vacation.Selector.Options[0].Value[0].Text
value = tp + "," + cast.ToString(content.Value.Vacation.Attendance.DateRange.NewBegin) + value = tp + "," + cast.ToString(content.Value.Vacation.Attendance.DateRange.NewBegin) +
@ -220,7 +174,7 @@ func (q *AppApprove) GetDetail(spNo string) (*ApproveDetail, error) {
} }
var rsp ApproveDetailRsp var rsp ApproveDetailRsp
fmt.Println("spno: %s, detail: %s", spNo, string(rspBody)) //fmt.Println("spno: %s, detail: %s", spNo, string(rspBody))
if err := json.Unmarshal(rspBody, &rsp); err != nil { if err := json.Unmarshal(rspBody, &rsp); err != nil {
log.Errorf("get body[%s] json error :%s", string(rspBody), err.Error()) log.Errorf("get body[%s] json error :%s", string(rspBody), err.Error())
return nil, err return nil, err

View File

@ -2,11 +2,12 @@ package qyweixin
import ( import (
"fmt" "fmt"
"git.u8t.cn/open/gosdk/util"
log "github.com/sirupsen/logrus"
"git.u8t.cn/open/goutil"
"github.com/spf13/cast"
"time" "time"
"git.u8t.cn/open/gosdk/util"
"git.u8t.cn/open/goutil"
log "github.com/sirupsen/logrus"
"github.com/spf13/cast"
) )
var ( var (
@ -14,6 +15,7 @@ var (
urlQyWeixinHrGetStaffInfo = "https://qyapi.weixin.qq.com/cgi-bin/hr/get_staff_info" urlQyWeixinHrGetStaffInfo = "https://qyapi.weixin.qq.com/cgi-bin/hr/get_staff_info"
urlQyWeixinHrGetDepartment = "https://qyapi.weixin.qq.com/cgi-bin/department/list" urlQyWeixinHrGetDepartment = "https://qyapi.weixin.qq.com/cgi-bin/department/list"
urlQyWeixinHrGetDepartmentUser = "https://qyapi.weixin.qq.com/cgi-bin/user/list" urlQyWeixinHrGetDepartmentUser = "https://qyapi.weixin.qq.com/cgi-bin/user/list"
urlQyWeixinHrGetContactInfo = "https://qyapi.weixin.qq.com/cgi-bin/user/get"
) )
type AppHr struct { type AppHr struct {
@ -22,10 +24,11 @@ type AppHr struct {
} }
type Department struct { type Department struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Pid int64 `json:"pid"` Pid int64 `json:"pid"`
Name string `json:"name"` Name string `json:"name"`
Leader []string `json:"leader"` Leader []string `json:"leader"`
Childchren []*Department `json:"childchren"`
} }
type StaffInfo struct { type StaffInfo struct {
@ -42,12 +45,38 @@ type StaffInfo struct {
BankCard string BankCard string
} }
type ContactInfo struct {
Realname string
DirectLeader []string
DepartmentId []int64
IsDepartmentLeader []int
}
func NewAppHr(cfg *AppConfig) *AppHr { func NewAppHr(cfg *AppConfig) *AppHr {
return &AppHr{ return &AppHr{
App: *NewApp(cfg), App: *NewApp(cfg),
} }
} }
func (h *AppHr) GetContactInfo(userId string) (*ContactInfo, error) {
reqUrl := fmt.Sprintf("%s?access_token=%s&userid=%s", urlQyWeixinHrGetContactInfo, h.GetToken(), userId)
rspBody, err := util.HttpGet(reqUrl, nil)
if err != nil {
return nil, err
}
contract := new(ContactInfo)
result, err := h.GetResult(rspBody)
if err != nil {
return nil, err
}
contract.Realname = cast.ToString(result["name"])
contract.DirectLeader = cast.ToStringSlice(result["direct_leader"])
contract.DepartmentId = cast.ToInt64Slice(result["department"])
contract.IsDepartmentLeader = cast.ToIntSlice(result["is_leader_in_dept"])
return contract, nil
}
func (h *AppHr) GetStaffInfo(userId string) (*StaffInfo, error) { func (h *AppHr) GetStaffInfo(userId string) (*StaffInfo, error) {
reqUrl := fmt.Sprintf("%s?access_token=%s", urlQyWeixinHrGetStaffInfo, h.GetToken()) reqUrl := fmt.Sprintf("%s?access_token=%s", urlQyWeixinHrGetStaffInfo, h.GetToken())
reqBody := make(map[string]interface{}) reqBody := make(map[string]interface{})
@ -108,6 +137,7 @@ func (h *AppHr) GetDepartment(id int64) ([]*Department, error) {
result := make([]*Department, 0) result := make([]*Department, 0)
departments := cast.ToSlice(resp["department"]) departments := cast.ToSlice(resp["department"])
for _, dd := range departments { for _, dd := range departments {
d := cast.ToStringMap(dd) d := cast.ToStringMap(dd)
r := new(Department) r := new(Department)
@ -116,7 +146,9 @@ func (h *AppHr) GetDepartment(id int64) ([]*Department, error) {
r.Id = cast.ToInt64(d["id"]) r.Id = cast.ToInt64(d["id"])
r.Pid = cast.ToInt64(d["parentid"]) r.Pid = cast.ToInt64(d["parentid"])
result = append(result, r) result = append(result, r)
} }
return result, nil return result, nil
} }

View File

@ -3,12 +3,14 @@ package storage
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/minio/minio-go" "net/http"
"net/url" "net/url"
"path" "path"
"strings" "strings"
"time" "time"
"github.com/minio/minio-go"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -61,9 +63,13 @@ func (s *Minio) Put(fileName, objectName string, onProcess func(fsize, uploaded
objectName = strings.TrimLeft(objectName, "/ ") objectName = strings.TrimLeft(objectName, "/ ")
ext := strings.TrimLeft(path.Ext(fileName), ".") ext := strings.TrimLeft(path.Ext(fileName), ".")
contentType := ext2ContentType(ext)
if contentType == "" {
contentType = detectContentType(fileName)
}
_, err := s.client.FPutObjectWithContext(ctx, s.config.Bucket, objectName, fileName, _, err := s.client.FPutObjectWithContext(ctx, s.config.Bucket, objectName, fileName,
minio.PutObjectOptions{ContentType: ext2ContentType(ext)}) minio.PutObjectOptions{ContentType: contentType})
if err != nil { if err != nil {
log.Errorf("upload file to minio error:%s", err.Error()) log.Errorf("upload file to minio error:%s", err.Error())
return err return err
@ -108,7 +114,7 @@ func (s *Minio) List(objectPrefix string) ([]string, error) {
return result, nil return result, nil
} }
func (s *Minio) Url(objectName string, expire time.Duration) string { func (s *Minio) Url(objectName string, expire time.Duration, https ...bool) string {
if err := s.Init(); err != nil { if err := s.Init(); err != nil {
return err.Error() return err.Error()
} }
@ -116,11 +122,19 @@ func (s *Minio) Url(objectName string, expire time.Duration) string {
if expire > time.Hour*24*7 || expire == -1 { if expire > time.Hour*24*7 || expire == -1 {
expire = time.Hour * 24 * 7 expire = time.Hour * 24 * 7
} }
baseUrl := s.config.BaseUrl
if len(https) > 0 && https[0] {
if strings.HasPrefix(baseUrl, "http://") {
baseUrl = "https://" + strings.TrimPrefix(baseUrl, "http://")
}
}
var params url.Values var params url.Values
u, err := s.client.PresignedGetObject(s.config.Bucket, objectName, expire, params) u, err := s.client.PresignedGetObject(s.config.Bucket, objectName, expire, params)
if err != nil { if err != nil {
log.Errorf("error:%s", err.Error()) log.Errorf("error:%s", err.Error())
return fmt.Sprintf("%s/%s/%s", s.config.BaseUrl, s.config.Bucket, objectName) return fmt.Sprintf("%s/%s/%s", baseUrl, s.config.Bucket, objectName)
} }
return u.String() return u.String()
@ -128,9 +142,78 @@ func (s *Minio) Url(objectName string, expire time.Duration) string {
} }
func (s *Minio) Stat(objectName string) (*ObjectInfo, error) { func (s *Minio) Stat(objectName string) (*ObjectInfo, error) {
return nil, nil if err := s.Init(); err != nil {
return nil, err
}
objectName = strings.TrimLeft(objectName, "/ ")
stat, err := s.client.StatObject(s.config.Bucket, objectName, minio.StatObjectOptions{})
if err != nil {
// 文件不存在不算错误,返回 nil, nil
if isMinioNotFound(err) {
return nil, nil
}
log.Errorf("stat object from minio error:%s", err.Error())
return nil, err
}
info := &ObjectInfo{
Size: stat.Size,
Hash: stat.ETag,
MimeType: stat.ContentType,
PutTime: stat.LastModified.Unix(),
}
return info, nil
} }
func (s *Minio) Fetch(url, objectName string) error { // isMinioNotFound 检查是否为 MinIO 文件不存在错误
func isMinioNotFound(err error) bool {
if err == nil {
return false
}
// MinIO 返回 NoSuchKey 表示文件不存在
if respErr, ok := err.(minio.ErrorResponse); ok {
return respErr.Code == "NoSuchKey" || respErr.Code == "NotFound"
}
return false
}
func (s *Minio) Fetch(urlStr, objectName string, local ...bool) error {
if err := s.Init(); err != nil {
return err
}
// 从 URL 下载文件
resp, err := http.Get(urlStr)
if err != nil {
log.Errorf("fetch file from url error:%s", err.Error())
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("fetch file from url failed, status: %d", resp.StatusCode)
}
// 获取文件名和 Content-Type
objectName = strings.TrimLeft(objectName, "/ ")
// 优先根据文件扩展名推断 Content-Type确保图片能正确预览
ext := strings.TrimLeft(path.Ext(objectName), ".")
contentType := ext2ContentType(ext)
if contentType == "" {
contentType = resp.Header.Get("Content-Type")
}
// 上传到 MinIO
ctx, cancel := context.WithTimeout(context.Background(), s.config.Timeout)
defer cancel()
_, err = s.client.PutObjectWithContext(ctx, s.config.Bucket, objectName, resp.Body, resp.ContentLength,
minio.PutObjectOptions{ContentType: contentType})
if err != nil {
log.Errorf("upload fetched file to minio error:%s", err.Error())
return err
}
return nil return nil
} }

View File

@ -4,10 +4,13 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"os"
"path"
"strings"
"time"
"github.com/qiniu/go-sdk/v7/auth/qbox" "github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage" "github.com/qiniu/go-sdk/v7/storage"
"os"
"time"
) )
type QiniuConfig struct { type QiniuConfig struct {
@ -152,7 +155,7 @@ func (s *Qiniu) Get(objectName, fileName string) error {
return Download(url, fileName) return Download(url, fileName)
} }
func (s *Qiniu) Url(objectName string, expire time.Duration) string { func (s *Qiniu) Url(objectName string, expire time.Duration, https ...bool) string {
mac := qbox.NewMac(s.config.AK, s.config.SK) mac := qbox.NewMac(s.config.AK, s.config.SK)
cfg := storage.Config{ cfg := storage.Config{
UseHTTPS: false, UseHTTPS: false,
@ -166,8 +169,11 @@ func (s *Qiniu) Url(objectName string, expire time.Duration) string {
if len(domains) <= 0 { if len(domains) <= 0 {
return "" return ""
} }
domain := "http://" + domains[0].Domain
if len(https) > 0 && https[0] {
domain = "https://" + domains[0].Domain
}
domain := "https://" + domains[0].Domain
if expire == 0 { if expire == 0 {
return storage.MakePublicURLv2(domain, objectName) return storage.MakePublicURLv2(domain, objectName)
} else { } else {
@ -185,6 +191,10 @@ func (s *Qiniu) Stat(objectName string) (*ObjectInfo, error) {
bucketManager := storage.NewBucketManager(mac, &cfg) bucketManager := storage.NewBucketManager(mac, &cfg)
fileInfo, err := bucketManager.Stat(s.config.Bucket, objectName) fileInfo, err := bucketManager.Stat(s.config.Bucket, objectName)
if err != nil { if err != nil {
// 文件不存在不算错误,返回 nil, nil
if isQiniuNotFound(err) {
return nil, nil
}
return nil, err return nil, err
} }
@ -196,7 +206,22 @@ func (s *Qiniu) Stat(objectName string) (*ObjectInfo, error) {
return info, nil return info, nil
} }
func (s *Qiniu) Fetch(url, objectName string) error { // isQiniuNotFound 检查是否为七牛云文件不存在错误
func isQiniuNotFound(err error) bool {
if err == nil {
return false
}
// 七牛云错误码 612 表示文件不存在
if respErr, ok := err.(*storage.ErrorInfo); ok {
return respErr.Code == 612
}
return false
}
func (s *Qiniu) Fetch(url, objectName string, local ...bool) error {
if len(local) > 0 && local[0] == true {
return s.fetchLocal(url, objectName)
}
mac := qbox.NewMac(s.config.AK, s.config.SK) mac := qbox.NewMac(s.config.AK, s.config.SK)
cfg := storage.Config{ cfg := storage.Config{
UseHTTPS: false, UseHTTPS: false,
@ -211,3 +236,19 @@ func (s *Qiniu) Fetch(url, objectName string) error {
return nil return nil
} }
func (s *Qiniu) fetchLocal(url, objectName string) error {
objectName = strings.TrimLeft(objectName, "/ ")
ext := strings.TrimLeft(path.Ext(objectName), ".")
tmpFile := fmt.Sprintf("%d.%s", time.Now().UnixMilli(), ext)
if err := Download(url, tmpFile); err != nil {
return err
}
defer os.Remove(tmpFile)
if err := s.Put(tmpFile, objectName, nil); err != nil {
return err
}
return nil
}

View File

@ -13,8 +13,8 @@ type Storage interface {
Put(fileName, objectName string, onProcess func(fsize, uploaded int64)) error Put(fileName, objectName string, onProcess func(fsize, uploaded int64)) error
Get(objectName, fileName string) error Get(objectName, fileName string) error
Del(objectName string) error Del(objectName string) error
Url(objectName string, expire time.Duration) string Url(objectName string, expire time.Duration, https ...bool) string //https参数是否生成https参数
Stat(objectName string) (*ObjectInfo, error) Stat(objectName string) (*ObjectInfo, error)
List(objectPrefix string) ([]string, error) List(objectPrefix string) ([]string, error)
Fetch(url, objectName string) error Fetch(url, objectName string, local ...bool) error //local参数决定是否先下载到本地在上传七牛云下载企业微信的文件需要不能直接下载
} }

View File

@ -21,16 +21,116 @@ func contentType2Ext(contentType string) string {
func ext2ContentType(ext string) string { func ext2ContentType(ext string) string {
ext = strings.ToLower(ext) ext = strings.ToLower(ext)
if ext == "jpg" || ext == "jpeg" { switch ext {
// 图片格式
case "jpg", "jpeg":
return "image/jpeg" return "image/jpeg"
} else if ext == "png" { case "png":
return "image/png" return "image/png"
} else if ext == "mp3" { case "gif":
return "image/gif"
case "webp":
return "image/webp"
case "bmp":
return "image/bmp"
case "svg", "svgz":
return "image/svg+xml"
case "ico":
return "image/x-icon"
case "tiff", "tif":
return "image/tiff"
case "avif":
return "image/avif"
case "apng":
return "image/apng"
// 音频格式
case "mp3":
return "audio/mpeg" return "audio/mpeg"
} else if ext == "mp4" { case "wav":
return "audio/wav"
case "ogg":
return "audio/ogg"
case "flac":
return "audio/flac"
case "aac":
return "audio/aac"
case "m4a":
return "audio/mp4"
// 视频格式
case "mp4":
return "video/mp4" return "video/mp4"
case "avi":
return "video/x-msvideo"
case "mov", "qt":
return "video/quicktime"
case "webm":
return "video/webm"
case "flv":
return "video/x-flv"
case "mkv":
return "video/x-matroska"
case "wmv":
return "video/x-ms-wmv"
case "m3u8":
return "application/vnd.apple.mpegurl"
case "ts":
return "video/mp2t"
// 文档格式
case "pdf":
return "application/pdf"
case "doc":
return "application/msword"
case "docx":
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
case "xls":
return "application/vnd.ms-excel"
case "xlsx":
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
case "ppt":
return "application/vnd.ms-powerpoint"
case "pptx":
return "application/vnd.openxmlformats-officedocument.presentationml.presentation"
case "txt":
return "text/plain"
case "html", "htm":
return "text/html"
case "css":
return "text/css"
case "js":
return "application/javascript"
case "json":
return "application/json"
case "xml":
return "application/xml"
case "zip":
return "application/zip"
case "rar":
return "application/x-rar-compressed"
case "7z":
return "application/x-7z-compressed"
case "gz":
return "application/gzip"
default:
return ""
} }
return "" }
// detectContentType 从文件内容检测 MIME 类型
func detectContentType(filePath string) string {
f, err := os.Open(filePath)
if err != nil {
return ""
}
defer f.Close()
// 读取前 512 字节用于检测
buf := make([]byte, 512)
n, err := f.Read(buf)
if err != nil && err != io.EOF {
return ""
}
return http.DetectContentType(buf[:n])
} }
func Download(url, path string) error { func Download(url, path string) error {

View File

@ -4,8 +4,10 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"git.u8t.cn/open/gosdk/util"
"net/url" "net/url"
"git.u8t.cn/open/gosdk/util"
"git.u8t.cn/open/goutil"
) )
var ( var (
@ -177,7 +179,7 @@ func (p *Pay) RefundOrder(req *RefundOrderReq) error {
errors.New("outTradeNo is nil") errors.New("outTradeNo is nil")
} }
reqUrl := fmt.Sprintf("%s/api/pay/order?outTradeNo=%s&reason=%s&refundFee=%d", p.address, req.OutTradeNo, url.QueryEscape(req.Reason), req.RefundFee) reqUrl := fmt.Sprintf("%s/api/pay/order?outTradeNo=%s&reason=%s&refundFee=%d&refundTarget=%s", p.address, req.OutTradeNo, url.QueryEscape(req.Reason), req.RefundFee, req.RefundTarget)
result, err := util.HttpDelete(reqUrl, map[string]string{ result, err := util.HttpDelete(reqUrl, map[string]string{
"x-token": p.token, "x-token": p.token,
}) })
@ -219,3 +221,27 @@ func (p *Pay) RefundPartnerOrder(req *RefundOrderReq) error {
return nil return nil
} }
func (p *Pay) Transfer(req *TransferReq) error {
if err := req.Check(); err != nil {
return err
}
reqUrl := fmt.Sprintf("%s/api/pay/transfer?", p.address)
result, err := util.HttpPostJson(reqUrl, map[string]string{
"x-token": p.token,
}, []byte(goutil.EncodeJSON(req)))
if err != nil {
return err
}
var rsp CommonResponse
if err := json.Unmarshal([]byte(result), &rsp); err != nil {
return err
}
if rsp.Code != 0 {
return fmt.Errorf("%d:%s", rsp.Code, rsp.Message)
}
return nil
}

View File

@ -1,5 +1,7 @@
package unify package unify
import "errors"
type OrderUser struct { type OrderUser struct {
UserId string `json:"userId"` UserId string `json:"userId"`
UserName string `json:"userName"` UserName string `json:"userName"`
@ -46,9 +48,19 @@ type CreatePartnerOrderReq struct {
} }
type RefundOrderReq struct { type RefundOrderReq struct {
OutTradeNo string `json:"outTradeNo"`
Reason string `json:"reason,omitempty"`
RefundFee int64 `json:"refundFee,omitempty"`
RefundTarget string `json:"refundTarget,omitempty"`
}
type TransferReq struct {
PayAmount int64 `json:"payAmount"`
PayType string `json:"payType"`
PayTitle string `json:"payTitle"`
PayChannel string `json:"payChannel"`
UserId string `json:"userId"`
OutTradeNo string `json:"outTradeNo"` OutTradeNo string `json:"outTradeNo"`
Reason string `json:"reason,omitempty"`
RefundFee int64 `json:"refundFee,omitempty"`
} }
type CommonResponse struct { type CommonResponse struct {
@ -56,3 +68,19 @@ type CommonResponse struct {
Message string `json:"message"` Message string `json:"message"`
Data map[string]interface{} `json:"data"` Data map[string]interface{} `json:"data"`
} }
func (r *TransferReq) Check() error {
if r.PayTitle == "" {
return errors.New("PayTitle is nil")
}
if r.PayType == "" {
return errors.New("PayType is nil")
}
if r.UserId == "" {
return errors.New("UserId is nil")
}
if r.PayAmount < 0 {
return errors.New("PayAmount is nil")
}
return nil
}

View File

@ -9,26 +9,36 @@ import (
"time" "time"
) )
var (
httpClient *http.Client
)
func init() {
httpClient = &http.Client{
Timeout: 20 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 50,
MaxConnsPerHost: 500,
IdleConnTimeout: 90 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
}
// PostJson 请求 // PostJson 请求
func HttpPostJson(link string, header map[string]string, json []byte) ([]byte, error) { func HttpPostJson(link string, header map[string]string, json []byte) ([]byte, error) {
client := &http.Client{Timeout: 20 * time.Second}
//忽略https的证书
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
req, err := http.NewRequest("POST", link, bytes.NewBuffer(json)) req, err := http.NewRequest("POST", link, bytes.NewBuffer(json))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if header != nil {
for k, v := range header { for k, v := range header {
req.Header.Add(k, v) req.Header.Add(k, v)
}
} }
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req) resp, err := httpClient.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -41,24 +51,17 @@ func HttpPostJson(link string, header map[string]string, json []byte) ([]byte, e
// PostJson 请求 // PostJson 请求
func HttpPutJson(link string, header map[string]string, json []byte) ([]byte, error) { func HttpPutJson(link string, header map[string]string, json []byte) ([]byte, error) {
client := &http.Client{Timeout: 20 * time.Second}
//忽略https的证书
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
req, err := http.NewRequest("PUT", link, bytes.NewBuffer(json)) req, err := http.NewRequest("PUT", link, bytes.NewBuffer(json))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if header != nil {
for k, v := range header { for k, v := range header {
req.Header.Add(k, v) req.Header.Add(k, v)
}
} }
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req) resp, err := httpClient.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -71,22 +74,15 @@ func HttpPutJson(link string, header map[string]string, json []byte) ([]byte, er
// Get 请求 link请求url // Get 请求 link请求url
func HttpGet(link string, header map[string]string) ([]byte, error) { func HttpGet(link string, header map[string]string) ([]byte, error) {
client := &http.Client{Timeout: 20 * time.Second}
//忽略https的证书
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
req, err := http.NewRequest("GET", link, nil) req, err := http.NewRequest("GET", link, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if header != nil {
for k, v := range header { for k, v := range header {
req.Header.Add(k, v) req.Header.Add(k, v)
}
} }
resp, err := client.Do(req) resp, err := httpClient.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -99,22 +95,15 @@ func HttpGet(link string, header map[string]string) ([]byte, error) {
// Get 请求 link请求url // Get 请求 link请求url
func HttpDelete(link string, header map[string]string) ([]byte, error) { func HttpDelete(link string, header map[string]string) ([]byte, error) {
client := &http.Client{Timeout: 20 * time.Second}
//忽略https的证书
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
req, err := http.NewRequest("DELETE", link, nil) req, err := http.NewRequest("DELETE", link, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if header != nil {
for k, v := range header { for k, v := range header {
req.Header.Add(k, v) req.Header.Add(k, v)
}
} }
resp, err := client.Do(req) resp, err := httpClient.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -2,11 +2,15 @@ package weixin
import ( import (
"fmt" "fmt"
"github.com/tidwall/gjson"
"io" "io"
"net/http" "net/http"
"sync" "sync"
"time" "time"
cache2 "git.u8t.cn/open/gosdk/cache"
"github.com/go-redis/redis"
uuid2 "github.com/google/uuid"
"github.com/tidwall/gjson"
) )
const ( const (
@ -87,6 +91,65 @@ func (o *BaseSdk) getAccessToken() (string, error) {
return o.accessToken, nil return o.accessToken, nil
} }
// redis share version
func (o *BaseSdk) getAccessToken2() (string, error) {
redix := o.getRedis()
tokenKey := fmt.Sprintf("weixin_oa_access_token_v2_%s", o.appid)
lockKey := fmt.Sprintf("get_access_token_unique_%s", o.appid)
uuid := uuid2.New().String()
// get token
token := redix.Get(tokenKey).Val()
if token != "" {
return token, nil
}
// setEX and expire
// if not set key. wait 200ms and get again and retry 5 times
if ok := redix.SetNX(lockKey, uuid, 10*time.Second).Val(); !ok {
for i := range 5 {
time.Sleep(time.Duration(200*(i+2)) * time.Millisecond)
if token = redix.Get(tokenKey).Val(); token != "" {
return token, nil
}
}
return "", fmt.Errorf("<get access_token v2> wait 5 times,but still not get token")
}
defer func() {
if redix.Get(lockKey).Val() == uuid {
redix.Del(lockKey)
}
}()
// get new token
url := fmt.Sprintf("%s?grant_type=client_credential&appid=%s&secret=%s", accessTokenUrl, o.appid, o.secret)
res, err := http.Get(url)
if err != nil {
return "", err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return "", err
}
g := gjson.ParseBytes(body)
token = g.Get("access_token").String()
if token == "" {
return "", fmt.Errorf("%d:%s", g.Get("errcode").Int(), g.Get("errmsg").String())
}
expire := g.Get("expires_in").Int() - 10
redix.Set(tokenKey, token, time.Second*time.Duration(expire))
return token, nil
}
func (o *BaseSdk) getRedis() *redis.Client {
return cache2.GetRedis()
}
func (o *BaseSdk) getUserInfoByCode(code string, auth bool) (*UserInfo, error) { func (o *BaseSdk) getUserInfoByCode(code string, auth bool) (*UserInfo, error) {
url := fmt.Sprintf("%s?appid=%s&secret=%s&code=%s&grant_type=authorization_code", url := fmt.Sprintf("%s?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
code2AccessTokenUrl, o.appid, o.secret, code) code2AccessTokenUrl, o.appid, o.secret, code)

View File

@ -4,11 +4,12 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/tidwall/gjson"
"io" "io"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"time" "time"
"github.com/tidwall/gjson"
) )
type OaSdk struct { type OaSdk struct {
@ -35,7 +36,7 @@ func (o *OaSdk) GetQrCode(sceneStr string) (string, error) {
} }
marshal, _ := json.Marshal(params) marshal, _ := json.Marshal(params)
accessToken, err := o.getAccessToken() accessToken, err := o.getAccessToken2()
if err != nil { if err != nil {
return "", err return "", err
} }
@ -68,7 +69,7 @@ func (o *OaSdk) GetUserInfoByCodeNoAuth(code string) (*UserInfo, error) {
} }
func (o *OaSdk) GetUserInfoByOpenid(openid string) (*UserInfo, error) { func (o *OaSdk) GetUserInfoByOpenid(openid string) (*UserInfo, error) {
accessToken, err := o.getAccessToken() accessToken, err := o.getAccessToken2()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -115,7 +116,7 @@ func (o *OaSdk) UploadFileByByte(file []byte, ext string) (string, string, error
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
token, err := o.getAccessToken() token, err := o.getAccessToken2()
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
@ -155,7 +156,7 @@ func (o *OaSdk) UploadFileByByte(file []byte, ext string) (string, string, error
} }
func (o *OaSdk) CreateMenu(buttons []*OaMenuButton) error { func (o *OaSdk) CreateMenu(buttons []*OaMenuButton) error {
accessToken, err := o.getAccessToken() accessToken, err := o.getAccessToken2()
if err != nil { if err != nil {
return err return err
} }
@ -182,7 +183,7 @@ func (o *OaSdk) CreateMenu(buttons []*OaMenuButton) error {
} }
func (o *OaSdk) DeleteMenu() error { func (o *OaSdk) DeleteMenu() error {
accessToken, err := o.getAccessToken() accessToken, err := o.getAccessToken2()
if err != nil { if err != nil {
return err return err
} }