feat: migrate serevices
Some checks failed
build quyun / Build (push) Failing after 2m50s

This commit is contained in:
2025-12-19 19:05:12 +08:00
parent 005585c53b
commit 557a641f41
71 changed files with 5626 additions and 280 deletions

View File

@@ -0,0 +1,56 @@
package ali
import (
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
"go.ipao.vip/atom/container"
"go.ipao.vip/atom/opt"
)
const DefaultPrefix = "Ali"
func DefaultProvider() container.ProviderContainer {
return container.ProviderContainer{
Provider: Provide,
Options: []opt.Option{
opt.Prefix(DefaultPrefix),
},
}
}
type Config struct {
AccessKeyId string
AccessKeySecret string
Bucket string
Region string
Host *string
CallbackURL string
}
func Provide(opts ...opt.Option) error {
o := opt.New(opts...)
var config Config
if err := o.UnmarshalConfig(&config); err != nil {
return err
}
return container.Container.Provide(func() (*OSSClient, error) {
cred := credentials.NewStaticCredentialsProvider(config.AccessKeyId, config.AccessKeySecret)
cfg := oss.LoadDefaultConfig().
WithCredentialsProvider(cred).
WithRegion(config.Region).
WithUseCName(true).
WithEndpoint(*config.Host)
cfgInternal := oss.LoadDefaultConfig().
WithCredentialsProvider(cred).
WithRegion(config.Region)
return &OSSClient{
client: oss.NewClient(cfg),
internalClient: oss.NewClient(cfgInternal),
config: &config,
}, nil
}, o.DiOptions()...)
}

View File

@@ -0,0 +1,165 @@
package ali
import (
"context"
"path/filepath"
"strings"
"time"
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
)
type OSSOption func(*OSSOptions)
type OSSOptions struct {
internal bool
expire *time.Duration
}
func WithExpire(expire time.Duration) OSSOption {
return func(o *OSSOptions) {
o.expire = &expire
}
}
func WithInternal() OSSOption {
return func(o *OSSOptions) {
o.internal = true
}
}
type OSSClient struct {
client *oss.Client
internalClient *oss.Client
config *Config
}
func (c *OSSClient) GetSavePath(path string) string {
return filepath.Join("quyun", strings.Trim(path, "/"))
}
func (c *OSSClient) GetClient() *oss.Client {
return c.client
}
func (c *OSSClient) PreSignUpload(ctx context.Context, path, mimeType string, opts ...OSSOption) (*oss.PresignResult, error) {
request := &oss.PutObjectRequest{
Bucket: oss.Ptr(c.config.Bucket),
Key: oss.Ptr(c.GetSavePath(path)),
ContentType: oss.Ptr(mimeType),
}
opt := &OSSOptions{}
for _, o := range opts {
o(opt)
}
client := c.client
if opt.internal {
client = c.internalClient
}
return client.Presign(ctx, request)
}
func (c *OSSClient) Download(ctx context.Context, path, dest string, opts ...OSSOption) error {
request := &oss.GetObjectRequest{
Bucket: oss.Ptr(c.config.Bucket),
Key: oss.Ptr(path),
}
opt := &OSSOptions{}
for _, o := range opts {
o(opt)
}
client := c.client
if opt.internal {
client = c.internalClient
}
ossFuncs := []func(*oss.Options){
oss.OpReadWriteTimeout(time.Minute * 20),
}
if _, err := client.GetObjectToFile(ctx, request, dest, ossFuncs...); err != nil {
return err
}
return nil
}
// GetSignedUrl
func (c *OSSClient) GetSignedUrl(ctx context.Context, path string, opts ...OSSOption) (string, error) {
request := &oss.GetObjectRequest{
Bucket: oss.Ptr(c.config.Bucket),
Key: oss.Ptr(path),
}
opt := &OSSOptions{}
for _, o := range opts {
o(opt)
}
expire := time.Minute * 5
if opt.expire != nil {
expire = *opt.expire
}
client := c.client
if opt.internal {
client = c.internalClient
}
preSign, err := client.Presign(ctx, request, oss.PresignExpires(expire))
if err != nil {
return "", err
}
return preSign.URL, nil
}
// Delete
func (c *OSSClient) Delete(ctx context.Context, path string, opts ...OSSOption) error {
request := &oss.DeleteObjectRequest{
Bucket: oss.Ptr(c.config.Bucket),
Key: oss.Ptr(path),
}
opt := &OSSOptions{}
for _, o := range opts {
o(opt)
}
client := c.client
if opt.internal {
client = c.internalClient
}
if _, err := client.DeleteObject(ctx, request); err != nil {
return err
}
return nil
}
// Upload
func (c *OSSClient) Upload(ctx context.Context, input, dst string, opts ...OSSOption) error {
request := &oss.PutObjectRequest{
Bucket: oss.Ptr(c.config.Bucket),
Key: oss.Ptr(dst),
}
opt := &OSSOptions{}
for _, o := range opts {
o(opt)
}
client := c.client
if opt.internal {
client = c.internalClient
}
ossFuncs := []func(*oss.Options){
oss.OpReadWriteTimeout(time.Minute * 20),
}
if _, err := client.PutObjectFromFile(ctx, request, input, ossFuncs...); err != nil {
return err
}
return nil
}

View File

