2026-08-15 19:19:15 +08:00
// 功能描述:提供平台工作人员资源管理与受控候选筛选。
// 版本: v1.1.0
2026-07-28 13:55:40 +08:00
package staff
2026-07-27 01:32:44 +08:00
import (
2026-08-11 19:15:53 +08:00
"strconv"
2026-07-29 15:54:20 +08:00
"strings"
2026-08-15 19:19:15 +08:00
"time"
2026-07-29 15:54:20 +08:00
2026-07-27 01:32:44 +08:00
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
2026-07-28 10:42:26 +08:00
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
2026-08-10 22:35:47 +08:00
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/upload"
2026-07-27 01:32:44 +08:00
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
)
2026-08-11 19:15:53 +08:00
type staffListFilters struct {
2026-08-15 19:19:15 +08:00
RoleCodes [ ] string
Status * int
WorkStatus string
ValidCredentialOnly bool
2026-08-11 19:15:53 +08:00
}
2026-08-12 00:37:58 +08:00
type staffOrganizationFilters struct {
GasIdentity string
DeliveryIdentity string
GasUnassigned bool
DeliveryUnassigned bool
}
2026-08-11 19:15:53 +08:00
// parseStaffListFilters 将工作人员列表查询参数收敛为闭集条件,拒绝含糊或冲突值。
2026-08-15 19:19:15 +08:00
func parseStaffListFilters ( roleCode , roleCodes , status , workStatus , validCredentialOnly string ) ( staffListFilters , bool ) {
2026-08-11 19:15:53 +08:00
filters := staffListFilters { }
roleCode = strings . TrimSpace ( roleCode )
roleCodes = strings . TrimSpace ( roleCodes )
if roleCode != "" && roleCodes != "" {
return filters , false
}
if roleCode != "" {
filters . RoleCodes = [ ] string { roleCode }
} else if roleCodes != "" {
seen := map [ string ] bool { }
for _ , value := range strings . Split ( roleCodes , "," ) {
value = strings . TrimSpace ( value )
if value == "" || seen [ value ] {
return staffListFilters { } , false
}
seen [ value ] = true
filters . RoleCodes = append ( filters . RoleCodes , value )
}
}
for _ , value := range filters . RoleCodes {
if ! validStaffRole ( value ) {
return staffListFilters { } , false
}
}
status = strings . TrimSpace ( status )
if status != "" {
value , err := strconv . Atoi ( status )
if err != nil || ! common . IsGenericRecordStatus ( value ) || value == common . StatusArchived {
return staffListFilters { } , false
}
filters . Status = & value
2026-07-29 15:54:20 +08:00
}
2026-08-11 19:15:53 +08:00
workStatus = strings . TrimSpace ( workStatus )
if workStatus != "" && ! validWorkStatus ( workStatus ) {
return staffListFilters { } , false
}
filters . WorkStatus = workStatus
2026-08-15 19:19:15 +08:00
validCredentialOnly = strings . TrimSpace ( validCredentialOnly )
if validCredentialOnly != "" && validCredentialOnly != "1" {
return staffListFilters { } , false
}
filters . ValidCredentialOnly = validCredentialOnly == "1"
2026-08-11 19:15:53 +08:00
return filters , true
}
2026-08-12 00:37:58 +08:00
// parseStaffOrganizationFilters 解析服务关系下拉使用的精确组织范围,并拒绝相互冲突的条件。
func parseStaffOrganizationFilters ( gasIdentity , deliveryIdentity , gasUnassigned , deliveryUnassigned string ) ( staffOrganizationFilters , bool ) {
filters := staffOrganizationFilters {
GasIdentity : strings . TrimSpace ( gasIdentity ) ,
DeliveryIdentity : strings . TrimSpace ( deliveryIdentity ) ,
}
if strings . Contains ( filters . GasIdentity , "," ) || strings . Contains ( filters . DeliveryIdentity , "," ) {
return staffOrganizationFilters { } , false
}
parseUnassigned := func ( value string ) ( bool , bool ) {
value = strings . TrimSpace ( value )
if value == "" {
return false , true
}
return value == "1" , value == "1"
}
var ok bool
filters . GasUnassigned , ok = parseUnassigned ( gasUnassigned )
if ! ok {
return staffOrganizationFilters { } , false
}
filters . DeliveryUnassigned , ok = parseUnassigned ( deliveryUnassigned )
if ! ok || filters . GasIdentity != "" && filters . GasUnassigned || filters . DeliveryIdentity != "" && filters . DeliveryUnassigned {
return staffOrganizationFilters { } , false
}
return filters , true
}
2026-08-11 19:15:53 +08:00
// ListStaff 查询服务人员分页列表,并应用调用方显式声明的角色与在岗状态条件。
func ListStaff ( ctx * gin . Context ) {
filters , ok := parseStaffListFilters (
ctx . Query ( "role_code" ) ,
ctx . Query ( "role_codes" ) ,
ctx . Query ( "status" ) ,
ctx . Query ( "work_status" ) ,
2026-08-15 19:19:15 +08:00
ctx . Query ( "valid_credential_only" ) ,
2026-08-11 19:15:53 +08:00
)
if ! ok {
2026-07-29 15:54:20 +08:00
infra . Response . Error ( ctx , errcode . ErrInvalidArgument )
return
}
2026-08-12 00:37:58 +08:00
organizationFilters , ok := parseStaffOrganizationFilters (
ctx . Query ( "gas_basic_identities" ) ,
ctx . Query ( "delivery_basic_identities" ) ,
ctx . Query ( "gas_basic_unassigned" ) ,
ctx . Query ( "delivery_basic_unassigned" ) ,
)
if ! ok {
infra . Response . Error ( ctx , errcode . ErrInvalidArgument )
return
}
2026-07-29 15:54:20 +08:00
page , size := common . PageSize ( ctx )
var list [ ] models . StaffAccount
var total int64
2026-08-11 19:15:53 +08:00
query := common . ActiveRecords ( impl . DBService . Model ( & models . StaffAccount { } ) )
if len ( filters . RoleCodes ) > 0 {
query = query . Where ( "role_code IN ?" , filters . RoleCodes )
}
if filters . Status != nil {
query = query . Where ( "status = ?" , * filters . Status )
}
if filters . WorkStatus != "" {
query = query . Where ( "work_status = ?" , filters . WorkStatus )
}
2026-08-15 19:19:15 +08:00
if filters . ValidCredentialOnly {
now := time . Now ( )
query = query . Where (
"EXISTS (SELECT 1 FROM staff_credential WHERE staff_credential.staff_account_id = staff_account.id AND staff_credential.status = ? AND (staff_credential.expired_at IS NULL OR staff_credential.expired_at > ?))" ,
common . StatusEnable ,
now ,
)
}
2026-08-12 00:37:58 +08:00
if organizationFilters . GasIdentity != "" {
query = query . Where (
"gas_basic_id IN (SELECT id FROM gas_basic WHERE identity = ? AND status <> ?)" ,
organizationFilters . GasIdentity ,
common . StatusArchived ,
)
} else if organizationFilters . GasUnassigned {
query = query . Where ( "gas_basic_id = 0" )
}
if organizationFilters . DeliveryIdentity != "" {
query = query . Where (
"delivery_basic_id IN (SELECT id FROM delivery_basic WHERE identity = ? AND status <> ?)" ,
organizationFilters . DeliveryIdentity ,
common . StatusArchived ,
)
} else if organizationFilters . DeliveryUnassigned {
query = query . Where ( "delivery_basic_id = 0" )
}
2026-08-11 19:15:53 +08:00
query = common . ApplyKeywordFilter ( ctx , query , & models . StaffAccount { } )
2026-07-29 15:54:20 +08:00
if err := query . Count ( & total ) . Error ; err != nil {
infra . Response . Error ( ctx , err )
return
}
if err := query . Order ( "created_at desc" ) . Offset ( ( page - 1 ) * size ) . Limit ( size ) . Find ( & list ) . Error ; err != nil {
infra . Response . Error ( ctx , err )
return
}
response , err := common . PublicResourceResponse ( list )
if err != nil {
infra . Response . Error ( ctx , err )
return
}
infra . Response . Success ( ctx , gin . H { "total" : total , "list" : common . ProtectPreciseLocation ( ctx , & models . StaffAccount { } , response ) } )
}
2026-07-27 01:32:44 +08:00
// GetStaff 查询一个服务人员档案。
2026-07-28 10:42:26 +08:00
func GetStaff ( ctx * gin . Context ) { common . GetByIdentity [ models . StaffAccount ] ( ctx ) }
2026-07-27 01:32:44 +08:00
2026-08-10 22:35:47 +08:00
// GetStaffAvatar 返回已鉴权工作人员资料页使用的头像二进制内容。
func GetStaffAvatar ( ctx * gin . Context ) {
var account models . StaffAccount
if err := common . ActiveRecords ( impl . DBService ) . Select ( "avatar" ) . Where ( "identity = ?" , ctx . Param ( "identity" ) ) . First ( & account ) . Error ; err != nil {
common . RespondRecordError ( ctx , err )
return
}
upload . ServeAvatar ( ctx , account . Avatar )
}
2026-07-27 01:32:44 +08:00
// CreateStaff 创建服务人员档案。
func CreateStaff ( ctx * gin . Context ) {
2026-07-27 02:03:26 +08:00
var request struct {
2026-07-27 10:11:07 +08:00
Username string ` json:"username" binding:"required,max=64" `
2026-07-30 10:01:19 +08:00
Password string ` json:"password" binding:"required" `
2026-07-27 10:11:07 +08:00
Name string ` json:"name" binding:"required,max=64" `
Phone string ` json:"phone" binding:"max=32" `
Avatar string ` json:"avatar" binding:"max=512" `
RoleCode string ` json:"role_code" binding:"max=64" `
GasBasicIdentity string ` json:"gas_basic_identity" `
DeliveryBasicIdentity string ` json:"delivery_basic_identity" `
WorkStatus string ` json:"work_status" binding:"max=32" `
2026-07-27 02:03:26 +08:00
}
2026-07-30 10:01:19 +08:00
if err := ctx . ShouldBindJSON ( & request ) ; err != nil ||
! common . IsValidAccountPassword ( request . Password ) ||
! validStaffRole ( request . RoleCode ) {
2026-07-27 01:32:44 +08:00
infra . Response . Error ( ctx , errcode . ErrInvalidArgument )
return
}
2026-07-28 13:55:40 +08:00
hash , err := common . PasswordHash ( request . Password )
2026-07-27 02:03:26 +08:00
if err != nil {
infra . Response . Error ( ctx , err )
return
}
2026-07-28 10:42:26 +08:00
gasBasicID , err := common . ResolveIdentityID ( & models . GasBasic { } , request . GasBasicIdentity , false )
2026-07-27 10:11:07 +08:00
if err != nil {
infra . Response . Error ( ctx , errcode . ErrInvalidArgument )
return
}
2026-07-28 10:42:26 +08:00
deliveryBasicID , err := common . ResolveIdentityID ( & models . DeliveryBasic { } , request . DeliveryBasicIdentity , false )
2026-07-27 10:11:07 +08:00
if err != nil {
infra . Response . Error ( ctx , errcode . ErrInvalidArgument )
return
}
2026-07-29 14:25:38 +08:00
if ! common . ValidateOrganizationIDs ( gasBasicID , deliveryBasicID , 0 ) {
infra . Response . Error ( ctx , errcode . ErrInvalidArgument )
return
}
2026-07-29 13:18:52 +08:00
staff := models . StaffAccount { Entity : common . NewEntity ( common . StatusDraft ) , Username : request . Username , PasswordHash : hash , Name : request . Name , Phone : request . Phone , Avatar : request . Avatar , RoleCode : request . RoleCode , GasBasicID : gasBasicID , DeliveryBasicID : deliveryBasicID , WorkStatus : request . WorkStatus }
2026-07-27 02:03:26 +08:00
if staff . WorkStatus == "" {
staff . WorkStatus = "off_duty"
2026-07-27 01:32:44 +08:00
}
2026-07-29 14:25:38 +08:00
if ! validWorkStatus ( staff . WorkStatus ) {
infra . Response . Error ( ctx , errcode . ErrInvalidArgument )
return
}
2026-08-11 13:37:44 +08:00
if err := common . CreateStaffRecord ( & staff ) ; err != nil {
2026-07-27 01:32:44 +08:00
infra . Response . Error ( ctx , err )
return
}
2026-07-28 10:42:26 +08:00
common . RespondCreatedResource ( ctx , staff )
2026-07-27 01:32:44 +08:00
}
// UpdateStaff 更新服务人员档案。
func UpdateStaff ( ctx * gin . Context ) {
var request struct {
2026-08-10 22:35:47 +08:00
Name string ` json:"name" binding:"required,max=64" `
Phone string ` json:"phone" binding:"max=32" `
Avatar * string ` json:"avatar" binding:"omitempty,max=512" `
RoleCode string ` json:"role_code" binding:"max=64" `
GasBasicIdentity string ` json:"gas_basic_identity" `
DeliveryBasicIdentity string ` json:"delivery_basic_identity" `
WorkStatus string ` json:"work_status" binding:"max=32" `
2026-07-27 01:32:44 +08:00
}
2026-07-29 15:54:20 +08:00
if err := ctx . ShouldBindJSON ( & request ) ; err != nil || ! validWorkStatus ( request . WorkStatus ) || ! validStaffRole ( request . RoleCode ) {
2026-07-27 01:32:44 +08:00
infra . Response . Error ( ctx , errcode . ErrInvalidArgument )
return
}
2026-07-28 10:42:26 +08:00
gasBasicID , err := common . ResolveIdentityID ( & models . GasBasic { } , request . GasBasicIdentity , false )
2026-07-27 10:11:07 +08:00
if err != nil {
infra . Response . Error ( ctx , errcode . ErrInvalidArgument )
return
}
2026-07-28 10:42:26 +08:00
deliveryBasicID , err := common . ResolveIdentityID ( & models . DeliveryBasic { } , request . DeliveryBasicIdentity , false )
2026-07-27 10:11:07 +08:00
if err != nil {
infra . Response . Error ( ctx , errcode . ErrInvalidArgument )
return
}
2026-07-29 14:25:38 +08:00
if ! common . ValidateOrganizationIDs ( gasBasicID , deliveryBasicID , 0 ) {
infra . Response . Error ( ctx , errcode . ErrInvalidArgument )
return
}
2026-08-10 22:35:47 +08:00
values := gin . H { "name" : request . Name , "phone" : request . Phone , "role_code" : request . RoleCode , "gas_basic_id" : gasBasicID , "delivery_basic_id" : deliveryBasicID , "work_status" : request . WorkStatus }
// 未选择新头像时不提交 avatar, 避免普通资料编辑误清空现有头像。
if request . Avatar != nil {
values [ "avatar" ] = * request . Avatar
}
2026-08-11 13:37:44 +08:00
common . UpdateAllowedByIdentityWithError ( ctx , & models . StaffAccount { } , values , [ ] string { "name" , "phone" , "avatar" , "role_code" , "gas_basic_id" , "delivery_basic_id" , "work_status" } , common . StaffWriteError )
2026-07-27 01:32:44 +08:00
}
2026-07-29 14:25:38 +08:00
func validWorkStatus ( status string ) bool { return status == "on_duty" || status == "off_duty" }
2026-07-29 15:54:20 +08:00
func validStaffRole ( role string ) bool {
return role == "installer" || role == "delivery" || role == "operations"
}