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:
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/queue"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/utils"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||||
"github.com/bytedance/sonic"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
@@ -26,6 +27,7 @@ type Service struct {
|
||||
walletStore *postgres.WalletStore
|
||||
purchaseValidationService *purchase_validation.Service
|
||||
allocationConfigStore *postgres.ShopSeriesAllocationConfigStore
|
||||
wechatPayment wechat.PaymentServiceInterface
|
||||
queueClient *queue.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
@@ -37,6 +39,7 @@ func New(
|
||||
walletStore *postgres.WalletStore,
|
||||
purchaseValidationService *purchase_validation.Service,
|
||||
allocationConfigStore *postgres.ShopSeriesAllocationConfigStore,
|
||||
wechatPayment wechat.PaymentServiceInterface,
|
||||
queueClient *queue.Client,
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
@@ -47,6 +50,7 @@ func New(
|
||||
walletStore: walletStore,
|
||||
purchaseValidationService: purchaseValidationService,
|
||||
allocationConfigStore: allocationConfigStore,
|
||||
wechatPayment: wechatPayment,
|
||||
queueClient: queueClient,
|
||||
logger: logger,
|
||||
}
|
||||
@@ -529,3 +533,116 @@ func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderIte
|
||||
UpdatedAt: order.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// WechatPayJSAPI 发起微信 JSAPI 支付
|
||||
func (s *Service) WechatPayJSAPI(ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint) (*dto.WechatPayJSAPIResponse, error) {
|
||||
if s.wechatPayment == nil {
|
||||
s.logger.Error("微信支付服务未配置")
|
||||
return nil, errors.New(errors.CodeWechatPayFailed, "微信支付服务未配置")
|
||||
}
|
||||
|
||||
order, err := s.orderStore.GetByID(ctx, orderID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询订单失败")
|
||||
}
|
||||
|
||||
if order.BuyerType != buyerType || order.BuyerID != buyerID {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权操作此订单")
|
||||
}
|
||||
|
||||
if order.PaymentStatus != model.PaymentStatusPending {
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
|
||||
}
|
||||
|
||||
items, err := s.orderItemStore.ListByOrderIDs(ctx, []uint{orderID})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询订单明细失败")
|
||||
}
|
||||
description := "套餐购买"
|
||||
if len(items) > 0 {
|
||||
description = items[0].PackageName
|
||||
}
|
||||
|
||||
result, err := s.wechatPayment.CreateJSAPIOrder(ctx, order.OrderNo, description, openID, int(order.TotalAmount))
|
||||
if err != nil {
|
||||
s.logger.Error("创建 JSAPI 支付失败",
|
||||
zap.Uint("order_id", orderID),
|
||||
zap.String("order_no", order.OrderNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("创建 JSAPI 支付成功",
|
||||
zap.Uint("order_id", orderID),
|
||||
zap.String("order_no", order.OrderNo),
|
||||
zap.String("prepay_id", result.PrepayID),
|
||||
)
|
||||
|
||||
payConfig, _ := result.PayConfig.(map[string]interface{})
|
||||
return &dto.WechatPayJSAPIResponse{
|
||||
PrepayID: result.PrepayID,
|
||||
PayConfig: payConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WechatPayH5 发起微信 H5 支付
|
||||
func (s *Service) WechatPayH5(ctx context.Context, orderID uint, sceneInfo *dto.WechatH5SceneInfo, buyerType string, buyerID uint) (*dto.WechatPayH5Response, error) {
|
||||
if s.wechatPayment == nil {
|
||||
s.logger.Error("微信支付服务未配置")
|
||||
return nil, errors.New(errors.CodeWechatPayFailed, "微信支付服务未配置")
|
||||
}
|
||||
|
||||
order, err := s.orderStore.GetByID(ctx, orderID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询订单失败")
|
||||
}
|
||||
|
||||
if order.BuyerType != buyerType || order.BuyerID != buyerID {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权操作此订单")
|
||||
}
|
||||
|
||||
if order.PaymentStatus != model.PaymentStatusPending {
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
|
||||
}
|
||||
|
||||
items, err := s.orderItemStore.ListByOrderIDs(ctx, []uint{orderID})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询订单明细失败")
|
||||
}
|
||||
description := "套餐购买"
|
||||
if len(items) > 0 {
|
||||
description = items[0].PackageName
|
||||
}
|
||||
|
||||
h5SceneInfo := &wechat.H5SceneInfo{
|
||||
PayerClientIP: sceneInfo.PayerClientIP,
|
||||
H5Type: sceneInfo.H5Info.Type,
|
||||
}
|
||||
|
||||
result, err := s.wechatPayment.CreateH5Order(ctx, order.OrderNo, description, int(order.TotalAmount), h5SceneInfo)
|
||||
if err != nil {
|
||||
s.logger.Error("创建 H5 支付失败",
|
||||
zap.Uint("order_id", orderID),
|
||||
zap.String("order_no", order.OrderNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("创建 H5 支付成功",
|
||||
zap.Uint("order_id", orderID),
|
||||
zap.String("order_no", order.OrderNo),
|
||||
zap.String("h5_url", result.H5URL),
|
||||
)
|
||||
|
||||
return &dto.WechatPayH5Response{
|
||||
H5URL: result.H5URL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ func setupOrderTestEnv(t *testing.T) *testEnv {
|
||||
|
||||
purchaseValidationSvc := purchase_validation.New(tx, iotCardStore, deviceStore, packageStore, seriesAllocationStore)
|
||||
logger := zap.NewNop()
|
||||
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil, nil, logger)
|
||||
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil, nil, nil, logger)
|
||||
|
||||
userCtx := middleware.SetUserContext(ctx, &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -536,7 +536,7 @@ func TestOrderService_IdempotencyAndConcurrency(t *testing.T) {
|
||||
|
||||
purchaseValidationSvc := purchase_validation.New(tx, iotCardStore, deviceStore, packageStore, seriesAllocationStore)
|
||||
logger := zap.NewNop()
|
||||
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil, nil, logger)
|
||||
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil, nil, nil, logger)
|
||||
|
||||
userCtx := middleware.SetUserContext(ctx, &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
|
||||
@@ -6,21 +6,24 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/verification"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auth"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Service 个人客户服务
|
||||
type Service struct {
|
||||
store *postgres.PersonalCustomerStore
|
||||
phoneStore *postgres.PersonalCustomerPhoneStore
|
||||
verificationService *verification.Service
|
||||
jwtManager *auth.JWTManager
|
||||
logger *zap.Logger
|
||||
store *postgres.PersonalCustomerStore
|
||||
phoneStore *postgres.PersonalCustomerPhoneStore
|
||||
verificationService *verification.Service
|
||||
jwtManager *auth.JWTManager
|
||||
wechatOfficialAccount wechat.OfficialAccountServiceInterface
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewService 创建个人客户服务实例
|
||||
@@ -29,14 +32,16 @@ func NewService(
|
||||
phoneStore *postgres.PersonalCustomerPhoneStore,
|
||||
verificationService *verification.Service,
|
||||
jwtManager *auth.JWTManager,
|
||||
wechatOfficialAccount wechat.OfficialAccountServiceInterface,
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
store: store,
|
||||
phoneStore: phoneStore,
|
||||
verificationService: verificationService,
|
||||
jwtManager: jwtManager,
|
||||
logger: logger,
|
||||
store: store,
|
||||
phoneStore: phoneStore,
|
||||
verificationService: verificationService,
|
||||
jwtManager: jwtManager,
|
||||
wechatOfficialAccount: wechatOfficialAccount,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,3 +241,190 @@ func (s *Service) GetProfileWithPhone(ctx context.Context, customerID uint) (*mo
|
||||
|
||||
return customer, phone, nil
|
||||
}
|
||||
|
||||
// WechatOAuthLogin 微信 OAuth 登录
|
||||
// 通过微信授权码登录,如果用户不存在则自动创建
|
||||
func (s *Service) WechatOAuthLogin(ctx context.Context, code string) (*dto.WechatOAuthResponse, error) {
|
||||
// 检查微信服务是否已配置
|
||||
if s.wechatOfficialAccount == nil {
|
||||
s.logger.Error("微信公众号服务未配置")
|
||||
return nil, errors.New(errors.CodeWechatOAuthFailed, "微信服务未配置")
|
||||
}
|
||||
|
||||
// 通过授权码获取用户详细信息
|
||||
userInfo, err := s.wechatOfficialAccount.GetUserInfoDetailed(ctx, code)
|
||||
if err != nil {
|
||||
s.logger.Error("获取微信用户信息失败",
|
||||
zap.String("code", code),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 通过 OpenID 查找现有客户
|
||||
customer, err := s.store.GetByWxOpenID(ctx, userInfo.OpenID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 客户不存在,创建新客户
|
||||
customer = &model.PersonalCustomer{
|
||||
WxOpenID: userInfo.OpenID,
|
||||
WxUnionID: userInfo.UnionID,
|
||||
Nickname: userInfo.Nickname,
|
||||
AvatarURL: userInfo.Avatar,
|
||||
Status: 1, // 默认启用
|
||||
}
|
||||
if err := s.store.Create(ctx, customer); err != nil {
|
||||
s.logger.Error("创建微信用户失败",
|
||||
zap.String("open_id", userInfo.OpenID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "创建用户失败")
|
||||
}
|
||||
s.logger.Info("通过微信创建新用户",
|
||||
zap.Uint("customer_id", customer.ID),
|
||||
zap.String("open_id", userInfo.OpenID),
|
||||
)
|
||||
} else {
|
||||
s.logger.Error("查询微信用户失败",
|
||||
zap.String("open_id", userInfo.OpenID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询用户失败")
|
||||
}
|
||||
} else {
|
||||
// 客户已存在,更新昵称和头像(如果有变化)
|
||||
needUpdate := false
|
||||
if userInfo.Nickname != "" && customer.Nickname != userInfo.Nickname {
|
||||
customer.Nickname = userInfo.Nickname
|
||||
needUpdate = true
|
||||
}
|
||||
if userInfo.Avatar != "" && customer.AvatarURL != userInfo.Avatar {
|
||||
customer.AvatarURL = userInfo.Avatar
|
||||
needUpdate = true
|
||||
}
|
||||
if needUpdate {
|
||||
if err := s.store.Update(ctx, customer); err != nil {
|
||||
s.logger.Warn("更新微信用户信息失败",
|
||||
zap.Uint("customer_id", customer.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
// 不阻断登录流程
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查客户状态
|
||||
if customer.Status == 0 {
|
||||
s.logger.Warn("微信用户已被禁用",
|
||||
zap.Uint("customer_id", customer.ID),
|
||||
zap.String("open_id", userInfo.OpenID),
|
||||
)
|
||||
return nil, errors.New(errors.CodeForbidden, "账号已被禁用")
|
||||
}
|
||||
|
||||
// 生成 JWT Token
|
||||
token, err := s.jwtManager.GeneratePersonalCustomerToken(customer.ID, "")
|
||||
if err != nil {
|
||||
s.logger.Error("生成 Token 失败",
|
||||
zap.Uint("customer_id", customer.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "生成 Token 失败")
|
||||
}
|
||||
|
||||
// 获取主手机号(如果有)
|
||||
phone := ""
|
||||
primaryPhone, err := s.phoneStore.GetPrimaryPhone(ctx, customer.ID)
|
||||
if err == nil {
|
||||
phone = primaryPhone.Phone
|
||||
}
|
||||
|
||||
s.logger.Info("微信 OAuth 登录成功",
|
||||
zap.Uint("customer_id", customer.ID),
|
||||
zap.String("open_id", userInfo.OpenID),
|
||||
)
|
||||
|
||||
return &dto.WechatOAuthResponse{
|
||||
AccessToken: token,
|
||||
ExpiresIn: 24 * 60 * 60, // 24 小时
|
||||
Customer: &dto.PersonalCustomerResponse{
|
||||
ID: customer.ID,
|
||||
Phone: phone,
|
||||
Nickname: customer.Nickname,
|
||||
AvatarURL: customer.AvatarURL,
|
||||
WxOpenID: customer.WxOpenID,
|
||||
WxUnionID: customer.WxUnionID,
|
||||
Status: customer.Status,
|
||||
CreatedAt: customer.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: customer.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BindWechatWithCode 通过微信授权码绑定微信
|
||||
// customerID: 当前登录的客户 ID
|
||||
// code: 微信授权码
|
||||
func (s *Service) BindWechatWithCode(ctx context.Context, customerID uint, code string) error {
|
||||
// 检查微信服务是否已配置
|
||||
if s.wechatOfficialAccount == nil {
|
||||
s.logger.Error("微信公众号服务未配置")
|
||||
return errors.New(errors.CodeWechatOAuthFailed, "微信服务未配置")
|
||||
}
|
||||
|
||||
// 获取客户信息
|
||||
customer, err := s.store.GetByID(ctx, customerID)
|
||||
if err != nil {
|
||||
s.logger.Error("查询个人客户失败",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "查询客户失败")
|
||||
}
|
||||
|
||||
// 获取微信用户信息
|
||||
userInfo, err := s.wechatOfficialAccount.GetUserInfoDetailed(ctx, code)
|
||||
if err != nil {
|
||||
s.logger.Error("获取微信用户信息失败",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.String("code", code),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查该 OpenID 是否已被其他用户绑定
|
||||
existingCustomer, err := s.store.GetByWxOpenID(ctx, userInfo.OpenID)
|
||||
if err == nil && existingCustomer.ID != customerID {
|
||||
s.logger.Warn("微信账号已被其他用户绑定",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.Uint("existing_customer_id", existingCustomer.ID),
|
||||
zap.String("open_id", userInfo.OpenID),
|
||||
)
|
||||
return errors.New(errors.CodeConflict, "该微信账号已被其他用户绑定")
|
||||
}
|
||||
|
||||
// 更新微信信息
|
||||
customer.WxOpenID = userInfo.OpenID
|
||||
customer.WxUnionID = userInfo.UnionID
|
||||
if userInfo.Nickname != "" {
|
||||
customer.Nickname = userInfo.Nickname
|
||||
}
|
||||
if userInfo.Avatar != "" {
|
||||
customer.AvatarURL = userInfo.Avatar
|
||||
}
|
||||
|
||||
if err := s.store.Update(ctx, customer); err != nil {
|
||||
s.logger.Error("绑定微信信息失败",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "绑定微信失败")
|
||||
}
|
||||
|
||||
s.logger.Info("绑定微信成功",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.String("open_id", userInfo.OpenID),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user