@@ -21,9 +21,13 @@ func DefaultProvider() container.ProviderContainer {
type AppMode string
type Config struct {
Mode AppMode
Cert *Cert
BaseURI *string
Mode AppMode
Cert *Cert
BaseURI *string
StoragePath string
DistAdmin string
DistWeChat string
RechargeWechat string
}
func (c *Config) IsDevMode() bool {

View File

@@ -0,0 +1,45 @@
package app
import (
"go.ipao.vip/atom/container"
"go.ipao.vip/atom/opt"
)
const DefaultPrefix = "App"
func DefaultProvider() container.ProviderContainer {
return container.ProviderContainer{
Provider: Provide,
Options: []opt.Option{
opt.Prefix(DefaultPrefix),
},
}
}
// swagger:enum AppMode
// ENUM(development, release, test)
type AppMode string
type Config struct {
Mode AppMode
Cert *Cert
BaseURI *string
}
func (c *Config) IsDevMode() bool {
return c.Mode == AppModeDevelopment
}
func (c *Config) IsReleaseMode() bool {
return c.Mode == AppModeRelease
}
func (c *Config) IsTestMode() bool {
return c.Mode == AppModeTest
}
type Cert struct {
CA string
Cert string
Key string
}

View File

@@ -0,0 +1,152 @@
package req
import (
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"quyun/v2/providers/req/cookiejar"
"github.com/imroc/req/v3"
"go.ipao.vip/atom/container"
"go.ipao.vip/atom/opt"
)
type Client struct {
client *req.Client
jar *cookiejar.Jar
}
func Provide(opts ...opt.Option) error {
o := opt.New(opts...)
var config Config
if err := o.UnmarshalConfig(&config); err != nil {
return err
}
return container.Container.Provide(func() (*Client, error) {
c := &Client{}
client := req.C()
if config.DevMode {
client.DevMode()
}
if config.CookieJarFile != "" {
dir := filepath.Dir(config.CookieJarFile)
if _, err := os.Stat(dir); os.IsNotExist(err) {
err = os.MkdirAll(dir, 0o755)
if err != nil {
return nil, err
}
}
jar, err := cookiejar.New(&cookiejar.Options{
Filename: config.CookieJarFile,
})
if err != nil {
return nil, err
}
c.jar = jar
client.SetCookieJar(jar)
}
if config.RootCa != nil {
client.SetRootCertsFromFile(config.RootCa...)
}
if config.InsecureSkipVerify {
client.EnableInsecureSkipVerify()
}
if config.UserAgent != "" {
client.SetUserAgent(config.UserAgent)
}
if config.Timeout > 0 {
client.SetTimeout(time.Duration(config.Timeout) * time.Second)
}
if config.CommonHeaders != nil {
client.SetCommonHeaders(config.CommonHeaders)
}
if config.AuthBasic.Username != "" && config.AuthBasic.Password != "" {
client.SetCommonBasicAuth(config.AuthBasic.Username, config.AuthBasic.Password)
}
if config.AuthBearerToken != "" {
client.SetCommonBearerAuthToken(config.AuthBearerToken)
}
if config.ProxyURL != "" {
client.SetProxyURL(config.ProxyURL)
}
if config.RedirectPolicy != nil {
client.SetRedirectPolicy(parsePolicies(config.RedirectPolicy)...)
}
c.client = client
return c, nil
}, o.DiOptions()...)
}
func parsePolicies(policies []string) []req.RedirectPolicy {
ps := []req.RedirectPolicy{}
for _, policy := range policies {
policyItems := strings.Split(policy, ":")
if len(policyItems) != 2 {
continue
}
switch policyItems[0] {
case "Max":
max, err := strconv.Atoi(policyItems[1])
if err != nil {
continue
}
ps = append(ps, req.MaxRedirectPolicy(max))
case "No":
ps = append(ps, req.NoRedirectPolicy())
case "SameDomain":
ps = append(ps, req.SameDomainRedirectPolicy())
case "SameHost":
ps = append(ps, req.SameHostRedirectPolicy())
case "AllowedHost":
ps = append(ps, req.AllowedHostRedirectPolicy(strings.Split(policyItems[1], ",")...))
case "AllowedDomain":
ps = append(ps, req.AllowedDomainRedirectPolicy(strings.Split(policyItems[1], ",")...))
}
}
return ps
}
func (c *Client) R() *req.Request {
return c.client.R()
}
func (c *Client) SaveCookJar() error {
return c.jar.Save()
}
func (c *Client) GetCookie(key string) (string, bool) {
kv := c.AllCookiesKV()
v, ok := kv[key]
return v, ok
}
func (c *Client) AllCookies() []*http.Cookie {
return c.jar.AllCookies()
}
func (c *Client) AllCookiesKV() map[string]string {
return c.jar.KVData()
}
func (c *Client) SetCookie(u *url.URL, cookies []*http.Cookie) {
c.jar.SetCookies(u, cookies)
}

View File

@@ -0,0 +1,34 @@
package req
import (
"go.ipao.vip/atom/container"
"go.ipao.vip/atom/opt"
)
const DefaultPrefix = "HttpClient"
func DefaultProvider() container.ProviderContainer {
return container.ProviderContainer{
Provider: Provide,
Options: []opt.Option{
opt.Prefix(DefaultPrefix),
},
}
}
type Config struct {
DevMode bool
CookieJarFile string
RootCa []string
UserAgent string
InsecureSkipVerify bool
CommonHeaders map[string]string
Timeout uint
AuthBasic struct {
Username string
Password string
}
AuthBearerToken string
ProxyURL string
RedirectPolicy []string // "Max:10;No;SameDomain;SameHost;AllowedHost:x,x,x,x,x,AllowedDomain:x,x,x,x,x"
}

View File

@@ -0,0 +1,704 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package cookiejar implements an in-memory RFC 6265-compliant http.CookieJar.
//
// This implementation is a fork of net/http/cookiejar which also
// implements methods for dumping the cookies to persistent
// storage and retrieving them.
package cookiejar
import (
"fmt"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"time"
"github.com/pkg/errors"
"golang.org/x/net/publicsuffix"
)
// PublicSuffixList provides the public suffix of a domain. For example:
// - the public suffix of "example.com" is "com",
// - the public suffix of "foo1.foo2.foo3.co.uk" is "co.uk", and
// - the public suffix of "bar.pvt.k12.ma.us" is "pvt.k12.ma.us".
//
// Implementations of PublicSuffixList must be safe for concurrent use by
// multiple goroutines.
//
// An implementation that always returns "" is valid and may be useful for
// testing but it is not secure: it means that the HTTP server for foo.com can
// set a cookie for bar.com.
//
// A public suffix list implementation is in the package
// golang.org/x/net/publicsuffix.
type PublicSuffixList interface {
// PublicSuffix returns the public suffix of domain.
//
// TODO: specify which of the caller and callee is responsible for IP
// addresses, for leading and trailing dots, for case sensitivity, and
// for IDN/Punycode.
PublicSuffix(domain string) string
// String returns a description of the source of this public suffix
// list. The description will typically contain something like a time
// stamp or version number.
String() string
}
// Options are the options for creating a new Jar.
type Options struct {
// PublicSuffixList is the public suffix list that determines whether
// an HTTP server can set a cookie for a domain.
//
// If this is nil, the public suffix list implementation in golang.org/x/net/publicsuffix
// is used.
PublicSuffixList PublicSuffixList
// Filename holds the file to use for storage of the cookies.
// If it is empty, the value of DefaultCookieFile will be used.
Filename string
// NoPersist specifies whether no persistence should be used
// (useful for tests). If this is true, the value of Filename will be
// ignored.
NoPersist bool
}
// Jar implements the http.CookieJar interface from the net/http package.
type Jar struct {
// filename holds the file that the cookies were loaded from.
filename string
psList PublicSuffixList
// mu locks the remaining fields.
mu sync.Mutex
// entries is a set of entries, keyed by their eTLD+1 and subkeyed by
// their name/domain/path.
entries map[string]map[string]entry
}
var noOptions Options
// New returns a new cookie jar. A nil *Options is equivalent to a zero
// Options.
//
// New will return an error if the cookies could not be loaded
// from the file for any reason than if the file does not exist.
func New(o *Options) (*Jar, error) {
return newAtTime(o, time.Now())
}
// newAtTime is like New but takes the current time as a parameter.
func newAtTime(o *Options, now time.Time) (*Jar, error) {
jar := &Jar{
entries: make(map[string]map[string]entry),
}
if o == nil {
o = &noOptions
}
if jar.psList = o.PublicSuffixList; jar.psList == nil {
jar.psList = publicsuffix.List
}
if !o.NoPersist {
if jar.filename = o.Filename; jar.filename == "" {
jar.filename = DefaultCookieFile()
}
if err := jar.load(); err != nil {
return nil, errors.Wrap(err, "cannot load cookies")
}
}
jar.deleteExpired(now)
return jar, nil
}
// homeDir returns the OS-specific home path as specified in the environment.
func homeDir() string {
if runtime.GOOS == "windows" {
return filepath.Join(os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH"))
}
return os.Getenv("HOME")
}
// entry is the internal representation of a cookie.
//
// This struct type is not used outside of this package per se, but the exported
// fields are those of RFC 6265.
// Note that this structure is marshaled to JSON, so backward-compatibility
// should be preserved.
type entry struct {
Name string
Value string
Domain string
Path string
Secure bool
HttpOnly bool
Persistent bool
HostOnly bool
Expires time.Time
Creation time.Time
LastAccess time.Time
// Updated records when the cookie was updated.
// This is different from creation time because a cookie
// can be changed without updating the creation time.
Updated time.Time
// CanonicalHost stores the original canonical host name
// that the cookie was associated with. We store this
// so that even if the public suffix list changes (for example
// when storing/loading cookies) we can still get the correct
// jar keys.
CanonicalHost string
}
// id returns the domain;path;name triple of e as an id.
func (e *entry) id() string {
return id(e.Domain, e.Path, e.Name)
}
// id returns the domain;path;name triple as an id.
func id(domain, path, name string) string {
return fmt.Sprintf("%s;%s;%s", domain, path, name)
}
// shouldSend determines whether e's cookie qualifies to be included in a
// request to host/path. It is the caller's responsibility to check if the
// cookie is expired.
func (e *entry) shouldSend(https bool, host, path string) bool {
return e.domainMatch(host) && e.pathMatch(path) && (https || !e.Secure)
}
// domainMatch implements "domain-match" of RFC 6265 section 5.1.3.
func (e *entry) domainMatch(host string) bool {
if e.Domain == host {
return true
}
return !e.HostOnly && hasDotSuffix(host, e.Domain)
}
// pathMatch implements "path-match" according to RFC 6265 section 5.1.4.
func (e *entry) pathMatch(requestPath string) bool {
if requestPath == e.Path {
return true
}
if strings.HasPrefix(requestPath, e.Path) {
if e.Path[len(e.Path)-1] == '/' {
return true // The "/any/" matches "/any/path" case.
} else if requestPath[len(e.Path)] == '/' {
return true // The "/any" matches "/any/path" case.
}
}
return false
}
// hasDotSuffix reports whether s ends in "."+suffix.
func hasDotSuffix(s, suffix string) bool {
return len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.' && s[len(s)-len(suffix):] == suffix
}
type byCanonicalHost struct {
byPathLength
}
func (s byCanonicalHost) Less(i, j int) bool {
e0, e1 := &s.byPathLength[i], &s.byPathLength[j]
if e0.CanonicalHost != e1.CanonicalHost {
return e0.CanonicalHost < e1.CanonicalHost
}
return s.byPathLength.Less(i, j)
}
// byPathLength is a []entry sort.Interface that sorts according to RFC 6265
// section 5.4 point 2: by longest path and then by earliest creation time.
type byPathLength []entry
func (s byPathLength) Len() int { return len(s) }
func (s byPathLength) Less(i, j int) bool {
e0, e1 := &s[i], &s[j]
if len(e0.Path) != len(e1.Path) {
return len(e0.Path) > len(e1.Path)
}
if !e0.Creation.Equal(e1.Creation) {
return e0.Creation.Before(e1.Creation)
}
// The following are not strictly necessary
// but are useful for providing deterministic
// behaviour in tests.
if e0.Name != e1.Name {
return e0.Name < e1.Name
}
return e0.Value < e1.Value
}
func (s byPathLength) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// Cookies implements the Cookies method of the http.CookieJar interface.
//
// It returns an empty slice if the URL's scheme is not HTTP or HTTPS.
func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) {
return j.cookies(u, time.Now())
}
// cookies is like Cookies but takes the current time as a parameter.
func (j *Jar) cookies(u *url.URL, now time.Time) (cookies []*http.Cookie) {
if u.Scheme != "http" && u.Scheme != "https" {
return cookies
}
host, err := canonicalHost(u.Host)
if err != nil {
return cookies
}
key := jarKey(host, j.psList)
j.mu.Lock()
defer j.mu.Unlock()
submap := j.entries[key]
if submap == nil {
return cookies
}
https := u.Scheme == "https"
path := u.Path
if path == "" {
path = "/"
}
var selected []entry
for id, e := range submap {
if !e.Expires.After(now) {
// Save some space by deleting the value when the cookie
// expires. We can't delete the cookie itself because then
// we wouldn't know that the cookie had expired when
// we merge with another cookie jar.
if e.Value != "" {
e.Value = ""
submap[id] = e
}
continue
}
if !e.shouldSend(https, host, path) {
continue
}
e.LastAccess = now
submap[id] = e
selected = append(selected, e)
}
sort.Sort(byPathLength(selected))
for _, e := range selected {
cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value})
}
return cookies
}
// AllCookies returns all cookies in the jar. The returned cookies will
// have Domain, Expires, HttpOnly, Name, Secure, Path, and Value filled
// out. Expired cookies will not be returned. This function does not
// modify the cookie jar.
func (j *Jar) AllCookies() (cookies []*http.Cookie) {
return j.allCookies(time.Now())
}
// allCookies is like AllCookies but takes the current time as a parameter.
func (j *Jar) allCookies(now time.Time) []*http.Cookie {
var selected []entry
j.mu.Lock()
defer j.mu.Unlock()
for _, submap := range j.entries {
for _, e := range submap {
if !e.Expires.After(now) {
// Do not return expired cookies.
continue
}
selected = append(selected, e)
}
}
sort.Sort(byCanonicalHost{byPathLength(selected)})
cookies := make([]*http.Cookie, len(selected))
for i, e := range selected {
// Note: The returned cookies do not contain sufficient
// information to recreate the database.
cookies[i] = &http.Cookie{
Name: e.Name,
Value: e.Value,
Path: e.Path,
Domain: e.Domain,
Expires: e.Expires,
Secure: e.Secure,
HttpOnly: e.HttpOnly,
}
}
return cookies
}
// RemoveCookie removes the cookie matching the name, domain and path
// specified by c.
func (j *Jar) RemoveCookie(c *http.Cookie) {
j.mu.Lock()
defer j.mu.Unlock()
id := id(c.Domain, c.Path, c.Name)
key := jarKey(c.Domain, j.psList)
if e, ok := j.entries[key][id]; ok {
e.Value = ""
e.Expires = time.Now().Add(-1 * time.Second)
j.entries[key][id] = e
}
}
// merge merges all the given entries into j. More recently changed
// cookies take precedence over older ones.
func (j *Jar) merge(entries []entry) {
for _, e := range entries {
if e.CanonicalHost == "" {
continue
}
key := jarKey(e.CanonicalHost, j.psList)
id := e.id()
submap := j.entries[key]
if submap == nil {
j.entries[key] = map[string]entry{
id: e,
}
continue
}
oldEntry, ok := submap[id]
if !ok || e.Updated.After(oldEntry.Updated) {
submap[id] = e
}
}
}
var expiryRemovalDuration = 24 * time.Hour
// deleteExpired deletes all entries that have expired for long enough
// that we can actually expect there to be no external copies of it that
// might resurrect the dead cookie.
func (j *Jar) deleteExpired(now time.Time) {
for tld, submap := range j.entries {
for id, e := range submap {
if !e.Expires.After(now) && !e.Updated.Add(expiryRemovalDuration).After(now) {
delete(submap, id)
}
}
if len(submap) == 0 {
delete(j.entries, tld)
}
}
}
// RemoveAllHost removes any cookies from the jar that were set for the given host.
func (j *Jar) RemoveAllHost(host string) {
host, err := canonicalHost(host)
if err != nil {
return
}
key := jarKey(host, j.psList)
j.mu.Lock()
defer j.mu.Unlock()
expired := time.Now().Add(-1 * time.Second)
submap := j.entries[key]
for id, e := range submap {
if e.CanonicalHost == host {
// Save some space by deleting the value when the cookie
// expires. We can't delete the cookie itself because then
// we wouldn't know that the cookie had expired when
// we merge with another cookie jar.
e.Value = ""
e.Expires = expired
submap[id] = e
}
}
}
// RemoveAll removes all the cookies from the jar.
func (j *Jar) RemoveAll() {
expired := time.Now().Add(-1 * time.Second)
j.mu.Lock()
defer j.mu.Unlock()
for _, submap := range j.entries {
for id, e := range submap {
// Save some space by deleting the value when the cookie
// expires. We can't delete the cookie itself because then
// we wouldn't know that the cookie had expired when
// we merge with another cookie jar.
e.Value = ""
e.Expires = expired
submap[id] = e
}
}
}
// SetCookies implements the SetCookies method of the http.CookieJar interface.
//
// It does nothing if the URL's scheme is not HTTP or HTTPS.
func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
j.setCookies(u, cookies, time.Now())
}
// setCookies is like SetCookies but takes the current time as parameter.
func (j *Jar) setCookies(u *url.URL, cookies []*http.Cookie, now time.Time) {
if len(cookies) == 0 {
return
}
if u.Scheme != "http" && u.Scheme != "https" {
// TODO is this really correct? It might be nice to send
// cookies to websocket connections, for example.
return
}
host, err := canonicalHost(u.Host)
if err != nil {
return
}
key := jarKey(host, j.psList)
defPath := defaultPath(u.Path)
j.mu.Lock()
defer j.mu.Unlock()
submap := j.entries[key]
for _, cookie := range cookies {
e, err := j.newEntry(cookie, now, defPath, host)
if err != nil {
continue
}
e.CanonicalHost = host
id := e.id()
if submap == nil {
submap = make(map[string]entry)
j.entries[key] = submap
}
if old, ok := submap[id]; ok {
e.Creation = old.Creation
} else {
e.Creation = now
}
e.Updated = now
e.LastAccess = now
submap[id] = e
}
}
// canonicalHost strips port from host if present and returns the canonicalized
// host name.
func canonicalHost(host string) (string, error) {
var err error
host = strings.ToLower(host)
if hasPort(host) {
host, _, err = net.SplitHostPort(host)
if err != nil {
return "", err
}
}
if strings.HasSuffix(host, ".") {
// Strip trailing dot from fully qualified domain names.
host = host[:len(host)-1]
}
return toASCII(host)
}
// hasPort reports whether host contains a port number. host may be a host
// name, an IPv4 or an IPv6 address.
func hasPort(host string) bool {
colons := strings.Count(host, ":")
if colons == 0 {
return false
}
if colons == 1 {
return true
}
return host[0] == '[' && strings.Contains(host, "]:")
}
// jarKey returns the key to use for a jar.
func jarKey(host string, psl PublicSuffixList) string {
if isIP(host) {
return host
}
var i int
if psl == nil {
i = strings.LastIndex(host, ".")
if i == -1 {
return host
}
} else {
suffix := psl.PublicSuffix(host)
if suffix == host {
return host
}
i = len(host) - len(suffix)
if i <= 0 || host[i-1] != '.' {
// The provided public suffix list psl is broken.
// Storing cookies under host is a safe stopgap.
return host
}
}
prevDot := strings.LastIndex(host[:i-1], ".")
return host[prevDot+1:]
}
// isIP reports whether host is an IP address.
func isIP(host string) bool {
return net.ParseIP(host) != nil
}
// defaultPath returns the directory part of an URL's path according to
// RFC 6265 section 5.1.4.
func defaultPath(path string) string {
if len(path) == 0 || path[0] != '/' {
return "/" // Path is empty or malformed.
}
i := strings.LastIndex(path, "/") // Path starts with "/", so i != -1.
if i == 0 {
return "/" // Path has the form "/abc".
}
return path[:i] // Path is either of form "/abc/xyz" or "/abc/xyz/".
}
// newEntry creates an entry from a http.Cookie c. now is the current
// time and is compared to c.Expires to determine deletion of c. defPath
// and host are the default-path and the canonical host name of the URL
// c was received from.
//
// The returned entry should be removed if its expiry time is in the
// past. In this case, e may be incomplete, but it will be valid to call
// e.id (which depends on e's Name, Domain and Path).
//
// A malformed c.Domain will result in an error.
func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e entry, err error) {
e.Name = c.Name
if c.Path == "" || c.Path[0] != '/' {
e.Path = defPath
} else {
e.Path = c.Path
}
e.Domain, e.HostOnly, err = j.domainAndType(host, c.Domain)
if err != nil {
return e, err
}
// MaxAge takes precedence over Expires.
if c.MaxAge != 0 {
e.Persistent = true
e.Expires = now.Add(time.Duration(c.MaxAge) * time.Second)
if c.MaxAge < 0 {
return e, nil
}
} else if c.Expires.IsZero() {
e.Expires = endOfTime
} else {
e.Persistent = true
e.Expires = c.Expires
if !c.Expires.After(now) {
return e, nil
}
}
e.Value = c.Value
e.Secure = c.Secure
e.HttpOnly = c.HttpOnly
return e, nil
}
var (
errIllegalDomain = errors.New("cookiejar: illegal cookie domain attribute")
errMalformedDomain = errors.New("cookiejar: malformed cookie domain attribute")
errNoHostname = errors.New("cookiejar: no host name available (IP only)")
)
// endOfTime is the time when session (non-persistent) cookies expire.
// This instant is representable in most date/time formats (not just
// Go's time.Time) and should be far enough in the future.
var endOfTime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
// domainAndType determines the cookie's domain and hostOnly attribute.
func (j *Jar) domainAndType(host, domain string) (string, bool, error) {
if domain == "" {
// No domain attribute in the SetCookie header indicates a
// host cookie.
return host, true, nil
}
if isIP(host) {
// According to RFC 6265 domain-matching includes not being
// an IP address.
// TODO: This might be relaxed as in common browsers.
return "", false, errNoHostname
}
// From here on: If the cookie is valid, it is a domain cookie (with
// the one exception of a public suffix below).
// See RFC 6265 section 5.2.3.
if domain[0] == '.' {
domain = domain[1:]
}
if len(domain) == 0 || domain[0] == '.' {
// Received either "Domain=." or "Domain=..some.thing",
// both are illegal.
return "", false, errMalformedDomain
}
domain = strings.ToLower(domain)
if domain[len(domain)-1] == '.' {
// We received stuff like "Domain=www.example.com.".
// Browsers do handle such stuff (actually differently) but
// RFC 6265 seems to be clear here (e.g. section 4.1.2.3) in
// requiring a reject. 4.1.2.3 is not normative, but
// "Domain Matching" (5.1.3) and "Canonicalized Host Names"
// (5.1.2) are.
return "", false, errMalformedDomain
}
// See RFC 6265 section 5.3 #5.
if j.psList != nil {
if ps := j.psList.PublicSuffix(domain); ps != "" && !hasDotSuffix(domain, ps) {
if host == domain {
// This is the one exception in which a cookie
// with a domain attribute is a host cookie.
return host, true, nil
}
return "", false, errIllegalDomain
}
}
// The domain must domain-match host: www.mycompany.com cannot
// set cookies for .ourcompetitors.com.
if host != domain && !hasDotSuffix(host, domain) {
return "", false, errIllegalDomain
}
return domain, false, nil
}
// DefaultCookieFile returns the default cookie file to use
// for persisting cookie data.
// The following names will be used in decending order of preference:
// - the value of the $GOCOOKIES environment variable.
// - $HOME/.go-cookies
func DefaultCookieFile() string {
if f := os.Getenv("GOCOOKIES"); f != "" {
return f
}
return filepath.Join(homeDir(), ".go-cookies")
}

