固化七月迭代审计治理进展以隔离线上热修
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 参数类型错误未通过。
This commit is contained in:
@@ -4,10 +4,12 @@ package client_auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/ArtisanCloud/PowerWeChat/v3/src/kernel"
|
||||
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/model/dto"
|
||||
customerBinding "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
|
||||
@@ -23,6 +25,7 @@ import (
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -52,6 +55,7 @@ type Service struct {
|
||||
logger *zap.Logger
|
||||
wechatCache kernel.CacheInterface
|
||||
customerBinding *customerBinding.Service
|
||||
accessAudit accessauditapp.Writer
|
||||
}
|
||||
|
||||
// New 创建 C 端认证服务实例
|
||||
@@ -68,6 +72,7 @@ func New(
|
||||
redisClient *redis.Client,
|
||||
logger *zap.Logger,
|
||||
binding *customerBinding.Service,
|
||||
accessAudit accessauditapp.Writer,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
@@ -83,6 +88,7 @@ func New(
|
||||
logger: logger,
|
||||
wechatCache: wechat.NewRedisCache(redisClient),
|
||||
customerBinding: binding,
|
||||
accessAudit: accessAudit,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,24 +299,38 @@ func (s *Service) BindPhone(ctx context.Context, customerID uint, req *dto.BindP
|
||||
if req == nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
if s.db == nil || s.accessAudit == nil {
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "个人客户审计接缝未配置")
|
||||
}
|
||||
if _, err := s.phoneStore.GetPrimaryPhone(ctx, customerID); err == nil {
|
||||
return nil, errors.New(errors.CodeAlreadyBoundPhone)
|
||||
appErr := errors.New(errors.CodeAlreadyBoundPhone)
|
||||
if customer, loadErr := s.customerStore.GetByID(ctx, customerID); loadErr == nil {
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneBound, "绑定个人手机号被拒绝", customer, nil, appErr)
|
||||
}
|
||||
return nil, appErr
|
||||
} else if err != gorm.ErrRecordNotFound {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询主手机号失败")
|
||||
}
|
||||
|
||||
if err := s.verificationService.VerifyCode(ctx, req.Phone, req.Code); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeVerificationCodeInvalid, err)
|
||||
customer, err := s.customerStore.GetByID(ctx, customerID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询个人客户失败")
|
||||
}
|
||||
if err := s.verificationService.VerifyCode(ctx, req.Phone, req.Code); err != nil {
|
||||
appErr := errors.Wrap(errors.CodeVerificationCodeInvalid, err)
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneBound, "绑定个人手机号被拒绝", customer, nil, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
if existed, err := s.phoneStore.GetByPhone(ctx, req.Phone); err == nil {
|
||||
appErr := errors.New(errors.CodeAlreadyBoundPhone)
|
||||
if existed.CustomerID != customerID {
|
||||
return nil, errors.New(errors.CodePhoneAlreadyBound)
|
||||
appErr = errors.New(errors.CodePhoneAlreadyBound)
|
||||
}
|
||||
return nil, errors.New(errors.CodeAlreadyBoundPhone)
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneBound, "绑定个人手机号被拒绝", customer, nil, appErr)
|
||||
return nil, appErr
|
||||
} else if err != gorm.ErrRecordNotFound {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询手机号绑定关系失败")
|
||||
appErr := errors.Wrap(errors.CodeInternalError, err, "查询手机号绑定关系失败")
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneBound, "绑定个人手机号失败", customer, nil, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
@@ -321,8 +341,38 @@ func (s *Service) BindPhone(ctx context.Context, customerID uint, req *dto.BindP
|
||||
VerifiedAt: &now,
|
||||
Status: 1,
|
||||
}
|
||||
if err := s.phoneStore.Create(ctx, record); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "创建手机号绑定记录失败")
|
||||
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, "查询个人客户失败")
|
||||
}
|
||||
var count int64
|
||||
if err := tx.Model(&model.PersonalCustomerPhone{}).
|
||||
Where("customer_id = ? AND is_primary = ? AND status = ?", customerID, true, 1).Count(&count).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "查询主手机号失败")
|
||||
}
|
||||
if count > 0 {
|
||||
return errors.New(errors.CodeAlreadyBoundPhone)
|
||||
}
|
||||
var existed model.PersonalCustomerPhone
|
||||
if err := tx.Where("phone = ? AND status = ?", req.Phone, 1).First(&existed).Error; err == nil {
|
||||
if existed.CustomerID != customerID {
|
||||
return errors.New(errors.CodePhoneAlreadyBound)
|
||||
}
|
||||
return errors.New(errors.CodeAlreadyBoundPhone)
|
||||
} else if err != gorm.ErrRecordNotFound {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "查询手机号绑定关系失败")
|
||||
}
|
||||
if err := tx.Create(record).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "创建手机号绑定记录失败")
|
||||
}
|
||||
return s.accessAudit.WriteAccessChange(ctx, tx, personalPhoneAudit(
|
||||
constants.AuditActionPersonalCustomerPhoneBound, "绑定个人手机号", customer, record, nil,
|
||||
map[string]any{"phone": record.Phone}, "手机号已绑定", constants.AuditResultSuccess,
|
||||
))
|
||||
})
|
||||
if err != nil {
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneBound, "绑定个人手机号失败", customer, nil, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.BindPhoneResponse{
|
||||
@@ -336,41 +386,86 @@ func (s *Service) ChangePhone(ctx context.Context, customerID uint, req *dto.Cha
|
||||
if req == nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
if s.db == nil || s.accessAudit == nil {
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "个人客户审计接缝未配置")
|
||||
}
|
||||
customer, err := s.customerStore.GetByID(ctx, customerID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询个人客户失败")
|
||||
}
|
||||
primary, err := s.phoneStore.GetPrimaryPhone(ctx, customerID)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeOldPhoneMismatch)
|
||||
appErr := errors.New(errors.CodeOldPhoneMismatch)
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneChanged, "更换个人手机号被拒绝", customer, nil, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询主手机号失败")
|
||||
appErr := errors.Wrap(errors.CodeInternalError, err, "查询主手机号失败")
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneChanged, "更换个人手机号失败", customer, nil, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
if primary.Phone != req.OldPhone {
|
||||
return nil, errors.New(errors.CodeOldPhoneMismatch)
|
||||
appErr := errors.New(errors.CodeOldPhoneMismatch)
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneChanged, "更换个人手机号被拒绝", customer, primary, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
if err := s.verificationService.VerifyCode(ctx, req.OldPhone, req.OldCode); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeVerificationCodeInvalid, err)
|
||||
appErr := errors.Wrap(errors.CodeVerificationCodeInvalid, err)
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneChanged, "更换个人手机号被拒绝", customer, primary, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
if err := s.verificationService.VerifyCode(ctx, req.NewPhone, req.NewCode); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeVerificationCodeInvalid, err)
|
||||
}
|
||||
|
||||
if existed, err := s.phoneStore.GetByPhone(ctx, req.NewPhone); err == nil && existed.CustomerID != customerID {
|
||||
return nil, errors.New(errors.CodePhoneAlreadyBound)
|
||||
} else if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询新手机号绑定关系失败")
|
||||
appErr := errors.Wrap(errors.CodeVerificationCodeInvalid, err)
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneChanged, "更换个人手机号被拒绝", customer, primary, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if err := s.db.WithContext(ctx).Model(&model.PersonalCustomerPhone{}).
|
||||
Where("id = ? AND customer_id = ?", primary.ID, customerID).
|
||||
Updates(map[string]any{
|
||||
var beforeData map[string]any
|
||||
var failurePhone *model.PersonalCustomerPhone
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("id = ? AND customer_id = ? AND is_primary = ? AND status = ?", primary.ID, customerID, true, 1).
|
||||
First(primary).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeOldPhoneMismatch)
|
||||
}
|
||||
return errors.Wrap(errors.CodeInternalError, err, "查询主手机号失败")
|
||||
}
|
||||
if primary.Phone != req.OldPhone {
|
||||
return errors.New(errors.CodeOldPhoneMismatch)
|
||||
}
|
||||
current := *primary
|
||||
failurePhone = ¤t
|
||||
beforeData = map[string]any{"phone": primary.Phone}
|
||||
var existed model.PersonalCustomerPhone
|
||||
if err := tx.Where("phone = ? AND status = ?", req.NewPhone, 1).First(&existed).Error; err == nil && existed.CustomerID != customerID {
|
||||
return errors.New(errors.CodePhoneAlreadyBound)
|
||||
} else if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "查询新手机号绑定关系失败")
|
||||
}
|
||||
if err := tx.Model(primary).Updates(map[string]any{
|
||||
"phone": req.NewPhone,
|
||||
"verified_at": now,
|
||||
"updated_at": now,
|
||||
}).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "更新手机号失败")
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新手机号失败")
|
||||
}
|
||||
primary.Phone = req.NewPhone
|
||||
primary.VerifiedAt = &now
|
||||
return s.accessAudit.WriteAccessChange(ctx, tx, personalPhoneAudit(
|
||||
constants.AuditActionPersonalCustomerPhoneChanged, "更换个人手机号", customer, primary, beforeData,
|
||||
map[string]any{"phone": primary.Phone}, "手机号已更换", constants.AuditResultSuccess,
|
||||
))
|
||||
})
|
||||
if err != nil {
|
||||
if failurePhone == nil {
|
||||
failurePhone = primary
|
||||
}
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneChanged, "更换个人手机号失败", customer, failurePhone, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.ChangePhoneResponse{
|
||||
@@ -379,6 +474,56 @@ func (s *Service) ChangePhone(ctx context.Context, customerID uint, req *dto.Cha
|
||||
}, nil
|
||||
}
|
||||
|
||||
func personalPhoneAudit(
|
||||
actionCode, summary string,
|
||||
customer *model.PersonalCustomer,
|
||||
phone *model.PersonalCustomerPhone,
|
||||
beforeData, afterData map[string]any,
|
||||
subjectSummary, result string,
|
||||
) accessauditapp.ChangeAudit {
|
||||
change := accessauditapp.ChangeAudit{
|
||||
ActionCode: actionCode, Summary: summary, Result: result,
|
||||
OperatorID: customer.ID, ActorKind: constants.AuditActorPersonalCustomer, ActorName: customer.Nickname,
|
||||
Source: constants.AuditSourcePersonalAPI, ScopeType: constants.AuditScopePersonalCustomer,
|
||||
PersonalCustomer: customer, SubjectVisibility: constants.AuditSubjectDetail,
|
||||
SubjectSummary: subjectSummary, SubjectData: afterData,
|
||||
}
|
||||
if phone != nil && phone.ID != 0 {
|
||||
change.PersonalPhones = []accessauditapp.PersonalCustomerPhoneChange{{
|
||||
Phone: phone, BeforeData: beforeData, AfterData: afterData,
|
||||
}}
|
||||
}
|
||||
return change
|
||||
}
|
||||
|
||||
func (s *Service) recordPersonalFailure(
|
||||
ctx context.Context,
|
||||
actionCode, summary string,
|
||||
customer *model.PersonalCustomer,
|
||||
phone *model.PersonalCustomerPhone,
|
||||
originalErr error,
|
||||
) {
|
||||
if customer == nil || customer.ID == 0 {
|
||||
return
|
||||
}
|
||||
subjectSummary := "个人身份资料操作失败"
|
||||
change := personalPhoneAudit(actionCode, summary, customer, phone, nil, nil, subjectSummary, personalAuditFailureResult(originalErr))
|
||||
accessauditapp.RecordFailure(ctx, s.db, s.accessAudit, change, originalErr)
|
||||
}
|
||||
|
||||
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,
|
||||
errors.CodeAlreadyBoundPhone, errors.CodePhoneAlreadyBound, errors.CodeOldPhoneMismatch,
|
||||
errors.CodeVerificationCodeInvalid:
|
||||
return constants.AuditResultDenied
|
||||
}
|
||||
}
|
||||
return constants.AuditResultFailed
|
||||
}
|
||||
|
||||
// Logout A7 退出登录
|
||||
func (s *Service) Logout(ctx context.Context, customerID uint) (*dto.LogoutResponse, error) {
|
||||
redisKey := constants.RedisPersonalCustomerTokenKey(customerID)
|
||||
@@ -509,13 +654,20 @@ func (s *Service) loginByOpenID(
|
||||
avatar string,
|
||||
appType string,
|
||||
) (uint, bool, error) {
|
||||
if s.db == nil || s.accessAudit == nil {
|
||||
return 0, false, errors.New(errors.CodeInvalidStatus, "个人客户审计接缝未配置")
|
||||
}
|
||||
var (
|
||||
customerID uint
|
||||
isNewUser bool
|
||||
customerID uint
|
||||
isNewUser bool
|
||||
identityAudit *accessauditapp.ChangeAudit
|
||||
)
|
||||
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
cid, created, findErr := s.findOrCreateCustomer(ctx, tx, appID, openID, unionID, nickname, avatar, appType)
|
||||
cid, created, change, findErr := s.findOrCreateCustomer(ctx, tx, appID, openID, unionID, nickname, avatar, appType)
|
||||
customerID = cid
|
||||
identityAudit = change
|
||||
isNewUser = created
|
||||
if findErr != nil {
|
||||
return findErr
|
||||
}
|
||||
@@ -523,11 +675,26 @@ func (s *Service) loginByOpenID(
|
||||
return bindErr
|
||||
}
|
||||
|
||||
customerID = cid
|
||||
isNewUser = created
|
||||
if identityAudit != nil {
|
||||
return s.accessAudit.WriteAccessChange(ctx, tx, *identityAudit)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if identityAudit != nil && customerID != 0 && !isNewUser {
|
||||
if identityAudit.ActionCode == constants.AuditActionPersonalCustomerProfileUpdated {
|
||||
identityAudit.Summary = "同步个人资料失败"
|
||||
identityAudit.SubjectSummary = "个人资料同步失败"
|
||||
} else {
|
||||
identityAudit.Summary = "同步个人微信主体失败"
|
||||
identityAudit.SubjectSummary = "微信登录身份同步失败"
|
||||
}
|
||||
identityAudit.Result = personalAuditFailureResult(err)
|
||||
identityAudit.SubjectData = nil
|
||||
identityAudit.PersonalOpenIDs = nil
|
||||
restorePersonalCustomerSnapshot(identityAudit)
|
||||
accessauditapp.RecordFailure(ctx, s.db, s.accessAudit, *identityAudit, err)
|
||||
}
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
@@ -544,7 +711,7 @@ func (s *Service) findOrCreateCustomer(
|
||||
nickname string,
|
||||
avatar string,
|
||||
appType string,
|
||||
) (uint, bool, error) {
|
||||
) (uint, bool, *accessauditapp.ChangeAudit, error) {
|
||||
openidStore := postgres.NewPersonalCustomerOpenIDStore(tx)
|
||||
customerStore := postgres.NewPersonalCustomerStore(tx, s.redis)
|
||||
|
||||
@@ -552,26 +719,36 @@ func (s *Service) findOrCreateCustomer(
|
||||
customer, getErr := customerStore.GetByID(ctx, existed.CustomerID)
|
||||
if getErr != nil {
|
||||
if getErr == gorm.ErrRecordNotFound {
|
||||
return 0, false, errors.New(errors.CodeCustomerNotFound)
|
||||
return 0, false, nil, errors.New(errors.CodeCustomerNotFound)
|
||||
}
|
||||
return 0, false, errors.Wrap(errors.CodeInternalError, getErr, "查询客户失败")
|
||||
return 0, false, nil, errors.Wrap(errors.CodeInternalError, getErr, "查询客户失败")
|
||||
}
|
||||
if customer.Status == 0 {
|
||||
return 0, false, errors.New(errors.CodeForbidden, "账号已被禁用")
|
||||
change := personalWechatAudit(customer, nil, nil, nil, appID, appType, constants.AuditResultDenied)
|
||||
return customer.ID, false, &change, errors.New(errors.CodeForbidden, "账号已被禁用")
|
||||
}
|
||||
|
||||
beforeData := personalCustomerProfileData(customer)
|
||||
changed := false
|
||||
if nickname != "" && customer.Nickname != nickname {
|
||||
customer.Nickname = nickname
|
||||
changed = true
|
||||
}
|
||||
if avatar != "" && customer.AvatarURL != avatar {
|
||||
customer.AvatarURL = avatar
|
||||
changed = true
|
||||
}
|
||||
var change *accessauditapp.ChangeAudit
|
||||
if changed {
|
||||
pending := personalProfileSyncAudit(customer, beforeData)
|
||||
change = &pending
|
||||
}
|
||||
if saveErr := customerStore.Update(ctx, customer); saveErr != nil {
|
||||
return 0, false, errors.Wrap(errors.CodeInternalError, saveErr, "更新客户信息失败")
|
||||
return customer.ID, false, change, errors.Wrap(errors.CodeInternalError, saveErr, "更新客户信息失败")
|
||||
}
|
||||
return customer.ID, false, nil
|
||||
return customer.ID, false, change, nil
|
||||
} else if err != gorm.ErrRecordNotFound {
|
||||
return 0, false, errors.Wrap(errors.CodeInternalError, err, "查询 OpenID 记录失败")
|
||||
return 0, false, nil, errors.Wrap(errors.CodeInternalError, err, "查询 OpenID 记录失败")
|
||||
}
|
||||
|
||||
if unionID != "" {
|
||||
@@ -579,14 +756,16 @@ func (s *Service) findOrCreateCustomer(
|
||||
customer, getErr := customerStore.GetByID(ctx, existed.CustomerID)
|
||||
if getErr != nil {
|
||||
if getErr == gorm.ErrRecordNotFound {
|
||||
return 0, false, errors.New(errors.CodeCustomerNotFound)
|
||||
return 0, false, nil, errors.New(errors.CodeCustomerNotFound)
|
||||
}
|
||||
return 0, false, errors.Wrap(errors.CodeInternalError, getErr, "查询客户失败")
|
||||
return 0, false, nil, errors.Wrap(errors.CodeInternalError, getErr, "查询客户失败")
|
||||
}
|
||||
if customer.Status == 0 {
|
||||
return 0, false, errors.New(errors.CodeForbidden, "账号已被禁用")
|
||||
change := personalWechatAudit(customer, nil, nil, nil, appID, appType, constants.AuditResultDenied)
|
||||
return customer.ID, false, &change, errors.New(errors.CodeForbidden, "账号已被禁用")
|
||||
}
|
||||
|
||||
beforeData := personalCustomerProfileData(customer)
|
||||
record := &model.PersonalCustomerOpenID{
|
||||
CustomerID: customer.ID,
|
||||
AppID: appID,
|
||||
@@ -594,8 +773,9 @@ func (s *Service) findOrCreateCustomer(
|
||||
UnionID: unionID,
|
||||
AppType: appType,
|
||||
}
|
||||
change := personalWechatAudit(customer, record, beforeData, nil, appID, appType, constants.AuditResultSuccess)
|
||||
if createErr := openidStore.Create(ctx, record); createErr != nil {
|
||||
return 0, false, errors.Wrap(errors.CodeInternalError, createErr, "创建 OpenID 关联失败")
|
||||
return customer.ID, false, &change, errors.Wrap(errors.CodeInternalError, createErr, "创建 OpenID 关联失败")
|
||||
}
|
||||
|
||||
if nickname != "" && customer.Nickname != nickname {
|
||||
@@ -605,12 +785,14 @@ func (s *Service) findOrCreateCustomer(
|
||||
customer.AvatarURL = avatar
|
||||
}
|
||||
if saveErr := customerStore.Update(ctx, customer); saveErr != nil {
|
||||
return 0, false, errors.Wrap(errors.CodeInternalError, saveErr, "更新客户信息失败")
|
||||
change = personalWechatAudit(customer, record, beforeData, personalCustomerProfileData(customer), appID, appType, constants.AuditResultSuccess)
|
||||
return customer.ID, false, &change, errors.Wrap(errors.CodeInternalError, saveErr, "更新客户信息失败")
|
||||
}
|
||||
|
||||
return customer.ID, false, nil
|
||||
change = personalWechatAudit(customer, record, beforeData, personalCustomerProfileData(customer), appID, appType, constants.AuditResultSuccess)
|
||||
return customer.ID, false, &change, nil
|
||||
} else if err != gorm.ErrRecordNotFound {
|
||||
return 0, false, errors.Wrap(errors.CodeInternalError, err, "按 UnionID 查询失败")
|
||||
return 0, false, nil, errors.Wrap(errors.CodeInternalError, err, "按 UnionID 查询失败")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,7 +804,7 @@ func (s *Service) findOrCreateCustomer(
|
||||
Status: 1,
|
||||
}
|
||||
if err := customerStore.Create(ctx, newCustomer); err != nil {
|
||||
return 0, false, errors.Wrap(errors.CodeInternalError, err, "创建客户失败")
|
||||
return 0, false, nil, errors.Wrap(errors.CodeInternalError, err, "创建客户失败")
|
||||
}
|
||||
|
||||
record := &model.PersonalCustomerOpenID{
|
||||
@@ -632,11 +814,64 @@ func (s *Service) findOrCreateCustomer(
|
||||
UnionID: unionID,
|
||||
AppType: appType,
|
||||
}
|
||||
change := personalWechatAudit(newCustomer, record, nil, personalCustomerProfileData(newCustomer), appID, appType, constants.AuditResultSuccess)
|
||||
if err := openidStore.Create(ctx, record); err != nil {
|
||||
return 0, false, errors.Wrap(errors.CodeInternalError, err, "创建 OpenID 关联失败")
|
||||
return newCustomer.ID, true, &change, errors.Wrap(errors.CodeInternalError, err, "创建 OpenID 关联失败")
|
||||
}
|
||||
|
||||
return newCustomer.ID, true, nil
|
||||
change = personalWechatAudit(newCustomer, record, nil, personalCustomerProfileData(newCustomer), appID, appType, constants.AuditResultSuccess)
|
||||
return newCustomer.ID, true, &change, nil
|
||||
}
|
||||
|
||||
func personalProfileSyncAudit(customer *model.PersonalCustomer, beforeData map[string]any) accessauditapp.ChangeAudit {
|
||||
return accessauditapp.ChangeAudit{
|
||||
ActionCode: constants.AuditActionPersonalCustomerProfileUpdated, Summary: "同步个人资料", Result: constants.AuditResultSuccess,
|
||||
OperatorID: customer.ID, ActorKind: constants.AuditActorPersonalCustomer, ActorName: customer.Nickname,
|
||||
Source: constants.AuditSourcePersonalAPI, ScopeType: constants.AuditScopePersonalCustomer,
|
||||
PersonalCustomer: customer, BeforeData: beforeData, AfterData: personalCustomerProfileData(customer),
|
||||
SubjectVisibility: constants.AuditSubjectDetail, SubjectSummary: "个人资料已同步",
|
||||
SubjectData: personalCustomerProfileData(customer),
|
||||
}
|
||||
}
|
||||
|
||||
func personalWechatAudit(
|
||||
customer *model.PersonalCustomer,
|
||||
openID *model.PersonalCustomerOpenID,
|
||||
beforeData, afterData map[string]any,
|
||||
appID, appType, result string,
|
||||
) accessauditapp.ChangeAudit {
|
||||
change := accessauditapp.ChangeAudit{
|
||||
ActionCode: constants.AuditActionPersonalCustomerWechatIdentityUpdated, Summary: "同步个人微信主体", Result: result,
|
||||
OperatorID: customer.ID, ActorKind: constants.AuditActorPersonalCustomer, ActorName: customer.Nickname,
|
||||
Source: constants.AuditSourcePersonalAPI, ScopeType: constants.AuditScopePersonalCustomer,
|
||||
PersonalCustomer: customer, BeforeData: beforeData, AfterData: afterData,
|
||||
SubjectVisibility: constants.AuditSubjectDetail, SubjectSummary: "微信登录身份已同步",
|
||||
SubjectData: map[string]any{"app_id": appID, "app_type": appType},
|
||||
}
|
||||
if openID != nil && openID.ID != 0 {
|
||||
change.PersonalOpenIDs = []accessauditapp.PersonalCustomerOpenIDChange{{
|
||||
OpenID: openID, AfterData: map[string]any{"app_id": openID.AppID, "app_type": openID.AppType},
|
||||
}}
|
||||
}
|
||||
return change
|
||||
}
|
||||
|
||||
func personalCustomerProfileData(customer *model.PersonalCustomer) map[string]any {
|
||||
return map[string]any{"nickname": customer.Nickname, "avatar_url": customer.AvatarURL}
|
||||
}
|
||||
|
||||
func restorePersonalCustomerSnapshot(change *accessauditapp.ChangeAudit) {
|
||||
if change.PersonalCustomer == nil || change.BeforeData == nil {
|
||||
return
|
||||
}
|
||||
customer := *change.PersonalCustomer
|
||||
if nickname, ok := change.BeforeData["nickname"].(string); ok {
|
||||
customer.Nickname = nickname
|
||||
}
|
||||
if avatarURL, ok := change.BeforeData["avatar_url"].(string); ok {
|
||||
customer.AvatarURL = avatarURL
|
||||
}
|
||||
change.PersonalCustomer = &customer
|
||||
}
|
||||
|
||||
// checkCardBoundToDevice 检查卡是否绑定了设备
|
||||
@@ -703,6 +938,9 @@ func (s *Service) issueLoginToken(ctx context.Context, customerID uint, assetTyp
|
||||
// 根据资产标识符查找或创建测试客户并直接签发 JWT,无需微信 OAuth
|
||||
// ⚠️ 仅限 logging.development=true 时由路由层暴露,严禁生产环境调用
|
||||
func (s *Service) DevLogin(ctx context.Context, identifier string) (string, uint, bool, error) {
|
||||
if s.db == nil || s.accessAudit == nil {
|
||||
return "", 0, false, errors.New(errors.CodeInvalidStatus, "个人客户审计接缝未配置")
|
||||
}
|
||||
assetType, assetID, _, err := s.resolveAsset(ctx, identifier)
|
||||
if err != nil {
|
||||
return "", 0, false, err
|
||||
@@ -719,13 +957,18 @@ func (s *Service) DevLogin(ctx context.Context, identifier string) (string, uint
|
||||
devOpenID := "dev_test_" + identifier
|
||||
devAppID := "dev_test_app"
|
||||
|
||||
cid, created, findErr := s.findOrCreateCustomer(ctx, tx, devAppID, devOpenID, "", "测试用户", "", "dev")
|
||||
cid, created, identityAudit, findErr := s.findOrCreateCustomer(ctx, tx, devAppID, devOpenID, "", "测试用户", "", "dev")
|
||||
if findErr != nil {
|
||||
return findErr
|
||||
}
|
||||
if bindErr := s.bindAsset(ctx, tx, cid, assetType, assetID); bindErr != nil {
|
||||
return bindErr
|
||||
}
|
||||
if identityAudit != nil {
|
||||
if auditErr := s.accessAudit.WriteAccessChange(ctx, tx, *identityAudit); auditErr != nil {
|
||||
return auditErr
|
||||
}
|
||||
}
|
||||
customerID = cid
|
||||
isNewUser = created
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user