fix platform authorization and workflow integrity
This commit is contained in:
@@ -74,11 +74,19 @@ func ActiveRecords(query *gorm.DB) *gorm.DB {
|
||||
}
|
||||
|
||||
func ListPage[T any](ctx *gin.Context) {
|
||||
ListPageFiltered[T](ctx, nil)
|
||||
}
|
||||
|
||||
// ListPageFiltered applies a resource-specific exact filter before pagination.
|
||||
func ListPageFiltered[T any](ctx *gin.Context, filter func(*gorm.DB) *gorm.DB) {
|
||||
page, size := PageSize(ctx)
|
||||
var list []T
|
||||
var total int64
|
||||
model := new(T)
|
||||
databaseQuery := ApplyKeywordFilter(ctx, ActiveRecords(impl.DBService.Model(model)), model)
|
||||
if filter != nil {
|
||||
databaseQuery = filter(databaseQuery)
|
||||
}
|
||||
if err := databaseQuery.Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -172,7 +180,7 @@ func gormColumn(tag string) string {
|
||||
|
||||
func GetByIdentity[T any](ctx *gin.Context) {
|
||||
var data T
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&data).Error; err != nil {
|
||||
if err := ActiveRecords(impl.DBService).Where("identity = ?", ctx.Param("identity")).First(&data).Error; err != nil {
|
||||
RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func ListResource(ctx *gin.Context, model any) {
|
||||
|
||||
func GetResource(ctx *gin.Context, model any) {
|
||||
data := reflect.New(reflect.TypeOf(model).Elem())
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(data.Interface()).Error; err != nil {
|
||||
if err := ActiveRecords(impl.DBService).Where("identity = ?", ctx.Param("identity")).First(data.Interface()).Error; err != nil {
|
||||
RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(¤t).Error; err != nil {
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("identity = ? AND status <> ?", ctx.Param("identity"), common.StatusArchived).
|
||||
First(¤t).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if current.Result != "pending" {
|
||||
|
||||
@@ -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(¤t).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"})
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(¤t).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"})
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
type Entity struct {
|
||||
ID uint64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 数据库自增主键
|
||||
Identity string `gorm:"column:identity;type:varchar(36);not null;uniqueIndex" json:"identity"` // UUID V7 业务标识
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null" json:"created_at"` // 创建时间
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null;index" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null" json:"updated_at"` // 更新时间
|
||||
Status int `gorm:"column:status;not null;default:0" json:"status"` // 业务状态
|
||||
Status int `gorm:"column:status;not null;default:0;index" json:"status"` // 通用记录状态
|
||||
}
|
||||
|
||||
// NewIdentity 生成时间有序的 UUID V7 字符串,生成失败属于不可恢复的运行时错误。
|
||||
|
||||
@@ -9,22 +9,22 @@ import (
|
||||
// WalletApplyCash 对应 wallet_apply_cash,保存提现申请及审核结果。
|
||||
type WalletApplyCash struct {
|
||||
Entity // 公共实体字段
|
||||
ApplyStatus int `gorm:"column:apply_status;not null;default:10;index" json:"apply_status"` // 提现申请业务状态
|
||||
WalletBasicID uint64 `gorm:"column:wallet_basic_id;not null;index" json:"wallet_basic_id"` // 钱包自增主键
|
||||
WalletBankID uint64 `gorm:"column:wallet_bank_id;not null;default:0;index" json:"wallet_bank_id"` // 银行卡自增主键
|
||||
CashNo string `gorm:"column:cash_no;type:varchar(64);not null;uniqueIndex" json:"cash_no"` // 内部提现单号
|
||||
RequestNo string `gorm:"column:request_no;type:varchar(128);not null;uniqueIndex" json:"request_no"` // 申请幂等号
|
||||
Amount int64 `gorm:"column:amount;not null;check:amount > 0" json:"amount"` // 提现金额,单位分
|
||||
Fee int64 `gorm:"column:fee;not null;default:0;check:fee >= 0" json:"fee"` // 提现手续费,单位分
|
||||
Channel string `gorm:"column:channel;type:varchar(32);not null" json:"channel"` // 提现渠道
|
||||
TradeNo string `gorm:"column:trade_no;type:varchar(128);not null;default:'';index" json:"trade_no"` // 第三方提现流水号
|
||||
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 申请备注
|
||||
CallbackMsg string `gorm:"column:callback_msg;type:text;not null;default:''" json:"callback_msg"` // 提现回调信息
|
||||
ReviewerIdentity string `gorm:"column:reviewer_identity;type:varchar(36);not null;default:'';index" json:"reviewer_identity"` // 审核人业务标识
|
||||
ReviewerName string `gorm:"column:reviewer_name;type:varchar(64);not null;default:''" json:"reviewer_name"` // 审核人姓名快照
|
||||
ReviewedAt *time.Time `gorm:"column:reviewed_at;type:timestamptz" json:"reviewed_at"` // 审核时间
|
||||
ReviewReason string `gorm:"column:review_reason;type:text;not null;default:''" json:"review_reason"` // 审核原因
|
||||
CompletedAt *time.Time `gorm:"column:completed_at;type:timestamptz" json:"completed_at"` // 完成时间
|
||||
ApplyStatus int `gorm:"column:apply_status;not null;default:10;index" json:"apply_status"` // 提现申请业务状态
|
||||
WalletBasicID uint64 `gorm:"column:wallet_basic_id;not null;index" json:"wallet_basic_id"` // 钱包自增主键
|
||||
WalletBankID uint64 `gorm:"column:wallet_bank_id;not null;default:0;index" json:"wallet_bank_id"` // 银行卡自增主键
|
||||
CashNo string `gorm:"column:cash_no;type:varchar(64);not null;uniqueIndex" json:"cash_no"` // 内部提现单号
|
||||
RequestNo string `gorm:"column:request_no;type:varchar(128);not null;uniqueIndex" json:"request_no"` // 申请幂等号
|
||||
Amount int64 `gorm:"column:amount;not null;check:amount > 0" json:"amount"` // 提现金额,单位分
|
||||
Fee int64 `gorm:"column:fee;not null;default:0;check:fee >= 0" json:"fee"` // 提现手续费,单位分
|
||||
Channel string `gorm:"column:channel;type:varchar(32);not null" json:"channel"` // 提现渠道
|
||||
TradeNo string `gorm:"column:trade_no;type:varchar(128);not null;default:'';uniqueIndex:idx_wallet_cash_trade,where:trade_no <> ''" json:"trade_no"` // 第三方提现流水号
|
||||
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 申请备注
|
||||
CallbackMsg string `gorm:"column:callback_msg;type:text;not null;default:''" json:"callback_msg"` // 提现回调信息
|
||||
ReviewerIdentity string `gorm:"column:reviewer_identity;type:varchar(36);not null;default:'';index" json:"reviewer_identity"` // 审核人业务标识
|
||||
ReviewerName string `gorm:"column:reviewer_name;type:varchar(64);not null;default:''" json:"reviewer_name"` // 审核人姓名快照
|
||||
ReviewedAt *time.Time `gorm:"column:reviewed_at;type:timestamptz" json:"reviewed_at"` // 审核时间
|
||||
ReviewReason string `gorm:"column:review_reason;type:text;not null;default:''" json:"review_reason"` // 审核原因
|
||||
CompletedAt *time.Time `gorm:"column:completed_at;type:timestamptz" json:"completed_at"` // 完成时间
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&WalletApplyCash{}) }
|
||||
|
||||
@@ -135,7 +135,7 @@ func registerWalletRoute(group *gin.RouterGroup) {
|
||||
basic.GET("/:identity", wallet.GetWalletBasic)
|
||||
basic.PATCH("/:identity/status", wallet.UpdateWalletBasicStatus)
|
||||
basic.POST("/:identity/recharge", wallet.RechargeWalletBasic)
|
||||
basic.GET("/owner/:owner_type/:owner_identity", wallet.GetOrCreateOwnerWallet)
|
||||
basic.POST("/owner/:owner_type/:owner_identity", wallet.GetOrCreateOwnerWallet)
|
||||
|
||||
bank := group.Group("/wallet_bank")
|
||||
bank.GET("", wallet.ListWalletBank)
|
||||
@@ -158,6 +158,7 @@ func registerWalletRoute(group *gin.RouterGroup) {
|
||||
applyCash.GET("/:identity", wallet.GetWalletApplyCash)
|
||||
applyCash.POST("/:identity/approve", wallet.ApproveWalletApplyCash)
|
||||
applyCash.POST("/:identity/reject", wallet.RejectWalletApplyCash)
|
||||
applyCash.POST("/:identity/complete", wallet.CompleteWalletApplyCash)
|
||||
}
|
||||
|
||||
func registerCommerceRoute(group *gin.RouterGroup) {
|
||||
|
||||
@@ -212,9 +212,10 @@ func TestPlatformFinanceContentRoutesFollowTheirContracts(t *testing.T) {
|
||||
}
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/wallet_basic/:identity/status", http.MethodPatch)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/wallet_basic/:identity/recharge", http.MethodPost)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/wallet_basic/owner/:owner_type/:owner_identity", http.MethodGet)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/wallet_basic/owner/:owner_type/:owner_identity", http.MethodPost)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/wallet_apply_cash/:identity/approve", http.MethodPost)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/wallet_apply_cash/:identity/reject", http.MethodPost)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/wallet_apply_cash/:identity/complete", http.MethodPost)
|
||||
for _, oldPath := range []string{
|
||||
"/heqi/platform/v1/wallet/wallet",
|
||||
"/heqi/platform/v1/wallet/wallet_ledger",
|
||||
|
||||
@@ -33,6 +33,7 @@ export type DetailAction = {
|
||||
method?: 'POST' | 'PUT' | 'PATCH';
|
||||
danger?: boolean;
|
||||
fields?: ResourceField[];
|
||||
visibleFor?: { field: string; values: Array<string | number> };
|
||||
};
|
||||
|
||||
export type ResourceUiDefinition = {
|
||||
@@ -303,24 +304,24 @@ export const resources: ResourceUiDefinition[] = [
|
||||
define('product_owner', '智能气阀归属记录', 'readonly', []),
|
||||
|
||||
define('gasorder_contract', '配送合同', 'managed', [f('contract_no', { required: true }), relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic', true), relation('delivery_basic_identity', '/delivery_basic'), f('title', { required: true }), f('terms'), f('file_uri'), f('default_delivery_fee'), f('signed_at', { required: true }), f('effective_at', { required: true }), f('expired_at')], 'list', [
|
||||
{ name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason },
|
||||
{ name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason] },
|
||||
{ name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason },
|
||||
{ name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason, visibleFor: { field: 'contract_status', values: [10] } },
|
||||
{ name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12] } },
|
||||
{ name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } },
|
||||
], { canCreate: true, canEdit: true }),
|
||||
define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true), relation('product_info_identity', '/product_info', true), f('unit_price')], 'list', [
|
||||
{ name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason },
|
||||
]),
|
||||
define('gasorder_contract_revision', '合同修订记录', 'readonly', []),
|
||||
define('gasorder_basic', '气体配送订单', 'append_only', [f('request_no', { required: true }), relation('gasorder_contract_identity', '/gasorder_contract', true), f('creator_type', { required: true, type: 'select', options: [{ label: '用户', value: 'user' }, { label: '工作人员', value: 'staff' }, { label: '配送站', value: 'delivery' }, { label: '气站', value: 'gas' }] }), f('creator_identity', { required: true }), relation('user_address_identity', '/user_address', true), f('gasorder_contract_product_identities', { required: true, type: 'identity-list', relation: '/gasorder_contract_product' }), f('contact_name', { required: true }), f('contact_phone', { required: true }), f('discount_amount'), f('remark')], 'list', [
|
||||
{ name: '分配订单', resource: '/gasorder_basic/:identity/assign', fields: [relation('delivery_basic_identity', '/delivery_basic', true), relation('staff_account_identity', '/staff_account', true), ...reason] },
|
||||
{ name: '开始罐装', resource: '/gasorder_basic/:identity/filling', fields: reason },
|
||||
{ name: '待配送', resource: '/gasorder_basic/:identity/ready', fields: reason },
|
||||
{ name: '开始配送', resource: '/gasorder_basic/:identity/delivering', fields: reason },
|
||||
{ name: '等待签收', resource: '/gasorder_basic/:identity/awaiting-confirmation', fields: reason },
|
||||
{ name: '完成订单', resource: '/gasorder_basic/:identity/complete', fields: [...reason, f('confirm_type', { required: true }), f('recipient_name', { required: true }), f('recipient_phone'), f('proof_uri'), f('remark')] },
|
||||
{ name: '标记异常', resource: '/gasorder_basic/:identity/exception', fields: reason },
|
||||
{ name: '恢复订单', resource: '/gasorder_basic/:identity/recover', fields: reason },
|
||||
{ name: '取消订单', resource: '/gasorder_basic/:identity/cancel', danger: true, fields: reason },
|
||||
{ name: '分配订单', resource: '/gasorder_basic/:identity/assign', fields: [relation('delivery_basic_identity', '/delivery_basic', true), relation('staff_account_identity', '/staff_account', true), ...reason], visibleFor: { field: 'order_status', values: [16, 18] } },
|
||||
{ name: '开始罐装', resource: '/gasorder_basic/:identity/filling', fields: reason, visibleFor: { field: 'order_status', values: [18] } },
|
||||
{ name: '待配送', resource: '/gasorder_basic/:identity/ready', fields: reason, visibleFor: { field: 'order_status', values: [19] } },
|
||||
{ name: '开始配送', resource: '/gasorder_basic/:identity/delivering', fields: reason, visibleFor: { field: 'order_status', values: [20] } },
|
||||
{ name: '等待签收', resource: '/gasorder_basic/:identity/awaiting-confirmation', fields: reason, visibleFor: { field: 'order_status', values: [33] } },
|
||||
{ name: '完成订单', resource: '/gasorder_basic/:identity/complete', fields: [...reason, f('confirm_type', { required: true }), f('recipient_name', { required: true }), f('recipient_phone'), f('proof_uri'), f('remark')], visibleFor: { field: 'order_status', values: [34] } },
|
||||
{ name: '标记异常', resource: '/gasorder_basic/:identity/exception', fields: reason, visibleFor: { field: 'order_status', values: [19, 20, 33, 34] } },
|
||||
{ name: '恢复订单', resource: '/gasorder_basic/:identity/recover', fields: reason, visibleFor: { field: 'order_status', values: [21] } },
|
||||
{ name: '取消订单', resource: '/gasorder_basic/:identity/cancel', danger: true, fields: reason, visibleFor: { field: 'order_status', values: [16, 18] } },
|
||||
]),
|
||||
define('gasorder_item', '订单明细', 'readonly', []),
|
||||
define('gasorder_assign', '分配记录', 'readonly', []),
|
||||
@@ -349,7 +350,7 @@ export const resources: ResourceUiDefinition[] = [
|
||||
define('wallet_refund', '退款记录', 'readonly', []),
|
||||
define('wallet_apply_cash', '提现记录', 'readonly', [
|
||||
f('cash_no'),
|
||||
relation('wallet_basic_identity', '/wallet_basic'),
|
||||
f('wallet_basic_identity', { type: 'identity' }),
|
||||
f('amount'),
|
||||
f('apply_status', { type: 'select', options: [
|
||||
{ label: '待处理', value: 10 },
|
||||
@@ -365,8 +366,9 @@ export const resources: ResourceUiDefinition[] = [
|
||||
f('trade_no'),
|
||||
f('remark'),
|
||||
], 'list', [
|
||||
{ name: '审核通过', resource: '/wallet_apply_cash/:identity/approve', fields: reason },
|
||||
{ name: '审核驳回', resource: '/wallet_apply_cash/:identity/reject', danger: true, fields: reason },
|
||||
{ name: '审核通过', resource: '/wallet_apply_cash/:identity/approve', fields: reason, visibleFor: { field: 'apply_status', values: [10] } },
|
||||
{ name: '审核驳回', resource: '/wallet_apply_cash/:identity/reject', danger: true, fields: reason, visibleFor: { field: 'apply_status', values: [10] } },
|
||||
{ name: '标记处理完成', resource: '/wallet_apply_cash/:identity/complete', fields: [f('trade_no', { required: true }), f('callback_msg')], visibleFor: { field: 'apply_status', values: [25] } },
|
||||
]),
|
||||
|
||||
define('fin_payment', '财务支付记录', 'readonly', []),
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -62,8 +62,8 @@ const routes: AppRouteRecordRaw[] = [
|
||||
group('organization', 'organization', '机构管理', 'icon-storage', 10, [
|
||||
child('organization', 'gas-basic', 'gas-basic', '气站管理', '/gas_basic', 'gas_basic'),
|
||||
child('organization', 'delivery-basic', 'delivery-basic', '配送点管理', '/delivery_basic', 'delivery_basic'),
|
||||
child('organization', 'gas-account', 'gas-account', '气站账户', '/gas_account', 'gas_basic', true, 'gas-basic'),
|
||||
child('organization', 'delivery-account', 'delivery-account', '配送点账户', '/delivery_account', 'delivery_basic', true, 'delivery-basic'),
|
||||
child('organization', 'gas-account', 'gas-account', '气站账户', '/gas_account', 'gas_basic', true, 'organization-gas-basic'),
|
||||
child('organization', 'delivery-account', 'delivery-account', '配送点账户', '/delivery_account', 'delivery_basic', true, 'organization-delivery-basic'),
|
||||
]),
|
||||
group('staff', 'staff', '工作人员管理', 'icon-user-group', 30, [
|
||||
{ ...child('staff', 'add', 'add', '新增工作人员', '/staff_account', 'staff_add'), meta: { title: '新增工作人员', resource: '/staff_account', requiresAuth: true, menuCode: 'staff_add', createMode: true } },
|
||||
@@ -97,10 +97,10 @@ const routes: AppRouteRecordRaw[] = [
|
||||
child('gasorder', 'track-points', 'track-points', '轨迹点', '/gasorder_track_point', 'gasorder_track', true, 'gasorder-tracks'),
|
||||
child('gasorder', 'confirms', 'confirms', '确认记录', '/gasorder_confirm', 'gasorder_basic', true, 'gasorder-orders'),
|
||||
child('gasorder', 'payments', 'payments', '订单支付记录', '/gasorder_payment', 'gasorder_basic', true, 'gasorder-orders'),
|
||||
], 'delivery'),
|
||||
group('ec', 'ec', '商城管理', 'icon-gift', 70, [
|
||||
], 'gasorder'),
|
||||
group('ec', 'ec', '电商平台管理', 'icon-gift', 70, [
|
||||
child('ec', 'categories', 'categories', '商品分类', '/ec_category', 'ec_category'),
|
||||
child('ec', 'products', 'products', '商品', '/ec_product', 'ec_product'),
|
||||
child('ec', 'products', 'products', '商品管理', '/ec_product', 'ec_product'),
|
||||
child('ec', 'attributes', 'attributes', '商品属性', '/ec_product_attribute', 'ec_product', true, 'ec-products'),
|
||||
child('ec', 'images', 'images', '商品图片', '/ec_product_image', 'ec_product', true, 'ec-products'),
|
||||
child('ec', 'carts', 'carts', '购物车', '/ec_cart', 'ec_cart'),
|
||||
|
||||
@@ -68,7 +68,7 @@ const cards: { key: CountKey; label: string; hint: string; money?: boolean }[] =
|
||||
{ key: 'paid_amount', label: '累计实收金额', hint: '支付成功口径', money: true },
|
||||
];
|
||||
const actions = [
|
||||
{ label: '新建气站', route: 'gas-basic', menu: 'gas_basic', icon: IconPlus },
|
||||
{ label: '新建气站', route: 'organization-gas-basic', menu: 'gas_basic', icon: IconPlus },
|
||||
{ label: '配送订单', route: 'gasorder-orders', menu: 'gasorder_basic', icon: IconFile },
|
||||
{ label: '智能气阀', route: 'product-info', menu: 'product_info', icon: IconStorage },
|
||||
{ label: '用户管理', route: 'user-account', menu: 'user_account', icon: IconUser },
|
||||
|
||||
@@ -14,19 +14,6 @@
|
||||
<a-button type="primary" html-type="submit">查询</a-button>
|
||||
<a-button @click="resetSearch">重置</a-button>
|
||||
</a-form>
|
||||
<a-form v-if="definition.name === 'wallet_basic'" :model="walletOwner" layout="inline" class="wallet-owner" @submit.prevent="getOwnerWallet">
|
||||
<a-form-item label="归属类型">
|
||||
<a-select v-model="walletOwner.type" style="width: 140px">
|
||||
<a-option value="user">用户</a-option>
|
||||
<a-option value="staff">工作人员</a-option>
|
||||
<a-option value="delivery">配送站</a-option>
|
||||
<a-option value="gas">气站</a-option>
|
||||
<a-option value="platform">平台</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="归属标识"><a-input v-model="walletOwner.identity" placeholder="请输入 identity" /></a-form-item>
|
||||
<a-button type="primary" html-type="submit">获取或创建钱包</a-button>
|
||||
</a-form>
|
||||
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity">
|
||||
<template #columns>
|
||||
<a-table-column title="业务标识" data-index="identity" :width="220" ellipsis tooltip />
|
||||
@@ -114,7 +101,7 @@
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<a-space class="detail-actions">
|
||||
<a-button v-for="action in definition.detailActions" :key="action.name" type="primary" :status="action.danger ? 'danger' : 'normal'" @click="openDetailAction(action)">{{ action.name }}</a-button>
|
||||
<a-button v-for="action in visibleDetailActions" :key="action.name" type="primary" :status="action.danger ? 'danger' : 'normal'" @click="openDetailAction(action)">{{ action.name }}</a-button>
|
||||
</a-space>
|
||||
</a-drawer>
|
||||
|
||||
@@ -173,7 +160,6 @@ const walletByOwner = ref<Record<string, Row>>({});
|
||||
const filters = reactive({
|
||||
keyword: typeof route.query.keyword === 'string' ? route.query.keyword : '',
|
||||
});
|
||||
const walletOwner = reactive({ type: 'user', identity: '' });
|
||||
const formVisible = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const editingIdentity = ref('');
|
||||
@@ -289,6 +275,14 @@ const currentIdentity = computed(() =>
|
||||
'',
|
||||
),
|
||||
);
|
||||
const visibleDetailActions = computed(() =>
|
||||
(props.definition.detailActions ?? []).filter((action) => {
|
||||
if (!action.visibleFor) return true;
|
||||
return action.visibleFor.values.includes(
|
||||
detail.value[action.visibleFor.field] as string | number,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
function resetForm(data?: Row) {
|
||||
for (const field of props.definition.fields) {
|
||||
@@ -306,31 +300,36 @@ async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
if (staffType.value) {
|
||||
const staff = await loadAllRows(props.definition.resource);
|
||||
const keyword = filters.keyword.trim().toLowerCase();
|
||||
const filtered = staff.filter(
|
||||
(row) =>
|
||||
String(row.role_code ?? '') === staffType.value &&
|
||||
(!keyword ||
|
||||
['username', 'name', 'phone'].some((key) =>
|
||||
String(row[key] ?? '').toLowerCase().includes(keyword),
|
||||
)),
|
||||
const result = await resourceApi.list<Row>(
|
||||
props.definition.resource,
|
||||
page.value,
|
||||
pageSize,
|
||||
{
|
||||
role_code: staffType.value,
|
||||
...(filters.keyword ? { keyword: filters.keyword } : {}),
|
||||
},
|
||||
);
|
||||
list.value = filtered.slice((page.value - 1) * pageSize, page.value * pageSize);
|
||||
total.value = filtered.length;
|
||||
list.value = result.list;
|
||||
total.value = result.total;
|
||||
} else if (managedOwnerIdentity.value && managedRelationKey.value) {
|
||||
const accounts = await loadAllRows(props.definition.resource);
|
||||
const keyword = filters.keyword.trim().toLowerCase();
|
||||
const filtered = accounts.filter(
|
||||
(row) =>
|
||||
String(row[managedRelationKey.value] ?? '') === managedOwnerIdentity.value &&
|
||||
(!keyword ||
|
||||
['username', 'display_name', 'role_code'].some((key) =>
|
||||
String(row[key] ?? '').toLowerCase().includes(keyword),
|
||||
)),
|
||||
const serverFilters: Record<string, string> = props.definition.name === 'staff_credential'
|
||||
? { staff_account_identity: managedOwnerIdentity.value }
|
||||
: props.definition.name === 'gas_account'
|
||||
? { gas_basic_identities: managedOwnerIdentity.value }
|
||||
: props.definition.name === 'delivery_account'
|
||||
? { delivery_basic_identities: managedOwnerIdentity.value }
|
||||
: {};
|
||||
const result = await resourceApi.list<Row>(
|
||||
props.definition.resource,
|
||||
page.value,
|
||||
pageSize,
|
||||
{
|
||||
...serverFilters,
|
||||
...(filters.keyword ? { keyword: filters.keyword } : {}),
|
||||
},
|
||||
);
|
||||
list.value = filtered.slice((page.value - 1) * pageSize, page.value * pageSize);
|
||||
total.value = filtered.length;
|
||||
list.value = result.list;
|
||||
total.value = result.total;
|
||||
} else {
|
||||
const result = await resourceApi.list<Row>(
|
||||
props.definition.resource,
|
||||
@@ -356,7 +355,17 @@ async function loadWallets() {
|
||||
walletByOwner.value = {};
|
||||
return;
|
||||
}
|
||||
const wallets = await loadAllRows('/wallet_basic');
|
||||
const ownerIdentities = list.value.map((row) => String(row.identity ?? '')).filter(Boolean);
|
||||
if (!ownerIdentities.length) {
|
||||
walletByOwner.value = {};
|
||||
return;
|
||||
}
|
||||
const wallets = (
|
||||
await resourceApi.list<Row>('/wallet_basic', 1, 100, {
|
||||
owner_type: ownerType,
|
||||
owner_identities: ownerIdentities.join(','),
|
||||
})
|
||||
).list;
|
||||
walletByOwner.value = wallets.reduce<Record<string, Row>>((result, wallet) => {
|
||||
if (wallet.owner_type === ownerType) {
|
||||
result[String(wallet.owner_identity ?? '')] = wallet;
|
||||
@@ -365,10 +374,10 @@ async function loadWallets() {
|
||||
}, {});
|
||||
}
|
||||
|
||||
async function loadAllRows(resource: string) {
|
||||
async function loadAllRows(resource: string, filters: Record<string, string> = {}) {
|
||||
const rows: Row[] = [];
|
||||
for (let currentPage = 1; currentPage <= 100; currentPage += 1) {
|
||||
const result = await resourceApi.list<Row>(resource, currentPage, 100);
|
||||
const result = await resourceApi.list<Row>(resource, currentPage, 100, filters);
|
||||
rows.push(...result.list);
|
||||
if (rows.length >= result.total || result.list.length < 100) break;
|
||||
}
|
||||
@@ -381,7 +390,17 @@ async function loadAccountCounts() {
|
||||
accountCounts.value = {};
|
||||
return;
|
||||
}
|
||||
const accounts = await loadAllRows(management.resource);
|
||||
const ownerIdentities = list.value.map((row) => String(row.identity ?? '')).filter(Boolean);
|
||||
if (!ownerIdentities.length) {
|
||||
accountCounts.value = {};
|
||||
return;
|
||||
}
|
||||
const filterKey = management.relationKey === 'gas_basic_identity'
|
||||
? 'gas_basic_identities'
|
||||
: 'delivery_basic_identities';
|
||||
const accounts = await loadAllRows(management.resource, {
|
||||
[filterKey]: ownerIdentities.join(','),
|
||||
});
|
||||
accountCounts.value = accounts.reduce<Record<string, number>>((counts, account) => {
|
||||
const identity = String(account[management.relationKey] ?? '');
|
||||
if (identity) counts[identity] = (counts[identity] ?? 0) + 1;
|
||||
@@ -619,23 +638,6 @@ async function changePage(next: number) {
|
||||
await load();
|
||||
}
|
||||
|
||||
async function getOwnerWallet() {
|
||||
if (!walletOwner.identity.trim()) {
|
||||
Message.warning('请输入归属标识');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
detail.value = await resourceApi.detail<Row>(
|
||||
'/wallet_basic/owner',
|
||||
`${walletOwner.type}/${walletOwner.identity.trim()}`,
|
||||
);
|
||||
detailVisible.value = true;
|
||||
await load();
|
||||
} catch (error) {
|
||||
Message.error((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
function formatValue(value: unknown) {
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
@@ -718,12 +720,14 @@ function displayValue(key: string, value: unknown) {
|
||||
async function loadRelation(resource: string, keyword = '') {
|
||||
relationLoading[resource] = true;
|
||||
try {
|
||||
const extraFilters: Record<string, string> = keyword ? { keyword } : {};
|
||||
if (resource === '/staff_account') extraFilters.role_code = 'delivery';
|
||||
relationOptions[resource] = (
|
||||
await resourceApi.list<Row>(
|
||||
resource,
|
||||
1,
|
||||
100,
|
||||
keyword ? { keyword } : {},
|
||||
extraFilters,
|
||||
)
|
||||
).list;
|
||||
} catch (error) {
|
||||
@@ -745,7 +749,9 @@ function searchRelation(resource: string | undefined, keyword: string) {
|
||||
|
||||
onMounted(async () => {
|
||||
if (!route.meta.createMode) await load();
|
||||
const actionFields = props.definition.detailActions?.flatMap((item) => item.fields ?? []) ?? [];
|
||||
const actionFields = route.meta.createMode
|
||||
? []
|
||||
: props.definition.detailActions?.flatMap((item) => item.fields ?? []) ?? [];
|
||||
const relationPaths = new Set(
|
||||
[...props.definition.fields, ...actionFields]
|
||||
.map((field) => field.relation)
|
||||
@@ -789,11 +795,6 @@ function optionLabel(option: Row) {
|
||||
.detail-actions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.wallet-owner {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: var(--color-fill-1);
|
||||
}
|
||||
.muted-text {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user