View File

@@ -0,0 +1,159 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cookiejar
// This file implements the Punycode algorithm from RFC 3492.
import (
"fmt"
"strings"
"unicode/utf8"
)
// These parameter values are specified in section 5.
//
// All computation is done with int32s, so that overflow behavior is identical
// regardless of whether int is 32-bit or 64-bit.
const (
base int32 = 36
damp int32 = 700
initialBias int32 = 72
initialN int32 = 128
skew int32 = 38
tmax int32 = 26
tmin int32 = 1
)
// encode encodes a string as specified in section 6.3 and prepends prefix to
// the result.
//
// The "while h < length(input)" line in the specification becomes "for
// remaining != 0" in the Go code, because len(s) in Go is in bytes, not runes.
func encode(prefix, s string) (string, error) {
output := make([]byte, len(prefix), len(prefix)+1+2*len(s))
copy(output, prefix)
delta, n, bias := int32(0), initialN, initialBias
b, remaining := int32(0), int32(0)
for _, r := range s {
if r < 0x80 {
b++
output = append(output, byte(r))
} else {
remaining++
}
}
h := b
if b > 0 {
output = append(output, '-')
}
for remaining != 0 {
m := int32(0x7fffffff)
for _, r := range s {
if m > r && r >= n {
m = r
}
}
delta += (m - n) * (h + 1)
if delta < 0 {
return "", fmt.Errorf("cookiejar: invalid label %q", s)
}
n = m
for _, r := range s {
if r < n {
delta++
if delta < 0 {
return "", fmt.Errorf("cookiejar: invalid label %q", s)
}
continue
}
if r > n {
continue
}
q := delta
for k := base; ; k += base {
t := k - bias
if t < tmin {
t = tmin
} else if t > tmax {
t = tmax
}
if q < t {
break
}
output = append(output, encodeDigit(t+(q-t)%(base-t)))
q = (q - t) / (base - t)
}
output = append(output, encodeDigit(q))
bias = adapt(delta, h+1, h == b)
delta = 0
h++
remaining--
}
delta++
n++
}
return string(output), nil
}
func encodeDigit(digit int32) byte {
switch {
case 0 <= digit && digit < 26:
return byte(digit + 'a')
case 26 <= digit && digit < 36:
return byte(digit + ('0' - 26))
}
panic("cookiejar: internal error in punycode encoding")
}
// adapt is the bias adaptation function specified in section 6.1.
func adapt(delta, numPoints int32, firstTime bool) int32 {
if firstTime {
delta /= damp
} else {
delta /= 2
}
delta += delta / numPoints
k := int32(0)
for delta > ((base-tmin)*tmax)/2 {
delta /= base - tmin
k += base
}
return k + (base-tmin+1)*delta/(delta+skew)
}
// Strictly speaking, the remaining code below deals with IDNA (RFC 5890 and
// friends) and not Punycode (RFC 3492) per se.
// acePrefix is the ASCII Compatible Encoding prefix.
const acePrefix = "xn--"
// toASCII converts a domain or domain label to its ASCII form. For example,
// toASCII("bücher.example.com") is "xn--bcher-kva.example.com", and
// toASCII("golang") is "golang".
func toASCII(s string) (string, error) {
if ascii(s) {
return s, nil
}
labels := strings.Split(s, ".")
for i, label := range labels {
if !ascii(label) {
a, err := encode(acePrefix, label)
if err != nil {
return "", err
}
labels[i] = a
}
}
return strings.Join(labels, "."), nil
}
func ascii(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] >= utf8.RuneSelf {
return false
}
}
return true
}

