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:
2026-01-30 17:32:33 +08:00
48 changed files with 9034 additions and 378 deletions

View File

@@ -6,6 +6,7 @@ import (
"github.com/break/junhong_cmp_fiber/pkg/auth"
"github.com/break/junhong_cmp_fiber/pkg/queue"
"github.com/break/junhong_cmp_fiber/pkg/storage"
"github.com/break/junhong_cmp_fiber/pkg/wechat"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"gorm.io/gorm"
@@ -14,6 +15,7 @@ import (
// Dependencies 封装所有基础依赖
// 这些是应用启动时初始化的核心组件
type Dependencies struct {
<<<<<<< HEAD
DB *gorm.DB // PostgreSQL 数据库连接
Redis *redis.Client // Redis 客户端
Logger *zap.Logger // 应用日志器
@@ -23,4 +25,16 @@ type Dependencies struct {
QueueClient *queue.Client // Asynq 任务队列客户端
StorageService *storage.Service // 对象存储服务(可选,配置缺失时为 nil
GatewayClient *gateway.Client // Gateway API 客户端(可选,配置缺失时为 nil
=======
DB *gorm.DB // PostgreSQL 数据库连接
Redis *redis.Client // Redis 客户端
Logger *zap.Logger // 应用日志器
JWTManager *auth.JWTManager // JWT 管理器(个人客户认证)
TokenManager *auth.TokenManager // Token 管理器后台和H5认证
VerificationService *verification.Service // 验证码服务
QueueClient *queue.Client // Asynq 任务队列客户端
StorageService *storage.Service // 对象存储服务(可选,配置缺失时为 nil
WechatOfficialAccount wechat.OfficialAccountServiceInterface // 微信公众号服务(可选)
WechatPayment wechat.PaymentServiceInterface // 微信支付服务(可选)
>>>>>>> emdash/wechat-official-account-payment-integration-30g
}

View File

@@ -45,6 +45,6 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
ShopPackageBatchPricing: admin.NewShopPackageBatchPricingHandler(svc.ShopPackageBatchPricing),
AdminOrder: admin.NewOrderHandler(svc.Order),
H5Order: h5.NewOrderHandler(svc.Order),
PaymentCallback: callback.NewPaymentHandler(svc.Order),
PaymentCallback: callback.NewPaymentHandler(svc.Order, deps.WechatPayment),
}
}

View File

@@ -76,7 +76,7 @@ func initServices(s *stores, deps *Dependencies) *services {
Account: accountSvc.New(s.Account, s.Role, s.AccountRole),
Role: roleSvc.New(s.Role, s.Permission, s.RolePermission),
Permission: permissionSvc.New(s.Permission, s.AccountRole, s.RolePermission, deps.Redis),
PersonalCustomer: personalCustomerSvc.NewService(s.PersonalCustomer, s.PersonalCustomerPhone, deps.VerificationService, deps.JWTManager, deps.Logger),
PersonalCustomer: personalCustomerSvc.NewService(s.PersonalCustomer, s.PersonalCustomerPhone, deps.VerificationService, deps.JWTManager, deps.WechatOfficialAccount, deps.Logger),
Shop: shopSvc.New(s.Shop, s.Account),
ShopAccount: shopAccountSvc.New(s.Account, s.Shop),
Auth: authSvc.New(s.Account, s.AccountRole, s.RolePermission, s.Permission, deps.TokenManager, deps.Logger),
@@ -119,6 +119,6 @@ func initServices(s *stores, deps *Dependencies) *services {
ShopPackageBatchPricing: shopPackageBatchPricingSvc.New(deps.DB, s.ShopPackageAllocation, s.ShopPackageAllocationPriceHistory, s.Shop),
CommissionStats: commissionStatsSvc.New(s.ShopSeriesCommissionStats),
PurchaseValidation: purchaseValidation,
Order: orderSvc.New(deps.DB, s.Order, s.OrderItem, s.Wallet, purchaseValidation, s.ShopSeriesAllocationConfig, deps.QueueClient, deps.Logger),
Order: orderSvc.New(deps.DB, s.Order, s.OrderItem, s.Wallet, purchaseValidation, s.ShopSeriesAllocationConfig, deps.WechatPayment, deps.QueueClient, deps.Logger),
}
}

View File

@@ -3,6 +3,7 @@
package app
import (
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/internal/service/personal_customer"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/response"
@@ -108,27 +109,49 @@ func (h *PersonalCustomerHandler) Login(c *fiber.Ctx) error {
return response.Success(c, resp)
}
// BindWechatRequest 绑定微信请求
type BindWechatRequest struct {
Code string `json:"code" validate:"required"` // 微信授权码
}
// BindWechat 绑定微信
// POST /api/c/v1/bind-wechat
// TODO: 实现微信 OAuth 授权逻辑
func (h *PersonalCustomerHandler) BindWechat(c *fiber.Ctx) error {
var req BindWechatRequest
// WechatOAuthLogin 微信 OAuth 登录
// POST /api/c/v1/wechat/auth
func (h *PersonalCustomerHandler) WechatOAuthLogin(c *fiber.Ctx) error {
var req dto.WechatOAuthRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
// TODO: 实现完整的微信绑定流程
// 1. 从 context 中获取当前登录的客户 ID
// 2. 使用微信授权码换取 OpenID 和 UnionID
// 3. 调用 service 层的 BindWechat 方法绑定微信
result, err := h.service.WechatOAuthLogin(c.Context(), req.Code)
if err != nil {
h.logger.Error("微信 OAuth 登录失败",
zap.String("code", req.Code),
zap.Error(err),
)
return err
}
return response.Success(c, result)
}
// BindWechat 绑定微信
// POST /api/c/v1/bind-wechat
func (h *PersonalCustomerHandler) BindWechat(c *fiber.Ctx) error {
var req dto.WechatOAuthRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
customerID, ok := c.Locals("customer_id").(uint)
if !ok {
return errors.New(errors.CodeUnauthorized, "未找到客户信息")
}
if err := h.service.BindWechatWithCode(c.Context(), customerID, req.Code); err != nil {
h.logger.Error("绑定微信失败",
zap.Uint("customer_id", customerID),
zap.Error(err),
)
return err
}
return response.Success(c, fiber.Map{
"message": "微信绑定功能暂未实现,待微信 SDK 对接后启用",
"message": "绑定成功",
})
}

View File

@@ -1,38 +1,51 @@
package callback
import (
"context"
"net/http"
"github.com/gofiber/fiber/v2"
"github.com/valyala/fasthttp/fasthttpadaptor"
"github.com/break/junhong_cmp_fiber/internal/model"
orderService "github.com/break/junhong_cmp_fiber/internal/service/order"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/response"
"github.com/break/junhong_cmp_fiber/pkg/wechat"
)
type PaymentHandler struct {
orderService *orderService.Service
orderService *orderService.Service
wechatPayment wechat.PaymentServiceInterface
}
func NewPaymentHandler(orderService *orderService.Service) *PaymentHandler {
return &PaymentHandler{orderService: orderService}
}
type WechatPayCallbackRequest struct {
OrderNo string `json:"order_no" xml:"out_trade_no"`
func NewPaymentHandler(orderService *orderService.Service, wechatPayment wechat.PaymentServiceInterface) *PaymentHandler {
return &PaymentHandler{
orderService: orderService,
wechatPayment: wechatPayment,
}
}
// WechatPayCallback 微信支付回调(带签名验证)
// POST /api/callback/wechat-pay
func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
var req WechatPayCallbackRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
if h.wechatPayment == nil {
return errors.New(errors.CodeWechatCallbackInvalid, "微信支付服务未配置")
}
if req.OrderNo == "" {
return errors.New(errors.CodeInvalidParam, "订单号不能为空")
}
var httpReq http.Request
fasthttpadaptor.ConvertRequest(c.Context(), &httpReq, true)
if err := h.orderService.HandlePaymentCallback(c.UserContext(), req.OrderNo, model.PaymentMethodWechat); err != nil {
return err
ctx := context.Background()
_, err := h.wechatPayment.HandlePaymentNotify(&httpReq, func(result *wechat.PaymentNotifyResult) error {
if result.TradeState != "SUCCESS" {
return nil
}
return h.orderService.HandlePaymentCallback(ctx, result.OutTradeNo, model.PaymentMethodWechat)
})
if err != nil {
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "处理微信支付回调失败")
}
return response.Success(c, map[string]string{"return_code": "SUCCESS"})

View File

@@ -129,3 +129,79 @@ func (h *OrderHandler) WalletPay(c *fiber.Ctx) error {
return response.Success(c, nil)
}
// WechatPayJSAPI 微信 JSAPI 支付
// POST /api/h5/orders/:id/wechat-pay/jsapi
func (h *OrderHandler) WechatPayJSAPI(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的订单ID")
}
var req dto.WechatPayJSAPIRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
ctx := c.UserContext()
userType := middleware.GetUserTypeFromContext(ctx)
var buyerType string
var buyerID uint
switch userType {
case constants.UserTypeAgent:
buyerType = model.BuyerTypeAgent
buyerID = middleware.GetShopIDFromContext(ctx)
case constants.UserTypePersonalCustomer:
buyerType = model.BuyerTypePersonal
buyerID = middleware.GetCustomerIDFromContext(ctx)
default:
return errors.New(errors.CodeForbidden, "不支持的用户类型")
}
result, err := h.service.WechatPayJSAPI(ctx, uint(id), req.OpenID, buyerType, buyerID)
if err != nil {
return err
}
return response.Success(c, result)
}
// WechatPayH5 微信 H5 支付
// POST /api/h5/orders/:id/wechat-pay/h5
func (h *OrderHandler) WechatPayH5(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的订单ID")
}
var req dto.WechatPayH5Request
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
ctx := c.UserContext()
userType := middleware.GetUserTypeFromContext(ctx)
var buyerType string
var buyerID uint
switch userType {
case constants.UserTypeAgent:
buyerType = model.BuyerTypeAgent
buyerID = middleware.GetShopIDFromContext(ctx)
case constants.UserTypePersonalCustomer:
buyerType = model.BuyerTypePersonal
buyerID = middleware.GetCustomerIDFromContext(ctx)
default:
return errors.New(errors.CodeForbidden, "不支持的用户类型")
}
result, err := h.service.WechatPayH5(ctx, uint(id), &req.SceneInfo, buyerType, buyerID)
if err != nil {
return err
}
return response.Success(c, result)
}

View File

@@ -0,0 +1,47 @@
package dto
type WechatOAuthRequest struct {
Code string `json:"code" validate:"required" required:"true" description:"微信授权码"`
}
type WechatOAuthResponse struct {
AccessToken string `json:"access_token" description:"访问令牌"`
ExpiresIn int64 `json:"expires_in" description:"令牌有效期(秒)"`
Customer *PersonalCustomerResponse `json:"customer" description:"客户信息"`
}
type WechatPayJSAPIRequest struct {
OpenID string `json:"openid" validate:"required" required:"true" description:"用户OpenID"`
}
type WechatPayJSAPIResponse struct {
PrepayID string `json:"prepay_id" description:"预支付交易会话标识"`
PayConfig map[string]interface{} `json:"pay_config" description:"JSSDK支付配置"`
}
type WechatPayH5Request struct {
SceneInfo WechatH5SceneInfo `json:"scene_info" validate:"required" required:"true" description:"场景信息"`
}
type WechatH5SceneInfo struct {
PayerClientIP string `json:"payer_client_ip" validate:"required,ip" required:"true" description:"用户终端IP"`
H5Info WechatH5Detail `json:"h5_info" description:"H5场景信息"`
}
type WechatH5Detail struct {
Type string `json:"type" validate:"omitempty,oneof=iOS Android Wap" description:"场景类型 (iOS:苹果, Android:安卓, Wap:浏览器)"`
}
type WechatPayH5Response struct {
H5URL string `json:"h5_url" description:"微信支付跳转URL"`
}
type WechatPayJSAPIParams struct {
ID uint `path:"id" description:"订单ID" required:"true"`
WechatPayJSAPIRequest
}
type WechatPayH5Params struct {
ID uint `path:"id" description:"订单ID" required:"true"`
WechatPayH5Request
}

View File

@@ -78,6 +78,22 @@ func registerH5OrderRoutes(router fiber.Router, handler *h5.OrderHandler, doc *o
Output: nil,
Auth: true,
})
Register(router, doc, basePath, "POST", "/orders/:id/wechat-pay/jsapi", handler.WechatPayJSAPI, RouteSpec{
Summary: "微信 JSAPI 支付",
Tags: []string{"H5 订单"},
Input: new(dto.WechatPayJSAPIParams),
Output: new(dto.WechatPayJSAPIResponse),
Auth: true,
})
Register(router, doc, basePath, "POST", "/orders/:id/wechat-pay/h5", handler.WechatPayH5, RouteSpec{
Summary: "微信 H5 支付",
Tags: []string{"H5 订单"},
Input: new(dto.WechatPayH5Params),
Output: new(dto.WechatPayH5Response),
Auth: true,
})
}
// registerPaymentCallbackRoutes 注册支付回调路由

