Files
junhong_cmp_fiber/internal/handler/app/personal_customer.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

203 lines
6.0 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 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)
}