View File

@@ -0,0 +1,188 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cookiejar
import (
"encoding/json"
"io"
"log"
"os"
"path/filepath"
"sort"
"strings"
"time"
"gopkg.in/retry.v1"
filelock "github.com/juju/go4/lock"
"github.com/pkg/errors"
)
// Save saves the cookies to the persistent cookie file.
// Before the file is written, it reads any cookies that
// have been stored from it and merges them into j.
func (j *Jar) Save() error {
if j.filename == "" {
return nil
}
return j.save(time.Now())
}
// MarshalJSON implements json.Marshaler by encoding all persistent cookies
// currently in the jar.
func (j *Jar) MarshalJSON() ([]byte, error) {
j.mu.Lock()
defer j.mu.Unlock()
// Marshaling entries can never fail.
data, _ := json.Marshal(j.allPersistentEntries())
return data, nil
}
// save is like Save but takes the current time as a parameter.
func (j *Jar) save(now time.Time) error {
locked, err := lockFile(lockFileName(j.filename))
if err != nil {
return err
}
defer locked.Close()
f, err := os.OpenFile(j.filename, os.O_RDWR|os.O_CREATE, 0o600)
if err != nil {
return err
}
defer f.Close()
// TODO optimization: if the file hasn't changed since we
// loaded it, don't bother with the merge step.
j.mu.Lock()
defer j.mu.Unlock()
if err := j.mergeFrom(f); err != nil {
// The cookie file is probably corrupt.
log.Printf("cannot read cookie file to merge it; ignoring it: %v", err)
}
j.deleteExpired(now)
if err := f.Truncate(0); err != nil {
return errors.Wrap(err, "cannot truncate file")
}
if _, err := f.Seek(0, 0); err != nil {
return err
}
return j.writeTo(f)
}
// load loads the cookies from j.filename. If the file does not exist,
// no error will be returned and no cookies will be loaded.
func (j *Jar) load() error {
if _, err := os.Stat(filepath.Dir(j.filename)); os.IsNotExist(err) {
// The directory that we'll store the cookie jar
// in doesn't exist, so don't bother trying
// to acquire the lock.
return nil
}
locked, err := lockFile(lockFileName(j.filename))
if err != nil {
return err
}
defer locked.Close()
f, err := os.Open(j.filename)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
defer f.Close()
if err := j.mergeFrom(f); err != nil {
return err
}
return nil
}
// mergeFrom reads all the cookies from r and stores them in the Jar.
func (j *Jar) mergeFrom(r io.Reader) error {
decoder := json.NewDecoder(r)
// Cope with old cookiejar format by just discarding
// cookies, but still return an error if it's invalid JSON.
var data json.RawMessage
if err := decoder.Decode(&data); err != nil {
if err == io.EOF {
// Empty file.
return nil
}
return err
}
var entries []entry
if err := json.Unmarshal(data, &entries); err != nil {
log.Printf("warning: discarding cookies in invalid format (error: %v)", err)
return nil
}
j.merge(entries)
return nil
}
// writeTo writes all the cookies in the jar to w
// as a JSON array.
func (j *Jar) writeTo(w io.Writer) error {
encoder := json.NewEncoder(w)
entries := j.allPersistentEntries()
if err := encoder.Encode(entries); err != nil {
return err
}
return nil
}
// allPersistentEntries returns all the entries in the jar, sorted by primarly by canonical host
// name and secondarily by path length.
func (j *Jar) allPersistentEntries() []entry {
var entries []entry
for _, submap := range j.entries {
for _, e := range submap {
if e.Persistent {
entries = append(entries, e)
}
}
}
sort.Sort(byCanonicalHost{entries})
return entries
}
func (j *Jar) KVData() map[string]string {
pairs := make(map[string]string)
entries := j.allPersistentEntries()
if len(entries) == 0 {
return pairs
}
for _, entry := range entries {
pairs[strings.ToLower(entry.Name)] = entry.Value
}
return pairs
}
// lockFileName returns the name of the lock file associated with
// the given path.
func lockFileName(path string) string {
return path + ".lock"
}
var attempt = retry.LimitTime(3*time.Second, retry.Exponential{
Initial: 100 * time.Microsecond,
Factor: 1.5,
MaxDelay: 100 * time.Millisecond,
})
func lockFile(path string) (io.Closer, error) {
for a := retry.Start(attempt, nil); a.Next(); {
locker, err := filelock.Lock(path)
if err == nil {
return locker, nil
}
if !a.More() {
return nil, errors.Wrap(err, "file locked for too long; giving up")
}
}
panic("unreachable")
}

Binary file not shown.

View File

