Constraint: 切换 main 前必须保存当前七月分支全部项目进展,套餐生效提案仅属于 Iteration/7-11。 Rejected: 将七月套餐修复直接移植到 main | 两个分支的可靠投递架构不同。 Confidence: medium Scope-risk: broad Directive: 不得将本提交整体 cherry-pick 到 main;main 套餐热修必须基于其纯 Asynq 代码独立实施。 Tested: git diff --check;openspec validate fix-package-activation-starvation --strict。 Not-tested: 按用户要求未运行自动化测试;go build ./... 因当前审计改造中的 Enterprise 模型字面量和 role.recordFailure 参数类型错误未通过。
152 lines
5.0 KiB
Go
152 lines
5.0 KiB
Go
// Package personal_customer 提供个人客户资料管理的业务逻辑服务
|
|
package personal_customer
|
|
|
|
import (
|
|
"context"
|
|
stderrors "errors"
|
|
|
|
accessauditapp "github.com/break/junhong_cmp_fiber/internal/application/accessaudit"
|
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
// Service 个人客户服务
|
|
type Service struct {
|
|
db *gorm.DB
|
|
store *postgres.PersonalCustomerStore
|
|
phoneStore *postgres.PersonalCustomerPhoneStore
|
|
logger *zap.Logger
|
|
accessAudit accessauditapp.Writer
|
|
}
|
|
|
|
// NewService 创建个人客户服务实例
|
|
func NewService(
|
|
db *gorm.DB,
|
|
store *postgres.PersonalCustomerStore,
|
|
phoneStore *postgres.PersonalCustomerPhoneStore,
|
|
logger *zap.Logger,
|
|
accessAudit accessauditapp.Writer,
|
|
) *Service {
|
|
return &Service{
|
|
db: db,
|
|
store: store,
|
|
phoneStore: phoneStore,
|
|
logger: logger,
|
|
accessAudit: accessAudit,
|
|
}
|
|
}
|
|
|
|
// UpdateProfile 更新个人资料
|
|
func (s *Service) UpdateProfile(ctx context.Context, customerID uint, nickname, avatarURL string) error {
|
|
if s.db == nil || s.accessAudit == nil {
|
|
return errors.New(errors.CodeInvalidStatus, "个人客户审计接缝未配置")
|
|
}
|
|
|
|
customer := &model.PersonalCustomer{Model: gorm.Model{ID: customerID}}
|
|
failureCustomer := customer
|
|
var beforeData map[string]any
|
|
loaded := false
|
|
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(customer, customerID).Error; err != nil {
|
|
return errors.Wrap(errors.CodeInternalError, err, "查询个人客户失败")
|
|
}
|
|
loaded = true
|
|
beforeData = personalProfileAuditData(customer)
|
|
current := *customer
|
|
failureCustomer = ¤t
|
|
if nickname != "" {
|
|
customer.Nickname = nickname
|
|
}
|
|
if avatarURL != "" {
|
|
customer.AvatarURL = avatarURL
|
|
}
|
|
if err := tx.Save(customer).Error; err != nil {
|
|
return errors.Wrap(errors.CodeInternalError, err, "更新个人资料失败")
|
|
}
|
|
return s.accessAudit.WriteAccessChange(ctx, tx, personalProfileAudit(customer, beforeData, constants.AuditResultSuccess))
|
|
})
|
|
if err != nil {
|
|
if !loaded {
|
|
s.logger.Error("查询个人客户失败", zap.Uint("customer_id", customerID), zap.Error(err))
|
|
return err
|
|
}
|
|
s.logger.Error("更新个人资料失败",
|
|
zap.Uint("customer_id", customerID),
|
|
zap.Error(err),
|
|
)
|
|
failure := personalProfileAudit(failureCustomer, beforeData, personalAuditFailureResult(err))
|
|
failure.Summary = "更新个人资料失败"
|
|
failure.SubjectSummary = "个人资料更新失败"
|
|
failure.SubjectData = nil
|
|
accessauditapp.RecordFailure(ctx, s.db, s.accessAudit, failure, err)
|
|
return err
|
|
}
|
|
|
|
s.logger.Info("更新个人资料成功",
|
|
zap.Uint("customer_id", customerID),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
func personalProfileAudit(customer *model.PersonalCustomer, beforeData map[string]any, result string) accessauditapp.ChangeAudit {
|
|
return accessauditapp.ChangeAudit{
|
|
ActionCode: constants.AuditActionPersonalCustomerProfileUpdated, Summary: "更新个人资料", Result: result,
|
|
OperatorID: customer.ID, ActorKind: constants.AuditActorPersonalCustomer, ActorName: customer.Nickname,
|
|
Source: constants.AuditSourcePersonalAPI, ScopeType: constants.AuditScopePersonalCustomer,
|
|
PersonalCustomer: customer, BeforeData: beforeData, AfterData: personalProfileAuditData(customer),
|
|
SubjectVisibility: constants.AuditSubjectDetail, SubjectSummary: "个人资料已更新",
|
|
SubjectData: map[string]any{"nickname": customer.Nickname, "avatar_url": customer.AvatarURL},
|
|
}
|
|
}
|
|
|
|
func personalProfileAuditData(customer *model.PersonalCustomer) map[string]any {
|
|
return map[string]any{"nickname": customer.Nickname, "avatar_url": customer.AvatarURL}
|
|
}
|
|
|
|
func personalAuditFailureResult(err error) string {
|
|
var appErr *errors.AppError
|
|
if stderrors.As(err, &appErr) {
|
|
switch appErr.Code {
|
|
case errors.CodeForbidden, errors.CodeInvalidParam, errors.CodeNotFound, errors.CodeCustomerNotFound:
|
|
return constants.AuditResultDenied
|
|
}
|
|
}
|
|
return constants.AuditResultFailed
|
|
}
|
|
|
|
// 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
|
|
}
|