fix platform authorization and workflow integrity

This commit is contained in:
david
2026-07-29 15:54:20 +08:00
parent a7d318a013
commit 969d7271f9
26 changed files with 566 additions and 161 deletions

View File

@@ -1,12 +1,15 @@
package delivery
import (
"strings"
"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/logic/common"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type accountRequest struct {
@@ -23,8 +26,23 @@ type accountUpdateRequest struct {
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
}
func ListDeliveryAccount(ctx *gin.Context) { common.ListPage[models.DeliveryAccount](ctx) }
func GetDeliveryAccount(ctx *gin.Context) { common.GetByIdentity[models.DeliveryAccount](ctx) }
func ListDeliveryAccount(ctx *gin.Context) {
identities := strings.Split(strings.TrimSpace(ctx.Query("delivery_basic_identities")), ",")
if len(identities) == 1 && identities[0] == "" {
identities = nil
}
if len(identities) > 100 {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
common.ListPageFiltered[models.DeliveryAccount](ctx, func(query *gorm.DB) *gorm.DB {
if len(identities) == 0 {
return query
}
return query.Where("delivery_basic_id IN (SELECT id FROM delivery_basic WHERE identity IN ? AND status <> ?)", identities, common.StatusArchived)
})
}
func GetDeliveryAccount(ctx *gin.Context) { common.GetByIdentity[models.DeliveryAccount](ctx) }
func CreateDeliveryAccount(ctx *gin.Context) {
var request accountRequest

View File

@@ -76,7 +76,7 @@ func ListEcCategory(ctx *gin.Context) {
func GetEcCategory(ctx *gin.Context) {
var category models.EcCategory
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&category).Error; err != nil {
if err := common.ActiveRecords(impl.DBService).Where("identity = ?", ctx.Param("identity")).First(&category).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}
@@ -114,7 +114,7 @@ func UpdateEcCategory(ctx *gin.Context) {
return
}
var category models.EcCategory
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&category).Error; err != nil {
if err := common.ActiveRecords(impl.DBService).Where("identity = ?", ctx.Param("identity")).First(&category).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}

View File

@@ -11,7 +11,7 @@ import (
// GetEcOrder returns the order together with its immutable item snapshots.
func GetEcOrder(ctx *gin.Context) {
var order models.EcOrder
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
if err := common.ActiveRecords(impl.DBService).Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}

View File

@@ -1,12 +1,15 @@
package gas
import (
"strings"
"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/logic/common"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type accountRequest struct {
@@ -25,8 +28,20 @@ type accountUpdateRequest struct {
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
}
func ListGasAccount(ctx *gin.Context) { common.ListPage[models.GasAccount](ctx) }
func GetGasAccount(ctx *gin.Context) { common.GetByIdentity[models.GasAccount](ctx) }
func ListGasAccount(ctx *gin.Context) {
identities := splitOwnerIdentities(ctx.Query("gas_basic_identities"))
if len(identities) > 100 {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
common.ListPageFiltered[models.GasAccount](ctx, func(query *gorm.DB) *gorm.DB {
if len(identities) == 0 {
return query
}
return query.Where("gas_basic_id IN (SELECT id FROM gas_basic WHERE identity IN ? AND status <> ?)", identities, common.StatusArchived)
})
}
func GetGasAccount(ctx *gin.Context) { common.GetByIdentity[models.GasAccount](ctx) }
func CreateGasAccount(ctx *gin.Context) {
var request accountRequest
@@ -65,3 +80,11 @@ func UpdateGasAccount(ctx *gin.Context) {
}
common.UpdateAllowedByIdentity(ctx, &models.GasAccount{}, gin.H{"gas_basic_id": gasBasicID, "display_name": request.DisplayName, "role_code": request.RoleCode}, []string{"gas_basic_id", "display_name", "role_code"})
}
func splitOwnerIdentities(value string) []string {
parts := strings.Split(strings.TrimSpace(value), ",")
if len(parts) == 1 && parts[0] == "" {
return nil
}
return parts
}

View File

@@ -2,6 +2,7 @@ package gasorder
import (
"errors"
"math"
"strings"
"time"
@@ -53,7 +54,7 @@ func GetGasorderPayment(ctx *gin.Context) { common.GetResource(ctx, &models.
func getGasorderContract(ctx *gin.Context) {
var contract models.GasorderContract
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&contract).Error; err != nil {
if err := common.ActiveRecords(impl.DBService).Where("identity = ?", ctx.Param("identity")).First(&contract).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}
@@ -77,7 +78,7 @@ func getGasorderContract(ctx *gin.Context) {
func getGasorderBasic(ctx *gin.Context) {
var order models.GasorderBasic
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
if err := common.ActiveRecords(impl.DBService).Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}
@@ -422,12 +423,19 @@ func CreateGasorderBasic(ctx *gin.Context) {
product.Status != common.StatusEnable || product.ProductStatus == common.StatusScrapped || product.UserAccountID != contract.UserAccountID {
return errors.New("contract product is no longer eligible")
}
if binding.UnitPrice <= 0 || productAmount > math.MaxInt64-binding.UnitPrice {
return errors.New("product amount overflow")
}
productAmount += binding.UnitPrice
}
payable := productAmount + contract.DefaultDeliveryFee - request.DiscountAmount
if payable <= 0 {
if contract.DefaultDeliveryFee < 0 || productAmount > math.MaxInt64-contract.DefaultDeliveryFee {
return errors.New("order amount overflow")
}
subtotal := productAmount + contract.DefaultDeliveryFee
if request.DiscountAmount >= subtotal {
return errors.New("invalid payable amount")
}
payable := subtotal - request.DiscountAmount
order = models.GasorderBasic{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable}, OrderStatus: common.StatusCreated,
OrderNo: models.NewIdentity(), RequestNo: request.RequestNo, GasorderContractID: contract.ID,
@@ -477,19 +485,20 @@ func AssignGasorderBasic(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
var delivery models.DeliveryBasic
if err := impl.DBService.Where("identity = ?", request.DeliveryIdentity).First(&delivery).Error; err != nil || delivery.Status != common.StatusEnable {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
var staff models.StaffAccount
if err := impl.DBService.Where("identity = ?", request.StaffIdentity).First(&staff).Error; err != nil ||
staff.Status != common.StatusEnable || staff.WorkStatus == "off_duty" {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
operatorIdentity, operatorName := common.PlatformOperator(ctx)
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
var delivery models.DeliveryBasic
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("identity = ? AND status = ?", request.DeliveryIdentity, common.StatusEnable).
First(&delivery).Error; err != nil {
return err
}
var staff models.StaffAccount
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("identity = ? AND status = ? AND work_status = ?", request.StaffIdentity, common.StatusEnable, "on_duty").
First(&staff).Error; err != nil {
return err
}
var order models.GasorderBasic
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
return err
@@ -743,7 +752,7 @@ func gasorderStatusRecord(orderID uint64, from, to int, reason, operatorIdentity
func getGasorderTrack(ctx *gin.Context) {
var track models.GasorderTrack
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&track).Error; err != nil {
if err := common.ActiveRecords(impl.DBService).Where("identity = ?", ctx.Param("identity")).First(&track).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}

View File

@@ -1,12 +1,19 @@
package platform
import (
"bytes"
"encoding/json"
"io"
"strings"
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
"git.apinb.com/bsm-sdk/core/middleware"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
platformbase "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
"strings"
)
const platformMenusContextKey = "platform_authorized_menus"
@@ -35,6 +42,10 @@ func platformMenuAllowsRequest(menus []platformbase.Menu, requestPath, method st
(menuIdentity == "gasorder_contract" || resource == "user_address") {
return true
}
if method == "GET" && menu.Identity == "gasorder_basic" &&
(resource == "delivery_basic" || resource == "staff_account") {
return true
}
if method == "GET" && relative == "wallet_basic" &&
(menu.Identity == "gas_basic" || menu.Identity == "delivery_basic" ||
menu.Identity == "staff" || menu.Identity == "user_account" ||
@@ -87,12 +98,21 @@ func RequirePlatformMenuAccess() gin.HandlerFunc {
ctx.Abort()
return
}
var account models.PlatformAccount
if err := impl.DBService.Select("id", "platform_role_code").
Where("identity = ? AND platform_role_code = ? AND status = ?", claims.Identity, claims.Role, common.StatusEnable).
First(&account).Error; err != nil {
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
ctx.Abort()
return
}
if claims.Role == "root" {
ctx.Next()
return
}
menus, err := platformbase.LoadPlatformMenus(claims.Role)
if err != nil || !platformMenuAllowsRequest(menus, ctx.Request.URL.Path, ctx.Request.Method) {
if err != nil || !platformMenuAllowsRequest(menus, ctx.Request.URL.Path, ctx.Request.Method) ||
!platformScopedRequestAllowed(ctx, menus) {
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
ctx.Abort()
return
@@ -101,3 +121,104 @@ func RequirePlatformMenuAccess() gin.HandlerFunc {
ctx.Next()
}
}
func platformScopedRequestAllowed(ctx *gin.Context, menus []platformbase.Menu) bool {
relative := strings.Trim(strings.SplitN(ctx.Request.URL.Path, "/platform/v1/", 2)[1], "/")
parts := strings.Split(relative, "/")
resource := parts[0]
if resource == "wallet_basic" && len(parts) == 1 && ctx.Request.Method == "GET" {
if hasMenuIdentity(menus, "wallet_apply_cash") {
return true
}
required := map[string]string{
"gas": "gas_basic", "delivery": "delivery_basic", "staff": "staff", "user": "user_account",
}[ctx.Query("owner_type")]
return required != "" && hasMenuIdentity(menus, required)
}
if resource == "staff_credential" {
return staffCredentialRequestAllowed(ctx, menus, parts)
}
if resource != "staff_account" {
return true
}
if len(parts) == 1 {
if ctx.Request.Method == "POST" {
return hasMenuIdentity(menus, "staff_add")
}
if ctx.Request.Method == "GET" {
required := staffMenuIdentity(ctx.Query("role_code"))
return required != "" && (hasMenuIdentity(menus, required) ||
(required == "staff_delivery" && hasMenuIdentity(menus, "gasorder_basic")))
}
return false
}
var staff models.StaffAccount
if err := common.ActiveRecords(impl.DBService).Select("role_code").
Where("identity = ?", parts[1]).First(&staff).Error; err != nil {
return false
}
return hasMenuIdentity(menus, staffMenuIdentity(staff.RoleCode))
}
func staffCredentialRequestAllowed(ctx *gin.Context, menus []platformbase.Menu, parts []string) bool {
var staffIdentity string
if len(parts) == 1 && ctx.Request.Method == "GET" {
staffIdentity = ctx.Query("staff_account_identity")
} else if (ctx.Request.Method == "POST" || ctx.Request.Method == "PUT") && ctx.Request.Body != nil {
body, err := io.ReadAll(ctx.Request.Body)
if err != nil {
return false
}
ctx.Request.Body = io.NopCloser(bytes.NewReader(body))
var payload struct {
StaffAccountIdentity string `json:"staff_account_identity"`
}
if json.Unmarshal(body, &payload) != nil {
return false
}
staffIdentity = payload.StaffAccountIdentity
} else if len(parts) > 1 {
var credential models.StaffCredential
if err := common.ActiveRecords(impl.DBService).Select("staff_account_id").
Where("identity = ?", parts[1]).First(&credential).Error; err != nil {
return false
}
var staff models.StaffAccount
if err := common.ActiveRecords(impl.DBService).Select("role_code").
Where("id = ?", credential.StaffAccountID).First(&staff).Error; err != nil {
return false
}
return hasMenuIdentity(menus, staffMenuIdentity(staff.RoleCode))
}
if staffIdentity == "" {
return false
}
var staff models.StaffAccount
if err := common.ActiveRecords(impl.DBService).Select("role_code").
Where("identity = ?", staffIdentity).First(&staff).Error; err != nil {
return false
}
return hasMenuIdentity(menus, staffMenuIdentity(staff.RoleCode))
}
func staffMenuIdentity(roleCode string) string {
switch roleCode {
case "installer":
return "staff_installer"
case "delivery":
return "staff_delivery"
case "operations":
return "staff_operations"
default:
return ""
}
}
func hasMenuIdentity(menus []platformbase.Menu, identity string) bool {
for _, menu := range menus {
if menu.Identity == identity {
return true
}
}
return false
}

View File

@@ -53,7 +53,7 @@ func platformAccountView(account models.PlatformAccount) map[string]any {
func GetPlatformAccount(ctx *gin.Context) {
var account models.PlatformAccount
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&account).Error; err != nil {
if err := common.ActiveRecords(impl.DBService).Where("identity = ?", ctx.Param("identity")).First(&account).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}
@@ -127,6 +127,9 @@ func UpdatePlatformAccountStatus(ctx *gin.Context) {
if !common.RequirePlatformRoot(ctx) {
return
}
if !modifiablePlatformAccount(ctx) {
return
}
common.UpdateRecordStatus(ctx, &models.PlatformAccount{})
}
@@ -135,9 +138,26 @@ func ArchivePlatformAccount(ctx *gin.Context) {
if !common.RequirePlatformRoot(ctx) {
return
}
if !modifiablePlatformAccount(ctx) {
return
}
common.ArchiveRecord(ctx, &models.PlatformAccount{})
}
func modifiablePlatformAccount(ctx *gin.Context) bool {
var account models.PlatformAccount
if err := common.ActiveRecords(impl.DBService).Select("platform_role_code").
Where("identity = ?", ctx.Param("identity")).First(&account).Error; err != nil {
common.RespondRecordError(ctx, err)
return false
}
if account.PlatformRoleCode == "root" {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return false
}
return true
}
func platformPasswordHash(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(hash), err

View File

@@ -55,7 +55,7 @@ func UpdatePlatformRole(ctx *gin.Context) {
return
}
var role models.PlatformRole
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil {
if err := common.ActiveRecords(impl.DBService).Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}

View File

@@ -30,7 +30,7 @@ func ReplacePlatformRoleMenus(ctx *gin.Context) {
}
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 {
if err := common.ActiveRecords(transaction).Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil {
return err
}
if role.IsSystem {
@@ -77,7 +77,7 @@ func ListPlatformRoleMenuIdentities(ctx *gin.Context) {
return
}
var role models.PlatformRole
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil {
if err := common.ActiveRecords(impl.DBService).Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}

View File

@@ -149,11 +149,45 @@ func UpdateProductInfoLifecycle(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
values := gin.H{"product_status": request.ProductStatus}
if request.ProductStatus == common.StatusScrapped {
values["status"] = common.StatusDisable
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
var product models.ProductInfo
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("identity = ? AND status <> ?", ctx.Param("identity"), common.StatusArchived).
First(&product).Error; err != nil {
return err
}
var pendingRepairs int64
if err := tx.Model(&models.ProductRepair{}).
Where("product_info_id = ? AND result = ? AND status <> ?", product.ID, "pending", common.StatusArchived).
Count(&pendingRepairs).Error; err != nil {
return err
}
if pendingRepairs > 0 && request.ProductStatus != common.StatusRepairing ||
pendingRepairs == 0 && request.ProductStatus == common.StatusRepairing {
return errors.New("product lifecycle conflicts with repair state")
}
if request.ProductStatus == common.StatusScrapped {
var activeOrders int64
if err := tx.Model(&models.GasorderItem{}).
Where("product_info_id = ? AND active = ?", product.ID, true).
Count(&activeOrders).Error; err != nil {
return err
}
if activeOrders != 0 {
return errors.New("product is occupied by an active order")
}
}
values := map[string]any{"product_status": request.ProductStatus}
if request.ProductStatus == common.StatusScrapped {
values["status"] = common.StatusDisable
}
return tx.Model(&product).Updates(values).Error
})
if err != nil {
common.RespondRecordError(ctx, err)
return
}
common.UpdateAllowedByIdentity(ctx, &models.ProductInfo{}, values, []string{"product_status", "status"})
infra.Response.Success(ctx, gin.H{"updated": true})
}
func UpdateProductInfoRecordStatus(ctx *gin.Context) {
@@ -202,9 +236,17 @@ func createProductRepair(ctx *gin.Context, fields []string, relations []common.R
}
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
var product models.ProductInfo
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&product, data.ProductInfoID).Error; err != nil {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ? AND status = ? AND product_status <> ?", data.ProductInfoID, common.StatusEnable, common.StatusScrapped).
First(&product).Error; err != nil {
return err
}
var activeOrders int64
if err := tx.Model(&models.GasorderItem{}).
Where("product_info_id = ? AND active = ?", data.ProductInfoID, true).
Count(&activeOrders).Error; err != nil || activeOrders != 0 {
return errors.New("product is occupied by an active order")
}
var pending int64
if err := tx.Model(&models.ProductRepair{}).Where("product_info_id = ? AND result = ?", data.ProductInfoID, "pending").Count(&pending).Error; err != nil || !canStartProductRepair(pending) {
return errors.New("product already has a pending repair")
@@ -234,7 +276,9 @@ func updateProductRepair(ctx *gin.Context, fields []string, relations []common.R
}
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
var current models.ProductRepair
if err := tx.Where("identity = ?", ctx.Param("identity")).First(&current).Error; err != nil {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("identity = ? AND status <> ?", ctx.Param("identity"), common.StatusArchived).
First(&current).Error; err != nil {
return err
}
if current.Result != "pending" {

View File

@@ -18,8 +18,40 @@ type staffCredentialRequest struct {
ExpiredAt *time.Time `json:"expired_at"`
}
func ListStaffCredential(ctx *gin.Context) { common.ListPage[models.StaffCredential](ctx) }
func GetStaffCredential(ctx *gin.Context) { common.GetByIdentity[models.StaffCredential](ctx) }
func ListStaffCredential(ctx *gin.Context) {
staffIdentity := ctx.Query("staff_account_identity")
if staffIdentity == "" {
common.ListPage[models.StaffCredential](ctx)
return
}
staffID, err := common.ResolveIdentityID(&models.StaffAccount{}, staffIdentity, true)
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
page, size := common.PageSize(ctx)
var list []models.StaffCredential
var total int64
query := common.ApplyKeywordFilter(ctx,
common.ActiveRecords(impl.DBService.Model(&models.StaffCredential{})).
Where("staff_account_id = ?", staffID),
&models.StaffCredential{})
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": response})
}
func GetStaffCredential(ctx *gin.Context) { common.GetByIdentity[models.StaffCredential](ctx) }
func CreateStaffCredential(ctx *gin.Context) {
var request staffCredentialRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
@@ -49,5 +81,15 @@ func UpdateStaffCredential(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
var current models.StaffCredential
if err := common.ActiveRecords(impl.DBService).Select("staff_account_id").
Where("identity = ?", ctx.Param("identity")).First(&current).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}
if current.StaffAccountID != staffAccountID {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
common.UpdateAllowedByIdentity(ctx, &models.StaffCredential{}, gin.H{"staff_account_id": staffAccountID, "credential_type": request.CredentialType, "credential_no": request.CredentialNo, "expired_at": request.ExpiredAt}, []string{"staff_account_id", "credential_type", "credential_no", "expired_at"})
}

View File

@@ -1,6 +1,8 @@
package staff
import (
"strings"
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
@@ -10,7 +12,37 @@ import (
)
// ListStaff 查询服务人员分页列表。
func ListStaff(ctx *gin.Context) { common.ListPage[models.StaffAccount](ctx) }
func ListStaff(ctx *gin.Context) {
roleCode := strings.TrimSpace(ctx.Query("role_code"))
if roleCode == "" {
common.ListPage[models.StaffAccount](ctx)
return
}
if !validStaffRole(roleCode) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
page, size := common.PageSize(ctx)
var list []models.StaffAccount
var total int64
query := common.ApplyKeywordFilter(ctx,
common.ActiveRecords(impl.DBService.Model(&models.StaffAccount{})).Where("role_code = ?", roleCode),
&models.StaffAccount{})
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)})
}
// GetStaff 查询一个服务人员档案。
func GetStaff(ctx *gin.Context) { common.GetByIdentity[models.StaffAccount](ctx) }
@@ -28,7 +60,7 @@ func CreateStaff(ctx *gin.Context) {
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
WorkStatus string `json:"work_status" binding:"max=32"`
}
if err := ctx.ShouldBindJSON(&request); err != nil {
if err := ctx.ShouldBindJSON(&request); err != nil || !validStaffRole(request.RoleCode) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
@@ -77,7 +109,7 @@ func UpdateStaff(ctx *gin.Context) {
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
WorkStatus string `json:"work_status" binding:"max=32"`
}
if err := ctx.ShouldBindJSON(&request); err != nil || !validWorkStatus(request.WorkStatus) {
if err := ctx.ShouldBindJSON(&request); err != nil || !validWorkStatus(request.WorkStatus) || !validStaffRole(request.RoleCode) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
@@ -99,3 +131,6 @@ func UpdateStaff(ctx *gin.Context) {
}
func validWorkStatus(status string) bool { return status == "on_duty" || status == "off_duty" }
func validStaffRole(role string) bool {
return role == "installer" || role == "delivery" || role == "operations"
}

View File

@@ -10,3 +10,16 @@ func TestWorkStatusIsClosedEnumeration(t *testing.T) {
t.Fatal("unknown work status was accepted as available")
}
}
func TestStaffRoleIsClosedEnumeration(t *testing.T) {
for _, role := range []string{"installer", "delivery", "operations"} {
if !validStaffRole(role) {
t.Fatalf("supported staff role %q was rejected", role)
}
}
for _, role := range []string{"", "admin", "root", "delivery_admin"} {
if validStaffRole(role) {
t.Fatalf("unsupported staff role %q was accepted", role)
}
}
}

View File

@@ -118,6 +118,16 @@ func UpdateUserServiceRelation(ctx *gin.Context) {
if !ok {
return
}
var current models.UserServiceRelation
if err := common.ActiveRecords(impl.DBService).Select("user_account_id").
Where("identity = ?", ctx.Param("identity")).First(&current).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}
if current.UserAccountID != userAccountID {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
common.UpdateAllowedByIdentity(ctx, &models.UserServiceRelation{}, gin.H{"user_account_id": userAccountID, "gas_basic_id": gasBasicID, "delivery_basic_id": deliveryBasicID, "staff_account_id": staffAccountID}, []string{"user_account_id", "gas_basic_id", "delivery_basic_id", "staff_account_id"})
}

View File

@@ -25,25 +25,46 @@ var walletOwnerModels = map[string]any{
"gas": &models.GasBasic{},
}
func ListWalletBasic(ctx *gin.Context) { listWalletPage[models.WalletBasic](ctx) }
func ListWalletBasic(ctx *gin.Context) {
ownerType := strings.TrimSpace(ctx.Query("owner_type"))
if ownerType != "" && ownerType != "user" && ownerType != "staff" && ownerType != "delivery" && ownerType != "gas" && ownerType != "platform" {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
ownerIdentities := strings.Split(strings.TrimSpace(ctx.Query("owner_identities")), ",")
if len(ownerIdentities) == 1 && ownerIdentities[0] == "" {
ownerIdentities = nil
}
if len(ownerIdentities) > 100 {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
listWalletPage[models.WalletBasic](ctx, ownerType, ownerIdentities)
}
func GetWalletBasic(ctx *gin.Context) { getWalletByIdentity[models.WalletBasic](ctx) }
func ListWalletBank(ctx *gin.Context) { listWalletPage[models.WalletBank](ctx) }
func ListWalletBank(ctx *gin.Context) { listWalletPage[models.WalletBank](ctx, "", nil) }
func GetWalletBank(ctx *gin.Context) { getWalletByIdentity[models.WalletBank](ctx) }
func ListWalletPayment(ctx *gin.Context) { listWalletPage[models.WalletPayment](ctx) }
func ListWalletPayment(ctx *gin.Context) { listWalletPage[models.WalletPayment](ctx, "", nil) }
func GetWalletPayment(ctx *gin.Context) { getWalletByIdentity[models.WalletPayment](ctx) }
func ListWalletRecord(ctx *gin.Context) { listWalletPage[models.WalletRecord](ctx) }
func ListWalletRecord(ctx *gin.Context) { listWalletPage[models.WalletRecord](ctx, "", nil) }
func GetWalletRecord(ctx *gin.Context) { getWalletByIdentity[models.WalletRecord](ctx) }
func ListWalletRefund(ctx *gin.Context) { listWalletPage[models.WalletRefund](ctx) }
func ListWalletRefund(ctx *gin.Context) { listWalletPage[models.WalletRefund](ctx, "", nil) }
func GetWalletRefund(ctx *gin.Context) { getWalletByIdentity[models.WalletRefund](ctx) }
func ListWalletApplyCash(ctx *gin.Context) { listWalletPage[models.WalletApplyCash](ctx) }
func ListWalletApplyCash(ctx *gin.Context) { listWalletPage[models.WalletApplyCash](ctx, "", nil) }
func GetWalletApplyCash(ctx *gin.Context) { getWalletByIdentity[models.WalletApplyCash](ctx) }
func listWalletPage[T any](ctx *gin.Context) {
func listWalletPage[T any](ctx *gin.Context, ownerType string, ownerIdentities []string) {
page, size := common.PageSize(ctx)
var list []T
var total int64
model := new(T)
query := common.ApplyKeywordFilter(ctx, common.ActiveRecords(impl.DBService.Model(model)), model)
if ownerType != "" {
query = query.Where("owner_type = ?", ownerType)
}
if len(ownerIdentities) > 0 {
query = query.Where("owner_identity IN ?", ownerIdentities)
}
if err := query.Count(&total).Error; err != nil {
infra.Response.Error(ctx, err)
return
@@ -63,7 +84,7 @@ func listWalletPage[T any](ctx *gin.Context) {
func getWalletByIdentity[T any](ctx *gin.Context) {
var data T
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&data).Error; err != nil {
if err := common.ActiveRecords(impl.DBService).Where("identity = ?", ctx.Param("identity")).First(&data).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}
@@ -268,9 +289,6 @@ func RejectWalletApplyCash(ctx *gin.Context) {
}
func reviewWalletApplyCash(ctx *gin.Context, targetStatus int) {
if !common.RequirePlatformRoot(ctx) {
return
}
var request struct {
Reason string `json:"reason" binding:"required,max=2000"`
}
@@ -282,7 +300,7 @@ func reviewWalletApplyCash(ctx *gin.Context, targetStatus int) {
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
var application models.WalletApplyCash
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("identity = ?", ctx.Param("identity")).First(&application).Error; err != nil {
Where("identity = ? AND status = ?", ctx.Param("identity"), common.StatusEnable).First(&application).Error; err != nil {
return err
}
if application.ApplyStatus == targetStatus {
@@ -315,6 +333,45 @@ func reviewWalletApplyCash(ctx *gin.Context, targetStatus int) {
infra.Response.Success(ctx, gin.H{"updated": true, "apply_status": targetStatus})
}
// CompleteWalletApplyCash records the external payout result after approval.
func CompleteWalletApplyCash(ctx *gin.Context) {
var request struct {
TradeNo string `json:"trade_no" binding:"required,max=128"`
CallbackMsg string `json:"callback_msg" binding:"max=4000"`
}
if err := ctx.ShouldBindJSON(&request); err != nil || strings.TrimSpace(request.TradeNo) == "" {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
var application models.WalletApplyCash
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("identity = ? AND status = ?", ctx.Param("identity"), common.StatusEnable).
First(&application).Error; err != nil {
return err
}
if application.ApplyStatus == common.StatusCompleted {
if application.TradeNo == request.TradeNo {
return nil
}
return errors.New("cash application already completed")
}
if application.ApplyStatus != common.StatusApproved {
return errors.New("cash application is not approved")
}
now := time.Now()
return tx.Model(&application).Updates(map[string]any{
"apply_status": common.StatusCompleted, "trade_no": strings.TrimSpace(request.TradeNo),
"callback_msg": request.CallbackMsg, "completed_at": &now,
}).Error
})
if err != nil {
common.RespondRecordError(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"updated": true, "apply_status": common.StatusCompleted})
}
func dateNumber(value time.Time, layout string) int32 {
number, _ := strconv.ParseInt(value.Format(layout), 10, 32)
return int32(number)