@@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIENDCCAxygAwIBAgIURWPsWEo1vIT7J6pBAMk0yakdWcowDQYJKoZIhvcNAQEL
BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
Q0EwHhcNMjUwNDE0MTIwMTI2WhcNMzAwNDEzMTIwMTI2WjCBjTETMBEGA1UEAwwK
MTcwMjY0NDk0NzEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMTkwNwYDVQQL
DDDkvbPoioPvvIjljJfkuqzvvInkvIHkuJrnrqHnkIblkqjor6LmnInpmZDlhazl
j7gxCzAJBgNVBAYTAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAL4aNZ3BGiuBBfBnyi5l5bxSxSCOJJQ8oskcY5lA
RJrT3GbOb4NVlY0I8Qcm/PVsOZI1dWxBRZET/7IzBJ9759qrR3gFmLDW54VtHKPh
XD/HsHa9jSLzKjRXJOdZ0LpBlFz5X51u48kzU6T5B/bKD41mHPde5Na9A6xwBz35
/dqPx+FclCVGY1vLvfrDSIO70RAW8+eRWzXT+VZHAgK/MRsQyrsPZJJdL+Vz+pLz
h4dhgKcfxvc0Y0K3uJ9Jc8l6wZP/6nAEqY95/pOUrSmOCqqIqW0/Tidh4/tSPPmv
y/8I95tQboi6G3cSMfziLhQKlwF0j+YPaskicn+OKIKM/u0CAwEAAaOBuTCBtjAJ
BgNVHRMEAjAAMAsGA1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGNoIGKoIGHhoGE
aHR0cDovL2V2Y2EuaXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2NybD9DQT0xQkQ0
MjIwRTUwREJDMDRCMDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNnPUhBQ0M0NzFC
NjU0MjJFMTJCMjdBOUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0GCSqGSIb3DQEB
CwUAA4IBAQC1sIAaLiXzhLJj0XzTFlCiJ3KPggLA4PnbNvzj6sma0ojx8mOHgfHb
hR216vGY0Ll9ZpbAYR9GdEuUWVawZ38Z4GJVFAOCr1pp6DqeM3A/dTk+V4vJawZz
85AtfL1/heU1xsW0AbyPrfDiMHMieHEDNvRvHjQmjZ42aRbHDdRzDH0TIt0paRPB
+ubwCmr947oMe01PWWvF8g032d6NxN4CTPuBuWnJG9OQOm2KQDb4z5GftiJnFbay
KB3WycuqEFbHXVFgn7jrc9+uX0oRE7+iIfGqpcfJrKD93lP2r9AZ6Oxhk3TaNFSQ
u+/uR1Lg1b6vIJqI8otjDH9j5QVLAj5k
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+GjWdwRorgQXw
Z8ouZeW8UsUgjiSUPKLJHGOZQESa09xmzm+DVZWNCPEHJvz1bDmSNXVsQUWRE/+y
MwSfe+faq0d4BZiw1ueFbRyj4Vw/x7B2vY0i8yo0VyTnWdC6QZRc+V+dbuPJM1Ok
+Qf2yg+NZhz3XuTWvQOscAc9+f3aj8fhXJQlRmNby736w0iDu9EQFvPnkVs10/lW
RwICvzEbEMq7D2SSXS/lc/qS84eHYYCnH8b3NGNCt7ifSXPJesGT/+pwBKmPef6T
lK0pjgqqiKltP04nYeP7Ujz5r8v/CPebUG6Iuht3EjH84i4UCpcBdI/mD2rJInJ/
jiiCjP7tAgMBAAECggEAVYVe94BGsKmTrWpT13m513X4/sNTi2iX5xehavExq+GB
trJKEnBvHgqWvBv7EsHESJVKJRBcJn8zucwf2UuZq5MATOtfnLahYzIJ/2PD52GD
bnepxb5VD0Tg8j9CmngkMYtyS1X2na48g+wQfCK8ymTUxSholH5l565iY6xSWn8r
SD/u/EBLv69i40uocG1hUUicrJZ1wc5T0ct3GpfiA1BfH462/dp6mROONdpwM8IT
ltRH4wjIc2nPgE7eNbXlHg+KkqyNNLA+BeN3yn001QwvP6Q0panuCTsVVlvEuGAY
RwXbu/0fHFbppIpgfr7AFGRWKTF66Peq3ozsG9jNgQKBgQDviSJxN2Mpdln4i5F3
74s8FMtZ5bY63RHHcvJ5/D9G1iDNHFgLJsgdrbAhLqBbqg73EsIT8TsPlAqKPKS8
EGKBg75MsMSYu7EmzIURV3Gy+Pou9jOkTUfQfblkiV+uJjWQPlBlfksL1bQnfSvZ
Pk1DCwGMb5DMDazAQLP9/wtLYQKBgQDLKz9YHF+wFsnfUjBQngDLCTkxrfxp8y84
s/z5IRZIEdfxmnaEeWJXYa0oeQumNLSVHrryvHm3vkBgKexN49TWUGIM3q54gi/R
FPXXJKarDEI7C86Th3g+3FPEez5v+CEncmlB9X3kBT0ZFROWD3HHaz2DUKPVmJe1
eUOtAN0LDQKBgCoulx8i5taFXgCz61EYoQdajhjtp/KjvZ7G8kZjEm2SBcK5DBQi
pzj6vjqJsHmT8AC4j+7dG055/oUresMXi5FNNvTgaC6RVvgDKifMo1wmFkCw4JU9
erkPetdmja/oUKRvJM9Kt0KFRq1xkIg4PXjh9krZ1sDoY5STkF7ZTA7hAoGAQhPv
xzV7Pac7wwFVK3MoKOD4FBtVRBRO4G9RsKk9OPVsuWyWbWGZRXhEPCyaSFVOAk37
WaVJJSSghWY9L9wQxh9gtHTcY99bs/HQP0fxWSJkjBW7+ymNR0ybhgTbeslF5zGD
4Gr6peW6SGUdeKnPRJ+xYvsgPgEiHmixRRxJyCUCgYEAoguVZdpDaRDZGGrTghwj
F4kMIyEczFeBZtK2JEGSLA6j8uj+oBZ26c6K4sh/Btc0l6IkiXijXbTaH87s52xZ
im8aIZZ9jDKUFxtjVUL0l9fjRsCLAvaBbWw3z4EdtOGuYlnhNCheeSd+/Lzqrb1q
pnTiwBHnQCMFFL/rNcz/Mmk=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,59 @@
package wechat
import (
"go.ipao.vip/atom/container"
"go.ipao.vip/atom/opt"
)
const DefaultPrefix = "WeChat"
func DefaultProvider() container.ProviderContainer {
return container.ProviderContainer{
Provider: Provide,
Options: []opt.Option{
opt.Prefix(DefaultPrefix),
},
}
}
func Provide(opts ...opt.Option) error {
o := opt.New(opts...)
var config Config
if err := o.UnmarshalConfig(&config); err != nil {
return err
}
return container.Container.Provide(func() (*Config, *Client, error) {
httpClient := DefaultClient
if config.DevMode {
httpClient = httpClient.DevMode()
}
return &config, New(
WithAppID(config.AppID),
WithAppSecret(config.AppSecret),
WithAESKey(config.EncodingAESKey),
WithToken(config.Token),
WithClient(httpClient),
), nil
}, o.DiOptions()...)
}
type Config struct {
AppID string
AppSecret string
Token string
EncodingAESKey string
DevMode bool
Pay *Pay
}
type Pay struct {
MchID string
SerialNo string
MechName string
NotifyURL string
ApiV3Key string
PrivateKey string
PublicKeyID string
PublicKey string
}

View File

@@ -0,0 +1,59 @@
package wechat
import "github.com/pkg/errors"
// -1 系统繁忙,此时请开发者稍候再试
// 0 请求成功
// 40001 AppSecret错误或者AppSecret不属于这个公众号请开发者确认AppSecret的正确性
// 40002 请确保grant_type字段值为client_credential
// 40164 调用接口的IP地址不在白名单中请在接口IP白名单中进行设置。
// 40243 AppSecret已被冻结请登录MP解冻后再次调用。
// 89503 此IP调用需要管理员确认,请联系管理员
// 89501 此IP正在等待管理员确认,请联系管理员
// 89506 24小时内该IP被管理员拒绝调用两次24小时内不可再使用该IP调用
// 89507 1小时内该IP被管理员拒绝调用一次1小时内不可再使用该IP调用
// 10003 redirect_uri域名与后台配置不一致
// 10004 此公众号被封禁
// 10005 此公众号并没有这些scope的权限
// 10006 必须关注此测试号
// 10009 操作太频繁了,请稍后重试
// 10010 scope不能为空
// 10011 redirect_uri不能为空
// 10012 appid不能为空
// 10013 state不能为空
// 10015 公众号未授权第三方平台,请检查授权状态
// 10016 不支持微信开放平台的Appid请使用公众号Appid
func translateError(errCode int, msg string) error {
if errCode == 0 {
return nil
}
errs := map[int]error{
0: nil,
-1: errors.New("系统繁忙,此时请开发者稍候再试"),
40001: errors.New("AppSecret错误或者AppSecret不属于这个公众号请开发者确认AppSecret的正确性"),
40002: errors.New("请确保grant_type字段值为client_credential"),
40164: errors.New("调用接口的IP地址不在白名单中请在接口IP白名单中进行设置"),
40243: errors.New("AppSecret已被冻结请登录MP解冻后再次调用"),
89503: errors.New("此IP调用需要管理员确认,请联系管理员"),
89501: errors.New("此IP正在等待管理员确认,请联系管理员"),
89506: errors.New("24小时内该IP被管理员拒绝调用两次24小时内不可再使用该IP调用"),
89507: errors.New("1小时内该IP被管理员拒绝调用一次1小时内不可再使用该IP调用"),
10003: errors.New("redirect_uri域名与后台配置不一致"),
10004: errors.New("此公众号被封禁"),
10005: errors.New("此公众号并没有这些scope的权限"),
10006: errors.New("必须关注此测试号"),
10009: errors.New("操作太频繁了,请稍后重试"),
10010: errors.New("scope不能为空"),
10011: errors.New("redirect_uri不能为空"),
10012: errors.New("appid不能为空"),
10013: errors.New("state不能为空"),
10015: errors.New("公众号未授权第三方平台,请检查授权状态"),
10016: errors.New("不支持微信开放平台的Appid请使用公众号Appid"),
}
if err, ok := errs[errCode]; ok {
return err
}
return errors.New(msg)
}

View File

@@ -0,0 +1,24 @@
package wechat
import (
"crypto/sha1"
"encoding/hex"
"math/rand"
)
// RandomString generate random size string
func randomString(size int) string {
// generate size string [0-9a-zA-Z]
const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
b := make([]byte, size)
for i := range b {
b[i] = chars[rand.Intn(len(chars))]
}
return string(b)
}
func hashSha1(input string) string {
h := sha1.New()
h.Write([]byte(input))
return hex.EncodeToString(h.Sum(nil))
}

View File

