Files
junhong_cmp_fiber/internal/service/personal_customer/service.go
huang 9c6d4a3bd4 实现个人客户微信认证和短信验证功能
- 添加个人客户微信登录和手机验证码登录接口
- 实现个人客户设备、ICCID、手机号关联管理
- 添加短信发送服务(HTTP 客户端)
- 添加微信认证服务(含 mock 实现)
- 添加 JWT Token 生成和验证工具
- 创建数据库迁移脚本(personal_customer 关联表)
- 修复测试文件中的路由注册参数错误
- 重构 scripts 目录结构(分离独立脚本到子目录)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 11:42:38 +08:00

237 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package personal_customer
import (
"context"
"fmt"
"github.com/break/junhong_cmp_fiber/internal/model"
"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"
"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
}
// NewService 创建个人客户服务实例
func NewService(
store *postgres.PersonalCustomerStore,
phoneStore *postgres.PersonalCustomerPhoneStore,
verificationService *verification.Service,
jwtManager *auth.JWTManager,
logger *zap.Logger,
) *Service {
return &Service{
store: store,
phoneStore: phoneStore,
verificationService: verificationService,
jwtManager: jwtManager,
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, fmt.Errorf("验证码验证失败: %w", 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, fmt.Errorf("创建个人客户失败: %w", 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, fmt.Errorf("查询个人客户失败: %w", err)
}
}
// 检查客户状态
if customer.Status == 0 {
s.logger.Warn("个人客户已被禁用",
zap.Uint("customer_id", customer.ID),
zap.String("phone", phone),
)
return "", nil, fmt.Errorf("账号已被禁用")
}
// 生成 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, fmt.Errorf("生成 Token 失败: %w", err)
}
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 fmt.Errorf("查询个人客户失败: %w", 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 fmt.Errorf("更新微信信息失败: %w", 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 fmt.Errorf("查询个人客户失败: %w", 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 fmt.Errorf("更新个人资料失败: %w", 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, fmt.Errorf("查询个人客户失败: %w", 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, "", fmt.Errorf("查询个人客户失败: %w", 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
}