material_manage_system/internal/service/organization_access.go
文睿 张 970ba5ccf7 feat: 组织列表为每家公司补充仓库摘要
组织初始化接口 GET /api/auth/organizations 现在为每项授权公司附带
warehouse 摘要({id, name, status, location: {id, name, status} | null} | null),
结构与切换接口一致,供普通用户在公司切换器直接展示,无需放开管理员仓库配置接口。

- model:新增 WarehouseSummary/LocationSummary/OrganizationSummary,
  OrganizationAccess 每项改为内嵌公司资料再加 warehouse 摘要。
- repository:新增 WarehouseRepository.Summary 复用切换接口的解析规则;
  ListAccessible 逐公司补齐摘要,仓库或库位配置不唯一时报错而非随意取第一条,
  无仓库、无库位或已停用如实返回,不回退旧公司数据。
- service:Switch 复用 List 已解析的摘要,响应格式保持不变。
- 测试:新增服务层 sqlmock 用例与仓储层 MySQL 集成用例,
  覆盖启用唯一仓库加启用库位、无仓库、仓库停用三类。
2026-10-09 10:43:35 +08:00

127 lines
3.6 KiB
Go

package service
import (
"database/sql"
"encoding/json"
"errors"
"github.com/wmms/wmms/internal/model"
"github.com/wmms/wmms/internal/repository"
"github.com/wmms/wmms/pkg/database"
)
var ErrInvalidOrganizationID = errors.New("请选择有效的公司")
var ErrOrganizationInaccessible = errors.New("当前公司不可访问")
type OrganizationAccessService struct {
repo *repository.OrganizationRepository
}
func NewOrganizationAccessService() *OrganizationAccessService {
return &OrganizationAccessService{repo: repository.NewOrganizationRepository()}
}
// EffectiveRoles reloads the principal for every authenticated request. A JWT
// authenticates identity; its role snapshot never grants current privileges.
func (s *OrganizationAccessService) EffectiveRoles(userID int64) ([]string, error) {
rows, err := database.DB.Query(`SELECT u.id, r.code FROM users u
LEFT JOIN user_roles ur ON ur.user_id=u.id
LEFT JOIN roles r ON r.id=ur.role_id AND r.enabled=1
WHERE u.id=? ORDER BY r.id`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
roles := []string{}
found := false
for rows.Next() {
var id int64
var code sql.NullString
if err := rows.Scan(&id, &code); err != nil {
return nil, err
}
found = true
if code.Valid {
roles = append(roles, code.String)
}
}
if err := rows.Err(); err != nil {
return nil, err
}
if !found {
return nil, sql.ErrNoRows
}
return roles, nil
}
func (s *OrganizationAccessService) List(userID int64) (*model.OrganizationAccess, error) {
return s.repo.ListAccessible(userID)
}
func (s *OrganizationAccessService) Validate(userID, organizationID int64) error {
if organizationID <= 0 {
return ErrInvalidOrganizationID
}
allowed, err := s.repo.CanAccess(userID, organizationID)
if err != nil {
return err
}
if !allowed {
return ErrOrganizationInaccessible
}
return nil
}
// 复用 model 中的仓库摘要结构,切换接口与组织初始化列表字段名、结构完全一致。
type LocationContext = model.LocationSummary
type WarehouseContext = model.WarehouseSummary
type OrganizationContext struct {
OrganizationID int64 `json:"organization_id"`
Organization model.Organization `json:"organization"`
Warehouse *WarehouseContext `json:"warehouse"`
}
// Switch validates the target independently of the client-reported previous
// context. It records an audit, without storing a server-wide selected company.
func (s *OrganizationAccessService) Switch(userID, organizationID, previousID int64) (*OrganizationContext, error) {
if organizationID <= 0 {
return nil, ErrInvalidOrganizationID
}
// List 已为每家公司解析好仓库摘要,直接复用,避免重复查询。
access, err := s.List(userID)
if err != nil {
return nil, err
}
var target *model.OrganizationSummary
for i := range access.Organizations {
if access.Organizations[i].ID == organizationID {
target = &access.Organizations[i]
break
}
}
if target == nil {
return nil, ErrOrganizationInaccessible
}
result := &OrganizationContext{
OrganizationID: organizationID,
Organization: target.Organization,
Warehouse: target.Warehouse,
}
oldValue, err := json.Marshal(map[string]int64{"organization_id": previousID})
if err != nil {
return nil, err
}
newValue, err := json.Marshal(map[string]int64{"organization_id": organizationID})
if err != nil {
return nil, err
}
err = repository.NewOrganizationAuditRepository().Create(&model.OrganizationAuditLog{
ActorUserID: userID, OrganizationID: &organizationID, Action: "organization_context_switched", OldValue: oldValue, NewValue: newValue,
})
if err != nil {
return nil, err
}
return result, nil
}