@@ -0,0 +1,76 @@
package wechat
import (
"net/url"
"github.com/imroc/req/v3"
)
type Options func(*Client)
func WithAppID(appID string) Options {
return func(we *Client) {
we.appID = appID
}
}
// WithAppSecret sets the app secret
func WithAppSecret(appSecret string) Options {
return func(we *Client) {
we.appSecret = appSecret
}
}
// WithToken sets the token
func WithToken(token string) Options {
return func(we *Client) {
we.token = token
}
}
// WithAESKey sets the AES key
func WithAESKey(aesKey string) Options {
return func(we *Client) {
we.aesKey = aesKey
}
}
// WithClient sets the http client
func WithClient(client *req.Client) Options {
return func(we *Client) {
we.client = client
}
}
type ScopeAuthorizeURLOptions func(url.Values)
func ScopeAuthorizeURLWithScope(scope AuthScope) ScopeAuthorizeURLOptions {
return func(v url.Values) {
v.Set("scope", scope.String())
}
}
func ScopeAuthorizeURLWithRedirectURI(uri string) ScopeAuthorizeURLOptions {
return func(v url.Values) {
v.Set("redirect_uri", uri)
}
}
func ScopeAuthorizeURLWithState(state string) ScopeAuthorizeURLOptions {
return func(v url.Values) {
v.Set("state", state)
}
}
func ScopeAuthorizeURLWithForcePopup() ScopeAuthorizeURLOptions {
return func(v url.Values) {
v.Set("forcePopup", "true")
}
}
func WithVerifySiteKeyPair(key, value string) Options {
return func(we *Client) {
we.verifyKey = key
we.verifyValue = value
}
}

View File

@@ -0,0 +1,17 @@
package wechat
type ErrorResponse struct {
ErrCode int `json:"errcode,omitempty"`
ErrMsg string `json:"errmsg,omitempty"`
}
func (r *ErrorResponse) Error() error {
return translateError(r.ErrCode, r.ErrMsg)
}
type AccessTokenResponse struct {
ErrorResponse
AccessToken string `json:"access_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int `json:"expires_in,omitempty"` // seconds
}

View File

@@ -0,0 +1,339 @@
package wechat
import (
"crypto/sha1"
"encoding/hex"
"fmt"
"net/url"
"sort"
"strings"
"time"
"quyun/v2/pkg/oauth"
"github.com/imroc/req/v3"
"github.com/pkg/errors"
)
const BaseURL = "https://api.weixin.qq.com/"
var DefaultClient = req.
NewClient().
SetBaseURL(BaseURL).
SetCommonHeader("Content-Type", "application/json")
const (
ScopeBase = "snsapi_base"
ScopeUserInfo = "snsapi_userinfo"
)
type AuthScope string
func (s AuthScope) String() string {
return string(s)
}
type Client struct {
client *req.Client
appID string
appSecret string
token string
aesKey string
verifyKey string
verifyValue string
}
func New(options ...Options) *Client {
we := &Client{
client: DefaultClient,
}
for _, opt := range options {
opt(we)
}
return we
}
func (we *Client) VerifySite(key string) (string, error) {
if key == we.verifyKey {
return we.verifyValue, nil
}
return "", errors.New("verify failed")
}
func (we *Client) Verify(signature, timestamp, nonce string) error {
params := []string{signature, timestamp, nonce, we.token}
sort.Strings(params)
str := strings.Join(params, "")
hash := sha1.Sum([]byte(str))
hashStr := hex.EncodeToString(hash[:])
if hashStr == signature {
return errors.New("Signature verification failed")
}
return nil
}
func (we *Client) wrapParams(params map[string]string) map[string]string {
if params == nil {
params = make(map[string]string)
}
params["appid"] = we.appID
params["secret"] = we.appSecret
return params
}
// RefreshAccessToken
func (we *Client) RefreshAccessToken(refreshToken string) (*AccessTokenResponse, error) {
params := we.wrapParams(map[string]string{
"grant_type": "refresh_token",
"refresh_token": refreshToken,
})
var data AccessTokenResponse
_, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/sns/oauth2/refresh_token")
if err != nil {
return nil, errors.Wrap(err, "call /sns/oauth2/refresh_token failed")
}
if data.ErrCode != 0 {
return nil, data.Error()
}
return &data, nil
}
func (we *Client) GetAccessToken() (*AccessTokenResponse, error) {
params := map[string]string{
"grant_type": "client_credential",
}
var data ErrorResponse
resp, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/cgi-bin/token")
if err != nil {
return nil, errors.Wrap(err, "call /cgi-bin/token failed")
}
if data.ErrCode != 0 {
return nil, data.Error()
}
var token AccessTokenResponse
if err := resp.Unmarshal(&token); err != nil {
return nil, errors.Wrap(err, "parse response failed")
}
return &token, nil
}
// ScopeAuthorizeURL
func (we *Client) ScopeAuthorizeURL(opts ...ScopeAuthorizeURLOptions) (*url.URL, error) {
params := url.Values{}
params.Add("appid", we.appID)
params.Add("response_type", "code")
for _, opt := range opts {
opt(params)
}
if params.Get("scope") == "" {
params.Add("scope", ScopeBase)
}
u, err := url.Parse("https://open.weixin.qq.com/connect/oauth2/authorize")
if err != nil {
return nil, errors.Wrap(err, "parse url failed")
}
u.Fragment = "wechat_redirect"
u.RawQuery = url.Values(params).Encode()
return u, nil
}
var _ oauth.OAuthInfo = (*AuthorizeAccessToken)(nil)
type AuthorizeAccessToken struct {
ErrorResponse
AccessToken string `json:"access_token,omitempty"`
ExpiresIn int64 `json:"expires_in,omitempty"`
IsSnapshotuser int64 `json:"is_snapshotuser,omitempty"`
Openid string `json:"openid,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
Scope string `json:"scope,omitempty"`
Unionid string `json:"unionid,omitempty"`
}
// GetAccessToken implements oauth.OAuthInfo.
func (a *AuthorizeAccessToken) GetAccessToken() string {
return a.AccessToken
}
// GetExpiredAt implements oauth.OAuthInfo.
func (a *AuthorizeAccessToken) GetExpiredAt() time.Time {
return time.Now().Add(time.Duration(a.ExpiresIn) * time.Second)
}
// GetOpenID implements oauth.OAuthInfo.
func (a *AuthorizeAccessToken) GetOpenID() string {
return a.Openid
}
// GetRefreshToken implements oauth.OAuthInfo.
func (a *AuthorizeAccessToken) GetRefreshToken() string {
return a.RefreshToken
}
// GetUnionID implements oauth.OAuthInfo.
func (a *AuthorizeAccessToken) GetUnionID() string {
return a.Unionid
}
type StableAccessToken struct {
AccessToken string `json:"access_token,omitempty"`
ExpiresIn int64 `json:"expires_in,omitempty"`
}
func (we *Client) GetStableAccessToken() (*StableAccessToken, error) {
params := we.wrapParams(map[string]string{
"grant_type": "client_credential",
})
var data StableAccessToken
_, err := we.client.R().SetSuccessResult(&data).SetBodyJsonMarshal(params).Post("/cgi-bin/stable_token")
if err != nil {
return nil, errors.Wrap(err, "call /cgi-bin/stable_token failed")
}
return &data, nil
}
func (we *Client) AuthorizeCode2Token(code string) (*AuthorizeAccessToken, error) {
params := we.wrapParams(map[string]string{
"code": code,
"grant_type": "authorization_code",
})
var data AuthorizeAccessToken
_, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/sns/oauth2/access_token")
if err != nil {
return nil, errors.Wrap(err, "call /sns/oauth2/access_token failed")
}
if err := data.Error(); err != nil {
return nil, err
}
return &data, nil
}
func (we *Client) AuthorizeRefreshAccessToken(accessToken string) (*AuthorizeAccessToken, error) {
params := we.wrapParams(map[string]string{
"refresh_token": accessToken,
"grant_type": "refresh_token",
})
var data AuthorizeAccessToken
_, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/sns/oauth2/refresh_token")
if err != nil {
return nil, errors.Wrap(err, "call /sns/oauth2/refresh_token failed")
}
if err := data.Error(); err != nil {
return nil, err
}
return &data, nil
}
type AuthorizeUserInfo struct {
ErrorResponse
City string `json:"city,omitempty"`
Country string `json:"country,omitempty"`
Headimgurl string `json:"headimgurl,omitempty"`
Nickname string `json:"nickname,omitempty"`
Openid string `json:"openid,omitempty"`
Privilege []string `json:"privilege,omitempty"`
Province string `json:"province,omitempty"`
Sex int64 `json:"sex,omitempty"`
Unionid string `json:"unionid,omitempty"`
}
func (we *Client) AuthorizeUserInfo(accessToken, openID string) (*AuthorizeUserInfo, error) {
params := (map[string]string{
"access_token": accessToken,
"openid": openID,
})
var data AuthorizeUserInfo
_, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/sns/userinfo")
if err != nil {
return nil, errors.Wrap(err, "call /sns/userinfo failed")
}
if err := data.Error(); err != nil {
return nil, err
}
return &data, nil
}
// GetJSTicket
func (we *Client) GetJSTicket(token string) (string, error) {
var data struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
Ticket string `json:"ticket"`
ExpiresIn int `json:"expires_in"`
}
params := map[string]string{
"access_token": token,
"type": "jsapi",
}
_, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/cgi-bin/ticket/getticket")
if err != nil {
return "", errors.Wrap(err, "call /cgi-bin/ticket/getticket failed")
}
if data.Errcode != 0 {
return "", errors.New("get wechat ticket failed: " + data.Errmsg)
}
return data.Ticket, nil
}
type JsSDK struct {
Debug bool `json:"debug"`
AppID string `json:"appId"`
Timestamp int64 `json:"timestamp"`
NonceStr string `json:"nonceStr"`
Signature string `json:"signature"`
}
// GetJSTicket
func (we *Client) GetJsSDK(token, url string) (*JsSDK, error) {
sdk := &JsSDK{
Debug: false,
AppID: we.appID,
Timestamp: time.Now().Unix(),
NonceStr: randomString(16),
Signature: "",
}
// get ticket
ticket, err := we.GetJSTicket(token)
if err != nil {
return nil, errors.Wrap(err, "get wechat ticket failed")
}
input := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s&timestamp=%d&url=%s", ticket, sdk.NonceStr, sdk.Timestamp, url)
sdk.Signature = hashSha1(input)
return sdk, nil
}

