package enterprise import ( "context" stderrors "errors" 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" "github.com/break/junhong_cmp_fiber/internal/store" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" "gorm.io/gorm/clause" ) type Service struct { db *gorm.DB enterpriseStore *postgres.EnterpriseStore shopStore *postgres.ShopStore accountStore *postgres.AccountStore accessAudit accessauditapp.Writer } // New 创建企业生命周期服务。 func New( db *gorm.DB, enterpriseStore *postgres.EnterpriseStore, shopStore *postgres.ShopStore, accountStore *postgres.AccountStore, accessAudit accessauditapp.Writer, ) *Service { return &Service{ db: db, enterpriseStore: enterpriseStore, shopStore: shopStore, accountStore: accountStore, accessAudit: accessAudit, } } func (s *Service) Create(ctx context.Context, req *dto.CreateEnterpriseReq) (*dto.CreateEnterpriseResp, error) { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return nil, errors.New(errors.CodeUnauthorized, "未授权访问") } if s.db == nil || s.accessAudit == nil { return nil, errors.New(errors.CodeInvalidStatus, "企业审计接缝未配置") } enterprise := &model.Enterprise{ EnterpriseName: req.EnterpriseName, EnterpriseCode: req.EnterpriseCode, OwnerShopID: req.OwnerShopID, LegalPerson: req.LegalPerson, ContactName: req.ContactName, ContactPhone: req.ContactPhone, BusinessLicense: req.BusinessLicense, Province: req.Province, City: req.City, District: req.District, Address: req.Address, Status: constants.StatusEnabled, } enterprise.Creator = currentUserID enterprise.Updater = currentUserID if middleware.GetUserTypeFromContext(ctx) == constants.UserTypeEnterprise { err := errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") s.recordFailure(ctx, constants.AuditActionEnterpriseCreated, "创建企业失败", enterprise, nil, nil, err) return nil, err } if middleware.GetUserTypeFromContext(ctx) == constants.UserTypeAgent && req.OwnerShopID == nil { err := errors.New(errors.CodeForbidden, "代理账号不能创建平台主管企业") s.recordFailure(ctx, constants.AuditActionEnterpriseCreated, "创建企业失败", enterprise, nil, nil, err) return nil, err } if req.EnterpriseCode != "" { existing, _ := s.enterpriseStore.GetByCode(ctx, req.EnterpriseCode) if existing != nil { err := errors.New(errors.CodeEnterpriseCodeExists, "企业编号已存在") s.recordFailure(ctx, constants.AuditActionEnterpriseCreated, "创建企业失败", enterprise, nil, nil, err) return nil, err } } existingAccount, _ := s.accountStore.GetByPhone(ctx, req.LoginPhone) if existingAccount != nil { err := errors.New(errors.CodePhoneExists, "手机号已被使用") s.recordFailure(ctx, constants.AuditActionEnterpriseCreated, "创建企业失败", enterprise, nil, nil, err) return nil, err } var ownerShop *model.Shop if req.OwnerShopID != nil { if err := middleware.CanManageShop(ctx, *req.OwnerShopID); err != nil { err = errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") s.recordFailure(ctx, constants.AuditActionEnterpriseCreated, "创建企业失败", enterprise, nil, nil, err) return nil, err } var err error ownerShop, err = s.shopStore.GetByID(ctx, *req.OwnerShopID) if err != nil { err = errors.New(errors.CodeShopNotFound, "归属店铺不存在或无效") s.recordFailure(ctx, constants.AuditActionEnterpriseCreated, "创建企业失败", enterprise, nil, nil, err) return nil, err } } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { appErr := errors.Wrap(errors.CodeInternalError, err, "密码加密失败") s.recordFailure(ctx, constants.AuditActionEnterpriseCreated, "创建企业失败", enterprise, ownerShop, nil, appErr) return nil, appErr } var account *model.Account err = s.db.Transaction(func(tx *gorm.DB) error { if err := tx.WithContext(ctx).Create(enterprise).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "创建企业失败") } account = &model.Account{ Username: req.EnterpriseName, Phone: req.LoginPhone, Password: string(hashedPassword), UserType: constants.UserTypeEnterprise, EnterpriseID: &enterprise.ID, Status: constants.StatusEnabled, } account.Creator = currentUserID account.Updater = currentUserID if err := tx.WithContext(ctx).Create(account).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "创建企业账号失败") } return s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionEnterpriseCreated, Summary: "创建企业", OperatorID: currentUserID, Enterprise: enterprise, Shop: ownerShop, Accounts: []accessauditapp.AccountChange{{ Account: account, Role: constants.AuditResourceRoleEnterpriseAccount, AfterData: map[string]any{"status": account.Status, "credentials_configured": true}, }}, AfterData: enterpriseProfileData(enterprise), }) }) if err != nil { failureEnterprise := *enterprise failureEnterprise.ID = 0 s.recordFailure(ctx, constants.AuditActionEnterpriseCreated, "创建企业失败", &failureEnterprise, ownerShop, nil, err) return nil, err } ownerShopName := "" if ownerShop != nil { ownerShopName = ownerShop.ShopName } return &dto.CreateEnterpriseResp{ Enterprise: dto.EnterpriseItem{ ID: enterprise.ID, EnterpriseName: enterprise.EnterpriseName, EnterpriseCode: enterprise.EnterpriseCode, OwnerShopID: enterprise.OwnerShopID, OwnerShopName: ownerShopName, LegalPerson: enterprise.LegalPerson, ContactName: enterprise.ContactName, ContactPhone: enterprise.ContactPhone, LoginPhone: req.LoginPhone, BusinessLicense: enterprise.BusinessLicense, Province: enterprise.Province, City: enterprise.City, District: enterprise.District, Address: enterprise.Address, Status: enterprise.Status, StatusName: constants.GetStatusName(enterprise.Status), CreatedAt: enterprise.CreatedAt.Format("2006-01-02 15:04:05"), }, AccountID: account.ID, }, nil } // Update 更新企业信息 func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateEnterpriseRequest) (*model.Enterprise, error) { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return nil, errors.New(errors.CodeUnauthorized, "未授权访问") } if s.db == nil || s.accessAudit == nil { return nil, errors.New(errors.CodeInvalidStatus, "企业审计接缝未配置") } if err := middleware.CanManageEnterprise(ctx, id, s.enterpriseStore); err != nil { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } var enterprise *model.Enterprise var before *model.Enterprise var ownerShop *model.Shop err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { locked, err := lockEnterprise(ctx, tx, id) if err != nil { return err } beforeValue := *locked before = &beforeValue enterprise = locked ownerShop = loadEnterpriseOwnerShop(tx, locked.OwnerShopID) if req.EnterpriseCode != nil && *req.EnterpriseCode != locked.EnterpriseCode { var count int64 if err := tx.Model(&model.Enterprise{}).Where("enterprise_code = ? AND id <> ?", *req.EnterpriseCode, id).Count(&count).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "检查企业编号失败") } if count > 0 { return errors.New(errors.CodeEnterpriseCodeExists, "企业编号已存在") } locked.EnterpriseCode = *req.EnterpriseCode } applyEnterpriseUpdate(locked, req) locked.Updater = currentUserID if err := tx.Save(locked).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "更新企业失败") } if !enterpriseProfileChanged(before, locked) { return nil } if err := s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionEnterpriseUpdated, Summary: "更新企业基础资料", OperatorID: currentUserID, Enterprise: locked, Shop: ownerShop, BeforeData: enterpriseProfileData(before), AfterData: enterpriseProfileData(locked), }); err != nil { return errors.Wrap(errors.CodeInternalError, err, "写入企业更新审计失败") } return nil }) if err != nil { if before != nil { s.recordFailure(ctx, constants.AuditActionEnterpriseUpdated, "更新企业基础资料失败", before, ownerShop, nil, err) } return nil, err } return enterprise, nil } func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return errors.New(errors.CodeUnauthorized, "未授权访问") } if s.db == nil || s.accessAudit == nil { return errors.New(errors.CodeInvalidStatus, "企业审计接缝未配置") } if err := middleware.CanManageEnterprise(ctx, id, s.enterpriseStore); err != nil { return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } var before *model.Enterprise var ownerShop *model.Shop var accounts []*model.Account err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { enterprise, err := lockEnterprise(ctx, tx, id) if err != nil { return err } beforeValue := *enterprise before = &beforeValue ownerShop = loadEnterpriseOwnerShop(tx, enterprise.OwnerShopID) if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("enterprise_id = ?", id).Find(&accounts).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询企业账号失败") } enterprise.Status = status enterprise.Updater = currentUserID if err := tx.Save(enterprise).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "更新企业状态失败") } if err := tx.Model(&model.Account{}). Where("enterprise_id = ?", id). Updates(map[string]interface{}{ "status": status, "updater": currentUserID, }).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "同步更新企业账号状态失败") } if before.Status == status { return nil } accountChanges := make([]accessauditapp.AccountChange, 0, len(accounts)) for _, account := range accounts { accountChanges = append(accountChanges, accessauditapp.AccountChange{ Account: account, Role: constants.AuditResourceRoleEnterpriseAccount, BeforeData: map[string]any{"status": account.Status}, AfterData: map[string]any{"status": status}, }) } if err := s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionEnterpriseStatusUpdated, Summary: "更新企业状态", OperatorID: currentUserID, Enterprise: enterprise, Shop: ownerShop, Accounts: accountChanges, BeforeData: map[string]any{"status": before.Status}, AfterData: map[string]any{"status": status}, }); err != nil { return errors.Wrap(errors.CodeInternalError, err, "写入企业状态审计失败") } return nil }) if err != nil { if before != nil { s.recordFailure(ctx, constants.AuditActionEnterpriseStatusUpdated, "更新企业状态失败", before, ownerShop, accounts, err) } return err } return nil } func (s *Service) UpdatePassword(ctx context.Context, id uint, password string) error { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return errors.New(errors.CodeUnauthorized, "未授权访问") } if s.db == nil || s.accessAudit == nil { return errors.New(errors.CodeInvalidStatus, "企业审计接缝未配置") } if err := middleware.CanManageEnterprise(ctx, id, s.enterpriseStore); err != nil { return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } enterprise, err := s.enterpriseStore.GetByID(ctx, id) if err != nil { return errors.New(errors.CodeEnterpriseNotFound, "企业不存在") } var ownerShop *model.Shop if enterprise.OwnerShopID != nil { ownerShop, _ = s.shopStore.GetByID(ctx, *enterprise.OwnerShopID) } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { appErr := errors.Wrap(errors.CodeInternalError, err, "密码加密失败") s.recordFailure(ctx, constants.AuditActionEnterprisePasswordUpdated, "更新企业账号密码失败", enterprise, ownerShop, nil, appErr) return appErr } var accounts []*model.Account err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { enterprise, err = lockEnterprise(ctx, tx, id) if err != nil { return err } ownerShop = loadEnterpriseOwnerShop(tx, enterprise.OwnerShopID) if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("enterprise_id = ?", id).Find(&accounts).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询企业账号失败") } if err := tx.Model(&model.Account{}).Where("enterprise_id = ?", id).Updates(map[string]interface{}{ "password": string(hashedPassword), "updater": currentUserID, }).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "更新企业账号密码失败") } accountChanges := make([]accessauditapp.AccountChange, 0, len(accounts)) for _, account := range accounts { accountChanges = append(accountChanges, accessauditapp.AccountChange{ Account: account, Role: constants.AuditResourceRoleEnterpriseAccount, BeforeData: map[string]any{"credentials_configured": account.Password != ""}, AfterData: map[string]any{"credentials_configured": true, "state": "changed"}, }) } if err := s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionEnterprisePasswordUpdated, Summary: "更新企业账号密码", OperatorID: currentUserID, Enterprise: enterprise, Shop: ownerShop, Accounts: accountChanges, BeforeData: map[string]any{"credentials_configured": len(accounts) > 0}, AfterData: map[string]any{"credentials_configured": len(accounts) > 0, "state": "changed"}, }); err != nil { return errors.Wrap(errors.CodeInternalError, err, "写入企业改密审计失败") } return nil }) if err != nil { if enterprise != nil { s.recordFailure(ctx, constants.AuditActionEnterprisePasswordUpdated, "更新企业账号密码失败", enterprise, ownerShop, accounts, err) } return err } return nil } func applyEnterpriseUpdate(enterprise *model.Enterprise, req *dto.UpdateEnterpriseRequest) { if req.EnterpriseName != nil { enterprise.EnterpriseName = *req.EnterpriseName } if req.LegalPerson != nil { enterprise.LegalPerson = *req.LegalPerson } if req.ContactName != nil { enterprise.ContactName = *req.ContactName } if req.ContactPhone != nil { enterprise.ContactPhone = *req.ContactPhone } if req.BusinessLicense != nil { enterprise.BusinessLicense = *req.BusinessLicense } if req.Province != nil { enterprise.Province = *req.Province } if req.City != nil { enterprise.City = *req.City } if req.District != nil { enterprise.District = *req.District } if req.Address != nil { enterprise.Address = *req.Address } } func enterpriseProfileData(enterprise *model.Enterprise) map[string]any { return map[string]any{ "enterprise_name": enterprise.EnterpriseName, "enterprise_code": enterprise.EnterpriseCode, "owner_shop_id": enterprise.OwnerShopID, "legal_person": enterprise.LegalPerson, "contact_name": enterprise.ContactName, "contact_phone": enterprise.ContactPhone, "business_license": enterprise.BusinessLicense, "province": enterprise.Province, "city": enterprise.City, "district": enterprise.District, "address": enterprise.Address, "status": enterprise.Status, } } func enterpriseProfileChanged(before, after *model.Enterprise) bool { return before.EnterpriseName != after.EnterpriseName || before.EnterpriseCode != after.EnterpriseCode || before.LegalPerson != after.LegalPerson || before.ContactName != after.ContactName || before.ContactPhone != after.ContactPhone || before.BusinessLicense != after.BusinessLicense || before.Province != after.Province || before.City != after.City || before.District != after.District || before.Address != after.Address } func lockEnterprise(ctx context.Context, tx *gorm.DB, id uint) (*model.Enterprise, error) { var enterprise model.Enterprise query := middleware.ApplyOwnerShopFilter(ctx, tx.WithContext(ctx).Where("id = ?", id)) if err := query.Clauses(clause.Locking{Strength: "UPDATE"}).First(&enterprise).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询企业失败") } return &enterprise, nil } func loadEnterpriseOwnerShop(tx *gorm.DB, ownerShopID *uint) *model.Shop { if ownerShopID == nil { return nil } var shop model.Shop if err := tx.Unscoped().First(&shop, *ownerShopID).Error; err != nil { return nil } return &shop } func (s *Service) recordFailure( ctx context.Context, actionCode, summary string, enterprise *model.Enterprise, ownerShop *model.Shop, accounts []*model.Account, originalErr error, ) { accountChanges := make([]accessauditapp.AccountChange, 0, len(accounts)) for _, account := range accounts { accountChanges = append(accountChanges, accessauditapp.AccountChange{ Account: account, Role: constants.AuditResourceRoleEnterpriseAccount, }) } accessauditapp.RecordFailure(ctx, s.db, s.accessAudit, accessauditapp.ChangeAudit{ ActionCode: actionCode, Summary: summary, Result: enterpriseAuditFailureResult(originalErr), OperatorID: middleware.GetUserIDFromContext(ctx), Enterprise: enterprise, Shop: ownerShop, Accounts: accountChanges, SubjectVisibility: constants.AuditSubjectInternalOnly, }, originalErr) } func enterpriseAuditFailureResult(err error) string { var appErr *errors.AppError if stderrors.As(err, &appErr) { switch appErr.Code { case errors.CodeForbidden, errors.CodeInvalidParam, errors.CodeNotFound, errors.CodeEnterpriseNotFound, errors.CodeEnterpriseCodeExists, errors.CodePhoneExists, errors.CodeShopNotFound: return constants.AuditResultDenied } } return constants.AuditResultFailed } func (s *Service) GetByID(ctx context.Context, id uint) (*model.Enterprise, error) { enterprise, err := s.enterpriseStore.GetByID(ctx, id) if err != nil { return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在") } return enterprise, nil } func (s *Service) List(ctx context.Context, req *dto.EnterpriseListReq) (*dto.EnterprisePageResult, error) { opts := &store.QueryOptions{ Page: req.Page, PageSize: req.PageSize, } if opts.Page == 0 { opts.Page = 1 } if opts.PageSize == 0 { opts.PageSize = constants.DefaultPageSize } filters := make(map[string]interface{}) if req.EnterpriseName != "" { filters["enterprise_name"] = req.EnterpriseName } if req.ContactPhone != "" { filters["contact_phone"] = req.ContactPhone } if req.OwnerShopID != nil { filters["owner_shop_id"] = *req.OwnerShopID } if req.Status != nil { filters["status"] = *req.Status } enterprises, total, err := s.enterpriseStore.List(ctx, opts, filters) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询企业列表失败") } enterpriseIDs := make([]uint, 0, len(enterprises)) shopIDs := make([]uint, 0) for _, e := range enterprises { enterpriseIDs = append(enterpriseIDs, e.ID) if e.OwnerShopID != nil { shopIDs = append(shopIDs, *e.OwnerShopID) } } accountMap := make(map[uint]string) if len(enterpriseIDs) > 0 { var accounts []model.Account s.db.WithContext(ctx).Where("enterprise_id IN ?", enterpriseIDs).Find(&accounts) for _, acc := range accounts { if acc.EnterpriseID != nil { accountMap[*acc.EnterpriseID] = acc.Phone } } } shopMap := make(map[uint]string) if len(shopIDs) > 0 { var shops []model.Shop // 使用 Unscoped() 包含已删除的店铺,确保能显示店铺名称 s.db.WithContext(ctx).Unscoped().Where("id IN ?", shopIDs).Find(&shops) for _, shop := range shops { shopMap[shop.ID] = shop.ShopName } } items := make([]dto.EnterpriseItem, 0, len(enterprises)) for _, e := range enterprises { ownerShopName := "" if e.OwnerShopID != nil { ownerShopName = shopMap[*e.OwnerShopID] } items = append(items, dto.EnterpriseItem{ ID: e.ID, EnterpriseName: e.EnterpriseName, EnterpriseCode: e.EnterpriseCode, OwnerShopID: e.OwnerShopID, OwnerShopName: ownerShopName, LegalPerson: e.LegalPerson, ContactName: e.ContactName, ContactPhone: e.ContactPhone, LoginPhone: accountMap[e.ID], BusinessLicense: e.BusinessLicense, Province: e.Province, City: e.City, District: e.District, Address: e.Address, Status: e.Status, StatusName: constants.GetStatusName(e.Status), CreatedAt: e.CreatedAt.Format("2006-01-02 15:04:05"), }) } return &dto.EnterprisePageResult{ Items: items, Total: total, Page: opts.Page, Size: opts.PageSize, }, nil }