// Package personal_customer 提供个人客户管理的业务逻辑服务 // 包含个人客户注册、登录、微信绑定、短信验证等功能 package personal_customer 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 wechatOfficialAccount wechat.OfficialAccountServiceInterface logger *zap.Logger } // NewService 创建个人客户服务实例 func NewService( store *postgres.PersonalCustomerStore, 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, wechatOfficialAccount: wechatOfficialAccount, logger: logger, } } // SendVerificationCode 发送验证码 func (s *Service) SendVerificationCode(ctx context.Context, phone string) error { return s.verificationService.SendCode(ctx, phone) } // VerifyCode 验证验证码 func (s *Service) VerifyCode(ctx context.Context, phone string, code string) error { return s.verificationService.VerifyCode(ctx, phone, code) } // LoginByPhone 通过手机号登录 // 如果手机号不存在,自动创建新的个人客户 // 注意:此方法是临时实现,完整的登录流程应该是先微信授权,再绑定手机号 func (s *Service) LoginByPhone(ctx context.Context, phone string, code string) (string, *model.PersonalCustomer, error) { // 验证验证码 if err := s.verificationService.VerifyCode(ctx, phone, code); err != nil { s.logger.Warn("验证码验证失败", zap.String("phone", phone), zap.Error(err), ) return "", nil, err } // 查找或创建个人客户 customer, err := s.store.GetByPhone(ctx, phone) if err != nil { if err == gorm.ErrRecordNotFound { // 客户不存在,创建新客户 // 注意:临时实现,使用空的微信信息(正式应该先微信授权) customer = &model.PersonalCustomer{ WxOpenID: "", // 临时为空,后续需绑定微信 WxUnionID: "", // 临时为空,后续需绑定微信 Status: 1, // 默认启用 } if err := s.store.Create(ctx, customer); err != nil { s.logger.Error("创建个人客户失败", zap.String("phone", phone), zap.Error(err), ) return "", nil, errors.Wrap(errors.CodeInternalError, err, "创建个人客户失败") } // 创建手机号绑定记录 // TODO: 这里需要通过 PersonalCustomerPhoneStore 来创建 // 暂时跳过,等待 PersonalCustomerPhoneStore 实现 s.logger.Info("创建新个人客户", zap.Uint("customer_id", customer.ID), zap.String("phone", phone), ) } else { s.logger.Error("查询个人客户失败", zap.String("phone", phone), zap.Error(err), ) return "", nil, errors.Wrap(errors.CodeInternalError, err, "查询个人客户失败") } } // 检查客户状态 if customer.Status == 0 { s.logger.Warn("个人客户已被禁用", zap.Uint("customer_id", customer.ID), zap.String("phone", phone), ) return "", nil, errors.New(errors.CodeForbidden, "账号已被禁用") } // 生成 Token(临时传递 phone,后续应该从 Token 中移除 phone 字段) token, err := s.jwtManager.GeneratePersonalCustomerToken(customer.ID, phone) if err != nil { s.logger.Error("生成 Token 失败", zap.Uint("customer_id", customer.ID), zap.String("phone", phone), zap.Error(err), ) return "", nil, errors.Wrap(errors.CodeInternalError, err, "生成 Token 失败") } s.logger.Info("个人客户登录成功", zap.Uint("customer_id", customer.ID), zap.String("phone", phone), ) return token, customer, nil } // BindWechat 绑定微信信息 func (s *Service) BindWechat(ctx context.Context, customerID uint, wxOpenID, wxUnionID string) error { // 获取客户 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, "查询个人客户失败") } // 更新微信信息 customer.WxOpenID = wxOpenID customer.WxUnionID = wxUnionID 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("wx_open_id", wxOpenID), ) return nil } // UpdateProfile 更新个人资料 func (s *Service) UpdateProfile(ctx context.Context, customerID uint, nickname, avatarURL string) error { 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, "查询个人客户失败") } // 更新资料 if nickname != "" { customer.Nickname = nickname } if avatarURL != "" { customer.AvatarURL = avatarURL } 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), ) return nil } // GetProfile 获取个人资料 func (s *Service) GetProfile(ctx context.Context, customerID uint) (*model.PersonalCustomer, error) { customer, err := s.store.GetByID(ctx, customerID) if err != nil { s.logger.Error("查询个人客户失败", zap.Uint("customer_id", customerID), zap.Error(err), ) return nil, errors.Wrap(errors.CodeInternalError, err, "查询个人客户失败") } return customer, nil } // GetProfileWithPhone 获取个人资料(包含主手机号) func (s *Service) GetProfileWithPhone(ctx context.Context, customerID uint) (*model.PersonalCustomer, string, error) { // 获取客户信息 customer, err := s.store.GetByID(ctx, customerID) if err != nil { s.logger.Error("查询个人客户失败", zap.Uint("customer_id", customerID), zap.Error(err), ) return nil, "", errors.Wrap(errors.CodeInternalError, err, "查询个人客户失败") } // 获取主手机号 phone := "" primaryPhone, err := s.phoneStore.GetPrimaryPhone(ctx, customerID) if err != nil { if err != gorm.ErrRecordNotFound { s.logger.Error("查询主手机号失败", zap.Uint("customer_id", customerID), zap.Error(err), ) // 不返回错误,继续返回客户信息(手机号为空) } } else { phone = primaryPhone.Phone } 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 }