View File

@@ -0,0 +1,107 @@
package wechat
import (
"testing"
log "github.com/sirupsen/logrus"
. "github.com/smartystreets/goconvey/convey"
)
const (
WechatAppID = "wx45745a8c51091ae0"
WechatAppSecret = "2ab33bc79d9b47efa4abef19d66e1977"
WechatToken = "W8Xhw5TivYBgY"
WechatAesKey = "F6AqCxAV4W1eCrY6llJ2zapphKK49CQN3RgtPDrjhnI"
)
func init() {
log.SetLevel(log.DebugLevel)
}
func getClient() *Client {
return New(
WithAppID(WechatAppID),
WithAppSecret(WechatAppSecret),
WithAESKey(WechatAesKey),
WithToken(WechatToken),
WithClient(DefaultClient.DevMode()),
)
}
func TestWechatClient_GetAccessToken(t *testing.T) {
Convey("Test GetAccessToken", t, func() {
token, err := getClient().GetAccessToken()
So(err, ShouldBeNil)
So(token.AccessToken, ShouldNotBeEmpty)
So(token.ExpiresIn, ShouldBeGreaterThan, 0)
t.Log("Access Token:", token.AccessToken)
})
}
func TestClient_ScopeAuthorizeURL(t *testing.T) {
Convey("Test ScopeAuthorizeURL", t, func() {
url, err := getClient().ScopeAuthorizeURL(
ScopeAuthorizeURLWithScope(ScopeBase),
ScopeAuthorizeURLWithRedirectURI("https://qvyun.mp.jdwan.com/"),
)
So(err, ShouldBeNil)
So(url, ShouldNotBeEmpty)
t.Log("URL:", url)
})
}
func TestClient_AuthorizeCode2Token(t *testing.T) {
code := "011W1sll2Xv4Ae4OjUnl2I7jvd2W1slX"
Convey("Test AuthorizeCode2Token", t, func() {
token, err := getClient().AuthorizeCode2Token(code)
So(err, ShouldBeNil)
t.Logf("token: %+v", token)
})
}
func TestClient_AuthorizeRefreshAccessToken(t *testing.T) {
token := "86_m_EAHq0RKlo6RzzGAsY8gVmiCqHqIiAJufxhm8mK8imyIW6yoE4NTcIr2vaukp7dexPWId0JWP1iZWYaLpXT_MJv1N7YQW8Qt3zOZDpJY90"
Convey("Test AuthorizeCode2Token", t, func() {
token, err := getClient().AuthorizeRefreshAccessToken(token)
So(err, ShouldBeNil)
t.Logf("token: %+v", token)
})
}
func TestClient_AuthorizeUserInfo(t *testing.T) {
token := "86_ZxJa8mIwbml5mDlHHbIUle_UKW8LA75nOuB0wqiome8AX5LlMWU8JwRKMZykdLEjDnKX8EJavz5GeQn3T1ot7TwpULp8imQvNIgFIjC4er8"
openID := "oMLa5tyJ2vRHa-HI4CMEkHztq3eU"
Convey("Test AuthorizeUserInfo", t, func() {
user, err := getClient().AuthorizeUserInfo(token, openID)
So(err, ShouldBeNil)
t.Logf("user: %+v", user)
})
}
func Test_GetJsTicket(t *testing.T) {
Convey("Test GetJsTicket", t, func() {
token := "91_0pKuAiBFquPdLakDyhYqOyNJkGLr7-Egx-IF4bRzw-2Lpm7wxgz6zVBNJ36FvMXmiu8bz9BTtspVICf1zDZ3XWuVLwTq6T3a6WG1k6NHv6E0PadT-G5x2Y85-xUECBcADATRQ"
ticket, err := getClient().GetJSTicket(token)
So(err, ShouldBeNil)
So(ticket, ShouldNotBeEmpty)
t.Log("Js Ticket:", ticket)
})
}
func Test_GetStableToken(t *testing.T) {
Convey("Test_GetStableToken GetJsTicket", t, func() {
token, err := getClient().GetStableAccessToken()
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
t.Logf("Stable Token: %+v", token)
})
}

View File

@@ -0,0 +1,58 @@
package wepay
import (
"time"
"go.ipao.vip/atom/container"
"go.ipao.vip/atom/opt"
)
const DefaultPrefix = "WePay"
func DefaultProvider() container.ProviderContainer {
return container.ProviderContainer{
Provider: Provide,
Options: []opt.Option{
opt.Prefix(DefaultPrefix),
},
}
}
type PayNotify struct {
Mchid string `json:"mchid"`
Appid string `json:"appid"`
OutTradeNo string `json:"out_trade_no"`
TransactionID string `json:"transaction_id"`
TradeType string `json:"trade_type"`
TradeState string `json:"trade_state"`
TradeStateDesc string `json:"trade_state_desc"`
BankType string `json:"bank_type"`
Attach string `json:"attach"`
SuccessTime time.Time `json:"success_time"`
Payer struct {
Openid string `json:"openid"`
} `json:"payer"`
Amount struct {
Total int64 `json:"total"`
PayerTotal int64 `json:"payer_total"`
Currency string `json:"currency"`
PayerCurrency string `json:"payer_currency"`
} `json:"amount"`
}
type RefundNotify struct {
Mchid string `json:"mchid"`
TransactionID string `json:"transaction_id"`
OutTradeNo string `json:"out_trade_no"`
RefundID string `json:"refund_id"`
OutRefundNo string `json:"out_refund_no"`
RefundStatus string `json:"refund_status"`
SuccessTime time.Time `json:"success_time"`
UserReceivedAccount string `json:"user_received_account"`
Amount struct {
Total int `json:"total"`
Refund int `json:"refund"`
PayerTotal int `json:"payer_total"`
PayerRefund int `json:"payer_refund"`
} `json:"amount"`
}

View File

