实现个人客户微信认证和短信验证功能
- 添加个人客户微信登录和手机验证码登录接口 - 实现个人客户设备、ICCID、手机号关联管理 - 添加短信发送服务(HTTP 客户端) - 添加微信认证服务(含 mock 实现) - 添加 JWT Token 生成和验证工具 - 创建数据库迁移脚本(personal_customer 关联表) - 修复测试文件中的路由注册参数错误 - 重构 scripts 目录结构(分离独立脚本到子目录) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
202
internal/handler/app/personal_customer.go
Normal file
202
internal/handler/app/personal_customer.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"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"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// PersonalCustomerHandler 个人客户处理器
|
||||
type PersonalCustomerHandler struct {
|
||||
service *personal_customer.Service
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewPersonalCustomerHandler 创建个人客户处理器实例
|
||||
func NewPersonalCustomerHandler(service *personal_customer.Service, logger *zap.Logger) *PersonalCustomerHandler {
|
||||
return &PersonalCustomerHandler{
|
||||
service: service,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SendCodeRequest 发送验证码请求
|
||||
type SendCodeRequest struct {
|
||||
Phone string `json:"phone" validate:"required,len=11"` // 手机号(11位)
|
||||
}
|
||||
|
||||
// SendCode 发送验证码
|
||||
// POST /api/c/v1/login/send-code
|
||||
func (h *PersonalCustomerHandler) SendCode(c *fiber.Ctx) error {
|
||||
var req SendCodeRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
if err := h.service.SendVerificationCode(c.Context(), req.Phone); err != nil {
|
||||
h.logger.Error("发送验证码失败",
|
||||
zap.String("phone", req.Phone),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, "发送验证码失败", err)
|
||||
}
|
||||
|
||||
return response.Success(c, fiber.Map{
|
||||
"message": "验证码已发送",
|
||||
})
|
||||
}
|
||||
|
||||
// LoginRequest 登录请求
|
||||
type LoginRequest struct {
|
||||
Phone string `json:"phone" validate:"required,len=11"` // 手机号(11位)
|
||||
Code string `json:"code" validate:"required,len=6"` // 验证码(6位)
|
||||
}
|
||||
|
||||
// LoginResponse 登录响应
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"` // 访问令牌
|
||||
Customer *PersonalCustomerDTO `json:"customer"` // 客户信息
|
||||
}
|
||||
|
||||
// PersonalCustomerDTO 个人客户 DTO
|
||||
type PersonalCustomerDTO struct {
|
||||
ID uint `json:"id"`
|
||||
Phone string `json:"phone"`
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
WxOpenID string `json:"wx_open_id"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// Login 登录(手机号 + 验证码)
|
||||
// POST /api/c/v1/login
|
||||
func (h *PersonalCustomerHandler) Login(c *fiber.Ctx) error {
|
||||
var req LoginRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
// 登录
|
||||
token, customer, err := h.service.LoginByPhone(c.Context(), req.Phone, req.Code)
|
||||
if err != nil {
|
||||
h.logger.Error("登录失败",
|
||||
zap.String("phone", req.Phone),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, "登录失败", err)
|
||||
}
|
||||
|
||||
// 构造响应
|
||||
// 注意:Phone 字段已从 PersonalCustomer 模型移除,需要从 PersonalCustomerPhone 表查询
|
||||
resp := &LoginResponse{
|
||||
Token: token,
|
||||
Customer: &PersonalCustomerDTO{
|
||||
ID: customer.ID,
|
||||
Phone: req.Phone, // 使用请求中的手机号(临时方案)
|
||||
Nickname: customer.Nickname,
|
||||
AvatarURL: customer.AvatarURL,
|
||||
WxOpenID: customer.WxOpenID,
|
||||
Status: customer.Status,
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
// TODO: 从 context 中获取当前登录的客户 ID
|
||||
// customerID := c.Locals("customer_id").(uint)
|
||||
|
||||
// TODO: 使用微信授权码换取 OpenID 和 UnionID
|
||||
// wxOpenID, wxUnionID, err := wechatService.GetUserInfo(req.Code)
|
||||
|
||||
// TODO: 绑定微信
|
||||
// if err := h.service.BindWechat(c.Context(), customerID, wxOpenID, wxUnionID); err != nil {
|
||||
// return errors.Wrap(errors.CodeInternalError, "绑定微信失败", err)
|
||||
// }
|
||||
|
||||
return response.Success(c, fiber.Map{
|
||||
"message": "微信绑定功能暂未实现,待微信 SDK 对接后启用",
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProfileRequest 更新个人资料请求
|
||||
type UpdateProfileRequest struct {
|
||||
Nickname string `json:"nickname"` // 昵称
|
||||
AvatarURL string `json:"avatar_url"` // 头像 URL
|
||||
}
|
||||
|
||||
// UpdateProfile 更新个人资料
|
||||
// PUT /api/c/v1/profile
|
||||
func (h *PersonalCustomerHandler) UpdateProfile(c *fiber.Ctx) error {
|
||||
var req UpdateProfileRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
// 从 context 中获取当前登录的客户 ID
|
||||
customerID, ok := c.Locals("customer_id").(uint)
|
||||
if !ok {
|
||||
return errors.New(errors.CodeUnauthorized, "未找到客户信息")
|
||||
}
|
||||
|
||||
if err := h.service.UpdateProfile(c.Context(), customerID, req.Nickname, req.AvatarURL); err != nil {
|
||||
h.logger.Error("更新个人资料失败",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, "更新个人资料失败", err)
|
||||
}
|
||||
|
||||
return response.Success(c, fiber.Map{
|
||||
"message": "更新成功",
|
||||
})
|
||||
}
|
||||
|
||||
// GetProfile 获取个人资料
|
||||
// GET /api/c/v1/profile
|
||||
func (h *PersonalCustomerHandler) GetProfile(c *fiber.Ctx) error {
|
||||
// 从 context 中获取当前登录的客户 ID
|
||||
customerID, ok := c.Locals("customer_id").(uint)
|
||||
if !ok {
|
||||
return errors.New(errors.CodeUnauthorized, "未找到客户信息")
|
||||
}
|
||||
|
||||
// 获取客户资料(包含主手机号)
|
||||
customer, phone, err := h.service.GetProfileWithPhone(c.Context(), customerID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取个人资料失败",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, "获取个人资料失败", err)
|
||||
}
|
||||
|
||||
// 构造响应
|
||||
resp := &PersonalCustomerDTO{
|
||||
ID: customer.ID,
|
||||
Phone: phone, // 使用查询到的主手机号
|
||||
Nickname: customer.Nickname,
|
||||
AvatarURL: customer.AvatarURL,
|
||||
WxOpenID: customer.WxOpenID,
|
||||
Status: customer.Status,
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
Reference in New Issue
Block a user