已完成用户APP首期功能开发
交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
This commit is contained in:
51
backend/api/internal/logic/platform/contract_ticket_guard.go
Normal file
51
backend/api/internal/logic/platform/contract_ticket_guard.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// 功能描述:平台治理编辑不能改变合同申请的用户和分类关联;版本:1.0.0。
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
)
|
||||
|
||||
// GuardContractTicketUpdate 保留普通工单编辑;合同申请允许治理字段编辑但禁止转移个人资料。
|
||||
func GuardContractTicketUpdate(next gin.HandlerFunc) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
var ticket models.CsTicket
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&ticket).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if ticket.GasorderContractID == 0 {
|
||||
next(ctx)
|
||||
return
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(ctx.Request.Body, 65537))
|
||||
if err != nil || len(body) > 65536 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var input map[string]any
|
||||
if json.Unmarshal(body, &input) != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if value, found := input["category"]; found && value != "contract_change" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if value, found := input["user_account_identity"]; found {
|
||||
var owner models.UserAccount
|
||||
if err := impl.DBService.First(&owner, ticket.UserAccountID).Error; err != nil || value != owner.Identity {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewReader(body))
|
||||
next(ctx)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// 功能描述:平台编辑保留合同申请归属并保持请求体可读;版本:1.0.0。
|
||||
package platform
|
||||
|
||||
import (
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContractTicketGuardPreservesOwner(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
body string
|
||||
ownerQuery, allowed bool
|
||||
}{
|
||||
{`{"category":"repair"}`, false, false},
|
||||
{`{"user_account_identity":"other"}`, true, false},
|
||||
{`{"user_account_identity":"owner","priority":"high","category":"contract_change"}`, true, true},
|
||||
} {
|
||||
t.Run(tc.body, func(t *testing.T) {
|
||||
connection, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer connection.Close()
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: connection}), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
previous := impl.DBService
|
||||
impl.DBService = db
|
||||
defer func() { impl.DBService = previous }()
|
||||
mock.ExpectQuery(`SELECT .* FROM "cs_ticket"`).WillReturnRows(sqlmock.NewRows([]string{"gasorder_contract_id", "user_account_id"}).AddRow(7, 1))
|
||||
if tc.ownerQuery {
|
||||
mock.ExpectQuery(`SELECT .* FROM "user_account"`).WillReturnRows(sqlmock.NewRows([]string{"identity"}).AddRow("owner"))
|
||||
}
|
||||
response := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(response)
|
||||
ctx.Params = gin.Params{{Key: "identity", Value: "ticket"}}
|
||||
ctx.Request = httptest.NewRequest("PUT", "/cs_ticket/ticket", strings.NewReader(tc.body))
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
called := false
|
||||
GuardContractTicketUpdate(func(ctx *gin.Context) {
|
||||
called = true
|
||||
var body map[string]any
|
||||
if ctx.ShouldBindJSON(&body) != nil || body["priority"] != "high" {
|
||||
t.Error("原请求体丢失")
|
||||
}
|
||||
})(ctx)
|
||||
if called != tc.allowed {
|
||||
t.Fatalf("allowed=%v", called)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
147
backend/api/internal/logic/platform/deposit_return.go
Normal file
147
backend/api/internal/logic/platform/deposit_return.go
Normal file
@@ -0,0 +1,147 @@
|
||||
// 功能描述:平台处理退瓶回收、验收扣减和押金入账;版本:1.0.0。
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
sdkmiddleware "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"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// ConfirmDepositPickup 确认实物已回收,进入待验收状态。
|
||||
func ConfirmDepositPickup(ctx *gin.Context) {
|
||||
transitionDepositReturn(ctx, 10, 20, func(request *models.DepositReturnRequest, now time.Time, operator string) map[string]any {
|
||||
return map[string]any{"return_status": 20, "picked_up_at": &now, "operator_identity": operator}
|
||||
})
|
||||
}
|
||||
|
||||
// InspectDepositReturn 记录可审计的扣减金额和理由。
|
||||
func InspectDepositReturn(ctx *gin.Context) {
|
||||
var input struct {
|
||||
DeductionAmount int64 `json:"deduction_amount" binding:"gte=0"`
|
||||
Remark string `json:"remark" binding:"required,max=2000"`
|
||||
}
|
||||
if ctx.ShouldBindJSON(&input) != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
transitionDepositReturn(ctx, 20, 30, func(request *models.DepositReturnRequest, now time.Time, operator string) map[string]any {
|
||||
if input.DeductionAmount > request.EstimatedAmount {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"return_status": 30, "deduction_amount": input.DeductionAmount,
|
||||
"refund_amount": request.EstimatedAmount - input.DeductionAmount,
|
||||
"inspection_remark": input.Remark, "inspected_at": &now, "operator_identity": operator,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// CompleteDepositRefund 在同一事务内退入余额、写不可变流水并完成押金状态。
|
||||
func CompleteDepositRefund(ctx *gin.Context) {
|
||||
claims, err := sdkmiddleware.ParseAuth(ctx)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
var request models.DepositReturnRequest
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&request).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if request.ReturnStatus == 40 {
|
||||
return nil
|
||||
}
|
||||
if request.ReturnStatus != 30 {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
var user models.UserAccount
|
||||
if err := tx.First(&user, request.UserAccountID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var wallet models.WalletBasic
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("owner_type = ? AND owner_identity = ?", "user", user.Identity).First(&wallet).Error; err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
wallet = models.WalletBasic{Entity: common.NewEntity(common.StatusEnable), OwnerType: "user", OwnerID: user.ID, OwnerIdentity: user.Identity}
|
||||
if err := tx.Create(&wallet).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if request.RefundAmount > 0 {
|
||||
wallet.Balance += request.RefundAmount
|
||||
if err := tx.Model(&wallet).Update("balance", wallet.Balance).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
year, month, day := now.Date()
|
||||
flow := models.WalletRecord{
|
||||
Entity: common.NewEntity(common.StatusEnable), WalletBasicID: wallet.ID,
|
||||
RecordNo: common.RecordNo("DR"), RequestNo: "deposit-refund:" + request.Identity,
|
||||
Direction: "income", TradeType: "deposit_refund", Amount: request.RefundAmount,
|
||||
BalanceAfter: wallet.Balance, WithdrawalBalanceAfter: wallet.WithdrawalBalance,
|
||||
InTradeNo: request.Identity, PayChannel: "wallet", OperatorIdentity: claims.Identity,
|
||||
Ymd: int32(year*10000 + int(month)*100 + day), Ym: int32(year*100 + int(month)), Remark: "退瓶验收后押金退回",
|
||||
}
|
||||
if err := tx.Create(&flow).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
depositUpdate := tx.Model(&models.DepositRecord{}).Where("id = ? AND deposit_status = ?", request.DepositRecordID, 20).
|
||||
Updates(map[string]any{"deposit_status": 23, "refunded_at": &now})
|
||||
if depositUpdate.Error != nil {
|
||||
return depositUpdate.Error
|
||||
}
|
||||
if depositUpdate.RowsAffected != 1 {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
return tx.Model(&request).Updates(map[string]any{"return_status": 40, "completed_at": &now, "operator_identity": claims.Identity}).Error
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrInvalidData) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"completed": true})
|
||||
}
|
||||
|
||||
func transitionDepositReturn(ctx *gin.Context, from, to int, values func(*models.DepositReturnRequest, time.Time, string) map[string]any) {
|
||||
claims, err := sdkmiddleware.ParseAuth(ctx)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
|
||||
return
|
||||
}
|
||||
var request models.DepositReturnRequest
|
||||
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ? AND return_status = ?", ctx.Param("identity"), from).First(&request).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
updates := values(&request, time.Now(), claims.Identity)
|
||||
if updates == nil {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
updates["return_status"] = to
|
||||
return tx.Model(&request).Updates(updates).Error
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrInvalidData) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"changed": true, "return_status": to})
|
||||
}
|
||||
@@ -147,6 +147,11 @@ func ServeGasorderContractAttachment(ctx *gin.Context) {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
ServeAuthorizedContractPDF(ctx, contract, operator)
|
||||
}
|
||||
|
||||
// ServeAuthorizedContractPDF 输出已由调用方完成归属鉴权的合同;不得直接注册为路由。
|
||||
func ServeAuthorizedContractPDF(ctx *gin.Context, contract models.GasorderContract, operator string) {
|
||||
path, err := contractAttachmentPath(contract.FileURI, false)
|
||||
if err != nil || !validStoredContractPDF(path) {
|
||||
logContractAttachment(operator, contract.Identity, "download", "unavailable", contract.FileURI)
|
||||
|
||||
@@ -48,6 +48,7 @@ var PlatformMenus = [][]Menu{
|
||||
{Identity: "product_type", ParentIdentity: "device", GroupCode: "device", Name: "类型管理", Path: "/product/product-type", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "product_warehouse", ParentIdentity: "device", GroupCode: "device", Name: "库房管理", Path: "/product/warehouse", SortNo: 3, Status: common.StatusEnable},
|
||||
{Identity: "product_info", ParentIdentity: "device", GroupCode: "device", Name: "智能气阀管理", Path: "/product/product-info", SortNo: 4, Status: common.StatusEnable},
|
||||
{Identity: "dev_usage_stat", ParentIdentity: "device", GroupCode: "device", Name: "用气统计数据", Path: "/product/usage-statistics", SortNo: 5, Status: common.StatusEnable},
|
||||
},
|
||||
{
|
||||
{Identity: "gasorder", GroupCode: "gasorder", Name: "气体配送订单管理", Icon: "icon-list", Path: "/gasorder", SortNo: 70, Status: common.StatusEnable},
|
||||
@@ -62,6 +63,9 @@ var PlatformMenus = [][]Menu{
|
||||
{Identity: "ec_cart", ParentIdentity: "ec", GroupCode: "ec", Name: "购物车", Path: "/ec/carts", SortNo: 3, Status: common.StatusEnable},
|
||||
{Identity: "ec_order", ParentIdentity: "ec", GroupCode: "ec", Name: "商城订单", Path: "/ec/orders", SortNo: 4, Status: common.StatusEnable},
|
||||
{Identity: "ec_review", ParentIdentity: "ec", GroupCode: "ec", Name: "商品评价", Path: "/ec/reviews", SortNo: 5, Status: common.StatusEnable},
|
||||
{Identity: "deposit_policy", ParentIdentity: "ec", GroupCode: "ec", Name: "押金规则", Path: "/ec/deposit-policies", SortNo: 6, Status: common.StatusEnable},
|
||||
{Identity: "deposit_record", ParentIdentity: "ec", GroupCode: "ec", Name: "押金记录", Path: "/ec/deposit-records", SortNo: 7, Status: common.StatusEnable},
|
||||
{Identity: "deposit_return_request", ParentIdentity: "ec", GroupCode: "ec", Name: "退瓶处理", Path: "/ec/deposit-returns", SortNo: 8, Status: common.StatusEnable},
|
||||
},
|
||||
{
|
||||
{Identity: "finance", GroupCode: "finance", Name: "财务管理", Icon: "icon-bar-chart", Path: "/finance", SortNo: 100, Status: common.StatusEnable},
|
||||
|
||||
@@ -93,6 +93,8 @@ func platformRouteMenuIdentity(resource string) string {
|
||||
return resource
|
||||
case strings.HasPrefix(resource, "product_"):
|
||||
return "product_info"
|
||||
case resource == "dev_usage_stat":
|
||||
return "dev_usage_stat"
|
||||
case strings.HasPrefix(resource, "cms_"):
|
||||
return "cms_content"
|
||||
case strings.HasPrefix(resource, "cs_"):
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// 功能:校验平台实体分类和厂商编号,禁止用名称推断设备能力;版本:1.0.0。
|
||||
package product
|
||||
|
||||
import (
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var vendorDevicePattern = regexp.MustCompile(`^[0-9]{16}$`)
|
||||
|
||||
// validDeviceMapping 空编号允许先建立档案;钢瓶和未分类不能占用设备编号。
|
||||
func validDeviceMapping(product models.ProductInfo) bool {
|
||||
switch product.DeviceKind {
|
||||
case "", "unknown", "cylinder":
|
||||
return product.VendorDeviceID == ""
|
||||
case "valve", "alarm":
|
||||
return product.VendorDeviceID == "" || (vendorDevicePattern.MatchString(product.VendorDeviceID) && product.VendorDeviceID != "0000000000000000")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 功能:实体设备分类和厂商编号边界测试;版本:1.0.0。
|
||||
package product
|
||||
|
||||
import (
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestDeviceMappingBoundary 检查未知分类、普通钢瓶、非十进制和缺失编号。
|
||||
func TestDeviceMappingBoundary(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
kind, id string
|
||||
valid bool
|
||||
}{
|
||||
{"unknown", "", true}, {"cylinder", "", true}, {"valve", "", true}, {"alarm", "1234567890123456", true},
|
||||
{"valve", "0000000000000001", true}, {"cylinder", "0000000000000001", false},
|
||||
{"unknown", "0000000000000001", false}, {"valve", "0000000000000000", false},
|
||||
{"valve", "1234", false}, {"alarm", "123456789012345x", false}, {"invented", "", false},
|
||||
} {
|
||||
if got := validDeviceMapping(models.ProductInfo{DeviceKind: tc.kind, VendorDeviceID: tc.id}); got != tc.valid {
|
||||
t.Errorf("分类%s编号%s结果%v", tc.kind, tc.id, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// 功能:平台产品档案、厂商映射与生命周期事务;版本:1.1.0。
|
||||
package product
|
||||
|
||||
import (
|
||||
@@ -28,7 +29,7 @@ var productLifecycleStatuses = map[int]bool{
|
||||
}
|
||||
|
||||
func ProductInfoHandlers(relations ...common.ResourceRelation) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
|
||||
fields := []string{"code", "name", "params", "produced_at", "action", "reason", "remark"}
|
||||
fields := []string{"code", "name", "params", "produced_at", "action", "reason", "remark", "device_kind", "vendor_device_id"}
|
||||
return listProductInfo,
|
||||
func(ctx *gin.Context) { createProductInfo(ctx, fields, relations) },
|
||||
func(ctx *gin.Context) { common.GetResource(ctx, &models.ProductInfo{}) },
|
||||
@@ -61,11 +62,15 @@ func createProductInfo(ctx *gin.Context, fields []string, relations []common.Res
|
||||
delete(values, "action")
|
||||
delete(values, "reason")
|
||||
delete(values, "remark")
|
||||
data := models.ProductInfo{Entity: common.NewEntity(common.StatusDisable), ProductStatus: common.StatusPending, Params: "{}"}
|
||||
data := models.ProductInfo{Entity: common.NewEntity(common.StatusDisable), ProductStatus: common.StatusPending, Params: "{}", DeviceKind: "unknown"}
|
||||
if err := decodeValues(values, &data); err != nil || data.Code == "" || data.Name == "" || !validProductProducer(data) || data.ProducedAt.IsZero() || !validProductOwnership(data) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if !validDeviceMapping(data) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
operatorIdentity, operatorName := productOperator(ctx)
|
||||
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(&data).Error; err != nil {
|
||||
@@ -112,10 +117,17 @@ func updateProductInfo(ctx *gin.Context, fields []string, relations []common.Res
|
||||
if err := decodeValues(values, &preview); err != nil || !validProductProducer(preview) || !validProductOwnership(preview) {
|
||||
return errors.New("product can have at most one current owner")
|
||||
}
|
||||
if !validDeviceMapping(preview) {
|
||||
return errcode.ErrInvalidArgument
|
||||
}
|
||||
ownershipChanged := ownershipValuesChanged(current, values)
|
||||
if ownershipChanged && !productOwnerActions[action] {
|
||||
return errors.New("invalid ownership action")
|
||||
}
|
||||
if ownershipChanged {
|
||||
// 设备分组只属于原用户;转移产品时必须解除归组,避免新用户继承旧用户结构。
|
||||
values["device_group_id"] = uint64(0)
|
||||
}
|
||||
if err := tx.Model(¤t).Updates(values).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -82,8 +82,9 @@ func ExpectedResources() []ResourceContract {
|
||||
resourceContract("delivery", "delivery_basic", Writable, "list"), resourceContract("delivery", "delivery_account", Writable, "list"),
|
||||
resourceContract("staff", "staff_account", Writable, "list"), resourceContract("staff", "staff_credential", Writable, "list"),
|
||||
resourceContract("user", "user_account", Writable, "list"), resourceContract("user", "user_address", Writable, "list"), resourceContract("user", "user_service_relation", Writable, "list"),
|
||||
resourceContract("product", "producer_account", Writable, "list"), resourceContract("product", "product_type", Editable, "list"), resourceContract("product", "product_warehouse", Editable, "list"), resourceContract("product", "product_info", Editable, "list"), resourceContract("product", "product_repair", Editable, "list"), resourceContract("product", "product_owner", ReadOnly, "list"),
|
||||
resourceContract("product", "producer_account", Writable, "list"), resourceContract("product", "product_type", Editable, "list"), resourceContract("product", "product_warehouse", Editable, "list"), resourceContract("product", "product_info", Editable, "list"), resourceContract("product", "product_repair", Editable, "list"), resourceContract("product", "product_owner", ReadOnly, "list"), resourceContract("product", "dev_usage_stat", Editable, "list"),
|
||||
resourceContract("ec", "ec_category", Writable, "list"), resourceContract("ec", "ec_product", Writable, "list"), resourceContract("ec", "ec_product_attribute", Writable, "list"), resourceContract("ec", "ec_product_image", Writable, "list"), resourceContract("ec", "ec_cart", ReadOnly, "list"), resourceContract("ec", "ec_order", ReadOnly, "list"), resourceContract("ec", "ec_order_item", ReadOnly, "list"), resourceContract("ec", "ec_review", ReadOnly, "list"),
|
||||
resourceContract("ec", "deposit_policy", Writable, "list"), resourceContract("ec", "deposit_record", ReadOnly, "list"), resourceContract("ec", "deposit_return_request", ReadOnly, "list"),
|
||||
resourceContract("gasorder", "gasorder_contract", Managed, "list"), resourceContract("gasorder", "gasorder_contract_product", AppendOnly, "list"), resourceContract("gasorder", "gasorder_contract_revision", ReadOnly, "list"),
|
||||
resourceContract("gasorder", "gasorder_basic", AppendOnly, "list"), resourceContract("gasorder", "gasorder_item", ReadOnly, "list"), resourceContract("gasorder", "gasorder_assign", ReadOnly, "list"), resourceContract("gasorder", "gasorder_status", ReadOnly, "list"),
|
||||
resourceContract("gasorder", "gasorder_track", ReadOnly, "list"), resourceContract("gasorder", "gasorder_track_point", ReadOnly, "list"), resourceContract("gasorder", "gasorder_confirm", ReadOnly, "list"), resourceContract("gasorder", "gasorder_payment", ReadOnly, "list"),
|
||||
|
||||
Reference in New Issue
Block a user