@@ -0,0 +1,330 @@
package wepay
import (
"context"
"crypto/rsa"
"encoding/json"
"fmt"
"net/http"
"time"
w "quyun/v2/providers/wechat"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/wechat/v3"
"github.com/go-pay/util/js"
"github.com/gofiber/fiber/v3"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"go.ipao.vip/atom/container"
"go.ipao.vip/atom/opt"
)
type Config struct{}
func Provide(opts ...opt.Option) error {
o := opt.New(opts...)
var config Config
if err := o.UnmarshalConfig(&config); err != nil {
return err
}
return container.Container.Provide(func(wechatConfig *w.Config) (*Client, error) {
client, err := wechat.NewClientV3(
wechatConfig.Pay.MchID,
wechatConfig.Pay.SerialNo,
wechatConfig.Pay.ApiV3Key,
wechatConfig.Pay.PrivateKey,
)
if err != nil {
return nil, err
}
client.DebugSwitch = gopay.DebugOff
if wechatConfig.DevMode {
client.DebugSwitch = gopay.DebugOn
}
err = client.AutoVerifySignByPublicKey([]byte(wechatConfig.Pay.PublicKey), wechatConfig.Pay.PublicKeyID)
if err != nil {
return nil, errors.Wrap(err, "AutoVerifySignByPublicKey")
}
return &Client{
payClient: client,
config: wechatConfig,
}, nil
}, o.DiOptions()...)
}
type Client struct {
payClient *wechat.ClientV3
config *w.Config
}
func (c *Client) GetClient() *wechat.ClientV3 {
return c.payClient
}
// WxPublicKeyMap
func (c *Client) WxPublicKeyMap() map[string]*rsa.PublicKey {
return c.payClient.WxPublicKeyMap()
}
type PrepayData struct {
client *Client
AppID string `json:"app_id"`
PrepayID string `json:"prepay_id"`
}
// PaySignOfJSAPI
func (pay *PrepayData) PaySignOfJSAPI() (*wechat.JSAPIPayParams, error) {
return pay.client.payClient.PaySignOfJSAPI(pay.AppID, pay.PrepayID)
}
func (c *Client) Refund(ctx context.Context, f func(*BodyMap)) (*wechat.RefundOrderResponse, error) {
bm := NewRefundBodyMap(c.config)
f(bm)
resp, err := c.payClient.V3Refund(ctx, bm.bm)
if err != nil {
return nil, err
}
if resp.Code != wechat.Success {
log.Errorf("WePay Refund error: %s", resp.Error)
return nil, errors.New(resp.Error)
}
return resp.Response, nil
}
func (c *Client) V3TransactionJsapi(ctx context.Context, f func(*BodyMap)) (*PrepayData, error) {
bm := NewBodyMap(c.config)
f(bm)
resp, err := c.payClient.V3TransactionJsapi(ctx, bm.bm)
if err != nil {
return nil, err
}
if resp.Code != wechat.Success {
b, _ := json.Marshal(resp)
log.Errorf("WePay V3TransactionJsapi error: %s", b)
return nil, errors.New(resp.Error)
}
return &PrepayData{
client: c,
AppID: c.config.AppID,
PrepayID: resp.Response.PrepayId,
}, nil
}
func (c *Client) ParseNotify(
ctx fiber.Ctx,
payCallback func(fiber.Ctx, *wechat.V3DecryptPayResult) error,
refundCallback func(fiber.Ctx, *wechat.V3DecryptRefundResult) error,
) error {
body := ctx.Body()
si := &wechat.SignInfo{
HeaderTimestamp: ctx.Get(wechat.HeaderTimestamp),
HeaderNonce: ctx.Get(wechat.HeaderNonce),
HeaderSignature: ctx.Get(wechat.HeaderSignature),
HeaderSerial: ctx.Get(wechat.HeaderSerial),
SignBody: string(body),
}
notifyReq := &wechat.V3NotifyReq{SignInfo: si}
if err := js.UnmarshalBytes(body, notifyReq); err != nil {
log.Errorf("json unmarshal error:%v", err)
return ctx.Status(http.StatusBadRequest).JSON(fiber.Map{"error": fmt.Sprintf("json unmarshal error:%v", err)})
}
// 获取微信平台证书
certMap := c.WxPublicKeyMap()
// 验证异步通知的签名
if err := notifyReq.VerifySignByPKMap(certMap); err != nil {
log.Errorf("verify sign error:%v", err)
return ctx.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "Invalid signature"})
}
// TRANSACTION.SUCCESS :支付成功通知
// REFUND.SUCCESS退款成功通知
// REFUND.ABNORMAL退款异常通知
// REFUND.CLOSED退款关闭通知
switch notifyReq.EventType {
case "TRANSACTION.SUCCESS":
var notifyData wechat.V3DecryptPayResult
if err := notifyReq.DecryptCipherTextToStruct(c.config.Pay.ApiV3Key, &notifyData); err != nil {
return ctx.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "Invalid cipher text"})
}
log.Infof("Successfully decrypted cipher text for pay notify data: %+v", notifyData)
if err := payCallback(ctx, &notifyData); err != nil {
log.Errorf("payCallback error:%v", err)
return err
}
case "REFUND.SUCCESS", "REFUND.ABNORMAL", "REFUND.CLOSED":
var notifyData wechat.V3DecryptRefundResult
if err := notifyReq.DecryptCipherTextToStruct(c.config.Pay.ApiV3Key, &notifyData); err != nil {
return ctx.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "Invalid cipher text"})
}
log.Infof("Successfully decrypted cipher text for refund notify data: %+v", notifyData)
if err := refundCallback(ctx, &notifyData); err != nil {
log.Errorf("refundCallback error:%v", err)
return err
}
}
return ctx.Status(http.StatusOK).JSON(&wechat.V3NotifyRsp{
Code: gopay.SUCCESS,
Message: "成功",
})
}
type BodyMap struct {
bm gopay.BodyMap
}
func NewRefundBodyMap(c *w.Config) *BodyMap {
bm := make(gopay.BodyMap)
bm.Set("notify_url", c.Pay.NotifyURL)
return &BodyMap{
bm: bm,
}
}
func NewBodyMap(c *w.Config) *BodyMap {
bm := make(gopay.BodyMap)
bm.Set("appid", c.AppID).
Set("mchid", c.Pay.MchID).
Set("notify_url", c.Pay.NotifyURL)
// .
// SetBodyMap("amount", func(bm gopay.BodyMap) {
// bm.Set("total", 1).
// Set("currency", "CNY")
// })
return &BodyMap{
bm: bm,
}
}
func (b *BodyMap) Set(key string, value interface{}) *BodyMap {
b.bm.Set(key, value)
return b
}
func (b *BodyMap) SetBodyMap(key string, f func(bm gopay.BodyMap)) *BodyMap {
b.bm.SetBodyMap(key, f)
return b
}
// Expire time
func (b *BodyMap) Expire(t time.Duration) *BodyMap {
return b.Set("time_expire", time.Now().Add(t).Format(time.RFC3339))
}
// Description
func (b *BodyMap) Description(desc string) *BodyMap {
return b.Set("description", desc)
}
// OutTradeNo
func (b *BodyMap) OutTradeNo(outTradeNo string) *BodyMap {
return b.Set("out_trade_no", outTradeNo)
}
// TransactionID
func (b *BodyMap) TransactionID(transactionID string) *BodyMap {
return b.Set("transaction_id", transactionID)
}
// OutRefundNo
func (b *BodyMap) OutRefundNo(outRefundNo string) *BodyMap {
return b.Set("out_refund_no", outRefundNo)
}
// RefundReason
func (b *BodyMap) RefundReason(refundReason string) *BodyMap {
return b.Set("reason", refundReason)
}
// RefundAmount
func (b *BodyMap) RefundAmount(total, refund int64, currency CURRENCY) *BodyMap {
return b.SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.
Set("total", total).
Set("refund", refund).
Set("currency", currency.String())
})
}
func (b *BodyMap) CNYRefundAmount(total, refund int64) *BodyMap {
return b.RefundAmount(total, refund, CNY)
}
type RefundGoodsInfo struct {
MerchantGoodsID string `json:"merchant_goods_id"`
GoodsName string `json:"goods_name"`
RefundQuantity int64 `json:"refund_quantity"`
RefundAmount int64 `json:"refund_amount"`
UnitPrice int64 `json:"unit_price"`
}
// RefundGoodsInfo
func (b *BodyMap) RefundGoods(goods []RefundGoodsInfo) *BodyMap {
return b.Set("goods_detail", goods)
}
// Amount
func (b *BodyMap) Amount(total int64, currency CURRENCY) *BodyMap {
return b.SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.
Set("total", total).
Set("currency", currency.String())
})
}
func (b *BodyMap) CNYAmount(total int64) *BodyMap {
return b.Amount(total, CNY)
}
type GoodsInfo struct {
MerchantGoodsID string `json:"merchant_goods_id"`
GoodsName string `json:"goods_name"`
Quantity int64 `json:"quantity"`
UnitPrice int64 `json:"unit_price"`
}
func (b *BodyMap) Detail(goods []GoodsInfo) *BodyMap {
return b.SetBodyMap("detail", func(bm gopay.BodyMap) {
bm.Set("goods_detail", goods)
})
}
// Payer
func (b *BodyMap) Payer(spOpenId string) *BodyMap {
return b.SetBodyMap("payer", func(bm gopay.BodyMap) {
bm.Set("openid", spOpenId)
})
}
// SubMchId
func (b *BodyMap) SubMchId(subMchId string) *BodyMap {
return b.Set("sub_mchid", subMchId)
}
type CURRENCY string
func (c CURRENCY) String() string {
return string(c)
}
const (
CNY CURRENCY = "CNY"
USD CURRENCY = "USD"
EUR CURRENCY = "EUR"
)

View File

@@ -0,0 +1,76 @@
package wepay
import (
"context"
"fmt"
"testing"
"time"
"quyun/app/service/testx"
"github.com/go-pay/gopay/wechat/v3"
"github.com/go-pay/util/js"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/suite"
"go.ipao.vip/atom/contracts"
"go.uber.org/dig"
)
type WePayInjectParams struct {
dig.In
Initials []contracts.Initial `group:"initials"`
Client *Client
}
type WePayTestSuite struct {
suite.Suite
WePayInjectParams
}
func Test_WePay(t *testing.T) {
providers := testx.Default().With(Provide)
testx.Serve(providers, t, func(params WePayInjectParams) {
suite.Run(t, &WePayTestSuite{WePayInjectParams: params})
})
}
func (s *WePayTestSuite) Test_PrePay() {
Convey("get prepay", s.T(), func() {
Convey("prepay", func() {
resp, err := s.Client.V3TransactionJsapi(context.Background(), func(bm *BodyMap) {
bm.
OutTradeNo(fmt.Sprintf("test_trade_no_%d", time.Now().Unix())).
Description("Test transaction").
Payer("o5Bzk644x3LOMJsKSZRlqWin74IU")
})
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
s.T().Logf("prepay response: %+v", resp)
sign, err := resp.PaySignOfJSAPI()
So(err, ShouldBeNil)
s.T().Logf("Sign: %+v", sign)
})
})
}
func (s *WePayTestSuite) Test_parseNotify() {
Convey("parse notify", s.T(), func() {
Convey("prepay", func() {
content := `{"id":"43d17a94-eb1e-5641-bb11-f59e5b6e8749","summary":"支付成功","resource":{"nonce":"avbpSc2seCN5","algorithm":"AEAD_AES_256_GCM","ciphertext":"20VGA2uItmbqFvGBxBug2K3eORRyy/xYswoDA7v4+Yi2ArHnXCXzScVn6kD3ZVpKLiFY7zcTPpTxk2JFJF3vG/6WGG7uuD8DDK7keJk0PZoAfvmSPskQzieOVz3Tgmqp3SkE74mJHX1MeMZHMXMmzMJ4Mp1OmYD2YpiWsF7jlAtiGqxHSC//YlKGaJ/9r0QG4TwZcFpm+X4qkdBNX+DcSCjYeXGyWIm2bVujj63rO43DEA5x0nytdBSrpup/T85khZzNVue1EcyF5XY7PguePU3Q2o+e1c/LnoL9nN7S+n2ljm+nN3uCAhz8eqkPn4uowiq37Tw4JZ2rx2rXCb9jYKmt+I8JHpOij4SgX6oQd7fLeZHsbHC/05s0A1qdLzeF5AKgrAOQT/T1yQ+LsWTnY2ftXAP6mnqGE8Z+vQm5PGo8xsQ8AycVaAhwaRLFvn/XtwlkumfuduAojimFRSNElWwHcApnT+ekqzBrKnAvKo8hdeygf9QWHENcNWVwqwjUWIHe/fGWgJbc6u595bEHb4MkcI8ESD/6bpay/Wk6SyvZCJHqS1WWaPaU0xh9","original_type":"transaction","associated_data":"transaction"},"event_type":"TRANSACTION.SUCCESS","create_time":"2025-04-30T19:25:51+08:00","resource_type":"encrypt-resource"}`
var notifyReq wechat.V3NotifyReq
err := js.UnmarshalBytes([]byte(content), &notifyReq)
So(err, ShouldBeNil)
s.T().Logf("notifyReq: %+v", notifyReq)
var obj struct{}
err = notifyReq.DecryptCipherTextToStruct("5UBDkxVDY44AKafkqN6YgYxgtkXP6Mw6", &obj)
So(err, ShouldBeNil)
s.T().Logf("Decrypted object: %+v", obj)
})
})
}