View File

@@ -6,6 +6,7 @@ import (
"github.com/break/junhong_cmp_fiber/internal/bootstrap"
apphandler "github.com/break/junhong_cmp_fiber/internal/handler/app"
"github.com/break/junhong_cmp_fiber/internal/middleware"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/openapi"
)
@@ -35,6 +36,16 @@ func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator,
Output: &apphandler.LoginResponse{},
})
// 微信 OAuth 登录(公开)
Register(publicGroup, doc, basePath, "POST", "/wechat/auth", handlers.PersonalCustomer.WechatOAuthLogin, RouteSpec{
Summary: "微信授权登录",
Description: "使用微信授权码登录,自动创建或关联用户",
Tags: []string{"个人客户 - 认证"},
Auth: false,
Input: &dto.WechatOAuthRequest{},
Output: &dto.WechatOAuthResponse{},
})
// 需要认证的路由
authGroup := router.Group("")
authGroup.Use(personalAuthMiddleware.Authenticate())
@@ -45,7 +56,7 @@ func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator,
Description: "绑定微信账号到当前个人客户",
Tags: []string{"个人客户 - 账户"},
Auth: true,
Input: &apphandler.BindWechatRequest{},
Input: &dto.WechatOAuthRequest{},
Output: nil,
})

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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
}