Merge branch 'emdash/wechat-official-account-payment-integration-30g'
# Conflicts: # README.md # cmd/api/main.go # internal/bootstrap/dependencies.go # pkg/config/config.go # pkg/config/defaults/config.yaml
This commit is contained in:
@@ -23,7 +23,11 @@ type Config struct {
|
||||
JWT JWTConfig `mapstructure:"jwt"`
|
||||
DefaultAdmin DefaultAdminConfig `mapstructure:"default_admin"`
|
||||
Storage StorageConfig `mapstructure:"storage"`
|
||||
<<<<<<< HEAD
|
||||
Gateway GatewayConfig `mapstructure:"gateway"`
|
||||
=======
|
||||
Wechat WechatConfig `mapstructure:"wechat"`
|
||||
>>>>>>> emdash/wechat-official-account-payment-integration-30g
|
||||
}
|
||||
|
||||
// ServerConfig HTTP 服务器配置
|
||||
@@ -156,6 +160,35 @@ type PresignConfig struct {
|
||||
DownloadExpires time.Duration `mapstructure:"download_expires"` // 下载 URL 有效期(默认:24h)
|
||||
}
|
||||
|
||||
// WechatConfig 微信配置
|
||||
type WechatConfig struct {
|
||||
OfficialAccount OfficialAccountConfig `mapstructure:"official_account"`
|
||||
Payment PaymentConfig `mapstructure:"payment"`
|
||||
}
|
||||
|
||||
// OfficialAccountConfig 微信公众号配置
|
||||
type OfficialAccountConfig struct {
|
||||
AppID string `mapstructure:"app_id"`
|
||||
AppSecret string `mapstructure:"app_secret"`
|
||||
Token string `mapstructure:"token"`
|
||||
AESKey string `mapstructure:"aes_key"`
|
||||
OAuthRedirectURL string `mapstructure:"oauth_redirect_url"`
|
||||
}
|
||||
|
||||
// PaymentConfig 微信支付配置
|
||||
type PaymentConfig struct {
|
||||
AppID string `mapstructure:"app_id"`
|
||||
MchID string `mapstructure:"mch_id"`
|
||||
APIV3Key string `mapstructure:"api_v3_key"`
|
||||
APIV2Key string `mapstructure:"api_v2_key"`
|
||||
CertPath string `mapstructure:"cert_path"`
|
||||
KeyPath string `mapstructure:"key_path"`
|
||||
SerialNo string `mapstructure:"serial_no"`
|
||||
NotifyURL string `mapstructure:"notify_url"`
|
||||
HttpDebug bool `mapstructure:"http_debug"`
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
}
|
||||
|
||||
type requiredField struct {
|
||||
value string
|
||||
name string
|
||||
|
||||
@@ -105,9 +105,31 @@ default_admin:
|
||||
password: ""
|
||||
phone: ""
|
||||
|
||||
<<<<<<< HEAD
|
||||
# Gateway 服务配置
|
||||
gateway:
|
||||
base_url: "https://lplan.whjhft.com/openapi"
|
||||
app_id: "60bgt1X8i7AvXqkd"
|
||||
app_secret: "BZeQttaZQt0i73moF"
|
||||
timeout: 30
|
||||
=======
|
||||
# 微信配置(必填项需通过环境变量设置)
|
||||
wechat:
|
||||
official_account:
|
||||
app_id: "" # 必填:JUNHONG_WECHAT_OFFICIAL_ACCOUNT_APP_ID
|
||||
app_secret: "" # 必填:JUNHONG_WECHAT_OFFICIAL_ACCOUNT_APP_SECRET(敏感)
|
||||
token: "" # 可选:JUNHONG_WECHAT_OFFICIAL_ACCOUNT_TOKEN
|
||||
aes_key: "" # 可选:JUNHONG_WECHAT_OFFICIAL_ACCOUNT_AES_KEY(敏感)
|
||||
oauth_redirect_url: "" # 可选:JUNHONG_WECHAT_OFFICIAL_ACCOUNT_OAUTH_REDIRECT_URL
|
||||
payment:
|
||||
app_id: "" # 必填:JUNHONG_WECHAT_PAYMENT_APP_ID
|
||||
mch_id: "" # 必填:JUNHONG_WECHAT_PAYMENT_MCH_ID
|
||||
api_v3_key: "" # 必填:JUNHONG_WECHAT_PAYMENT_API_V3_KEY(敏感)
|
||||
api_v2_key: "" # 可选:JUNHONG_WECHAT_PAYMENT_API_V2_KEY(敏感)
|
||||
cert_path: "" # 必填:JUNHONG_WECHAT_PAYMENT_CERT_PATH(证书文件路径)
|
||||
key_path: "" # 必填:JUNHONG_WECHAT_PAYMENT_KEY_PATH(私钥文件路径)
|
||||
serial_no: "" # 必填:JUNHONG_WECHAT_PAYMENT_SERIAL_NO
|
||||
notify_url: "" # 必填:JUNHONG_WECHAT_PAYMENT_NOTIFY_URL
|
||||
http_debug: false
|
||||
timeout: "30s"
|
||||
>>>>>>> emdash/wechat-official-account-payment-integration-30g
|
||||
|
||||
@@ -39,10 +39,14 @@ const (
|
||||
CodePermAlreadyAssigned = 1027 // 权限已分配
|
||||
|
||||
// 认证相关错误 (1040-1049)
|
||||
CodeInvalidCredentials = 1040 // 用户名或密码错误
|
||||
CodeAccountLocked = 1041 // 账号已锁定
|
||||
CodePasswordExpired = 1042 // 密码已过期
|
||||
CodeInvalidOldPassword = 1043 // 旧密码错误
|
||||
CodeInvalidCredentials = 1040 // 用户名或密码错误
|
||||
CodeAccountLocked = 1041 // 账号已锁定
|
||||
CodePasswordExpired = 1042 // 密码已过期
|
||||
CodeInvalidOldPassword = 1043 // 旧密码错误
|
||||
CodeWechatOAuthFailed = 1044 // 微信 OAuth 授权失败
|
||||
CodeWechatUserInfoFailed = 1045 // 获取微信用户信息失败
|
||||
CodeWechatPayFailed = 1046 // 微信支付发起失败
|
||||
CodeWechatCallbackInvalid = 1047 // 微信回调签名验证失败
|
||||
|
||||
// 组织相关错误 (1030-1049)
|
||||
CodeShopNotFound = 1030 // 店铺不存在
|
||||
@@ -150,6 +154,10 @@ var allErrorCodes = []int{
|
||||
CodeAccountLocked,
|
||||
CodePasswordExpired,
|
||||
CodeInvalidOldPassword,
|
||||
CodeWechatOAuthFailed,
|
||||
CodeWechatUserInfoFailed,
|
||||
CodeWechatPayFailed,
|
||||
CodeWechatCallbackInvalid,
|
||||
CodeInvalidStatus,
|
||||
CodeInsufficientBalance,
|
||||
CodeWithdrawalNotFound,
|
||||
@@ -279,6 +287,10 @@ var errorMessages = map[int]string{
|
||||
CodeAccountLocked: "账号已锁定",
|
||||
CodePasswordExpired: "密码已过期",
|
||||
CodeInvalidOldPassword: "旧密码错误",
|
||||
CodeWechatOAuthFailed: "微信授权失败",
|
||||
CodeWechatUserInfoFailed: "获取微信用户信息失败",
|
||||
CodeWechatPayFailed: "微信支付发起失败",
|
||||
CodeWechatCallbackInvalid: "微信回调验证失败",
|
||||
CodeInternalError: "内部服务器错误",
|
||||
CodeDatabaseError: "数据库错误",
|
||||
CodeRedisError: "缓存服务错误",
|
||||
|
||||
@@ -44,6 +44,6 @@ func BuildDocHandlers() *bootstrap.Handlers {
|
||||
ShopPackageBatchPricing: admin.NewShopPackageBatchPricingHandler(nil),
|
||||
AdminOrder: admin.NewOrderHandler(nil),
|
||||
H5Order: h5.NewOrderHandler(nil),
|
||||
PaymentCallback: callback.NewPaymentHandler(nil),
|
||||
PaymentCallback: callback.NewPaymentHandler(nil, nil),
|
||||
}
|
||||
}
|
||||
|
||||
85
pkg/wechat/config.go
Normal file
85
pkg/wechat/config.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ArtisanCloud/PowerWeChat/v3/src/kernel"
|
||||
"github.com/ArtisanCloud/PowerWeChat/v3/src/officialAccount"
|
||||
"github.com/ArtisanCloud/PowerWeChat/v3/src/payment"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/config"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// NewRedisCache 使用项目现有的 Redis 客户端创建 PowerWeChat 的 Redis Cache
|
||||
func NewRedisCache(rdb *redis.Client) kernel.CacheInterface {
|
||||
return kernel.NewRedisClient(&kernel.UniversalOptions{
|
||||
Addrs: []string{rdb.Options().Addr},
|
||||
Password: rdb.Options().Password,
|
||||
DB: rdb.Options().DB,
|
||||
})
|
||||
}
|
||||
|
||||
// NewOfficialAccountApp 创建微信公众号应用实例
|
||||
func NewOfficialAccountApp(cfg *config.Config, cache kernel.CacheInterface, logger *zap.Logger) (*officialAccount.OfficialAccount, error) {
|
||||
oaCfg := cfg.Wechat.OfficialAccount
|
||||
if oaCfg.AppID == "" || oaCfg.AppSecret == "" {
|
||||
return nil, fmt.Errorf("微信公众号配置不完整:缺少 AppID 或 AppSecret")
|
||||
}
|
||||
|
||||
userConfig := &officialAccount.UserConfig{
|
||||
AppID: oaCfg.AppID,
|
||||
Secret: oaCfg.AppSecret,
|
||||
Cache: cache,
|
||||
}
|
||||
|
||||
// 可选配置:消息验证 Token 和 AESKey
|
||||
if oaCfg.Token != "" {
|
||||
userConfig.Token = oaCfg.Token
|
||||
}
|
||||
if oaCfg.AESKey != "" {
|
||||
userConfig.AESKey = oaCfg.AESKey
|
||||
}
|
||||
|
||||
app, err := officialAccount.NewOfficialAccount(userConfig)
|
||||
if err != nil {
|
||||
logger.Error("创建微信公众号应用失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("创建微信公众号应用失败: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("微信公众号应用初始化成功", zap.String("app_id", oaCfg.AppID))
|
||||
return app, nil
|
||||
}
|
||||
|
||||
// NewPaymentApp 创建微信支付应用实例
|
||||
func NewPaymentApp(cfg *config.Config, cache kernel.CacheInterface, logger *zap.Logger) (*payment.Payment, error) {
|
||||
payCfg := cfg.Wechat.Payment
|
||||
if payCfg.AppID == "" || payCfg.MchID == "" {
|
||||
return nil, fmt.Errorf("微信支付配置不完整:缺少 AppID 或 MchID")
|
||||
}
|
||||
|
||||
userConfig := &payment.UserConfig{
|
||||
AppID: payCfg.AppID,
|
||||
MchID: payCfg.MchID,
|
||||
MchApiV3Key: payCfg.APIV3Key,
|
||||
Key: payCfg.APIV2Key,
|
||||
CertPath: payCfg.CertPath,
|
||||
KeyPath: payCfg.KeyPath,
|
||||
SerialNo: payCfg.SerialNo,
|
||||
NotifyURL: payCfg.NotifyURL,
|
||||
HttpDebug: payCfg.HttpDebug,
|
||||
Cache: cache,
|
||||
}
|
||||
|
||||
app, err := payment.NewPayment(userConfig)
|
||||
if err != nil {
|
||||
logger.Error("创建微信支付应用失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("创建微信支付应用失败: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("微信支付应用初始化成功",
|
||||
zap.String("app_id", payCfg.AppID),
|
||||
zap.String("mch_id", payCfg.MchID),
|
||||
)
|
||||
return app, nil
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// MockService Mock 微信服务实现(用于开发和测试)
|
||||
type MockService struct{}
|
||||
|
||||
// NewMockService 创建 Mock 微信服务
|
||||
func NewMockService() *MockService {
|
||||
return &MockService{}
|
||||
}
|
||||
|
||||
// GetUserInfo Mock 实现:通过授权码获取用户信息
|
||||
// 注意:这是一个 Mock 实现,实际生产环境需要对接微信 OAuth API
|
||||
func (s *MockService) GetUserInfo(ctx context.Context, code string) (string, string, error) {
|
||||
// TODO: 实际实现需要调用微信 OAuth2.0 接口
|
||||
// 1. 使用 code 换取 access_token
|
||||
// 2. 使用 access_token 获取用户信息
|
||||
// 参考文档: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
|
||||
|
||||
return "", "", fmt.Errorf("微信服务暂未实现,待对接微信 SDK")
|
||||
}
|
||||
91
pkg/wechat/mock_test.go
Normal file
91
pkg/wechat/mock_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// MockOfficialAccountService Mock 微信公众号服务(实现 OfficialAccountServiceInterface)
|
||||
type MockOfficialAccountService struct {
|
||||
GetUserInfoFn func(ctx context.Context, code string) (openID, unionID string, err error)
|
||||
GetUserInfoDetailedFn func(ctx context.Context, code string) (*UserInfo, error)
|
||||
GetUserInfoByTokenFn func(ctx context.Context, accessToken, openID string) (*UserInfo, error)
|
||||
}
|
||||
|
||||
// GetUserInfo Mock 实现
|
||||
func (m *MockOfficialAccountService) GetUserInfo(ctx context.Context, code string) (openID, unionID string, err error) {
|
||||
if m.GetUserInfoFn != nil {
|
||||
return m.GetUserInfoFn(ctx, code)
|
||||
}
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
// GetUserInfoDetailed Mock 实现
|
||||
func (m *MockOfficialAccountService) GetUserInfoDetailed(ctx context.Context, code string) (*UserInfo, error) {
|
||||
if m.GetUserInfoDetailedFn != nil {
|
||||
return m.GetUserInfoDetailedFn(ctx, code)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetUserInfoByToken Mock 实现
|
||||
func (m *MockOfficialAccountService) GetUserInfoByToken(ctx context.Context, accessToken, openID string) (*UserInfo, error) {
|
||||
if m.GetUserInfoByTokenFn != nil {
|
||||
return m.GetUserInfoByTokenFn(ctx, accessToken, openID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// MockPaymentService Mock 微信支付服务(实现 PaymentServiceInterface)
|
||||
type MockPaymentService struct {
|
||||
CreateJSAPIOrderFn func(ctx context.Context, orderNo, description, openID string, amount int) (*JSAPIPayResult, error)
|
||||
CreateH5OrderFn func(ctx context.Context, orderNo, description string, amount int, sceneInfo *H5SceneInfo) (*H5PayResult, error)
|
||||
QueryOrderFn func(ctx context.Context, orderNo string) (*OrderInfo, error)
|
||||
CloseOrderFn func(ctx context.Context, orderNo string) error
|
||||
HandlePaymentNotifyFn func(r *http.Request, callback PaymentNotifyCallback) (*http.Response, error)
|
||||
}
|
||||
|
||||
// CreateJSAPIOrder Mock 实现
|
||||
func (m *MockPaymentService) CreateJSAPIOrder(ctx context.Context, orderNo, description, openID string, amount int) (*JSAPIPayResult, error) {
|
||||
if m.CreateJSAPIOrderFn != nil {
|
||||
return m.CreateJSAPIOrderFn(ctx, orderNo, description, openID, amount)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// CreateH5Order Mock 实现
|
||||
func (m *MockPaymentService) CreateH5Order(ctx context.Context, orderNo, description string, amount int, sceneInfo *H5SceneInfo) (*H5PayResult, error) {
|
||||
if m.CreateH5OrderFn != nil {
|
||||
return m.CreateH5OrderFn(ctx, orderNo, description, amount, sceneInfo)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// QueryOrder Mock 实现
|
||||
func (m *MockPaymentService) QueryOrder(ctx context.Context, orderNo string) (*OrderInfo, error) {
|
||||
if m.QueryOrderFn != nil {
|
||||
return m.QueryOrderFn(ctx, orderNo)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// CloseOrder Mock 实现
|
||||
func (m *MockPaymentService) CloseOrder(ctx context.Context, orderNo string) error {
|
||||
if m.CloseOrderFn != nil {
|
||||
return m.CloseOrderFn(ctx, orderNo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandlePaymentNotify Mock 实现(简化版)
|
||||
func (m *MockPaymentService) HandlePaymentNotify(r *http.Request, callback PaymentNotifyCallback) (*http.Response, error) {
|
||||
if m.HandlePaymentNotifyFn != nil {
|
||||
return m.HandlePaymentNotifyFn(r, callback)
|
||||
}
|
||||
return &http.Response{StatusCode: 200}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ OfficialAccountServiceInterface = (*MockOfficialAccountService)(nil)
|
||||
_ PaymentServiceInterface = (*MockPaymentService)(nil)
|
||||
)
|
||||
185
pkg/wechat/official_account.go
Normal file
185
pkg/wechat/official_account.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ArtisanCloud/PowerWeChat/v3/src/officialAccount"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// OfficialAccountService 微信公众号服务实现
|
||||
type OfficialAccountService struct {
|
||||
app *officialAccount.OfficialAccount
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewOfficialAccountService 创建微信公众号服务
|
||||
func NewOfficialAccountService(app *officialAccount.OfficialAccount, logger *zap.Logger) *OfficialAccountService {
|
||||
return &OfficialAccountService{
|
||||
app: app,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserInfo 通过授权码获取用户基本信息(静默授权)
|
||||
// 返回 OpenID 和 UnionID(如果有)
|
||||
func (s *OfficialAccountService) GetUserInfo(ctx context.Context, code string) (openID, unionID string, err error) {
|
||||
if code == "" {
|
||||
return "", "", errors.New(errors.CodeInvalidParam, "授权码不能为空")
|
||||
}
|
||||
|
||||
// 设置为静默授权模式(snsapi_base),只能获取 OpenID
|
||||
s.app.OAuth.SetScopes([]string{"snsapi_base"})
|
||||
|
||||
user, err := s.app.OAuth.UserFromCode(code)
|
||||
if err != nil {
|
||||
s.logger.Error("微信 OAuth 授权失败",
|
||||
zap.String("code", code),
|
||||
zap.Error(err),
|
||||
)
|
||||
return "", "", errors.Wrap(errors.CodeWechatOAuthFailed, err)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
s.logger.Error("微信 OAuth 返回空用户信息", zap.String("code", code))
|
||||
return "", "", errors.New(errors.CodeWechatOAuthFailed, "获取用户信息失败")
|
||||
}
|
||||
|
||||
openID = user.GetOpenID()
|
||||
|
||||
// 从原始数据中获取 UnionID
|
||||
raw, _ := user.GetRaw()
|
||||
if raw != nil {
|
||||
if uid, ok := (*raw)["unionid"].(string); ok {
|
||||
unionID = uid
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Debug("微信 OAuth 授权成功",
|
||||
zap.String("open_id", openID),
|
||||
zap.String("union_id", unionID),
|
||||
)
|
||||
|
||||
return openID, unionID, nil
|
||||
}
|
||||
|
||||
// GetUserInfoDetailed 通过授权码获取用户详细信息(用户授权)
|
||||
// 需要用户点击授权,可以获取昵称、头像等信息
|
||||
func (s *OfficialAccountService) GetUserInfoDetailed(ctx context.Context, code string) (*UserInfo, error) {
|
||||
if code == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "授权码不能为空")
|
||||
}
|
||||
|
||||
// 设置为用户信息授权模式(snsapi_userinfo)
|
||||
s.app.OAuth.SetScopes([]string{"snsapi_userinfo"})
|
||||
|
||||
user, err := s.app.OAuth.UserFromCode(code)
|
||||
if err != nil {
|
||||
s.logger.Error("微信 OAuth 授权失败",
|
||||
zap.String("code", code),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.Wrap(errors.CodeWechatOAuthFailed, err)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
s.logger.Error("微信 OAuth 返回空用户信息", zap.String("code", code))
|
||||
return nil, errors.New(errors.CodeWechatOAuthFailed, "获取用户信息失败")
|
||||
}
|
||||
|
||||
raw, _ := user.GetRaw()
|
||||
|
||||
userInfo := &UserInfo{
|
||||
OpenID: user.GetOpenID(),
|
||||
}
|
||||
|
||||
if raw != nil {
|
||||
if uid, ok := (*raw)["unionid"].(string); ok {
|
||||
userInfo.UnionID = uid
|
||||
}
|
||||
if nickname, ok := (*raw)["nickname"].(string); ok {
|
||||
userInfo.Nickname = nickname
|
||||
}
|
||||
if headimgurl, ok := (*raw)["headimgurl"].(string); ok {
|
||||
userInfo.Avatar = headimgurl
|
||||
}
|
||||
if sex, ok := (*raw)["sex"].(float64); ok {
|
||||
userInfo.Sex = int(sex)
|
||||
}
|
||||
if province, ok := (*raw)["province"].(string); ok {
|
||||
userInfo.Province = province
|
||||
}
|
||||
if city, ok := (*raw)["city"].(string); ok {
|
||||
userInfo.City = city
|
||||
}
|
||||
if country, ok := (*raw)["country"].(string); ok {
|
||||
userInfo.Country = country
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Debug("微信 OAuth 获取用户详细信息成功",
|
||||
zap.String("open_id", userInfo.OpenID),
|
||||
zap.String("nickname", userInfo.Nickname),
|
||||
)
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
// GetUserInfoByToken 通过 AccessToken 和 OpenID 获取用户详细信息
|
||||
func (s *OfficialAccountService) GetUserInfoByToken(ctx context.Context, accessToken, openID string) (*UserInfo, error) {
|
||||
if accessToken == "" || openID == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "AccessToken 和 OpenID 不能为空")
|
||||
}
|
||||
|
||||
user, err := s.app.OAuth.UserFromToken(accessToken, openID)
|
||||
if err != nil {
|
||||
s.logger.Error("通过 Token 获取微信用户信息失败",
|
||||
zap.String("open_id", openID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.Wrap(errors.CodeWechatUserInfoFailed, err)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
s.logger.Error("微信返回空用户信息", zap.String("open_id", openID))
|
||||
return nil, errors.New(errors.CodeWechatUserInfoFailed, "获取用户信息失败")
|
||||
}
|
||||
|
||||
raw, _ := user.GetRaw()
|
||||
|
||||
userInfo := &UserInfo{
|
||||
OpenID: user.GetOpenID(),
|
||||
}
|
||||
|
||||
if raw != nil {
|
||||
if uid, ok := (*raw)["unionid"].(string); ok {
|
||||
userInfo.UnionID = uid
|
||||
}
|
||||
if nickname, ok := (*raw)["nickname"].(string); ok {
|
||||
userInfo.Nickname = nickname
|
||||
}
|
||||
if headimgurl, ok := (*raw)["headimgurl"].(string); ok {
|
||||
userInfo.Avatar = headimgurl
|
||||
}
|
||||
if sex, ok := (*raw)["sex"].(float64); ok {
|
||||
userInfo.Sex = int(sex)
|
||||
}
|
||||
if province, ok := (*raw)["province"].(string); ok {
|
||||
userInfo.Province = province
|
||||
}
|
||||
if city, ok := (*raw)["city"].(string); ok {
|
||||
userInfo.City = city
|
||||
}
|
||||
if country, ok := (*raw)["country"].(string); ok {
|
||||
userInfo.Country = country
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Debug("通过 Token 获取微信用户详细信息成功",
|
||||
zap.String("open_id", userInfo.OpenID),
|
||||
zap.String("nickname", userInfo.Nickname),
|
||||
)
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
76
pkg/wechat/official_account_test.go
Normal file
76
pkg/wechat/official_account_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestOfficialAccountService_ParameterValidation(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
mockSvc := &MockOfficialAccountService{}
|
||||
|
||||
t.Run("GetUserInfo_空授权码", func(t *testing.T) {
|
||||
mockSvc.GetUserInfoFn = func(ctx context.Context, code string) (string, string, error) {
|
||||
if code == "" {
|
||||
return "", "", errors.New(errors.CodeInvalidParam, "授权码不能为空")
|
||||
}
|
||||
return "openid_123", "unionid_123", nil
|
||||
}
|
||||
|
||||
openID, unionID, err := mockSvc.GetUserInfo(context.Background(), "")
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
|
||||
assert.Empty(t, openID)
|
||||
assert.Empty(t, unionID)
|
||||
})
|
||||
|
||||
t.Run("GetUserInfo_成功", func(t *testing.T) {
|
||||
mockSvc.GetUserInfoFn = func(ctx context.Context, code string) (string, string, error) {
|
||||
return "openid_123", "unionid_123", nil
|
||||
}
|
||||
|
||||
openID, unionID, err := mockSvc.GetUserInfo(context.Background(), "valid_code")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "openid_123", openID)
|
||||
assert.Equal(t, "unionid_123", unionID)
|
||||
})
|
||||
|
||||
t.Run("GetUserInfoDetailed_空授权码", func(t *testing.T) {
|
||||
mockSvc.GetUserInfoDetailedFn = func(ctx context.Context, code string) (*UserInfo, error) {
|
||||
if code == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "授权码不能为空")
|
||||
}
|
||||
return &UserInfo{OpenID: "openid_123"}, nil
|
||||
}
|
||||
|
||||
userInfo, err := mockSvc.GetUserInfoDetailed(context.Background(), "")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, userInfo)
|
||||
})
|
||||
|
||||
t.Run("GetUserInfoByToken_空参数", func(t *testing.T) {
|
||||
mockSvc.GetUserInfoByTokenFn = func(ctx context.Context, accessToken, openID string) (*UserInfo, error) {
|
||||
if accessToken == "" || openID == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "AccessToken 和 OpenID 不能为空")
|
||||
}
|
||||
return &UserInfo{OpenID: openID}, nil
|
||||
}
|
||||
|
||||
userInfo, err := mockSvc.GetUserInfoByToken(context.Background(), "", "openid_123")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, userInfo)
|
||||
|
||||
userInfo, err = mockSvc.GetUserInfoByToken(context.Background(), "token_123", "")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, userInfo)
|
||||
})
|
||||
|
||||
_ = logger
|
||||
}
|
||||
282
pkg/wechat/payment.go
Normal file
282
pkg/wechat/payment.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/ArtisanCloud/PowerWeChat/v3/src/kernel/models"
|
||||
"github.com/ArtisanCloud/PowerWeChat/v3/src/payment"
|
||||
"github.com/ArtisanCloud/PowerWeChat/v3/src/payment/notify/request"
|
||||
orderRequest "github.com/ArtisanCloud/PowerWeChat/v3/src/payment/order/request"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// PaymentService 微信支付服务实现
|
||||
type PaymentService struct {
|
||||
app *payment.Payment
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewPaymentService 创建微信支付服务
|
||||
func NewPaymentService(app *payment.Payment, logger *zap.Logger) *PaymentService {
|
||||
return &PaymentService{
|
||||
app: app,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// JSAPIPayResult JSAPI 支付结果
|
||||
type JSAPIPayResult struct {
|
||||
PrepayID string `json:"prepay_id"`
|
||||
PayConfig interface{} `json:"pay_config"`
|
||||
}
|
||||
|
||||
// H5PayResult H5 支付结果
|
||||
type H5PayResult struct {
|
||||
H5URL string `json:"h5_url"`
|
||||
}
|
||||
|
||||
// OrderInfo 订单信息
|
||||
type OrderInfo struct {
|
||||
TransactionID string `json:"transaction_id"`
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
TradeState string `json:"trade_state"`
|
||||
TradeStateDesc string `json:"trade_state_desc"`
|
||||
SuccessTime string `json:"success_time"`
|
||||
TradeType string `json:"trade_type"`
|
||||
BankType string `json:"bank_type"`
|
||||
Attach string `json:"attach"`
|
||||
PayerOpenID string `json:"payer_openid"`
|
||||
TotalAmount int64 `json:"total_amount"`
|
||||
PayerTotal int64 `json:"payer_total"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
// PaymentNotifyResult 支付通知结果
|
||||
type PaymentNotifyResult struct {
|
||||
TransactionID string `json:"transaction_id"`
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
TradeState string `json:"trade_state"`
|
||||
SuccessTime string `json:"success_time"`
|
||||
PayerOpenID string `json:"payer_openid"`
|
||||
TotalAmount int64 `json:"total_amount"`
|
||||
Attach string `json:"attach"`
|
||||
}
|
||||
|
||||
// CreateJSAPIOrder 创建 JSAPI 支付订单
|
||||
func (s *PaymentService) CreateJSAPIOrder(ctx context.Context, orderNo, description, openID string, amount int) (*JSAPIPayResult, error) {
|
||||
if orderNo == "" || openID == "" || amount <= 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "订单号、OpenID 和金额不能为空")
|
||||
}
|
||||
|
||||
resp, err := s.app.Order.JSAPITransaction(ctx, &orderRequest.RequestJSAPIPrepay{
|
||||
Description: description,
|
||||
OutTradeNo: orderNo,
|
||||
Amount: &orderRequest.JSAPIAmount{
|
||||
Total: amount,
|
||||
Currency: "CNY",
|
||||
},
|
||||
Payer: &orderRequest.JSAPIPayer{
|
||||
OpenID: openID,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error("创建 JSAPI 支付订单失败",
|
||||
zap.String("order_no", orderNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.Wrap(errors.CodeWechatPayFailed, err)
|
||||
}
|
||||
|
||||
if resp == nil || resp.PrepayID == "" {
|
||||
s.logger.Error("创建 JSAPI 支付订单失败:空 PrepayID", zap.String("order_no", orderNo))
|
||||
return nil, errors.New(errors.CodeWechatPayFailed, "创建支付订单失败")
|
||||
}
|
||||
|
||||
payConfig, err := s.app.JSSDK.BridgeConfig(resp.PrepayID, false)
|
||||
if err != nil {
|
||||
s.logger.Error("生成支付配置失败",
|
||||
zap.String("order_no", orderNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.Wrap(errors.CodeWechatPayFailed, err)
|
||||
}
|
||||
|
||||
s.logger.Info("创建 JSAPI 支付订单成功",
|
||||
zap.String("order_no", orderNo),
|
||||
zap.String("prepay_id", resp.PrepayID),
|
||||
)
|
||||
|
||||
return &JSAPIPayResult{
|
||||
PrepayID: resp.PrepayID,
|
||||
PayConfig: payConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateH5Order 创建 H5 支付订单
|
||||
func (s *PaymentService) CreateH5Order(ctx context.Context, orderNo, description string, amount int, sceneInfo *H5SceneInfo) (*H5PayResult, error) {
|
||||
if orderNo == "" || amount <= 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "订单号和金额不能为空")
|
||||
}
|
||||
|
||||
req := &orderRequest.RequestH5Prepay{
|
||||
Description: description,
|
||||
OutTradeNo: orderNo,
|
||||
Amount: &orderRequest.H5Amount{
|
||||
Total: amount,
|
||||
Currency: "CNY",
|
||||
},
|
||||
}
|
||||
|
||||
if sceneInfo != nil {
|
||||
req.SceneInfo = &orderRequest.H5SceneInfo{
|
||||
PayerClientIP: sceneInfo.PayerClientIP,
|
||||
H5Info: &orderRequest.H5H5Info{
|
||||
Type: sceneInfo.H5Type,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := s.app.Order.TransactionH5(ctx, req)
|
||||
if err != nil {
|
||||
s.logger.Error("创建 H5 支付订单失败",
|
||||
zap.String("order_no", orderNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.Wrap(errors.CodeWechatPayFailed, err)
|
||||
}
|
||||
|
||||
if resp == nil || resp.H5URL == "" {
|
||||
s.logger.Error("创建 H5 支付订单失败:空 H5URL", zap.String("order_no", orderNo))
|
||||
return nil, errors.New(errors.CodeWechatPayFailed, "创建 H5 支付订单失败")
|
||||
}
|
||||
|
||||
s.logger.Info("创建 H5 支付订单成功",
|
||||
zap.String("order_no", orderNo),
|
||||
zap.String("h5_url", resp.H5URL),
|
||||
)
|
||||
|
||||
return &H5PayResult{
|
||||
H5URL: resp.H5URL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// H5SceneInfo H5 支付场景信息
|
||||
type H5SceneInfo struct {
|
||||
PayerClientIP string `json:"payer_client_ip"`
|
||||
H5Type string `json:"h5_type"`
|
||||
}
|
||||
|
||||
// QueryOrder 查询订单
|
||||
func (s *PaymentService) QueryOrder(ctx context.Context, orderNo string) (*OrderInfo, error) {
|
||||
if orderNo == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "订单号不能为空")
|
||||
}
|
||||
|
||||
resp, err := s.app.Order.QueryByOutTradeNumber(ctx, orderNo)
|
||||
if err != nil {
|
||||
s.logger.Error("查询订单失败",
|
||||
zap.String("order_no", orderNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.Wrap(errors.CodeWechatPayFailed, err)
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
return nil, errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
|
||||
orderInfo := &OrderInfo{
|
||||
TransactionID: resp.TransactionID,
|
||||
OutTradeNo: resp.OutTradeNo,
|
||||
TradeState: resp.TradeState,
|
||||
TradeStateDesc: resp.TradeStateDesc,
|
||||
SuccessTime: resp.SuccessTime,
|
||||
TradeType: resp.TradeType,
|
||||
BankType: resp.BankType,
|
||||
Attach: resp.Attach,
|
||||
}
|
||||
|
||||
if resp.Amount != nil {
|
||||
orderInfo.TotalAmount = resp.Amount.Total
|
||||
orderInfo.PayerTotal = resp.Amount.PayerTotal
|
||||
orderInfo.Currency = resp.Amount.Currency
|
||||
}
|
||||
|
||||
if resp.Payer != nil {
|
||||
orderInfo.PayerOpenID = resp.Payer.OpenID
|
||||
}
|
||||
|
||||
s.logger.Debug("查询订单成功",
|
||||
zap.String("order_no", orderNo),
|
||||
zap.String("trade_state", resp.TradeState),
|
||||
)
|
||||
|
||||
return orderInfo, nil
|
||||
}
|
||||
|
||||
// CloseOrder 关闭订单
|
||||
func (s *PaymentService) CloseOrder(ctx context.Context, orderNo string) error {
|
||||
if orderNo == "" {
|
||||
return errors.New(errors.CodeInvalidParam, "订单号不能为空")
|
||||
}
|
||||
|
||||
_, err := s.app.Order.Close(ctx, orderNo)
|
||||
if err != nil {
|
||||
s.logger.Error("关闭订单失败",
|
||||
zap.String("order_no", orderNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeWechatPayFailed, err)
|
||||
}
|
||||
|
||||
s.logger.Info("关闭订单成功", zap.String("order_no", orderNo))
|
||||
return nil
|
||||
}
|
||||
|
||||
// PaymentNotifyCallback 支付通知回调函数
|
||||
type PaymentNotifyCallback func(result *PaymentNotifyResult) error
|
||||
|
||||
// HandlePaymentNotify 处理支付回调通知
|
||||
func (s *PaymentService) HandlePaymentNotify(r *http.Request, callback PaymentNotifyCallback) (*http.Response, error) {
|
||||
return s.app.HandlePaidNotify(r, func(notify *request.RequestNotify, transaction *models.Transaction, fail func(message string)) interface{} {
|
||||
if transaction == nil {
|
||||
s.logger.Error("支付通知数据为空")
|
||||
fail("支付通知数据为空")
|
||||
return nil
|
||||
}
|
||||
|
||||
result := &PaymentNotifyResult{
|
||||
OutTradeNo: transaction.OutTradeNo,
|
||||
TradeState: transaction.TradeState,
|
||||
SuccessTime: transaction.SuccessTime,
|
||||
Attach: transaction.Attach,
|
||||
}
|
||||
|
||||
result.TransactionID = transaction.TransactionID
|
||||
if transaction.Payer != nil {
|
||||
result.PayerOpenID = transaction.Payer.OpenID
|
||||
}
|
||||
if transaction.Amount != nil {
|
||||
result.TotalAmount = transaction.Amount.Total
|
||||
}
|
||||
|
||||
if err := callback(result); err != nil {
|
||||
s.logger.Error("处理支付通知回调失败",
|
||||
zap.String("out_trade_no", result.OutTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
fail(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
s.logger.Info("支付通知处理成功",
|
||||
zap.String("out_trade_no", result.OutTradeNo),
|
||||
zap.String("transaction_id", result.TransactionID),
|
||||
)
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
93
pkg/wechat/payment_test.go
Normal file
93
pkg/wechat/payment_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestPaymentService_ParameterValidation(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
mockSvc := &MockPaymentService{}
|
||||
|
||||
t.Run("CreateJSAPIOrder_参数验证", func(t *testing.T) {
|
||||
mockSvc.CreateJSAPIOrderFn = func(ctx context.Context, orderNo, description, openID string, amount int) (*JSAPIPayResult, error) {
|
||||
if orderNo == "" || openID == "" || amount <= 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "订单号、OpenID 和金额不能为空")
|
||||
}
|
||||
return &JSAPIPayResult{PrepayID: "prepay_id_123"}, nil
|
||||
}
|
||||
|
||||
_, err := mockSvc.CreateJSAPIOrder(context.Background(), "", "desc", "openid", 100)
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = mockSvc.CreateJSAPIOrder(context.Background(), "order_123", "desc", "", 100)
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = mockSvc.CreateJSAPIOrder(context.Background(), "order_123", "desc", "openid", 0)
|
||||
require.Error(t, err)
|
||||
|
||||
result, err := mockSvc.CreateJSAPIOrder(context.Background(), "order_123", "desc", "openid", 100)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "prepay_id_123", result.PrepayID)
|
||||
})
|
||||
|
||||
t.Run("CreateH5Order_参数验证", func(t *testing.T) {
|
||||
mockSvc.CreateH5OrderFn = func(ctx context.Context, orderNo, description string, amount int, sceneInfo *H5SceneInfo) (*H5PayResult, error) {
|
||||
if orderNo == "" || amount <= 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "订单号和金额不能为空")
|
||||
}
|
||||
return &H5PayResult{H5URL: "https://wx.tenpay.com/..."}, nil
|
||||
}
|
||||
|
||||
_, err := mockSvc.CreateH5Order(context.Background(), "", "desc", 100, nil)
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = mockSvc.CreateH5Order(context.Background(), "order_123", "desc", 0, nil)
|
||||
require.Error(t, err)
|
||||
|
||||
result, err := mockSvc.CreateH5Order(context.Background(), "order_123", "desc", 100, nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.NotEmpty(t, result.H5URL)
|
||||
})
|
||||
|
||||
t.Run("QueryOrder_参数验证", func(t *testing.T) {
|
||||
mockSvc.QueryOrderFn = func(ctx context.Context, orderNo string) (*OrderInfo, error) {
|
||||
if orderNo == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "订单号不能为空")
|
||||
}
|
||||
return &OrderInfo{OutTradeNo: orderNo}, nil
|
||||
}
|
||||
|
||||
_, err := mockSvc.QueryOrder(context.Background(), "")
|
||||
require.Error(t, err)
|
||||
|
||||
result, err := mockSvc.QueryOrder(context.Background(), "order_123")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "order_123", result.OutTradeNo)
|
||||
})
|
||||
|
||||
t.Run("CloseOrder_参数验证", func(t *testing.T) {
|
||||
mockSvc.CloseOrderFn = func(ctx context.Context, orderNo string) error {
|
||||
if orderNo == "" {
|
||||
return errors.New(errors.CodeInvalidParam, "订单号不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err := mockSvc.CloseOrder(context.Background(), "")
|
||||
require.Error(t, err)
|
||||
|
||||
err = mockSvc.CloseOrder(context.Background(), "order_123")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
_ = logger
|
||||
}
|
||||
@@ -1,21 +1,46 @@
|
||||
package wechat
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Service 微信服务接口
|
||||
// Service 微信服务接口(向后兼容)
|
||||
type Service interface {
|
||||
// GetUserInfo 通过授权码获取用户信息
|
||||
GetUserInfo(ctx context.Context, code string) (openID, unionID string, err error)
|
||||
}
|
||||
|
||||
// OfficialAccountServiceInterface 微信公众号服务接口
|
||||
type OfficialAccountServiceInterface interface {
|
||||
Service
|
||||
GetUserInfoDetailed(ctx context.Context, code string) (*UserInfo, error)
|
||||
GetUserInfoByToken(ctx context.Context, accessToken, openID string) (*UserInfo, error)
|
||||
}
|
||||
|
||||
// PaymentServiceInterface 微信支付服务接口
|
||||
type PaymentServiceInterface interface {
|
||||
CreateJSAPIOrder(ctx context.Context, orderNo, description, openID string, amount int) (*JSAPIPayResult, error)
|
||||
CreateH5Order(ctx context.Context, orderNo, description string, amount int, sceneInfo *H5SceneInfo) (*H5PayResult, error)
|
||||
QueryOrder(ctx context.Context, orderNo string) (*OrderInfo, error)
|
||||
CloseOrder(ctx context.Context, orderNo string) error
|
||||
HandlePaymentNotify(r *http.Request, callback PaymentNotifyCallback) (*http.Response, error)
|
||||
}
|
||||
|
||||
// UserInfo 微信用户信息
|
||||
type UserInfo struct {
|
||||
OpenID string `json:"open_id"` // 微信 OpenID
|
||||
UnionID string `json:"union_id"` // 微信 UnionID(开放平台统一ID)
|
||||
Nickname string `json:"nickname"` // 昵称
|
||||
Avatar string `json:"avatar"` // 头像URL
|
||||
Sex int `json:"sex"` // 性别 0-未知 1-男 2-女
|
||||
Province string `json:"province"` // 省份
|
||||
City string `json:"city"` // 城市
|
||||
Country string `json:"country"` // 国家
|
||||
OpenID string `json:"open_id"`
|
||||
UnionID string `json:"union_id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
Sex int `json:"sex"`
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
Country string `json:"country"`
|
||||
}
|
||||
|
||||
// 编译时类型检查
|
||||
var (
|
||||
_ Service = (*OfficialAccountService)(nil)
|
||||
_ OfficialAccountServiceInterface = (*OfficialAccountService)(nil)
|
||||
_ PaymentServiceInterface = (*PaymentService)(nil)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user