fix: resolve platform relation identities
This commit is contained in:
@@ -36,3 +36,10 @@ The tree page consumes `parent_identity`; the corresponding menu API must expose
|
||||
- Safety disposal is a detail action on `saf_event` (`/safety/saf_event/:identity/disposals`) and has no standalone menu or page.
|
||||
- `ec_category` and `platform_menu` use tree pages. The shared tree preserves identity-first behavior and uses backend `parent_id` only for in-memory hierarchy adaptation when `parent_identity` is unavailable.
|
||||
- The audit transpiles and evaluates `resources.ts`, then compares the resulting definitions, allowlists, required identities, modes, page types, tree semantics, and identifier-safety rules to the complete expected contract.
|
||||
|
||||
## Fix round 3
|
||||
|
||||
- Added tested backend identity-to-key resolution for public account and relationship requests; clients send `*_identity`, while numeric keys remain internal persistence details.
|
||||
- Platform-menu and e-commerce-category tree responses now expose `parent_identity` and omit `parent_id`; the front-end tree no longer falls back to numeric keys.
|
||||
- Settlement input accepts `subject_identity` with supported gas, delivery, and staff subject types.
|
||||
- The platform audit now cross-checks every front-end definition against the backend resource catalogue and registered route source.
|
||||
|
||||
@@ -10,19 +10,19 @@ import (
|
||||
)
|
||||
|
||||
type accountRequest struct {
|
||||
Username string `json:"username" binding:"required,max=64"`
|
||||
Password string `json:"password" binding:"required,min=8,max=128"`
|
||||
DisplayName string `json:"display_name" binding:"max=64"`
|
||||
RoleCode string `json:"role_code" binding:"max=64"`
|
||||
GasBasicID uint64 `json:"gas_basic_id"`
|
||||
DeliveryBasicID uint64 `json:"delivery_basic_id"`
|
||||
Username string `json:"username" binding:"required,max=64"`
|
||||
Password string `json:"password" binding:"required,min=8,max=128"`
|
||||
DisplayName string `json:"display_name" binding:"max=64"`
|
||||
RoleCode string `json:"role_code" binding:"max=64"`
|
||||
GasBasicIdentity string `json:"gas_basic_identity"`
|
||||
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
|
||||
}
|
||||
|
||||
type accountUpdateRequest struct {
|
||||
DisplayName string `json:"display_name" binding:"max=64"`
|
||||
RoleCode string `json:"role_code" binding:"max=64"`
|
||||
GasBasicID uint64 `json:"gas_basic_id"`
|
||||
DeliveryBasicID uint64 `json:"delivery_basic_id"`
|
||||
DisplayName string `json:"display_name" binding:"max=64"`
|
||||
RoleCode string `json:"role_code" binding:"max=64"`
|
||||
GasBasicIdentity string `json:"gas_basic_identity"`
|
||||
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
|
||||
}
|
||||
|
||||
func passwordHash(password string) (string, error) {
|
||||
@@ -35,7 +35,12 @@ func GetGasAccount(ctx *gin.Context) { getByIdentity[models.GasAccount](ctx) }
|
||||
|
||||
func CreateGasAccount(ctx *gin.Context) {
|
||||
var request accountRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.GasBasicID == 0 {
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
gasBasicID, err := resolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
@@ -44,7 +49,7 @@ func CreateGasAccount(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
account := models.GasAccount{Entity: newEntity("enabled"), GasBasicID: request.GasBasicID, Username: request.Username, DisplayName: request.DisplayName, PasswordHash: hash, RoleCode: request.RoleCode}
|
||||
account := models.GasAccount{Entity: newEntity("enabled"), GasBasicID: gasBasicID, Username: request.Username, DisplayName: request.DisplayName, PasswordHash: hash, RoleCode: request.RoleCode}
|
||||
if err := impl.DBService.Create(&account).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -58,7 +63,12 @@ func UpdateGasAccount(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.GasAccount{}, gin.H{"gas_basic_id": request.GasBasicID, "display_name": request.DisplayName, "role_code": request.RoleCode}, []string{"gas_basic_id", "display_name", "role_code"})
|
||||
gasBasicID, err := resolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
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 ListDeliveryAccount(ctx *gin.Context) { listPage[models.DeliveryAccount](ctx) }
|
||||
@@ -66,7 +76,12 @@ func GetDeliveryAccount(ctx *gin.Context) { getByIdentity[models.DeliveryAccoun
|
||||
|
||||
func CreateDeliveryAccount(ctx *gin.Context) {
|
||||
var request accountRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.DeliveryBasicID == 0 {
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
deliveryBasicID, err := resolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
@@ -75,7 +90,7 @@ func CreateDeliveryAccount(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
account := models.DeliveryAccount{Entity: newEntity("enabled"), DeliveryBasicID: request.DeliveryBasicID, Username: request.Username, DisplayName: request.DisplayName, PasswordHash: hash, RoleCode: request.RoleCode}
|
||||
account := models.DeliveryAccount{Entity: newEntity("enabled"), DeliveryBasicID: deliveryBasicID, Username: request.Username, DisplayName: request.DisplayName, PasswordHash: hash, RoleCode: request.RoleCode}
|
||||
if err := impl.DBService.Create(&account).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -89,5 +104,10 @@ func UpdateDeliveryAccount(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.DeliveryAccount{}, gin.H{"delivery_basic_id": request.DeliveryBasicID, "display_name": request.DisplayName, "role_code": request.RoleCode}, []string{"delivery_basic_id", "display_name", "role_code"})
|
||||
deliveryBasicID, err := resolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.DeliveryAccount{}, gin.H{"delivery_basic_id": deliveryBasicID, "display_name": request.DisplayName, "role_code": request.RoleCode}, []string{"delivery_basic_id", "display_name", "role_code"})
|
||||
}
|
||||
|
||||
@@ -16,30 +16,46 @@ func GetDeliveryBasic(ctx *gin.Context) { getByIdentity[models.DeliveryBasic](ct
|
||||
|
||||
// CreateDeliveryBasic 创建配送点档案。
|
||||
func CreateDeliveryBasic(ctx *gin.Context) {
|
||||
var request models.DeliveryBasic
|
||||
var request struct {
|
||||
DeliveryCode string `json:"delivery_code"`
|
||||
GasBasicIdentity string `json:"gas_basic_identity"`
|
||||
Name string `json:"name"`
|
||||
Principal string `json:"principal"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.DeliveryCode == "" || request.Name == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
request.Entity = newEntity("draft")
|
||||
if err := impl.DBService.Create(&request).Error; err != nil {
|
||||
gasBasicID, err := resolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
delivery := models.DeliveryBasic{Entity: newEntity("draft"), DeliveryCode: request.DeliveryCode, GasBasicID: gasBasicID, Name: request.Name, Principal: request.Principal, Address: request.Address}
|
||||
if err := impl.DBService.Create(&delivery).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, request)
|
||||
infra.Response.Success(ctx, resourceResponse(delivery))
|
||||
}
|
||||
|
||||
// UpdateDeliveryBasic 更新配送点基础资料。
|
||||
func UpdateDeliveryBasic(ctx *gin.Context) {
|
||||
var request struct {
|
||||
GasBasicID uint64 `json:"gas_basic_id"`
|
||||
Name string `json:"name" binding:"required,max=128"`
|
||||
Principal string `json:"principal" binding:"max=64"`
|
||||
Address string `json:"address" binding:"max=255"`
|
||||
GasBasicIdentity string `json:"gas_basic_identity"`
|
||||
Name string `json:"name" binding:"required,max=128"`
|
||||
Principal string `json:"principal" binding:"max=64"`
|
||||
Address string `json:"address" binding:"max=255"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.DeliveryBasic{}, gin.H{"gas_basic_id": request.GasBasicID, "name": request.Name, "principal": request.Principal, "address": request.Address}, []string{"gas_basic_id", "name", "principal", "address"})
|
||||
gasBasicID, err := resolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.DeliveryBasic{}, gin.H{"gas_basic_id": gasBasicID, "name": request.Name, "principal": request.Principal, "address": request.Address}, []string{"gas_basic_id", "name", "principal", "address"})
|
||||
}
|
||||
|
||||
@@ -214,6 +214,40 @@ func TestPrepareResourceValuesResolvesRequiredIdentityRelationsAndRejectsInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateGasAccountResolvesGasBasicIdentityBeforePersisting(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id" FROM "gas_basic" WHERE identity = $1 ORDER BY "gas_basic"."id" LIMIT $2`)).
|
||||
WithArgs("gas-a", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uint64(8)))
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(`INSERT INTO "gas_account"`).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uint64(1)))
|
||||
mock.ExpectCommit()
|
||||
|
||||
ctx, recorder := updateContext(http.MethodPost, "/gas/gas_account", "", []byte(`{"username":"operator","password":"password-123","gas_basic_identity":"gas-a"}`))
|
||||
CreateGasAccount(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, 0)
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestListPlatformMenuReturnsParentIdentityWithoutParentID(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platform_menu" ORDER BY sort_no asc, id asc`)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "parent_id", "menu_code", "name", "icon", "path", "sort_no"}).
|
||||
AddRow(uint64(1), "root-a", nil, nil, "enabled", 1, uint64(0), "root", "Root", "", "", 1).
|
||||
AddRow(uint64(2), "child-a", nil, nil, "enabled", 1, uint64(1), "child", "Child", "", "/child", 2))
|
||||
|
||||
ctx, recorder := updateContext(http.MethodGet, "/platform/platform_menu", "", nil)
|
||||
ListPlatformMenu(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, 0)
|
||||
if !strings.Contains(recorder.Body.String(), `"parent_identity":"root-a"`) || strings.Contains(recorder.Body.String(), `"parent_id"`) {
|
||||
t.Fatalf("menu response leaked parent_id or omitted identity: %s", recorder.Body.String())
|
||||
}
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestResourceResponseDoesNotExposeAutoIncrementRelationIDs(t *testing.T) {
|
||||
response := resourceResponse(map[string]any{"identity": "item-a", "id": uint64(1), "ec_order_id": uint64(2), "ec_product_id": uint64(3), "items": []any{map[string]any{"identity": "child-a", "delivery_task_id": uint64(4)}}})
|
||||
encoded, err := json.Marshal(response)
|
||||
|
||||
@@ -61,15 +61,53 @@ func UpdatePlatformRole(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
type platformMenuRequest struct {
|
||||
ParentID uint64 `json:"parent_id"`
|
||||
MenuCode string `json:"menu_code" binding:"required,max=64"`
|
||||
Name string `json:"name" binding:"required,max=64"`
|
||||
Icon string `json:"icon" binding:"max=64"`
|
||||
Path string `json:"path" binding:"max=255"`
|
||||
SortNo int `json:"sort_no"`
|
||||
ParentIdentity string `json:"parent_identity"`
|
||||
MenuCode string `json:"menu_code" binding:"required,max=64"`
|
||||
Name string `json:"name" binding:"required,max=64"`
|
||||
Icon string `json:"icon" binding:"max=64"`
|
||||
Path string `json:"path" binding:"max=255"`
|
||||
SortNo int `json:"sort_no"`
|
||||
}
|
||||
|
||||
func GetPlatformMenu(ctx *gin.Context) { getByIdentity[models.PlatformMenu](ctx) }
|
||||
type platformMenuView struct {
|
||||
Identity string `json:"identity"`
|
||||
ParentIdentity string `json:"parent_identity,omitempty"`
|
||||
MenuCode string `json:"menu_code"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Path string `json:"path"`
|
||||
SortNo int `json:"sort_no"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func platformMenuViews(list []models.PlatformMenu) []platformMenuView {
|
||||
identities := make(map[uint64]string, len(list))
|
||||
for _, item := range list {
|
||||
identities[item.ID] = item.Identity
|
||||
}
|
||||
views := make([]platformMenuView, 0, len(list))
|
||||
for _, item := range list {
|
||||
views = append(views, platformMenuView{Identity: item.Identity, ParentIdentity: identities[item.ParentID], MenuCode: item.MenuCode, Name: item.Name, Icon: item.Icon, Path: item.Path, SortNo: item.SortNo, Status: item.Status})
|
||||
}
|
||||
return views
|
||||
}
|
||||
|
||||
func GetPlatformMenu(ctx *gin.Context) {
|
||||
var menu models.PlatformMenu
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&menu).Error; err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
list := []models.PlatformMenu{menu}
|
||||
if menu.ParentID != 0 {
|
||||
var parent models.PlatformMenu
|
||||
if err := impl.DBService.Select("id", "identity").First(&parent, menu.ParentID).Error; err == nil {
|
||||
list = append(list, parent)
|
||||
}
|
||||
}
|
||||
views := platformMenuViews(list)
|
||||
infra.Response.Success(ctx, views[0])
|
||||
}
|
||||
|
||||
func CreatePlatformMenu(ctx *gin.Context) {
|
||||
var request platformMenuRequest
|
||||
@@ -77,7 +115,12 @@ func CreatePlatformMenu(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
menu := models.PlatformMenu{Entity: newEntity("enabled"), ParentID: request.ParentID, MenuCode: request.MenuCode, Name: request.Name, Icon: request.Icon, Path: request.Path, SortNo: request.SortNo}
|
||||
parentID, err := resolveIdentityID(&models.PlatformMenu{}, request.ParentIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
menu := models.PlatformMenu{Entity: newEntity("enabled"), ParentID: parentID, MenuCode: request.MenuCode, Name: request.Name, Icon: request.Icon, Path: request.Path, SortNo: request.SortNo}
|
||||
if err := impl.DBService.Create(&menu).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -91,7 +134,12 @@ func UpdatePlatformMenu(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.PlatformMenu{}, gin.H{"parent_id": request.ParentID, "name": request.Name, "icon": request.Icon, "path": request.Path, "sort_no": request.SortNo}, []string{"parent_id", "name", "icon", "path", "sort_no"})
|
||||
parentID, err := resolveIdentityID(&models.PlatformMenu{}, request.ParentIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.PlatformMenu{}, gin.H{"parent_id": parentID, "name": request.Name, "icon": request.Icon, "path": request.Path, "sort_no": request.SortNo}, []string{"parent_id", "name", "icon", "path", "sort_no"})
|
||||
}
|
||||
|
||||
type platformRoleMenusRequest struct {
|
||||
@@ -189,7 +237,7 @@ func ListPlatformMenu(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": len(list), "list": list})
|
||||
infra.Response.Success(ctx, gin.H{"total": len(list), "list": platformMenuViews(list)})
|
||||
}
|
||||
|
||||
// ListPlatfromAccount 查询平台账号列表,手机号在展示层脱敏。
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
)
|
||||
|
||||
type staffCredentialRequest struct {
|
||||
StaffAccountID uint64 `json:"staff_account_id" binding:"required"`
|
||||
CredentialType string `json:"credential_type" binding:"required,max=64"`
|
||||
CredentialNo string `json:"credential_no" binding:"max=128"`
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
StaffAccountIdentity string `json:"staff_account_identity" binding:"required"`
|
||||
CredentialType string `json:"credential_type" binding:"required,max=64"`
|
||||
CredentialNo string `json:"credential_no" binding:"max=128"`
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
}
|
||||
|
||||
func ListStaffCredential(ctx *gin.Context) { listPage[models.StaffCredential](ctx) }
|
||||
@@ -25,7 +25,12 @@ func CreateStaffCredential(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
credential := models.StaffCredential{Entity: newEntity("enabled"), StaffAccountID: request.StaffAccountID, CredentialType: request.CredentialType, CredentialNo: request.CredentialNo, ExpiredAt: request.ExpiredAt}
|
||||
staffAccountID, err := resolveIdentityID(&models.StaffAccount{}, request.StaffAccountIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
credential := models.StaffCredential{Entity: newEntity("enabled"), StaffAccountID: staffAccountID, CredentialType: request.CredentialType, CredentialNo: request.CredentialNo, ExpiredAt: request.ExpiredAt}
|
||||
if err := impl.DBService.Create(&credential).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -38,15 +43,20 @@ func UpdateStaffCredential(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.StaffCredential{}, gin.H{"staff_account_id": request.StaffAccountID, "credential_type": request.CredentialType, "credential_no": request.CredentialNo, "expired_at": request.ExpiredAt}, []string{"staff_account_id", "credential_type", "credential_no", "expired_at"})
|
||||
staffAccountID, err := resolveIdentityID(&models.StaffAccount{}, request.StaffAccountIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
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"})
|
||||
}
|
||||
|
||||
type userAddressRequest struct {
|
||||
UserAccountID uint64 `json:"user_account_id" binding:"required"`
|
||||
Address string `json:"address" binding:"required,max=255"`
|
||||
Longitude string `json:"longitude" binding:"max=32"`
|
||||
Latitude string `json:"latitude" binding:"max=32"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
UserAccountIdentity string `json:"user_account_identity" binding:"required"`
|
||||
Address string `json:"address" binding:"required,max=255"`
|
||||
Longitude string `json:"longitude" binding:"max=32"`
|
||||
Latitude string `json:"latitude" binding:"max=32"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
func ListUserAddress(ctx *gin.Context) { listPage[models.UserAddress](ctx) }
|
||||
@@ -57,7 +67,12 @@ func CreateUserAddress(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
address := models.UserAddress{Entity: newEntity("enabled"), UserAccountID: request.UserAccountID, Address: request.Address, Longitude: request.Longitude, Latitude: request.Latitude, IsDefault: request.IsDefault}
|
||||
userAccountID, err := resolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
address := models.UserAddress{Entity: newEntity("enabled"), UserAccountID: userAccountID, Address: request.Address, Longitude: request.Longitude, Latitude: request.Latitude, IsDefault: request.IsDefault}
|
||||
if err := impl.DBService.Create(&address).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -70,14 +85,19 @@ func UpdateUserAddress(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.UserAddress{}, gin.H{"user_account_id": request.UserAccountID, "address": request.Address, "longitude": request.Longitude, "latitude": request.Latitude, "is_default": request.IsDefault}, []string{"user_account_id", "address", "longitude", "latitude", "is_default"})
|
||||
userAccountID, err := resolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.UserAddress{}, gin.H{"user_account_id": userAccountID, "address": request.Address, "longitude": request.Longitude, "latitude": request.Latitude, "is_default": request.IsDefault}, []string{"user_account_id", "address", "longitude", "latitude", "is_default"})
|
||||
}
|
||||
|
||||
type userServiceRelationRequest struct {
|
||||
UserAccountID uint64 `json:"user_account_id" binding:"required"`
|
||||
GasBasicID uint64 `json:"gas_basic_id"`
|
||||
DeliveryBasicID uint64 `json:"delivery_basic_id"`
|
||||
StaffAccountID uint64 `json:"staff_account_id"`
|
||||
UserAccountIdentity string `json:"user_account_identity" binding:"required"`
|
||||
GasBasicIdentity string `json:"gas_basic_identity"`
|
||||
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
|
||||
StaffAccountIdentity string `json:"staff_account_identity"`
|
||||
}
|
||||
|
||||
func ListUserServiceRelation(ctx *gin.Context) { listPage[models.UserServiceRelation](ctx) }
|
||||
@@ -88,7 +108,27 @@ func CreateUserServiceRelation(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
relation := models.UserServiceRelation{Entity: newEntity("enabled"), UserAccountID: request.UserAccountID, GasBasicID: request.GasBasicID, DeliveryBasicID: request.DeliveryBasicID, StaffAccountID: request.StaffAccountID}
|
||||
userAccountID, err := resolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
gasBasicID, err := resolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
deliveryBasicID, err := resolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
staffAccountID, err := resolveIdentityID(&models.StaffAccount{}, request.StaffAccountIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
relation := models.UserServiceRelation{Entity: newEntity("enabled"), UserAccountID: userAccountID, GasBasicID: gasBasicID, DeliveryBasicID: deliveryBasicID, StaffAccountID: staffAccountID}
|
||||
if err := impl.DBService.Create(&relation).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -101,5 +141,25 @@ func UpdateUserServiceRelation(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.UserServiceRelation{}, gin.H{"user_account_id": request.UserAccountID, "gas_basic_id": request.GasBasicID, "delivery_basic_id": request.DeliveryBasicID, "staff_account_id": request.StaffAccountID}, []string{"user_account_id", "gas_basic_id", "delivery_basic_id", "staff_account_id"})
|
||||
userAccountID, err := resolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
gasBasicID, err := resolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
deliveryBasicID, err := resolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
staffAccountID, err := resolveIdentityID(&models.StaffAccount{}, request.StaffAccountIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
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"})
|
||||
}
|
||||
|
||||
@@ -17,15 +17,15 @@ func GetStaff(ctx *gin.Context) { getByIdentity[models.StaffAccount](ctx) }
|
||||
// CreateStaff 创建服务人员档案。
|
||||
func CreateStaff(ctx *gin.Context) {
|
||||
var request struct {
|
||||
Username string `json:"username" binding:"required,max=64"`
|
||||
Password string `json:"password" binding:"required,min=8,max=128"`
|
||||
Name string `json:"name" binding:"required,max=64"`
|
||||
Phone string `json:"phone" binding:"max=32"`
|
||||
Avatar string `json:"avatar" binding:"max=512"`
|
||||
RoleCode string `json:"role_code" binding:"max=64"`
|
||||
GasBasicID uint64 `json:"gas_basic_id"`
|
||||
DeliveryBasicID uint64 `json:"delivery_basic_id"`
|
||||
WorkStatus string `json:"work_status" binding:"max=32"`
|
||||
Username string `json:"username" binding:"required,max=64"`
|
||||
Password string `json:"password" binding:"required,min=8,max=128"`
|
||||
Name string `json:"name" binding:"required,max=64"`
|
||||
Phone string `json:"phone" binding:"max=32"`
|
||||
Avatar string `json:"avatar" binding:"max=512"`
|
||||
RoleCode string `json:"role_code" binding:"max=64"`
|
||||
GasBasicIdentity string `json:"gas_basic_identity"`
|
||||
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
|
||||
WorkStatus string `json:"work_status" binding:"max=32"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
@@ -36,7 +36,17 @@ func CreateStaff(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
staff := models.StaffAccount{Entity: newEntity("draft"), Username: request.Username, PasswordHash: hash, Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RoleCode: request.RoleCode, GasBasicID: request.GasBasicID, DeliveryBasicID: request.DeliveryBasicID, WorkStatus: request.WorkStatus}
|
||||
gasBasicID, err := resolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
deliveryBasicID, err := resolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
staff := models.StaffAccount{Entity: newEntity("draft"), Username: request.Username, PasswordHash: hash, Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RoleCode: request.RoleCode, GasBasicID: gasBasicID, DeliveryBasicID: deliveryBasicID, WorkStatus: request.WorkStatus}
|
||||
if staff.WorkStatus == "" {
|
||||
staff.WorkStatus = "off_duty"
|
||||
}
|
||||
@@ -50,17 +60,27 @@ func CreateStaff(ctx *gin.Context) {
|
||||
// UpdateStaff 更新服务人员档案。
|
||||
func UpdateStaff(ctx *gin.Context) {
|
||||
var request struct {
|
||||
Name string `json:"name" binding:"required,max=64"`
|
||||
Phone string `json:"phone" binding:"max=32"`
|
||||
Avatar string `json:"avatar" binding:"max=512"`
|
||||
RoleCode string `json:"role_code" binding:"max=64"`
|
||||
GasBasicID uint64 `json:"gas_basic_id"`
|
||||
DeliveryBasicID uint64 `json:"delivery_basic_id"`
|
||||
WorkStatus string `json:"work_status" binding:"max=32"`
|
||||
Name string `json:"name" binding:"required,max=64"`
|
||||
Phone string `json:"phone" binding:"max=32"`
|
||||
Avatar string `json:"avatar" binding:"max=512"`
|
||||
RoleCode string `json:"role_code" binding:"max=64"`
|
||||
GasBasicIdentity string `json:"gas_basic_identity"`
|
||||
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
|
||||
WorkStatus string `json:"work_status" binding:"max=32"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.StaffAccount{}, gin.H{"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "role_code": request.RoleCode, "gas_basic_id": request.GasBasicID, "delivery_basic_id": request.DeliveryBasicID, "work_status": request.WorkStatus}, []string{"name", "phone", "avatar", "role_code", "gas_basic_id", "delivery_basic_id", "work_status"})
|
||||
gasBasicID, err := resolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
deliveryBasicID, err := resolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.StaffAccount{}, gin.H{"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "role_code": request.RoleCode, "gas_basic_id": gasBasicID, "delivery_basic_id": deliveryBasicID, "work_status": request.WorkStatus}, []string{"name", "phone", "avatar", "role_code", "gas_basic_id", "delivery_basic_id", "work_status"})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
@@ -33,6 +35,60 @@ func ResourceHandlers(model any, createFields, updateFields []string, relations
|
||||
func(ctx *gin.Context) { updateResource(ctx, model, updateFields, relations) }
|
||||
}
|
||||
|
||||
// FinSettlementHandlers accepts a public subject_identity and derives the
|
||||
// polymorphic storage key from its declared subject_type.
|
||||
func FinSettlementHandlers() (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
|
||||
fields := []string{"settlement_no", "subject_type", "subject_id", "period_start", "period_end"}
|
||||
return func(ctx *gin.Context) { listResource(ctx, &models.FinSettlement{}) },
|
||||
func(ctx *gin.Context) {
|
||||
if err := rewriteSettlementSubject(ctx); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
createResource(ctx, &models.FinSettlement{}, fields, nil)
|
||||
},
|
||||
func(ctx *gin.Context) { getResource(ctx, &models.FinSettlement{}) },
|
||||
func(ctx *gin.Context) {
|
||||
if err := rewriteSettlementSubject(ctx); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateResource(ctx, &models.FinSettlement{}, fields, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func rewriteSettlementSubject(ctx *gin.Context) error {
|
||||
var input map[string]any
|
||||
if err := ctx.ShouldBindJSON(&input); err != nil {
|
||||
return err
|
||||
}
|
||||
subjectType, _ := input["subject_type"].(string)
|
||||
identity, _ := input["subject_identity"].(string)
|
||||
var model any
|
||||
switch subjectType {
|
||||
case "gas", "gas_basic":
|
||||
model = &models.GasBasic{}
|
||||
case "delivery", "delivery_basic":
|
||||
model = &models.DeliveryBasic{}
|
||||
case "staff", "staff_account":
|
||||
model = &models.StaffAccount{}
|
||||
default:
|
||||
return errors.New("invalid settlement subject")
|
||||
}
|
||||
id, err := resolveIdentityID(model, identity, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delete(input, "subject_identity")
|
||||
input["subject_id"] = id
|
||||
encoded, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewReader(encoded))
|
||||
return nil
|
||||
}
|
||||
|
||||
func listResource(ctx *gin.Context, model any) {
|
||||
page, size := pageSize(ctx)
|
||||
list := reflect.New(reflect.SliceOf(reflect.TypeOf(model).Elem()))
|
||||
@@ -126,15 +182,32 @@ func resolveResourceRelations(input map[string]any, allowedFields []string, rela
|
||||
if !ok || strings.TrimSpace(identity) == "" {
|
||||
return nil, errors.New("invalid relation identity")
|
||||
}
|
||||
var related struct{ ID uint64 }
|
||||
if err := impl.DBService.Model(relation.Model).Select("id").Where("identity = ?", identity).First(&related).Error; err != nil {
|
||||
id, err := resolveIdentityID(relation.Model, identity, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values[relation.Column] = related.ID
|
||||
values[relation.Column] = id
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// resolveIdentityID is the only boundary that converts a public identity to a
|
||||
// persistence-only numeric key. Callers must never bind a client supplied ID.
|
||||
func resolveIdentityID(model any, identity string, required bool) (uint64, error) {
|
||||
identity = strings.TrimSpace(identity)
|
||||
if identity == "" {
|
||||
if required {
|
||||
return 0, errors.New("missing required relation")
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
var related struct{ ID uint64 }
|
||||
if err := impl.DBService.Model(model).Select("id").Where("identity = ?", identity).First(&related).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return related.ID, nil
|
||||
}
|
||||
|
||||
func relationColumns(relations []ResourceRelation) []string {
|
||||
columns := make([]string, 0, len(relations))
|
||||
for _, relation := range relations {
|
||||
@@ -266,3 +339,75 @@ func GetDeliveryTrack(ctx *gin.Context) {
|
||||
}
|
||||
infra.Response.Success(ctx, resourceResponse(gin.H{"track": track, "points": points}))
|
||||
}
|
||||
|
||||
type ecCategoryView struct {
|
||||
Identity string `json:"identity"`
|
||||
ParentIdentity string `json:"parent_identity,omitempty"`
|
||||
Name string `json:"name"`
|
||||
SortNo int `json:"sort_no"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func ecCategoryViews(list []models.EcCategory) ([]ecCategoryView, error) {
|
||||
parents := make(map[uint64]string)
|
||||
for _, item := range list {
|
||||
if item.ParentID != 0 {
|
||||
parents[item.ParentID] = ""
|
||||
}
|
||||
}
|
||||
if len(parents) > 0 {
|
||||
var rows []struct {
|
||||
ID uint64
|
||||
Identity string
|
||||
}
|
||||
ids := make([]uint64, 0, len(parents))
|
||||
for id := range parents {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if err := impl.DBService.Model(&models.EcCategory{}).Select("id", "identity").Where("id IN ?", ids).Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range rows {
|
||||
parents[row.ID] = row.Identity
|
||||
}
|
||||
}
|
||||
views := make([]ecCategoryView, 0, len(list))
|
||||
for _, item := range list {
|
||||
views = append(views, ecCategoryView{Identity: item.Identity, ParentIdentity: parents[item.ParentID], Name: item.Name, SortNo: item.SortNo, Status: item.Status})
|
||||
}
|
||||
return views, nil
|
||||
}
|
||||
|
||||
func ListEcCategory(ctx *gin.Context) {
|
||||
page, size := pageSize(ctx)
|
||||
var list []models.EcCategory
|
||||
var total int64
|
||||
if err := impl.DBService.Model(&models.EcCategory{}).Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := impl.DBService.Order("sort_no asc, id asc").Offset((page - 1) * size).Limit(size).Find(&list).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
views, err := ecCategoryViews(list)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": views})
|
||||
}
|
||||
|
||||
func GetEcCategory(ctx *gin.Context) {
|
||||
var category models.EcCategory
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&category).Error; err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
views, err := ecCategoryViews([]models.EcCategory{category})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, views[0])
|
||||
}
|
||||
|
||||
@@ -68,7 +68,9 @@ func registerSafetyRoute(group *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
func registerCommerceRoute(group *gin.RouterGroup) {
|
||||
registerRestrictedWritableResource(group, "/ec/ec_category", &models.EcCategory{}, []string{"name", "sort_no"}, optionalRelation("parent_identity", "parent_id", &models.EcCategory{}))
|
||||
categoryRelations := []platform.ResourceRelation{optionalRelation("parent_identity", "parent_id", &models.EcCategory{})}
|
||||
_, categoryCreate, _, categoryUpdate := platform.ResourceHandlers(&models.EcCategory{}, []string{"name", "sort_no"}, []string{"name", "sort_no"}, categoryRelations...)
|
||||
registerWritableResource(group, "/ec/ec_category", platform.ListEcCategory, categoryCreate, platform.GetEcCategory, categoryUpdate, &models.EcCategory{})
|
||||
registerRestrictedWritableResource(group, "/ec/ec_product", &models.EcProduct{}, []string{"product_code", "name", "price_amount", "stock_quantity"}, requiredRelation("ec_category_identity", "ec_category_id", &models.EcCategory{}))
|
||||
registerRestrictedWritableResource(group, "/ec/ec_product_attribute", &models.EcProductAttribute{}, []string{"name", "value", "sort_no"}, requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}))
|
||||
registerRestrictedWritableResource(group, "/ec/ec_product_image", &models.EcProductImage{}, []string{"image_uri", "sort_no", "is_cover"}, requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}))
|
||||
@@ -106,7 +108,8 @@ func registerPlatformRoute(group *gin.RouterGroup) {
|
||||
|
||||
func registerFinanceRoute(group *gin.RouterGroup) {
|
||||
registerRestrictedWritableResource(group, "/finance/fin_payment", &models.FinPayment{}, []string{"channel", "amount", "paid_at"}, requiredRelation("ec_order_identity", "ec_order_id", &models.EcOrder{}))
|
||||
registerRestrictedWritableResource(group, "/finance/fin_settlement", &models.FinSettlement{}, []string{"settlement_no", "subject_type", "subject_id", "period_start", "period_end"})
|
||||
settlementList, settlementCreate, settlementGet, settlementUpdate := platform.FinSettlementHandlers()
|
||||
registerWritableResource(group, "/finance/fin_settlement", settlementList, settlementCreate, settlementGet, settlementUpdate, &models.FinSettlement{})
|
||||
registerRestrictedWritableResource(group, "/finance/fin_reconciliation", &models.FinReconciliation{}, []string{"channel", "bill_date", "difference_amount"})
|
||||
|
||||
registerReadOnlyResource(group, "/wallet/wallet", &models.Wallet{})
|
||||
|
||||
@@ -1195,6 +1195,8 @@ const contracts = [
|
||||
}
|
||||
];
|
||||
const source = fs.readFileSync('src/api/resources.ts', 'utf8');
|
||||
const backendContractSource = fs.readFileSync('../../backend/api/internal/logic/platform/resource.go', 'utf8');
|
||||
const backendRouterSource = fs.readFileSync('../../backend/api/internal/routers/platform.go', 'utf8');
|
||||
const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }).outputText;
|
||||
const resourceModule = { exports: {} };
|
||||
vm.runInNewContext(compiled, { module: resourceModule, exports: resourceModule.exports });
|
||||
@@ -1208,6 +1210,8 @@ function checkContracts() {
|
||||
const definition = resources.find((resource) => resource.name === contract.name);
|
||||
const label = `${contract.domain}/${contract.name}`;
|
||||
if (!definition || definition.resource !== contract.resource || definition.mode !== contract.mode || definition.pageKind !== contract.pageKind || !sameFields(definition.fields, contract.fields)) failures.push(`${label}: mismatched contract or allowlist`);
|
||||
if (!backendContractSource.includes(`Domain: "${contract.domain}"`) || !backendContractSource.includes(`Name: "${contract.name}"`)) failures.push(`${label}: absent from backend ExpectedResources`);
|
||||
if (contract.name !== 'saf_event_disposal' && !backendRouterSource.includes(contract.resource)) failures.push(`${label}: absent from backend router`);
|
||||
const requiredIdentity = contract.fields.filter((field) => field.required && field.key.endsWith('_identity')).map((field) => field.key);
|
||||
if (definition && JSON.stringify(definition.requiredIdentities ?? []) !== JSON.stringify(requiredIdentity)) failures.push(`${label}: required identity fields do not match allowlist`);
|
||||
if (contract.name === 'saf_event_disposal') continue;
|
||||
@@ -1224,7 +1228,7 @@ function checkContracts() {
|
||||
const readonlyPage = fs.readFileSync('src/views/shared/ReadOnlyListPage.vue', 'utf8');
|
||||
if (/(CrudListPage|resourceApi\\.(create|update|archive|updateStatus))/.test(readonlyPage)) failures.push('readonly shared page: contains write surface');
|
||||
const treePage = fs.readFileSync('src/views/shared/TreePage.vue', 'utf8');
|
||||
if (!treePage.includes('<a-tree') || !/parent_identity|parent_id/.test(treePage) || treePage.includes('CrudListPage')) failures.push('tree shared page: missing tree semantics');
|
||||
if (!treePage.includes('<a-tree') || !treePage.includes('parent_identity') || /\bparent_id\b|\bid\b/.test(treePage) || treePage.includes('CrudListPage')) failures.push('tree shared page: must be identity-first');
|
||||
for (const name of ['ec_category', 'platform_menu']) { const view = path.join('src/views', name === 'ec_category' ? 'ec' : 'platform', name, 'TreePage.vue'); if (!fs.existsSync(view)) failures.push(`${name}: must use TreePage`); }
|
||||
const safetyEvent = resources.find((resource) => resource.name === 'saf_event');
|
||||
if (!safetyEvent?.detailActions?.some((item) => item.name === 'saf_event_disposal' && item.resource === '/safety/saf_event/:identity/disposals')) failures.push('safety event: missing disposal detail action');
|
||||
|
||||
@@ -4,10 +4,10 @@ import { Message } from '@arco-design/web-vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { resourceApi } from '@/api/resource';
|
||||
import type { ResourceUiDefinition } from '@/api/resources';
|
||||
type Node = Record<string, unknown> & { identity: string; parent_identity?: string; id?: number; parent_id?: number; children: Node[] };
|
||||
type Node = Record<string, unknown> & { identity: string; parent_identity?: string; children: Node[] };
|
||||
const props = defineProps<{ definition: ResourceUiDefinition }>();
|
||||
const loading = ref(false); const list = ref<Node[]>([]);
|
||||
const tree = computed(() => { const byIdentity = new Map<string, Node>(); const byInternalId = new Map<number, Node>(); const roots: Node[] = []; list.value.forEach((item) => { const node = { ...item, children: [] }; byIdentity.set(node.identity, node); if (typeof node.id === 'number') byInternalId.set(node.id, node); }); byIdentity.forEach((item) => { const parent = item.parent_identity ? byIdentity.get(item.parent_identity) : typeof item.parent_id === 'number' ? byInternalId.get(item.parent_id) : undefined; if (parent) parent.children.push(item); else roots.push(item); }); return roots; });
|
||||
const tree = computed(() => { const byIdentity = new Map<string, Node>(); const roots: Node[] = []; list.value.forEach((item) => byIdentity.set(item.identity, { ...item, children: [] })); byIdentity.forEach((item) => { const parent = item.parent_identity ? byIdentity.get(item.parent_identity) : undefined; if (parent) parent.children.push(item); else roots.push(item); }); return roots; });
|
||||
async function load() { loading.value = true; try { list.value = (await resourceApi.list<Node>(props.definition.resource, 1, 500)).list; } catch (error) { Message.error((error as Error).message); } finally { loading.value = false; } }
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user