Files
junhong_cmp_fiber/internal/infrastructure/audit/business_user_group.go
break c7f9e005af
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 1h43m42s
feat(业务用户组): AUG26-003 业务用户组与店铺负责人分组导入
- 迁移 000221:新增 tb_business_user_group、tb_business_user_group_member、tb_shop_business_owner_import_task,成员一账号一行由部分唯一索引保证,店铺所属组按当前负责人实时推导,不回填历史分组。
- 用户组 CRUD、成员改组/清空归属、店铺批量交接(原子失败不部分写入)。
- 店铺负责人 CSV 导入任务:逐行独立事务、逐行明细、任务级与行级失败分离。
- 读侧推导与筛选:未分组、业务线、停用组可筛出并带停用标记。
- 补齐操作审计动作与资源、openapi 清单、发布门禁巡检表清单。
- 归档 add-shop-salesperson-groups 变更并同步 openspec/specs/business-user-group,补齐 AUG26-003 验证证据链。
2026-09-14 16:51:44 +08:00

89 lines
4.2 KiB
Go

package audit
import (
"context"
"strconv"
"github.com/google/uuid"
"gorm.io/gorm"
shopapp "github.com/break/junhong_cmp_fiber/internal/application/shop"
"github.com/break/junhong_cmp_fiber/pkg/constants"
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
)
// WriteBusinessOwnerBatch 将店铺负责人批量交接写为批次根事件与逐店子事件。
// 成功批次写平台作用域根事件(承载批次汇总)+ 店铺作用域子事件;
// 业务回滚后的失败或拒绝只写根事件,由调用方在独立短事务内提交。
func (w *Writer) WriteBusinessOwnerBatch(ctx context.Context, tx *gorm.DB, batch shopapp.BusinessOwnerBatchAudit) error {
if w == nil || w.registry == nil || tx == nil {
return pkgerrors.New(pkgerrors.CodeInvalidStatus, "店铺负责人批量交接审计 Writer 未正确配置")
}
if batch.BatchKey == "" || batch.OperatorID == 0 || batch.Total <= 0 {
return pkgerrors.New(pkgerrors.CodeInvalidParam, "店铺负责人批量交接审计事实不完整")
}
root := AppendInput{
EventID: batch.BatchKey, ActionCode: constants.AuditActionShopBusinessOwnerBatchUpdated,
Summary: "批量交接店铺负责人", ScopeType: constants.AuditScopePlatform,
Result: constants.AuditResultSuccess, BatchTotal: batch.Total, SuccessCount: len(batch.Changes),
Actor: ActorInput{Kind: constants.AuditActorAccount, ID: strconv.FormatUint(uint64(batch.OperatorID), 10)},
Source: constants.AuditSourceAdminAPI,
Metadata: map[string]any{
"operation": batch.Operation, "shop_count": batch.Total,
},
Resources: []ResourceInput{{
Type: constants.AuditResourceShopBusinessOwnerBatch, Key: batch.BatchKey,
DisplayName: "店铺负责人批量交接",
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleShopBusinessOwnerBatch,
IdentitySnapshot: map[string]any{
"batch_key": batch.BatchKey, "operation": batch.Operation, "shop_count": batch.Total,
},
}},
}
if batch.Owner != nil {
root.Metadata["business_owner_account_id"] = batch.Owner.ID
resource := AccountResource(batch.Owner, constants.AuditResourceRelationReference, constants.AuditResourceRoleShopBusinessOwner)
resource.SortOrder = 1
root.Resources = append(root.Resources, resource)
}
if batch.Result != "" {
root.Result = batch.Result
root.SuccessCount = 0
root.FailCount = batch.Total
return w.Append(ctx, tx, root)
}
children := make([]AppendInput, 0, len(batch.Changes))
for _, change := range batch.Changes {
if change.Shop == nil || change.Shop.ID == 0 {
return pkgerrors.New(pkgerrors.CodeInvalidParam, "店铺负责人批量交接审计资源不完整")
}
shopResource := ShopResource(change.Shop, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleShopTarget)
shopResource.BeforeData = map[string]any{"business_owner_account_id": change.BeforeOwnerID}
shopResource.AfterData = map[string]any{"business_owner_account_id": change.AfterOwnerID}
shopResource.SubjectVisibility = constants.AuditSubjectResult
shopResource.SubjectSummary = "店铺业务员归属已更新"
resources := []ResourceInput{shopResource}
if change.PreviousOwner != nil {
resource := AccountResource(change.PreviousOwner, constants.AuditResourceRelationReference, constants.AuditResourceRoleShopPreviousBusinessOwner)
resource.BeforeData = map[string]any{"assigned": true}
resource.AfterData = map[string]any{"assigned": false}
resource.SortOrder = 1
resources = append(resources, resource)
}
if change.Owner != nil {
resource := AccountResource(change.Owner, constants.AuditResourceRelationReference, constants.AuditResourceRoleShopBusinessOwner)
resource.BeforeData = map[string]any{"assigned": false}
resource.AfterData = map[string]any{"assigned": true}
resource.SortOrder = 2
resources = append(resources, resource)
}
children = append(children, AppendInput{
EventID: "evt_" + uuid.NewString(),
ActionCode: constants.AuditActionShopBusinessOwnerUpdated, Summary: "批量更新店铺业务员归属",
ScopeType: constants.AuditScopeShop, ScopeID: strconv.FormatUint(uint64(change.Shop.ID), 10),
Result: constants.AuditResultSuccess, Resources: resources,
})
}
return w.AppendBatch(ctx, tx, BatchInput{Root: root, Children: children})
}