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
pkg
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"` //表单名称
Key string `json:"key"` //表单KEY
Value interface{} `json:"value"` //表单值
Default interface{} `json:"default"` //默认值
Disable bool `json:"disable,omitempty"` //是否禁用
Tips string `json:"tips"` //表单提示
Option []*FormOption `json:"option"` //表单选项radio和checkbox需要
@ -50,6 +51,8 @@ func NewFroms(tplConfig, saveConfig string) ([]*Form, error) {
for _, form := range forms {
if _, ok := cfg[form.Key]; ok {
form.Value = cfg[form.Key]
} else {
form.Value = form.Default
}
}
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/eclipse/paho.mqtt.golang v1.5.1
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/google/uuid v1.6.0
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c
github.com/minio/minio-go v6.0.14+incompatible
github.com/qiniu/go-sdk/v7 v7.25.4
@ -15,7 +17,7 @@ require (
github.com/spf13/cast v1.10.0
github.com/tidwall/gjson v1.18.0
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
)
@ -47,6 +49,8 @@ require (
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // 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/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
@ -58,12 +62,12 @@ require (
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/image v0.31.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.41.0 // indirect
google.golang.org/protobuf v1.36.9 // 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
}
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) {
ticket, err := q.getJsapiTicket()
if err != nil {

View File

@ -3,11 +3,12 @@ package qyweixin
import (
"encoding/json"
"fmt"
"strings"
"git.u8t.cn/open/gosdk/util"
"git.u8t.cn/open/goutil"
log "github.com/sirupsen/logrus"
"github.com/spf13/cast"
"strings"
)
type Applyer struct {
@ -90,57 +91,8 @@ type AppApprove struct {
}
func (d *ApproveDetail) GetValue(title string) string {
for _, content := range d.ApplyData.Contents {
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 ""
data := d.GetData()
return data[title]
}
func (d *ApproveDetail) GetData() map[string]string {
@ -165,9 +117,11 @@ func (d *ApproveDetail) GetData() map[string]string {
} else if content.Control == "Money" {
value = content.Value.NewMoney
} else if content.Control == "File" {
if len(content.Value.Files) > 0 {
value = content.Value.Files[0].FileId
fileIds := make([]string, 0)
for _, v := range content.Value.Files {
fileIds = append(fileIds, v.FileId)
}
value = strings.Join(fileIds, ",")
} 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) +
@ -220,7 +174,7 @@ func (q *AppApprove) GetDetail(spNo string) (*ApproveDetail, error) {
}
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 {
log.Errorf("get body[%s] json error :%s", string(rspBody), err.Error())
return nil, err

View File

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

View File

@ -3,12 +3,14 @@ package storage
import (
"context"
"fmt"
"github.com/minio/minio-go"
"net/http"
"net/url"
"path"
"strings"
"time"
"github.com/minio/minio-go"
log "github.com/sirupsen/logrus"
)
@ -61,9 +63,13 @@ func (s *Minio) Put(fileName, objectName string, onProcess func(fsize, uploaded
objectName = strings.TrimLeft(objectName, "/ ")
ext := strings.TrimLeft(path.Ext(fileName), ".")
contentType := ext2ContentType(ext)
if contentType == "" {
contentType = detectContentType(fileName)
}
_, err := s.client.FPutObjectWithContext(ctx, s.config.Bucket, objectName, fileName,
minio.PutObjectOptions{ContentType: ext2ContentType(ext)})
minio.PutObjectOptions{ContentType: contentType})
if err != nil {
log.Errorf("upload file to minio error:%s", err.Error())
return err
@ -108,7 +114,7 @@ func (s *Minio) List(objectPrefix string) ([]string, error) {
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 {
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 {
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
u, err := s.client.PresignedGetObject(s.config.Bucket, objectName, expire, params)
if err != nil {
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()
@ -128,9 +142,78 @@ func (s *Minio) Url(objectName string, expire time.Duration) string {
}
func (s *Minio) Stat(objectName string) (*ObjectInfo, error) {
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
}

View File

@ -4,10 +4,13 @@ import (
"context"
"errors"
"fmt"
"os"
"path"
"strings"
"time"
"github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage"
"os"
"time"
)
type QiniuConfig struct {
@ -152,7 +155,7 @@ func (s *Qiniu) Get(objectName, fileName string) error {
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)
cfg := storage.Config{
UseHTTPS: false,
@ -166,8 +169,11 @@ func (s *Qiniu) Url(objectName string, expire time.Duration) string {
if len(domains) <= 0 {
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 {
return storage.MakePublicURLv2(domain, objectName)
} else {
@ -185,6 +191,10 @@ func (s *Qiniu) Stat(objectName string) (*ObjectInfo, error) {
bucketManager := storage.NewBucketManager(mac, &cfg)
fileInfo, err := bucketManager.Stat(s.config.Bucket, objectName)
if err != nil {
// 文件不存在不算错误,返回 nil, nil
if isQiniuNotFound(err) {
return nil, nil
}
return nil, err
}
@ -196,7 +206,22 @@ func (s *Qiniu) Stat(objectName string) (*ObjectInfo, error) {
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)
cfg := storage.Config{
UseHTTPS: false,
@ -211,3 +236,19 @@ func (s *Qiniu) Fetch(url, objectName string) error {
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
Get(objectName, fileName 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)
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 {
ext = strings.ToLower(ext)
if ext == "jpg" || ext == "jpeg" {
switch ext {
// 图片格式
case "jpg", "jpeg":
return "image/jpeg"
} else if ext == "png" {
case "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"
} 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"
}
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 ""
}
}
// 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 {

View File

@ -4,8 +4,10 @@ import (
"encoding/json"
"errors"
"fmt"
"git.u8t.cn/open/gosdk/util"
"net/url"
"git.u8t.cn/open/gosdk/util"
"git.u8t.cn/open/goutil"
)
var (
@ -177,7 +179,7 @@ func (p *Pay) RefundOrder(req *RefundOrderReq) error {
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{
"x-token": p.token,
})
@ -219,3 +221,27 @@ func (p *Pay) RefundPartnerOrder(req *RefundOrderReq) error {
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
import "errors"
type OrderUser struct {
UserId string `json:"userId"`
UserName string `json:"userName"`
@ -49,6 +51,16 @@ 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"`
}
type CommonResponse struct {
@ -56,3 +68,19 @@ type CommonResponse struct {
Message string `json:"message"`
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"
)
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 请求
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))
if err != nil {
return nil, err
}
if header != nil {
for k, v := range header {
req.Header.Add(k, v)
}
}
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
@ -41,24 +51,17 @@ func HttpPostJson(link string, header map[string]string, json []byte) ([]byte, e
// PostJson 请求
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))
if err != nil {
return nil, err
}
if header != nil {
for k, v := range header {
req.Header.Add(k, v)
}
}
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
@ -71,22 +74,15 @@ func HttpPutJson(link string, header map[string]string, json []byte) ([]byte, er
// Get 请求 link请求url
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)
if err != nil {
return nil, err
}
if header != nil {
for k, v := range header {
req.Header.Add(k, v)
}
}
resp, err := client.Do(req)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
@ -99,22 +95,15 @@ func HttpGet(link string, header map[string]string) ([]byte, error) {
// Get 请求 link请求url
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)
if err != nil {
return nil, err
}
if header != nil {
for k, v := range header {
req.Header.Add(k, v)
}
}
resp, err := client.Do(req)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}

View File

@ -2,11 +2,15 @@ package weixin
import (
"fmt"
"github.com/tidwall/gjson"
"io"
"net/http"
"sync"
"time"
cache2 "git.u8t.cn/open/gosdk/cache"
"github.com/go-redis/redis"
uuid2 "github.com/google/uuid"
"github.com/tidwall/gjson"
)
const (
@ -87,6 +91,65 @@ func (o *BaseSdk) getAccessToken() (string, error) {
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) {
url := fmt.Sprintf("%s?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
code2AccessTokenUrl, o.appid, o.secret, code)

View File

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