diff --git a/backend/api/internal/logic/platform/account.go b/backend/api/internal/logic/platform/account.go new file mode 100644 index 0000000..f97aaff --- /dev/null +++ b/backend/api/internal/logic/platform/account.go @@ -0,0 +1,93 @@ +package platform + +import ( + "git.apinb.com/bsm-sdk/core/errcode" + "git.apinb.com/bsm-sdk/core/infra" + "git.apinb.com/heqiapp/platforms/backend/api/internal/impl" + "git.apinb.com/heqiapp/platforms/backend/api/internal/models" + "github.com/gin-gonic/gin" + "golang.org/x/crypto/bcrypt" +) + +type accountRequest struct { + Username string `json:"username" binding:"required,max=64"` + Password string `json:"password" binding:"required,min=8,max=128"` + DisplayName string `json:"display_name" binding:"max=64"` + RoleCode string `json:"role_code" binding:"max=64"` + GasBasicID uint64 `json:"gas_basic_id"` + DeliveryBasicID uint64 `json:"delivery_basic_id"` +} + +type accountUpdateRequest struct { + DisplayName string `json:"display_name" binding:"max=64"` + RoleCode string `json:"role_code" binding:"max=64"` + GasBasicID uint64 `json:"gas_basic_id"` + DeliveryBasicID uint64 `json:"delivery_basic_id"` +} + +func passwordHash(password string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(hash), err +} + +func ListGasAccount(ctx *gin.Context) { listPage[models.GasAccount](ctx) } +func GetGasAccount(ctx *gin.Context) { getByIdentity[models.GasAccount](ctx) } + +func CreateGasAccount(ctx *gin.Context) { + var request accountRequest + if err := ctx.ShouldBindJSON(&request); err != nil || request.GasBasicID == 0 { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } + hash, err := passwordHash(request.Password) + if err != nil { + infra.Response.Error(ctx, err) + return + } + account := models.GasAccount{Entity: newEntity("enabled"), GasBasicID: request.GasBasicID, Username: request.Username, DisplayName: request.DisplayName, PasswordHash: hash, RoleCode: request.RoleCode} + if err := impl.DBService.Create(&account).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + infra.Response.Success(ctx, account) +} + +func UpdateGasAccount(ctx *gin.Context) { + var request accountUpdateRequest + if err := ctx.ShouldBindJSON(&request); err != nil { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } + updateAllowedByIdentity(ctx, &models.GasAccount{}, gin.H{"gas_basic_id": request.GasBasicID, "display_name": request.DisplayName, "role_code": request.RoleCode}, []string{"gas_basic_id", "display_name", "role_code"}) +} + +func ListDeliveryAccount(ctx *gin.Context) { listPage[models.DeliveryAccount](ctx) } +func GetDeliveryAccount(ctx *gin.Context) { getByIdentity[models.DeliveryAccount](ctx) } + +func CreateDeliveryAccount(ctx *gin.Context) { + var request accountRequest + if err := ctx.ShouldBindJSON(&request); err != nil || request.DeliveryBasicID == 0 { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } + hash, err := passwordHash(request.Password) + if err != nil { + infra.Response.Error(ctx, err) + return + } + account := models.DeliveryAccount{Entity: newEntity("enabled"), DeliveryBasicID: request.DeliveryBasicID, Username: request.Username, DisplayName: request.DisplayName, PasswordHash: hash, RoleCode: request.RoleCode} + if err := impl.DBService.Create(&account).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + infra.Response.Success(ctx, account) +} + +func UpdateDeliveryAccount(ctx *gin.Context) { + var request accountUpdateRequest + if err := ctx.ShouldBindJSON(&request); err != nil { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } + updateAllowedByIdentity(ctx, &models.DeliveryAccount{}, gin.H{"delivery_basic_id": request.DeliveryBasicID, "display_name": request.DisplayName, "role_code": request.RoleCode}, []string{"delivery_basic_id", "display_name", "role_code"}) +} diff --git a/backend/api/internal/logic/platform/delivery.go b/backend/api/internal/logic/platform/delivery.go index 83a5236..7409331 100644 --- a/backend/api/internal/logic/platform/delivery.go +++ b/backend/api/internal/logic/platform/delivery.go @@ -41,5 +41,5 @@ func UpdateDeliveryBasic(ctx *gin.Context) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } - updateByIdentity(ctx, &models.DeliveryBasic{}, gin.H{"gas_basic_id": request.GasBasicID, "name": request.Name, "principal": request.Principal, "address": request.Address}) + updateAllowedByIdentity(ctx, &models.DeliveryBasic{}, gin.H{"gas_basic_id": request.GasBasicID, "name": request.Name, "principal": request.Principal, "address": request.Address}, []string{"gas_basic_id", "name", "principal", "address"}) } diff --git a/backend/api/internal/logic/platform/gas.go b/backend/api/internal/logic/platform/gas.go index 21e9792..606cf5d 100644 --- a/backend/api/internal/logic/platform/gas.go +++ b/backend/api/internal/logic/platform/gas.go @@ -43,5 +43,5 @@ func UpdateGasBasic(ctx *gin.Context) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } - updateByIdentity(ctx, &models.GasBasic{}, gin.H{"name": request.Name, "credit_code": request.CreditCode, "principal": request.Principal, "address": request.Address, "longitude": request.Longitude, "latitude": request.Latitude}) + updateAllowedByIdentity(ctx, &models.GasBasic{}, gin.H{"name": request.Name, "credit_code": request.CreditCode, "principal": request.Principal, "address": request.Address, "longitude": request.Longitude, "latitude": request.Latitude}, []string{"name", "credit_code", "principal", "address", "longitude", "latitude"}) } diff --git a/backend/api/internal/logic/platform/resource_test.go b/backend/api/internal/logic/platform/resource_test.go index 8029484..3158998 100644 --- a/backend/api/internal/logic/platform/resource_test.go +++ b/backend/api/internal/logic/platform/resource_test.go @@ -159,6 +159,24 @@ func TestPlatformRoleStatusAndArchiveReturnNotFoundWhenUpdateAffectsZeroRows(t * } } +func TestReplacePlatformRoleMenusAllowsAnEmptySetToClearAssignmentsTransactionally(t *testing.T) { + _, mock := setupPlatformRoleDatabase(t) + mock.ExpectBegin() + mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platform_role" WHERE identity = $1 ORDER BY "platform_role"."id" LIMIT $2`)). + WithArgs("role-a", 1). + WillReturnRows(platformRoleRows("role-a")) + mock.ExpectExec(regexp.QuoteMeta(`DELETE FROM "platform_role_menu_relation" WHERE platform_role_id = $1`)). + WithArgs(uint64(1)). + WillReturnResult(sqlmock.NewResult(0, 2)) + mock.ExpectCommit() + + ctx, recorder := updateContext(http.MethodPut, "/roles/role-a/menus", "role-a", []byte(`{"menu_identities":[]}`)) + ReplacePlatformRoleMenus(ctx) + + assertResponseCode(t, recorder, 0) + assertMockExpectations(t, mock) +} + type responseBody struct { Code int32 `json:"code"` Message string `json:"message"` diff --git a/backend/api/internal/logic/platform/role.go b/backend/api/internal/logic/platform/role.go index 8ac7ffe..23f9464 100644 --- a/backend/api/internal/logic/platform/role.go +++ b/backend/api/internal/logic/platform/role.go @@ -1,11 +1,14 @@ package platform import ( + "errors" + "git.apinb.com/bsm-sdk/core/errcode" "git.apinb.com/bsm-sdk/core/infra" "git.apinb.com/heqiapp/platforms/backend/api/internal/impl" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "github.com/gin-gonic/gin" + "gorm.io/gorm" ) // ListPlatformRole 查询平台角色分页列表。 @@ -52,11 +55,87 @@ func UpdatePlatformRole(ctx *gin.Context) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } - if err := impl.DBService.Model(&role).Updates(gin.H{"name": request.Name, "data_scope": request.DataScope}).Error; err != nil { + updateAllowedByIdentity(ctx, &models.PlatformRole{}, gin.H{"name": request.Name, "data_scope": request.DataScope}, []string{"name", "data_scope"}) +} + +type platformMenuRequest struct { + ParentID uint64 `json:"parent_id"` + MenuCode string `json:"menu_code" binding:"required,max=64"` + Name string `json:"name" binding:"required,max=64"` + Icon string `json:"icon" binding:"max=64"` + Path string `json:"path" binding:"max=255"` + SortNo int `json:"sort_no"` +} + +func GetPlatformMenu(ctx *gin.Context) { getByIdentity[models.PlatformMenu](ctx) } + +func CreatePlatformMenu(ctx *gin.Context) { + var request platformMenuRequest + if err := ctx.ShouldBindJSON(&request); err != nil { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } + menu := models.PlatformMenu{Entity: newEntity("enabled"), ParentID: request.ParentID, MenuCode: request.MenuCode, Name: request.Name, Icon: request.Icon, Path: request.Path, SortNo: request.SortNo} + if err := impl.DBService.Create(&menu).Error; err != nil { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, role) + infra.Response.Success(ctx, menu) +} + +func UpdatePlatformMenu(ctx *gin.Context) { + var request platformMenuRequest + if err := ctx.ShouldBindJSON(&request); err != nil { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } + updateAllowedByIdentity(ctx, &models.PlatformMenu{}, gin.H{"parent_id": request.ParentID, "name": request.Name, "icon": request.Icon, "path": request.Path, "sort_no": request.SortNo}, []string{"parent_id", "name", "icon", "path", "sort_no"}) +} + +type platformRoleMenusRequest struct { + MenuIdentities []string `json:"menu_identities" binding:"required"` +} + +// ReplacePlatformRoleMenus replaces every menu assignment for a role atomically. +func ReplacePlatformRoleMenus(ctx *gin.Context) { + var request platformRoleMenusRequest + if err := ctx.ShouldBindJSON(&request); err != nil { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } + if err := impl.DBService.Transaction(func(transaction *gorm.DB) error { + var role models.PlatformRole + if err := transaction.Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil { + return err + } + var menus []models.PlatformMenu + if len(request.MenuIdentities) > 0 { + if err := transaction.Where("identity IN ?", request.MenuIdentities).Find(&menus).Error; err != nil { + return err + } + if len(menus) != len(request.MenuIdentities) { + return gorm.ErrRecordNotFound + } + } + if err := transaction.Where("platform_role_id = ?", role.ID).Delete(&models.PlatformRoleMenuRelation{}).Error; err != nil { + return err + } + for _, menu := range menus { + relation := models.PlatformRoleMenuRelation{PlatformRoleID: role.ID, PlatformMenuID: menu.ID} + if err := transaction.Create(&relation).Error; err != nil { + return err + } + } + return nil + }); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + infra.Response.Error(ctx, errcode.ErrRecordNotFound) + return + } + infra.Response.Error(ctx, err) + return + } + infra.Response.Success(ctx, gin.H{"updated": true}) } // UpdatePlatformRoleStatus 更新非内置平台角色状态,系统角色始终受保护。 @@ -118,3 +197,71 @@ func ListPlatfromAccount(ctx *gin.Context) { } infra.Response.Success(ctx, gin.H{"total": total, "list": views}) } + +type platfromAccountRequest struct { + Username string `json:"username" binding:"required,max=64"` + Password string `json:"password" binding:"required,min=8,max=128"` + DisplayName string `json:"display_name" binding:"max=64"` + Avatar string `json:"avatar" binding:"max=512"` + PlatformRoleCode string `json:"platform_role_code" binding:"max=64"` + Phone string `json:"phone" binding:"max=32"` +} + +type platfromAccountView struct { + Identity string `json:"identity"` + Username string `json:"username"` + DisplayName string `json:"display_name"` + Avatar string `json:"avatar"` + PhoneMasked string `json:"phone_masked"` + PlatformRoleCode string `json:"platform_role_code"` + Status string `json:"status"` +} + +func platformAccountView(account models.PlatfromAccount) platfromAccountView { + return platfromAccountView{Identity: account.Identity, Username: account.Username, DisplayName: account.DisplayName, Avatar: account.Avatar, PhoneMasked: maskPhone(account.Phone), PlatformRoleCode: account.PlatformRoleCode, Status: account.Status} +} + +func GetPlatfromAccount(ctx *gin.Context) { + var account models.PlatfromAccount + if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&account).Error; err != nil { + respondRecordError(ctx, err) + return + } + infra.Response.Success(ctx, platformAccountView(account)) +} + +func CreatePlatfromAccount(ctx *gin.Context) { + var request platfromAccountRequest + if err := ctx.ShouldBindJSON(&request); err != nil { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } + hash, err := passwordHash(request.Password) + if err != nil { + infra.Response.Error(ctx, err) + return + } + account := models.PlatfromAccount{Entity: newEntity("enabled"), Username: request.Username, DisplayName: request.DisplayName, Avatar: request.Avatar, PasswordHash: hash, PlatformRoleCode: request.PlatformRoleCode, Phone: request.Phone} + if account.PlatformRoleCode == "" { + account.PlatformRoleCode = "root" + } + if err := impl.DBService.Create(&account).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + infra.Response.Success(ctx, platformAccountView(account)) +} + +func UpdatePlatfromAccount(ctx *gin.Context) { + var request struct { + DisplayName string `json:"display_name" binding:"max=64"` + Avatar string `json:"avatar" binding:"max=512"` + PlatformRoleCode string `json:"platform_role_code" binding:"max=64"` + Phone string `json:"phone" binding:"max=32"` + } + if err := ctx.ShouldBindJSON(&request); err != nil { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } + updateAllowedByIdentity(ctx, &models.PlatfromAccount{}, gin.H{"display_name": request.DisplayName, "avatar": request.Avatar, "platform_role_code": request.PlatformRoleCode, "phone": request.Phone}, []string{"display_name", "avatar", "platform_role_code", "phone"}) +} diff --git a/backend/api/internal/logic/platform/secondary_resource.go b/backend/api/internal/logic/platform/secondary_resource.go new file mode 100644 index 0000000..550c819 --- /dev/null +++ b/backend/api/internal/logic/platform/secondary_resource.go @@ -0,0 +1,105 @@ +package platform + +import ( + "time" + + "git.apinb.com/bsm-sdk/core/errcode" + "git.apinb.com/bsm-sdk/core/infra" + "git.apinb.com/heqiapp/platforms/backend/api/internal/impl" + "git.apinb.com/heqiapp/platforms/backend/api/internal/models" + "github.com/gin-gonic/gin" +) + +type staffCredentialRequest struct { + StaffAccountID uint64 `json:"staff_account_id" binding:"required"` + CredentialType string `json:"credential_type" binding:"required,max=64"` + CredentialNo string `json:"credential_no" binding:"max=128"` + ExpiredAt *time.Time `json:"expired_at"` +} + +func ListStaffCredential(ctx *gin.Context) { listPage[models.StaffCredential](ctx) } +func GetStaffCredential(ctx *gin.Context) { getByIdentity[models.StaffCredential](ctx) } +func CreateStaffCredential(ctx *gin.Context) { + var request staffCredentialRequest + if err := ctx.ShouldBindJSON(&request); err != nil { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } + credential := models.StaffCredential{Entity: newEntity("enabled"), StaffAccountID: request.StaffAccountID, CredentialType: request.CredentialType, CredentialNo: request.CredentialNo, ExpiredAt: request.ExpiredAt} + if err := impl.DBService.Create(&credential).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + infra.Response.Success(ctx, credential) +} +func UpdateStaffCredential(ctx *gin.Context) { + var request staffCredentialRequest + if err := ctx.ShouldBindJSON(&request); err != nil { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } + updateAllowedByIdentity(ctx, &models.StaffCredential{}, gin.H{"staff_account_id": request.StaffAccountID, "credential_type": request.CredentialType, "credential_no": request.CredentialNo, "expired_at": request.ExpiredAt}, []string{"staff_account_id", "credential_type", "credential_no", "expired_at"}) +} + +type userAddressRequest struct { + UserAccountID uint64 `json:"user_account_id" binding:"required"` + Address string `json:"address" binding:"required,max=255"` + Longitude string `json:"longitude" binding:"max=32"` + Latitude string `json:"latitude" binding:"max=32"` + IsDefault bool `json:"is_default"` +} + +func ListUserAddress(ctx *gin.Context) { listPage[models.UserAddress](ctx) } +func GetUserAddress(ctx *gin.Context) { getByIdentity[models.UserAddress](ctx) } +func CreateUserAddress(ctx *gin.Context) { + var request userAddressRequest + if err := ctx.ShouldBindJSON(&request); err != nil { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } + address := models.UserAddress{Entity: newEntity("enabled"), UserAccountID: request.UserAccountID, Address: request.Address, Longitude: request.Longitude, Latitude: request.Latitude, IsDefault: request.IsDefault} + if err := impl.DBService.Create(&address).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + infra.Response.Success(ctx, address) +} +func UpdateUserAddress(ctx *gin.Context) { + var request userAddressRequest + if err := ctx.ShouldBindJSON(&request); err != nil { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } + updateAllowedByIdentity(ctx, &models.UserAddress{}, gin.H{"user_account_id": request.UserAccountID, "address": request.Address, "longitude": request.Longitude, "latitude": request.Latitude, "is_default": request.IsDefault}, []string{"user_account_id", "address", "longitude", "latitude", "is_default"}) +} + +type userServiceRelationRequest struct { + UserAccountID uint64 `json:"user_account_id" binding:"required"` + GasBasicID uint64 `json:"gas_basic_id"` + DeliveryBasicID uint64 `json:"delivery_basic_id"` + StaffAccountID uint64 `json:"staff_account_id"` +} + +func ListUserServiceRelation(ctx *gin.Context) { listPage[models.UserServiceRelation](ctx) } +func GetUserServiceRelation(ctx *gin.Context) { getByIdentity[models.UserServiceRelation](ctx) } +func CreateUserServiceRelation(ctx *gin.Context) { + var request userServiceRelationRequest + if err := ctx.ShouldBindJSON(&request); err != nil { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } + relation := models.UserServiceRelation{Entity: newEntity("enabled"), UserAccountID: request.UserAccountID, GasBasicID: request.GasBasicID, DeliveryBasicID: request.DeliveryBasicID, StaffAccountID: request.StaffAccountID} + if err := impl.DBService.Create(&relation).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + infra.Response.Success(ctx, relation) +} +func UpdateUserServiceRelation(ctx *gin.Context) { + var request userServiceRelationRequest + if err := ctx.ShouldBindJSON(&request); err != nil { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } + updateAllowedByIdentity(ctx, &models.UserServiceRelation{}, gin.H{"user_account_id": request.UserAccountID, "gas_basic_id": request.GasBasicID, "delivery_basic_id": request.DeliveryBasicID, "staff_account_id": request.StaffAccountID}, []string{"user_account_id", "gas_basic_id", "delivery_basic_id", "staff_account_id"}) +} diff --git a/backend/api/internal/logic/platform/staff.go b/backend/api/internal/logic/platform/staff.go index 317f1c3..0d484ef 100644 --- a/backend/api/internal/logic/platform/staff.go +++ b/backend/api/internal/logic/platform/staff.go @@ -16,20 +16,35 @@ func GetStaff(ctx *gin.Context) { getByIdentity[models.StaffAccount](ctx) } // CreateStaff 创建服务人员档案。 func CreateStaff(ctx *gin.Context) { - var request models.StaffAccount - if err := ctx.ShouldBindJSON(&request); err != nil || request.Name == "" { + var request struct { + Username string `json:"username" binding:"required,max=64"` + Password string `json:"password" binding:"required,min=8,max=128"` + 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"` + GasBasicID uint64 `json:"gas_basic_id"` + DeliveryBasicID uint64 `json:"delivery_basic_id"` + WorkStatus string `json:"work_status" binding:"max=32"` + } + if err := ctx.ShouldBindJSON(&request); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } - request.Entity = newEntity("draft") - if request.WorkStatus == "" { - request.WorkStatus = "off_duty" - } - if err := impl.DBService.Create(&request).Error; err != nil { + hash, err := passwordHash(request.Password) + if err != nil { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, request) + staff := models.StaffAccount{Entity: newEntity("draft"), Username: request.Username, PasswordHash: hash, Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RoleCode: request.RoleCode, GasBasicID: request.GasBasicID, DeliveryBasicID: request.DeliveryBasicID, WorkStatus: request.WorkStatus} + if staff.WorkStatus == "" { + staff.WorkStatus = "off_duty" + } + if err := impl.DBService.Create(&staff).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + infra.Response.Success(ctx, staff) } // UpdateStaff 更新服务人员档案。 @@ -47,5 +62,5 @@ func UpdateStaff(ctx *gin.Context) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } - updateByIdentity(ctx, &models.StaffAccount{}, gin.H{"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "role_code": request.RoleCode, "gas_basic_id": request.GasBasicID, "delivery_basic_id": request.DeliveryBasicID, "work_status": request.WorkStatus}) + updateAllowedByIdentity(ctx, &models.StaffAccount{}, gin.H{"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "role_code": request.RoleCode, "gas_basic_id": request.GasBasicID, "delivery_basic_id": request.DeliveryBasicID, "work_status": request.WorkStatus}, []string{"name", "phone", "avatar", "role_code", "gas_basic_id", "delivery_basic_id", "work_status"}) } diff --git a/backend/api/internal/logic/platform/user.go b/backend/api/internal/logic/platform/user.go index 63fee53..06fb217 100644 --- a/backend/api/internal/logic/platform/user.go +++ b/backend/api/internal/logic/platform/user.go @@ -16,17 +16,29 @@ func GetUser(ctx *gin.Context) { getByIdentity[models.UserAccount](ctx) } // CreateUser 创建业主客户档案。 func CreateUser(ctx *gin.Context) { - var request models.UserAccount - if err := ctx.ShouldBindJSON(&request); err != nil || request.Name == "" { + var request struct { + Username string `json:"username" binding:"required,max=64"` + Password string `json:"password" binding:"required,min=8,max=128"` + Name string `json:"name" binding:"required,max=64"` + Phone string `json:"phone" binding:"max=32"` + Avatar string `json:"avatar" binding:"max=512"` + RealName string `json:"real_name" binding:"max=64"` + } + if err := ctx.ShouldBindJSON(&request); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } - request.Entity = newEntity("enabled") - if err := impl.DBService.Create(&request).Error; err != nil { + hash, err := passwordHash(request.Password) + if err != nil { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, request) + user := models.UserAccount{Entity: newEntity("enabled"), Username: request.Username, PasswordHash: hash, Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RealName: request.RealName} + if err := impl.DBService.Create(&user).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + infra.Response.Success(ctx, user) } // UpdateUser 更新业主客户档案。 @@ -41,5 +53,5 @@ func UpdateUser(ctx *gin.Context) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } - updateByIdentity(ctx, &models.UserAccount{}, gin.H{"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "real_name": request.RealName}) + updateAllowedByIdentity(ctx, &models.UserAccount{}, gin.H{"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "real_name": request.RealName}, []string{"name", "phone", "avatar", "real_name"}) } diff --git a/backend/api/internal/routers/platform.go b/backend/api/internal/routers/platform.go index eb64b46..18f9a54 100644 --- a/backend/api/internal/routers/platform.go +++ b/backend/api/internal/routers/platform.go @@ -30,47 +30,28 @@ func RegisterPlatform(serviceKey string, engine *gin.Engine) { } func registerGasRoute(group *gin.RouterGroup) { - resource := group.Group("/gas/gas_basic") - resource.GET("", platform.ListGasBasic) - resource.POST("", platform.CreateGasBasic) - resource.GET("/:identity", platform.GetGasBasic) - resource.PUT("/:identity", platform.UpdateGasBasic) - resource.PATCH("/:identity/status", func(ctx *gin.Context) { platform.UpdateRecordStatus(ctx, &models.GasBasic{}) }) - resource.DELETE("/:identity", func(ctx *gin.Context) { platform.ArchiveRecord(ctx, &models.GasBasic{}) }) + registerWritableResource(group, "/gas/gas_basic", platform.ListGasBasic, platform.CreateGasBasic, platform.GetGasBasic, platform.UpdateGasBasic, &models.GasBasic{}) + registerWritableResource(group, "/gas/gas_account", platform.ListGasAccount, platform.CreateGasAccount, platform.GetGasAccount, platform.UpdateGasAccount, &models.GasAccount{}) } func registerDeliveryRoute(group *gin.RouterGroup) { - resource := group.Group("/delivery/delivery_basic") - resource.GET("", platform.ListDeliveryBasic) - resource.POST("", platform.CreateDeliveryBasic) - resource.GET("/:identity", platform.GetDeliveryBasic) - resource.PUT("/:identity", platform.UpdateDeliveryBasic) - resource.PATCH("/:identity/status", func(ctx *gin.Context) { platform.UpdateRecordStatus(ctx, &models.DeliveryBasic{}) }) - resource.DELETE("/:identity", func(ctx *gin.Context) { platform.ArchiveRecord(ctx, &models.DeliveryBasic{}) }) + registerWritableResource(group, "/delivery/delivery_basic", platform.ListDeliveryBasic, platform.CreateDeliveryBasic, platform.GetDeliveryBasic, platform.UpdateDeliveryBasic, &models.DeliveryBasic{}) + registerWritableResource(group, "/delivery/delivery_account", platform.ListDeliveryAccount, platform.CreateDeliveryAccount, platform.GetDeliveryAccount, platform.UpdateDeliveryAccount, &models.DeliveryAccount{}) } func registerStaffRoute(group *gin.RouterGroup) { - resource := group.Group("/staff") - resource.GET("/account", platform.ListStaff) - resource.POST("/account", platform.CreateStaff) - resource.GET("/:identity", platform.GetStaff) - resource.PUT("/:identity", platform.UpdateStaff) - resource.PATCH("/:identity/status", func(ctx *gin.Context) { platform.UpdateRecordStatus(ctx, &models.StaffAccount{}) }) - resource.DELETE("/:identity", func(ctx *gin.Context) { platform.ArchiveRecord(ctx, &models.StaffAccount{}) }) + registerWritableResource(group, "/staff/account", platform.ListStaff, platform.CreateStaff, platform.GetStaff, platform.UpdateStaff, &models.StaffAccount{}) + registerWritableResource(group, "/staff/credential", platform.ListStaffCredential, platform.CreateStaffCredential, platform.GetStaffCredential, platform.UpdateStaffCredential, &models.StaffCredential{}) } func registerUserRoute(group *gin.RouterGroup) { - resource := group.Group("/user") - resource.GET("/account", platform.ListUser) - resource.POST("/account", platform.CreateUser) - resource.GET("/:identity", platform.GetUser) - resource.PUT("/:identity", platform.UpdateUser) - resource.PATCH("/:identity/status", func(ctx *gin.Context) { platform.UpdateRecordStatus(ctx, &models.UserAccount{}) }) - resource.DELETE("/:identity", func(ctx *gin.Context) { platform.ArchiveRecord(ctx, &models.UserAccount{}) }) + registerWritableResource(group, "/user/account", platform.ListUser, platform.CreateUser, platform.GetUser, platform.UpdateUser, &models.UserAccount{}) + registerWritableResource(group, "/user/address", platform.ListUserAddress, platform.CreateUserAddress, platform.GetUserAddress, platform.UpdateUserAddress, &models.UserAddress{}) + registerWritableResource(group, "/user/service_relation", platform.ListUserServiceRelation, platform.CreateUserServiceRelation, platform.GetUserServiceRelation, platform.UpdateUserServiceRelation, &models.UserServiceRelation{}) } func registerPlatformRoute(group *gin.RouterGroup) { - group.GET("/platform/platfrom_account", platform.ListPlatfromAccount) + registerWritableResource(group, "/platform/platfrom_account", platform.ListPlatfromAccount, platform.CreatePlatfromAccount, platform.GetPlatfromAccount, platform.UpdatePlatfromAccount, &models.PlatfromAccount{}) role := group.Group("/platform/platform_role") role.GET("", platform.ListPlatformRole) role.POST("", platform.CreatePlatformRole) @@ -78,5 +59,16 @@ func registerPlatformRoute(group *gin.RouterGroup) { role.PUT("/:identity", platform.UpdatePlatformRole) role.PATCH("/:identity/status", platform.UpdatePlatformRoleStatus) role.DELETE("/:identity", platform.ArchivePlatformRole) - group.GET("/platform/platform_menu", platform.ListPlatformMenu) + role.PUT("/:identity/menus", platform.ReplacePlatformRoleMenus) + registerWritableResource(group, "/platform/platform_menu", platform.ListPlatformMenu, platform.CreatePlatformMenu, platform.GetPlatformMenu, platform.UpdatePlatformMenu, &models.PlatformMenu{}) +} + +func registerWritableResource(group *gin.RouterGroup, path string, list, create, get, update gin.HandlerFunc, model any) { + resource := group.Group(path) + resource.GET("", list) + resource.POST("", create) + resource.GET("/:identity", get) + resource.PUT("/:identity", update) + resource.PATCH("/:identity/status", func(ctx *gin.Context) { platform.UpdateRecordStatus(ctx, model) }) + resource.DELETE("/:identity", func(ctx *gin.Context) { platform.ArchiveRecord(ctx, model) }) } diff --git a/backend/api/internal/routers/platform_test.go b/backend/api/internal/routers/platform_test.go index 8337092..331f70c 100644 --- a/backend/api/internal/routers/platform_test.go +++ b/backend/api/internal/routers/platform_test.go @@ -1,6 +1,7 @@ package routers import ( + "net/http" "testing" "github.com/gin-gonic/gin" @@ -20,3 +21,46 @@ func TestPlatformGasRouteUsesGasBasic(t *testing.T) { } t.Fatal("gas_basic list route is not registered") } + +func TestPlatformOrganizationAndAccountRoutesExposeResourceCRUD(t *testing.T) { + engine := gin.New() + RegisterPlatform("heqi", engine) + + routes := make(map[string]map[string]bool) + for _, route := range engine.Routes() { + if routes[route.Path] == nil { + routes[route.Path] = make(map[string]bool) + } + routes[route.Path][route.Method] = true + } + + for _, resource := range []string{ + "/gas/gas_basic", + "/gas/gas_account", + "/delivery/delivery_basic", + "/delivery/delivery_account", + "/staff/account", + "/staff/credential", + "/user/account", + "/user/address", + "/user/service_relation", + "/platform/platfrom_account", + "/platform/platform_role", + "/platform/platform_menu", + } { + assertRouteMethods(t, routes, "/heqi/platform/v1"+resource, http.MethodGet, http.MethodPost) + assertRouteMethods(t, routes, "/heqi/platform/v1"+resource+"/:identity", http.MethodGet, http.MethodPut, http.MethodDelete) + assertRouteMethods(t, routes, "/heqi/platform/v1"+resource+"/:identity/status", http.MethodPatch) + } + + assertRouteMethods(t, routes, "/heqi/platform/v1/platform/platform_role/:identity/menus", http.MethodPut) +} + +func assertRouteMethods(t *testing.T, routes map[string]map[string]bool, path string, methods ...string) { + t.Helper() + for _, method := range methods { + if !routes[path][method] { + t.Errorf("route %s %s is not registered", method, path) + } + } +}