All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m2s
- 新增成对迁移 000223(tb_phone_asset_association,含有效关系部分唯一索引与 down 守卫)与 000224(解绑导入任务表),不回填历史 - H5:need_bind_phone 三支判定(开关关闭完全短路);已有主号幂等建联;十项上限按手机号 advisory 串行化(含换绑到全新号的并发场景);换绑原子迁移与冲突整单回滚;不写遗留列 - 后台:关联列表、单项/批量解绑、CSV 导入解绑(B1–B16),超管/平台 gate + 资产数据范围复核,三态统一文案 - 读侧:卡/设备列表与详情按页一次 IN 聚合;两类导出补「关联手机号」列并保留历史表头反解兼容 - 脱敏:关联审计走独立动作/资源只写脱敏手机号;访问日志手机号类字段脱敏 - 同步主 Spec openspec/specs/phone-asset-association 并归档 AUG26-009,补齐 requirement-evidence 与入口矩阵,context-health 通过
264 lines
8.0 KiB
Go
264 lines
8.0 KiB
Go
// Package accessaudit 定义账号权限与组织简单写用例的统一审计接缝。
|
||
package accessaudit
|
||
|
||
import (
|
||
"context"
|
||
stderrors "errors"
|
||
"strconv"
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
|
||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||
"github.com/break/junhong_cmp_fiber/pkg/auditfailure"
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
apperrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||
)
|
||
|
||
// ChangeAudit 是账号权限与组织变更交给统一审计 Port 的事实。
|
||
type ChangeAudit struct {
|
||
ActionCode string
|
||
Summary string
|
||
Result string
|
||
ErrorCode string
|
||
ErrorSummary string
|
||
OperatorID uint
|
||
ActorKind string
|
||
ActorName string
|
||
Source string
|
||
ScopeType string
|
||
Account *model.Account
|
||
Accounts []AccountChange
|
||
Shop *model.Shop
|
||
ParentShop *model.Shop
|
||
Enterprise *model.Enterprise
|
||
Cards []IotCardChange
|
||
CardAuthorizations []EnterpriseCardAuthorizationChange
|
||
Devices []DeviceChange
|
||
DeviceBindings []DeviceSimBindingChange
|
||
DeviceAuthorizations []EnterpriseDeviceAuthorizationChange
|
||
PersonalCustomer *model.PersonalCustomer
|
||
PersonalPhones []PersonalCustomerPhoneChange
|
||
PersonalOpenIDs []PersonalCustomerOpenIDChange
|
||
PersonalDevices []PersonalCustomerDeviceChange
|
||
PersonalICCIDs []PersonalCustomerICCIDChange
|
||
PhoneAssociations []PhoneAssetAssociationChange
|
||
Role *model.Role
|
||
Roles []RoleChange
|
||
Permissions []PermissionChange
|
||
BeforeData map[string]any
|
||
AfterData map[string]any
|
||
SubjectVisibility string
|
||
SubjectSummary string
|
||
SubjectData map[string]any
|
||
}
|
||
|
||
// PersonalCustomerPhoneChange 保存个人客户手机号资源变化。
|
||
type PersonalCustomerPhoneChange struct {
|
||
Phone *model.PersonalCustomerPhone
|
||
BeforeData map[string]any
|
||
AfterData map[string]any
|
||
}
|
||
|
||
// PhoneAssetAssociationChange 保存手机号—资产关联资源的前后变化。
|
||
// 手机号一律传入脱敏值:关联用例不得把完整手机号写入审计(ENG-LOG-001)。
|
||
// 关联指向的资产以资产类型与资产 ID 声明,由 Writer 组装为参考资源。
|
||
type PhoneAssetAssociationChange struct {
|
||
AssociationID uint
|
||
PhoneMasked string
|
||
AssetType string
|
||
AssetID uint
|
||
AssetDisplayName string
|
||
Status int
|
||
Source string
|
||
InvalidatedAt *time.Time
|
||
InvalidationMethod string
|
||
InvalidationReason string
|
||
BeforeData map[string]any
|
||
AfterData map[string]any
|
||
}
|
||
|
||
// PersonalCustomerOpenIDChange 保存个人客户微信主体资源变化。
|
||
type PersonalCustomerOpenIDChange struct {
|
||
OpenID *model.PersonalCustomerOpenID
|
||
BeforeData map[string]any
|
||
AfterData map[string]any
|
||
}
|
||
|
||
// PersonalCustomerDeviceChange 保存个人客户设备号绑定资源变化。
|
||
type PersonalCustomerDeviceChange struct {
|
||
Binding *model.PersonalCustomerDevice
|
||
Relation string
|
||
Role string
|
||
BeforeData map[string]any
|
||
AfterData map[string]any
|
||
}
|
||
|
||
// PersonalCustomerICCIDChange 保存个人客户 ICCID 绑定资源变化。
|
||
type PersonalCustomerICCIDChange struct {
|
||
Binding *model.PersonalCustomerICCID
|
||
Relation string
|
||
Role string
|
||
BeforeData map[string]any
|
||
AfterData map[string]any
|
||
}
|
||
|
||
// DeviceChange 保存组织操作关联设备的资源变化与主体安全投影。
|
||
type DeviceChange struct {
|
||
Device *model.Device
|
||
Relation string
|
||
Role string
|
||
BeforeData map[string]any
|
||
AfterData map[string]any
|
||
SubjectVisibility string
|
||
SubjectSummary string
|
||
SubjectData map[string]any
|
||
}
|
||
|
||
// DeviceSimBindingChange 保存企业设备授权涉及的卡槽绑定快照。
|
||
type DeviceSimBindingChange struct {
|
||
Binding *model.DeviceSimBinding
|
||
Relation string
|
||
Role string
|
||
BeforeData map[string]any
|
||
AfterData map[string]any
|
||
}
|
||
|
||
// EnterpriseDeviceAuthorizationChange 保存企业设备授权记录的直接变化。
|
||
type EnterpriseDeviceAuthorizationChange struct {
|
||
Authorization *model.EnterpriseDeviceAuthorization
|
||
Relation string
|
||
Role string
|
||
BeforeData map[string]any
|
||
AfterData map[string]any
|
||
}
|
||
|
||
// IotCardChange 保存组织操作关联卡的资源变化与主体安全投影。
|
||
type IotCardChange struct {
|
||
Card *model.IotCard
|
||
Relation string
|
||
Role string
|
||
BeforeData map[string]any
|
||
AfterData map[string]any
|
||
SubjectVisibility string
|
||
SubjectSummary string
|
||
SubjectData map[string]any
|
||
}
|
||
|
||
// EnterpriseCardAuthorizationChange 保存企业卡授权记录的直接变化。
|
||
type EnterpriseCardAuthorizationChange struct {
|
||
Authorization *model.EnterpriseCardAuthorization
|
||
BeforeData map[string]any
|
||
AfterData map[string]any
|
||
}
|
||
|
||
// AccountChange 保存店铺操作关联账号的资源角色与直接变化。
|
||
type AccountChange struct {
|
||
Account *model.Account
|
||
Relation string
|
||
Role string
|
||
BeforeData map[string]any
|
||
AfterData map[string]any
|
||
}
|
||
|
||
// RoleChange 保存主体授权中单个角色资源的前后变化。
|
||
type RoleChange struct {
|
||
Role *model.Role
|
||
BeforeData map[string]any
|
||
AfterData map[string]any
|
||
}
|
||
|
||
// PermissionChange 保存单个权限资源的前后变化。
|
||
type PermissionChange struct {
|
||
Permission *model.Permission
|
||
BeforeData map[string]any
|
||
AfterData map[string]any
|
||
}
|
||
|
||
// Writer 接收账号权限与组织事务内审计事实。
|
||
type Writer interface {
|
||
WriteAccessChange(context.Context, *gorm.DB, ChangeAudit) error
|
||
}
|
||
|
||
// RecordFailure 在业务回滚后使用独立短事务记录失败或拒绝事实。
|
||
func RecordFailure(ctx context.Context, db *gorm.DB, writer Writer, change ChangeAudit, originalErr error) {
|
||
fillFailure(changeError(originalErr), &change)
|
||
if db == nil || writer == nil {
|
||
recordSecondaryFailure(ctx, change, apperrors.New(apperrors.CodeInvalidStatus, "统一组织审计接缝未配置"))
|
||
return
|
||
}
|
||
if err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
return writer.WriteAccessChange(ctx, tx, change)
|
||
}); err != nil {
|
||
recordSecondaryFailure(ctx, change, err)
|
||
}
|
||
}
|
||
|
||
func changeError(err error) *apperrors.AppError {
|
||
var appErr *apperrors.AppError
|
||
if stderrors.As(err, &appErr) {
|
||
return appErr
|
||
}
|
||
return apperrors.New(apperrors.CodeInternalError, "账号权限或组织操作失败")
|
||
}
|
||
|
||
func fillFailure(appErr *apperrors.AppError, change *ChangeAudit) {
|
||
if change.Result == "" {
|
||
change.Result = constants.AuditResultFailed
|
||
}
|
||
if change.ErrorCode == "" {
|
||
change.ErrorCode = strconv.Itoa(appErr.Code)
|
||
}
|
||
if change.ErrorSummary == "" {
|
||
change.ErrorSummary = appErr.Message
|
||
}
|
||
}
|
||
|
||
func recordSecondaryFailure(ctx context.Context, change ChangeAudit, err error) {
|
||
value := auditcontext.From(ctx)
|
||
auditfailure.RecordSecondaryWriteFailure(
|
||
change.ActionCode, resourceKey(change), value.RequestID, value.CorrelationID, change.ErrorCode, err,
|
||
)
|
||
}
|
||
|
||
func resourceKey(change ChangeAudit) string {
|
||
if change.Account != nil {
|
||
if change.Account.ID != 0 {
|
||
return strconv.FormatUint(uint64(change.Account.ID), 10)
|
||
}
|
||
return change.Account.Username
|
||
}
|
||
if change.Enterprise != nil {
|
||
if change.Enterprise.ID != 0 {
|
||
return strconv.FormatUint(uint64(change.Enterprise.ID), 10)
|
||
}
|
||
return change.Enterprise.EnterpriseCode
|
||
}
|
||
if change.PersonalCustomer != nil {
|
||
return strconv.FormatUint(uint64(change.PersonalCustomer.ID), 10)
|
||
}
|
||
if change.Shop != nil {
|
||
if change.Shop.ID != 0 {
|
||
return strconv.FormatUint(uint64(change.Shop.ID), 10)
|
||
}
|
||
return change.Shop.ShopCode
|
||
}
|
||
if change.Role != nil {
|
||
if change.Role.ID != 0 {
|
||
return strconv.FormatUint(uint64(change.Role.ID), 10)
|
||
}
|
||
return change.Role.RoleName
|
||
}
|
||
for _, permission := range change.Permissions {
|
||
if permission.Permission == nil {
|
||
continue
|
||
}
|
||
if permission.Permission.ID != 0 {
|
||
return strconv.FormatUint(uint64(permission.Permission.ID), 10)
|
||
}
|
||
return permission.Permission.PermCode
|
||
}
|
||
return "unknown"
|
||
}
|