feat(手机号资产关联): AUG26-009 手机号—资产关联、十项上限与后台解绑
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m2s
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 通过
This commit is contained in:
@@ -189,7 +189,7 @@ func accessChangeResourceKey(change accessauditapp.ChangeAudit) string {
|
||||
}
|
||||
|
||||
func accessResources(change accessauditapp.ChangeAudit, primaryResource string) ([]ResourceInput, error) {
|
||||
resources := make([]ResourceInput, 0, 2+len(change.Accounts)+len(change.Cards)+len(change.CardAuthorizations)+len(change.Devices)+len(change.DeviceBindings)+len(change.DeviceAuthorizations)+len(change.PersonalPhones)+len(change.PersonalOpenIDs)+len(change.PersonalDevices)+len(change.PersonalICCIDs)+len(change.Roles)+len(change.Permissions))
|
||||
resources := make([]ResourceInput, 0, 2+len(change.Accounts)+len(change.Cards)+len(change.CardAuthorizations)+len(change.Devices)+len(change.DeviceBindings)+len(change.DeviceAuthorizations)+len(change.PersonalPhones)+len(change.PersonalOpenIDs)+len(change.PersonalDevices)+len(change.PersonalICCIDs)+len(change.PhoneAssociations)*2+len(change.Roles)+len(change.Permissions))
|
||||
switch primaryResource {
|
||||
case constants.AuditResourceAccount:
|
||||
if change.Account == nil || (change.Account.ID == 0 && change.Account.Username == "") {
|
||||
@@ -271,6 +271,43 @@ func accessResources(change accessauditapp.ChangeAudit, primaryResource string)
|
||||
IdentitySnapshot: personalCustomerIdentity(change.PersonalCustomer), BeforeData: change.BeforeData, AfterData: change.AfterData,
|
||||
SubjectVisibility: change.SubjectVisibility, SubjectSummary: change.SubjectSummary, SubjectData: change.SubjectData,
|
||||
})
|
||||
case constants.AuditResourcePhoneAssetAssociation:
|
||||
if len(change.PhoneAssociations) == 0 {
|
||||
return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "手机号—资产关联审计资源不完整")
|
||||
}
|
||||
for index, item := range change.PhoneAssociations {
|
||||
if item.AssociationID == 0 {
|
||||
return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "手机号—资产关联审计资源不完整")
|
||||
}
|
||||
relation := constants.AuditResourceRelationReference
|
||||
if index == 0 {
|
||||
relation = constants.AuditResourceRelationPrimary
|
||||
}
|
||||
resources = append(resources, ResourceInput{
|
||||
Type: constants.AuditResourcePhoneAssetAssociation, ID: optionalResourceID(item.AssociationID),
|
||||
Key: strconv.FormatUint(uint64(item.AssociationID), 10), DisplayName: item.PhoneMasked,
|
||||
Relation: relation, Role: constants.AuditResourceRolePhoneAssetAssociationTarget,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": item.AssociationID, "phone_masked": item.PhoneMasked,
|
||||
"asset_type": item.AssetType, "asset_id": item.AssetID, "status": item.Status,
|
||||
"source": item.Source, "invalidated_at": item.InvalidatedAt,
|
||||
"invalidation_method": item.InvalidationMethod, "invalidation_reason": item.InvalidationReason,
|
||||
},
|
||||
BeforeData: item.BeforeData, AfterData: item.AfterData,
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly, SortOrder: index + 1,
|
||||
})
|
||||
// 关联指向的资产以参考资源落事件,便于按资产回溯;资产标识缺失时不写资产资源。
|
||||
if item.AssetType == "" || item.AssetID == 0 {
|
||||
continue
|
||||
}
|
||||
assetID := strconv.FormatUint(uint64(item.AssetID), 10)
|
||||
resources = append(resources, ResourceInput{
|
||||
Type: item.AssetType, ID: &assetID, Key: assetID, DisplayName: item.AssetDisplayName,
|
||||
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRolePhoneAssetAssociationAsset,
|
||||
IdentitySnapshot: map[string]any{"id": item.AssetID},
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
})
|
||||
}
|
||||
}
|
||||
for index, item := range change.Accounts {
|
||||
if item.Account == nil || (item.Account.ID == 0 && item.Account.Username == "") {
|
||||
@@ -396,7 +433,7 @@ func accessResources(change accessauditapp.ChangeAudit, primaryResource string)
|
||||
}
|
||||
resources = append(resources, ResourceInput{
|
||||
Type: constants.AuditResourcePersonalCustomerPhone, ID: optionalResourceID(item.Phone.ID),
|
||||
Key: strconv.FormatUint(uint64(item.Phone.ID), 10), DisplayName: item.Phone.Phone,
|
||||
Key: strconv.FormatUint(uint64(item.Phone.ID), 10), DisplayName: sanitizer.MaskPhone(item.Phone.Phone),
|
||||
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRolePersonalCustomerPhone,
|
||||
IdentitySnapshot: personalCustomerPhoneIdentity(item.Phone), BeforeData: item.BeforeData, AfterData: item.AfterData,
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly, SortOrder: index + 1,
|
||||
@@ -611,9 +648,11 @@ func personalCustomerIdentity(customer *model.PersonalCustomer) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
// personalCustomerPhoneIdentity 生成个人客户手机号资源身份快照。
|
||||
// 手机号一律为脱敏值:审计只出现前 3 位 + **** + 后 4 位,资源行 ID 仍可回溯真实记录。
|
||||
func personalCustomerPhoneIdentity(phone *model.PersonalCustomerPhone) map[string]any {
|
||||
return map[string]any{
|
||||
"id": phone.ID, "customer_id": phone.CustomerID, "phone": phone.Phone,
|
||||
"id": phone.ID, "customer_id": phone.CustomerID, "phone": sanitizer.MaskPhone(phone.Phone),
|
||||
"is_primary": phone.IsPrimary, "verified_at": phone.VerifiedAt, "status": phone.Status,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user