feat(业务用户组): AUG26-003 业务用户组与店铺负责人分组导入
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 1h43m42s

- 迁移 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 验证证据链。
This commit is contained in:
2026-09-14 16:51:44 +08:00
parent 957a235585
commit c7f9e005af
56 changed files with 4166 additions and 344 deletions

View File

@@ -0,0 +1,88 @@
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})
}

View File

@@ -208,6 +208,16 @@ func NewRegistry() *Registry {
orderPackageInvalidateTaskCreated := taskAction(constants.AuditActionOrderPackageInvalidateTaskCreated, "创建订单套餐批量失效任务", constants.AuditResourceOrderPackageInvalidateTask, constants.AuditActorAccount, constants.AuditSourceAdminAPI)
orderPackageInvalidateTaskCompleted := taskAction(constants.AuditActionOrderPackageInvalidateTaskCompleted, "完成订单套餐批量失效任务", constants.AuditResourceOrderPackageInvalidateTask, constants.AuditActorSystemTask, constants.AuditSourceWorker)
orderPackageInvalidateItem := taskAction(constants.AuditActionOrderPackageInvalidateItem, "失效订单套餐权益", constants.AuditResourceOrder, constants.AuditActorSystemTask, constants.AuditSourceWorker)
businessUserGroupCreated := businessUserGroupAction(constants.AuditActionBusinessUserGroupCreated, "创建业务用户组", constants.AuditRiskNormal, constants.AuditResourceBusinessUserGroup)
businessUserGroupUpdated := businessUserGroupAction(constants.AuditActionBusinessUserGroupUpdated, "更新业务用户组", constants.AuditRiskNormal, constants.AuditResourceBusinessUserGroup)
businessUserGroupEnabled := businessUserGroupAction(constants.AuditActionBusinessUserGroupEnabled, "启用业务用户组", constants.AuditRiskNormal, constants.AuditResourceBusinessUserGroup)
businessUserGroupDisabled := businessUserGroupAction(constants.AuditActionBusinessUserGroupDisabled, "停用业务用户组", constants.AuditRiskNormal, constants.AuditResourceBusinessUserGroup)
businessUserGroupDeleted := businessUserGroupAction(constants.AuditActionBusinessUserGroupDeleted, "删除业务用户组", constants.AuditRiskHigh, constants.AuditResourceBusinessUserGroup)
businessUserGroupMembersUpdated := businessUserGroupAction(constants.AuditActionBusinessUserGroupMembersUpdated, "批量维护业务用户组成员", constants.AuditRiskNormal, constants.AuditResourceAccount)
shopBusinessOwnerBatchUpdated := batchRootAction(constants.AuditActionShopBusinessOwnerBatchUpdated, "批量交接店铺负责人", constants.AuditResourceShopBusinessOwnerBatch)
shopBusinessOwnerImported := taskAction(constants.AuditActionShopBusinessOwnerImported, "导入变更店铺负责人", constants.AuditResourceShop, constants.AuditActorSystemTask, constants.AuditSourceWorker)
shopBusinessOwnerImportTaskCreated := taskAction(constants.AuditActionShopBusinessOwnerImportTaskCreated, "创建店铺负责人导入任务", constants.AuditResourceShopBusinessOwnerImportTask, constants.AuditActorAccount, constants.AuditSourceAdminAPI)
shopBusinessOwnerImportTaskCompleted := taskAction(constants.AuditActionShopBusinessOwnerImportTaskCompleted, "完成店铺负责人导入任务", constants.AuditResourceShopBusinessOwnerImportTask, constants.AuditActorSystemTask, constants.AuditSourceWorker)
exportTaskCreated := taskAction(constants.AuditActionExportTaskCreated, "创建业务导出任务", constants.AuditResourceExportTask, constants.AuditActorAccount, constants.AuditSourceAdminAPI)
exportTaskCancelled := taskAction(constants.AuditActionExportTaskCancelled, "取消业务导出任务", constants.AuditResourceExportTask, constants.AuditActorAccount, constants.AuditSourceAdminAPI)
notificationDelivered := notificationAction(constants.AuditActionNotificationDelivered, "生成站内通知", constants.AuditResourceNotification, constants.AuditActorSystemTask, constants.AuditSourceWorker)
@@ -554,6 +564,16 @@ func NewRegistry() *Registry {
constants.AuditActionOrderPackageInvalidateTaskCreated: orderPackageInvalidateTaskCreated,
constants.AuditActionOrderPackageInvalidateTaskCompleted: orderPackageInvalidateTaskCompleted,
constants.AuditActionOrderPackageInvalidateItem: orderPackageInvalidateItem,
constants.AuditActionBusinessUserGroupCreated: businessUserGroupCreated,
constants.AuditActionBusinessUserGroupUpdated: businessUserGroupUpdated,
constants.AuditActionBusinessUserGroupEnabled: businessUserGroupEnabled,
constants.AuditActionBusinessUserGroupDisabled: businessUserGroupDisabled,
constants.AuditActionBusinessUserGroupDeleted: businessUserGroupDeleted,
constants.AuditActionBusinessUserGroupMembersUpdated: businessUserGroupMembersUpdated,
constants.AuditActionShopBusinessOwnerBatchUpdated: shopBusinessOwnerBatchUpdated,
constants.AuditActionShopBusinessOwnerImported: shopBusinessOwnerImported,
constants.AuditActionShopBusinessOwnerImportTaskCreated: shopBusinessOwnerImportTaskCreated,
constants.AuditActionShopBusinessOwnerImportTaskCompleted: shopBusinessOwnerImportTaskCompleted,
constants.AuditActionExportTaskCreated: exportTaskCreated,
constants.AuditActionExportTaskCancelled: exportTaskCancelled,
constants.AuditActionNotificationDelivered: notificationDelivered,
@@ -760,6 +780,18 @@ func NewRegistry() *Registry {
Type: constants.AuditResourceOrderPackageInvalidateTask, Name: "订单套餐批量失效任务",
IdentityFields: []string{"id", "task_no", "file_name"},
},
constants.AuditResourceShopBusinessOwnerImportTask: {
Type: constants.AuditResourceShopBusinessOwnerImportTask, Name: "店铺负责人导入任务",
IdentityFields: []string{"id", "task_no", "file_name"},
},
constants.AuditResourceBusinessUserGroup: {
Type: constants.AuditResourceBusinessUserGroup, Name: "业务用户组",
IdentityFields: []string{"id", "code", "name", "business_line", "status"},
},
constants.AuditResourceShopBusinessOwnerBatch: {
Type: constants.AuditResourceShopBusinessOwnerBatch, Name: "店铺负责人批量交接批次",
IdentityFields: []string{"batch_key", "operation", "shop_count"},
},
constants.AuditResourceExportTask: {
Type: constants.AuditResourceExportTask, Name: "业务导出任务",
IdentityFields: []string{"id", "task_no", "scene", "format", "creator_user_id", "creator_user_type", "creator_shop_id", "creator_enterprise_id", "scope_shop_ids"},
@@ -1303,6 +1335,28 @@ func taskAction(code, name, primaryResource, actor, source string) ActionDefinit
}
}
// businessUserGroupAction 定义业务用户组维护动作;组只承载业务分类,主体不可见事件细节。
func businessUserGroupAction(code, name, risk, primaryResource string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryBusiness, Risk: risk,
PrimaryResource: primaryResource, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
}
// batchRootAction 定义同步后台批次根动作;子事件自带店铺或资源作用域。
func batchRootAction(code, name, primaryResource string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskNormal,
PrimaryResource: primaryResource, AllowedActor: constants.AuditActorAccount,
Source: constants.AuditSourceAdminAPI, RequireTransaction: true,
DefaultVisibility: constants.AuditSubjectInternalOnly,
AllowedVisibility: []string{constants.AuditSubjectInternalOnly},
}
}
func notificationAction(code, name, primaryResource, actor, source string) ActionDefinition {
return ActionDefinition{
Code: code, Name: name, Category: constants.AuditCategoryBusiness, Risk: constants.AuditRiskLow,