Files
junhong_cmp_fiber/internal/service/personal_customer/service.go
huang 409a68d60b
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m45s
feat: OpenAPI 契约对齐与框架优化
主要变更:
1. OpenAPI 文档契约对齐
   - 统一错误响应字段名为 msg(非 message)
   - 规范 envelope 响应结构(code, msg, data, timestamp)
   - 个人客户路由纳入文档体系(使用 Register 机制)
   - 新增 BuildDocHandlers() 统一管理 handler 构造
   - 确保文档生成的幂等性

2. Service 层错误处理统一
   - 全面替换 fmt.Errorf 为 errors.New/Wrap
   - 统一错误码使用规范
   - Handler 层参数校验不泄露底层细节
   - 新增错误码验证集成测试

3. 代码质量提升
   - 删除未使用的 Task handler 和路由
   - 新增代码规范检查脚本(check-service-errors.sh)
   - 新增注释路径一致性检查(check-comment-paths.sh)
   - 更新 API 文档生成指南

4. OpenSpec 归档
   - 归档 openapi-contract-alignment 变更(63 tasks)
   - 归档 service-error-unify-core 变更
   - 归档 service-error-unify-support 变更
   - 归档 code-cleanup-docs-update 变更
   - 归档 handler-validation-security 变更
   - 同步 delta specs 到主规范文件

影响范围:
- pkg/openapi: 新增 handlers.go,优化 generator.go
- internal/service/*: 48 个 service 文件错误处理统一
- internal/handler/admin: 优化参数校验错误提示
- internal/routes: 个人客户路由改造,删除 task 路由
- scripts: 新增 3 个代码检查脚本
- docs: 更新 OpenAPI 文档(15750+ 行)
- openspec/specs: 同步 3 个主规范文件

破坏性变更:无
向后兼容:是
2026-01-30 11:40:36 +08:00

239 lines
7.1 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 提供个人客户管理的业务逻辑服务
// 包含个人客户注册、登录、微信绑定、短信验证等功能
package personal_customer
import (
"context"
"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"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"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, 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
}