refactor platform logic and add gasorder domain
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// 平台 API 的版本命令行工具。
|
||||
// 平台 API 命令行工具。
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -8,12 +8,24 @@ import (
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("usage: platform-cli <version>")
|
||||
return
|
||||
}
|
||||
if os.Args[1] != "version" {
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
switch os.Args[1] {
|
||||
case "version":
|
||||
fmt.Println("platform-cli 0.1.0")
|
||||
case "resource-contract":
|
||||
if err := writeResourceContract(os.Stdout); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Fprintln(os.Stderr, "usage: platform-cli <version|resource-contract>")
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform"
|
||||
@@ -14,6 +14,7 @@ type route struct {
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type contract struct {
|
||||
Domain string `json:"domain"`
|
||||
Name string `json:"name"`
|
||||
@@ -21,12 +22,13 @@ type contract struct {
|
||||
PageKind string `json:"pageKind"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
type manifest struct {
|
||||
Resources []contract `json:"resources"`
|
||||
Routes []route `json:"routes"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
func writeResourceContract(output io.Writer) error {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
engine := gin.New()
|
||||
routers.RegisterPlatform("heqi", engine)
|
||||
@@ -37,9 +39,10 @@ func main() {
|
||||
expected := platform.ExpectedResources()
|
||||
contracts := make([]contract, 0, len(expected))
|
||||
for _, item := range expected {
|
||||
contracts = append(contracts, contract{Domain: item.Domain, Name: item.Name, Path: item.Path, PageKind: item.PageKind, Mode: string(item.Mode)})
|
||||
}
|
||||
if err := json.NewEncoder(os.Stdout).Encode(manifest{Resources: contracts, Routes: routes}); err != nil {
|
||||
panic(err)
|
||||
contracts = append(contracts, contract{
|
||||
Domain: item.Domain, Name: item.Name, Path: item.Path,
|
||||
PageKind: item.PageKind, Mode: string(item.Mode),
|
||||
})
|
||||
}
|
||||
return json.NewEncoder(output).Encode(manifest{Resources: contracts, Routes: routes})
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package platform 提供平台总后台的同步 HTTP 业务逻辑。
|
||||
package platform
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -15,6 +15,21 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// FilterFields keeps only explicitly allowed persistence fields.
|
||||
func FilterFields(values map[string]any, allowedFields []string) gin.H {
|
||||
allowed := make(map[string]struct{}, len(allowedFields))
|
||||
for _, field := range allowedFields {
|
||||
allowed[field] = struct{}{}
|
||||
}
|
||||
filtered := make(gin.H, len(allowed))
|
||||
for field, value := range values {
|
||||
if _, ok := allowed[field]; ok {
|
||||
filtered[field] = value
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// UpdateRecordStatus 更新主表状态,停用和归档均保留历史记录。
|
||||
func UpdateRecordStatus(ctx *gin.Context, model any) {
|
||||
var request struct {
|
||||
@@ -24,24 +39,24 @@ func UpdateRecordStatus(ctx *gin.Context, model any) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, model, gin.H{"status": request.Status}, []string{"status"})
|
||||
UpdateAllowedByIdentity(ctx, model, gin.H{"status": request.Status}, []string{"status"})
|
||||
}
|
||||
|
||||
// ArchiveRecord 通过 archived 状态实现逻辑删除,不物理删除主数据。
|
||||
func ArchiveRecord(ctx *gin.Context, model any) {
|
||||
updateAllowedByIdentity(ctx, model, archiveValues(), []string{"status"})
|
||||
UpdateAllowedByIdentity(ctx, model, gin.H{"status": "archived"}, []string{"status"})
|
||||
}
|
||||
|
||||
func newEntity(status string) models.Entity {
|
||||
func NewEntity(status string) models.Entity {
|
||||
return models.Entity{Identity: models.NewIdentity(), Status: status, Version: 1}
|
||||
}
|
||||
|
||||
func listPage[T any](ctx *gin.Context) {
|
||||
page, size := pageSize(ctx)
|
||||
func ListPage[T any](ctx *gin.Context) {
|
||||
page, size := PageSize(ctx)
|
||||
var list []T
|
||||
var total int64
|
||||
model := new(T)
|
||||
databaseQuery := applyKeywordFilter(ctx, impl.DBService.Model(model), model)
|
||||
databaseQuery := ApplyKeywordFilter(ctx, impl.DBService.Model(model), model)
|
||||
if err := databaseQuery.Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -50,12 +65,12 @@ func listPage[T any](ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := publicResourceResponse(list)
|
||||
response, err := PublicResourceResponse(list)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": protectPreciseLocation(ctx, model, response)})
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": ProtectPreciseLocation(ctx, model, response)})
|
||||
}
|
||||
|
||||
var keywordSafeColumns = map[string]bool{
|
||||
@@ -74,9 +89,11 @@ var keywordSafeColumns = map[string]bool{
|
||||
"record_no": true, "request_no": true, "refund_no": true,
|
||||
"cash_no": true, "trade_no": true, "trade_type": true,
|
||||
"pay_channel": true, "payment_type": true,
|
||||
"contract_no": true, "creator_type": true, "from_status": true,
|
||||
"to_status": true, "confirm_type": true, "product_type_name": true,
|
||||
}
|
||||
|
||||
func applyKeywordFilter(ctx *gin.Context, query *gorm.DB, model any) *gorm.DB {
|
||||
func ApplyKeywordFilter(ctx *gin.Context, query *gorm.DB, model any) *gorm.DB {
|
||||
keyword := strings.ToLower(strings.TrimSpace(ctx.Query("keyword")))
|
||||
if keyword == "" {
|
||||
return query
|
||||
@@ -131,22 +148,22 @@ func gormColumn(tag string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func getByIdentity[T any](ctx *gin.Context) {
|
||||
func GetByIdentity[T any](ctx *gin.Context) {
|
||||
var data T
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&data).Error; err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := publicResourceResponse(data)
|
||||
response, err := PublicResourceResponse(data)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, protectPreciseLocation(ctx, new(T), response))
|
||||
infra.Response.Success(ctx, ProtectPreciseLocation(ctx, new(T), response))
|
||||
}
|
||||
|
||||
func updateAllowedByIdentity(ctx *gin.Context, model any, values map[string]any, allowedFields []string) {
|
||||
values = filterFields(values, allowedFields)
|
||||
func UpdateAllowedByIdentity(ctx *gin.Context, model any, values map[string]any, allowedFields []string) {
|
||||
values = FilterFields(values, allowedFields)
|
||||
if len(values) == 0 {
|
||||
infra.Response.Success(ctx, gin.H{"updated": false})
|
||||
return
|
||||
@@ -164,7 +181,7 @@ func updateAllowedByIdentity(ctx *gin.Context, model any, values map[string]any,
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func updateByIdentity(ctx *gin.Context, model any, values map[string]any) {
|
||||
func UpdateByIdentity(ctx *gin.Context, model any, values map[string]any) {
|
||||
result := impl.DBService.Model(model).Where("identity = ?", ctx.Param("identity")).Updates(values)
|
||||
if result.Error != nil {
|
||||
infra.Response.Error(ctx, result.Error)
|
||||
@@ -177,7 +194,7 @@ func updateByIdentity(ctx *gin.Context, model any, values map[string]any) {
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func respondRecordError(ctx *gin.Context, err error) {
|
||||
func RespondRecordError(ctx *gin.Context, err error) {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return
|
||||
@@ -185,7 +202,7 @@ func respondRecordError(ctx *gin.Context, err error) {
|
||||
infra.Response.Error(ctx, err)
|
||||
}
|
||||
|
||||
func pageSize(ctx *gin.Context) (int, int) {
|
||||
func PageSize(ctx *gin.Context) (int, int) {
|
||||
page := utils.String2Int(ctx.DefaultQuery("page", "1"))
|
||||
size := utils.String2Int(ctx.DefaultQuery("size", "20"))
|
||||
if page < 1 {
|
||||
@@ -197,7 +214,7 @@ func pageSize(ctx *gin.Context) (int, int) {
|
||||
return page, size
|
||||
}
|
||||
|
||||
func maskPhone(phone string) string {
|
||||
func MaskPhone(phone string) string {
|
||||
if len(phone) < 7 {
|
||||
return "***"
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
package platform
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -29,71 +27,17 @@ type ResourceRelation struct {
|
||||
// ResourceHandlers supplies the common identity-based CRUD boundary used by
|
||||
// platform resources whose writable fields are explicitly declared by routes.
|
||||
func ResourceHandlers(model any, createFields, updateFields []string, relations ...ResourceRelation) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
|
||||
return func(ctx *gin.Context) { listResource(ctx, model) },
|
||||
return func(ctx *gin.Context) { ListResource(ctx, model) },
|
||||
func(ctx *gin.Context) { createResource(ctx, model, createFields, relations) },
|
||||
func(ctx *gin.Context) { getResource(ctx, model) },
|
||||
func(ctx *gin.Context) { GetResource(ctx, model) },
|
||||
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)
|
||||
func ListResource(ctx *gin.Context, model any) {
|
||||
page, size := PageSize(ctx)
|
||||
list := reflect.New(reflect.SliceOf(reflect.TypeOf(model).Elem()))
|
||||
var total int64
|
||||
query := applyKeywordFilter(ctx, impl.DBService.Model(model), model)
|
||||
query := ApplyKeywordFilter(ctx, impl.DBService.Model(model), model)
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -102,30 +46,30 @@ func listResource(ctx *gin.Context, model any) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := publicResourceResponse(list.Elem().Interface())
|
||||
response, err := PublicResourceResponse(list.Elem().Interface())
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": protectPreciseLocation(ctx, model, response)})
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": ProtectPreciseLocation(ctx, model, response)})
|
||||
}
|
||||
|
||||
func getResource(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 {
|
||||
respondRecordError(ctx, err)
|
||||
RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := publicResourceResponse(data.Interface())
|
||||
response, err := PublicResourceResponse(data.Interface())
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, protectPreciseLocation(ctx, model, response))
|
||||
infra.Response.Success(ctx, ProtectPreciseLocation(ctx, model, response))
|
||||
}
|
||||
|
||||
func createResource(ctx *gin.Context, model any, allowedFields []string, relations []ResourceRelation) {
|
||||
values, err := prepareResourceValues(ctx, model, allowedFields, relations)
|
||||
values, err := PrepareResourceValues(ctx, model, allowedFields, relations)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
@@ -140,16 +84,16 @@ func createResource(ctx *gin.Context, model any, allowedFields []string, relatio
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
data.Elem().FieldByName("Entity").Set(reflect.ValueOf(newEntity("draft")))
|
||||
data.Elem().FieldByName("Entity").Set(reflect.ValueOf(NewEntity("draft")))
|
||||
if err := impl.DBService.Create(data.Interface()).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
respondCreatedResource(ctx, data.Interface())
|
||||
RespondCreatedResource(ctx, data.Interface())
|
||||
}
|
||||
|
||||
func respondCreatedResource(ctx *gin.Context, value any) {
|
||||
response, err := publicResourceResponse(value)
|
||||
func RespondCreatedResource(ctx *gin.Context, value any) {
|
||||
response, err := PublicResourceResponse(value)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -186,11 +130,11 @@ func isCreatedResponseField(key string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func protectPreciseLocation(ctx *gin.Context, model, value any) any {
|
||||
func ProtectPreciseLocation(ctx *gin.Context, model, value any) any {
|
||||
maskPersonalName := reflect.TypeOf(model) == reflect.TypeOf(&models.UserAccount{}) ||
|
||||
reflect.TypeOf(model) == reflect.TypeOf(&models.StaffAccount{})
|
||||
maskDisplayName := reflect.TypeOf(model) == reflect.TypeOf(&models.PlatfromAccount{})
|
||||
protectPublicFields(value, maskPersonalName, maskDisplayName, hasPreciseLocationScope(ctx))
|
||||
ProtectPublicFields(value, maskPersonalName, maskDisplayName, HasPreciseLocationScope(ctx))
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -200,31 +144,44 @@ var sensitiveResponseFields = map[string]bool{
|
||||
"attachment_uri": true, "attachment_url": true,
|
||||
"certificate_uri": true, "certificate_url": true,
|
||||
"credential_uri": true, "credential_url": true,
|
||||
"proof_uri": true,
|
||||
}
|
||||
|
||||
func protectPublicFields(value any, maskPersonalName, maskDisplayName, retainCoordinates bool) {
|
||||
func ProtectPublicFields(value any, maskPersonalName, maskDisplayName, retainCoordinates bool) {
|
||||
switch data := value.(type) {
|
||||
case map[string]any:
|
||||
if phone, ok := data["phone"].(string); ok && phone != "" {
|
||||
data["phone_masked"] = maskPhone(phone)
|
||||
data["phone_masked"] = MaskPhone(phone)
|
||||
}
|
||||
delete(data, "phone")
|
||||
for _, key := range []string{"contact_phone", "recipient_phone"} {
|
||||
if phone, ok := data[key].(string); ok && phone != "" {
|
||||
data[key+"_masked"] = MaskPhone(phone)
|
||||
}
|
||||
delete(data, key)
|
||||
}
|
||||
for _, key := range []string{"contact_name", "recipient_name"} {
|
||||
if name, ok := data[key].(string); ok && name != "" {
|
||||
data[key+"_masked"] = MaskPersonalNameValue(name)
|
||||
}
|
||||
delete(data, key)
|
||||
}
|
||||
for key := range sensitiveResponseFields {
|
||||
delete(data, key)
|
||||
}
|
||||
if maskPersonalName {
|
||||
if name, ok := data["name"].(string); ok && name != "" {
|
||||
data["name_masked"] = maskPersonalNameValue(name)
|
||||
data["name_masked"] = MaskPersonalNameValue(name)
|
||||
}
|
||||
delete(data, "name")
|
||||
if name, ok := data["real_name"].(string); ok && name != "" {
|
||||
data["real_name_masked"] = maskPersonalNameValue(name)
|
||||
data["real_name_masked"] = MaskPersonalNameValue(name)
|
||||
}
|
||||
delete(data, "real_name")
|
||||
}
|
||||
if maskDisplayName {
|
||||
if name, ok := data["display_name"].(string); ok && name != "" {
|
||||
data["display_name_masked"] = maskPersonalNameValue(name)
|
||||
data["display_name_masked"] = MaskPersonalNameValue(name)
|
||||
}
|
||||
delete(data, "display_name")
|
||||
}
|
||||
@@ -233,16 +190,16 @@ func protectPublicFields(value any, maskPersonalName, maskDisplayName, retainCoo
|
||||
delete(data, "latitude")
|
||||
}
|
||||
for _, item := range data {
|
||||
protectPublicFields(item, maskPersonalName, maskDisplayName, retainCoordinates)
|
||||
ProtectPublicFields(item, maskPersonalName, maskDisplayName, retainCoordinates)
|
||||
}
|
||||
case []any:
|
||||
for _, item := range data {
|
||||
protectPublicFields(item, maskPersonalName, maskDisplayName, retainCoordinates)
|
||||
ProtectPublicFields(item, maskPersonalName, maskDisplayName, retainCoordinates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func maskPersonalNameValue(name string) string {
|
||||
func MaskPersonalNameValue(name string) string {
|
||||
runes := []rune(name)
|
||||
if len(runes) == 0 {
|
||||
return ""
|
||||
@@ -253,7 +210,7 @@ func maskPersonalNameValue(name string) string {
|
||||
return string(runes[0]) + strings.Repeat("*", len(runes)-1)
|
||||
}
|
||||
|
||||
func hasPreciseLocationScope(ctx *gin.Context) bool {
|
||||
func HasPreciseLocationScope(ctx *gin.Context) bool {
|
||||
claims, err := middleware.ParseAuth(ctx)
|
||||
return err == nil && claims.Extend["location_scope"] == "precise"
|
||||
}
|
||||
@@ -264,7 +221,7 @@ func updateResource(ctx *gin.Context, model any, allowedFields []string, relatio
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
values, err := resolveResourceRelations(input, allowedFields, relations, false)
|
||||
values, err := ResolveResourceRelations(input, allowedFields, relations, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
@@ -273,23 +230,23 @@ func updateResource(ctx *gin.Context, model any, allowedFields []string, relatio
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, model, values, append(allowedFields, relationColumns(relations)...))
|
||||
UpdateAllowedByIdentity(ctx, model, values, append(allowedFields, relationColumns(relations)...))
|
||||
}
|
||||
|
||||
func prepareResourceValues(ctx *gin.Context, model any, allowedFields []string, relations []ResourceRelation) (map[string]any, error) {
|
||||
func PrepareResourceValues(ctx *gin.Context, model any, allowedFields []string, relations []ResourceRelation) (map[string]any, error) {
|
||||
var input map[string]any
|
||||
if err := ctx.ShouldBindJSON(&input); err != nil || len(input) == 0 {
|
||||
return nil, errors.New("invalid resource payload")
|
||||
}
|
||||
values, err := resolveResourceRelations(input, allowedFields, relations, true)
|
||||
values, err := ResolveResourceRelations(input, allowedFields, relations, true)
|
||||
if err != nil || len(values) == 0 {
|
||||
return nil, errors.New("invalid resource payload")
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func resolveResourceRelations(input map[string]any, allowedFields []string, relations []ResourceRelation, requireRelations bool) (map[string]any, error) {
|
||||
values := filterFields(input, allowedFields)
|
||||
func ResolveResourceRelations(input map[string]any, allowedFields []string, relations []ResourceRelation, requireRelations bool) (map[string]any, error) {
|
||||
values := FilterFields(input, allowedFields)
|
||||
for _, relation := range relations {
|
||||
raw, exists := input[relation.Input]
|
||||
if !exists {
|
||||
@@ -309,7 +266,7 @@ func resolveResourceRelations(input map[string]any, allowedFields []string, rela
|
||||
if strings.TrimSpace(identity) == "" {
|
||||
return nil, errors.New("invalid relation identity")
|
||||
}
|
||||
id, err := resolveIdentityID(relation.Model, identity, true)
|
||||
id, err := ResolveIdentityID(relation.Model, identity, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -318,9 +275,9 @@ func resolveResourceRelations(input map[string]any, allowedFields []string, rela
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// resolveIdentityID is the only boundary that converts a public identity to a
|
||||
// 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) {
|
||||
func ResolveIdentityID(model any, identity string, required bool) (uint64, error) {
|
||||
identity = strings.TrimSpace(identity)
|
||||
if identity == "" {
|
||||
if required {
|
||||
@@ -343,9 +300,9 @@ func relationColumns(relations []ResourceRelation) []string {
|
||||
return columns
|
||||
}
|
||||
|
||||
// resourceResponse strips database surrogate IDs from API data. Business
|
||||
// ResourceResponse strips database surrogate IDs from API data. Business
|
||||
// identities are the only public relation keys accepted or returned.
|
||||
func resourceResponse(value any) any {
|
||||
func ResourceResponse(value any) any {
|
||||
encoded, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return value
|
||||
@@ -357,10 +314,10 @@ func resourceResponse(value any) any {
|
||||
return stripInternalIDs(decoded)
|
||||
}
|
||||
|
||||
// publicResourceResponse additionally resolves persisted relation keys into
|
||||
// PublicResourceResponse additionally resolves persisted relation keys into
|
||||
// their public identities. It is used by list/detail endpoints so an edit form
|
||||
// can round-trip the relation without ever receiving a surrogate database ID.
|
||||
func publicResourceResponse(value any) (any, error) {
|
||||
func PublicResourceResponse(value any) (any, error) {
|
||||
encoded, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -385,8 +342,10 @@ var relationIdentityModels = map[string]any{
|
||||
"ec_category_id": &models.EcCategory{},
|
||||
"ec_product_id": &models.EcProduct{},
|
||||
"ec_order_id": &models.EcOrder{},
|
||||
"delivery_task_id": &models.DeliveryTask{},
|
||||
"delivery_track_id": &models.DeliveryTrack{},
|
||||
"gasorder_contract_id": &models.GasorderContract{},
|
||||
"gasorder_contract_product_id": &models.GasorderContractProduct{},
|
||||
"gasorder_basic_id": &models.GasorderBasic{},
|
||||
"gasorder_track_id": &models.GasorderTrack{},
|
||||
"platform_role_id": &models.PlatformRole{},
|
||||
"platform_menu_id": &models.PlatformMenu{},
|
||||
"wallet_basic_id": &models.WalletBasic{},
|
||||
@@ -540,122 +499,3 @@ func stripInternalIDs(value any) any {
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// 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 {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
var items []models.EcOrderItem
|
||||
if err := impl.DBService.Where("ec_order_id = ?", order.ID).Order("id asc").Find(&items).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := publicResourceResponse(gin.H{"order": order, "items": items})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, response)
|
||||
}
|
||||
|
||||
// GetDeliveryTrack returns time-ordered points. Precise coordinates are only
|
||||
// exposed to tokens explicitly granted the location_scope=precise claim.
|
||||
func GetDeliveryTrack(ctx *gin.Context) {
|
||||
var track models.DeliveryTrack
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&track).Error; err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
var points []models.DeliveryTrackPoint
|
||||
if err := impl.DBService.Where("delivery_track_id = ?", track.ID).Order("occurred_at asc").Find(&points).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if !hasPreciseLocationScope(ctx) {
|
||||
for index := range points {
|
||||
points[index].Longitude = ""
|
||||
points[index].Latitude = ""
|
||||
}
|
||||
}
|
||||
response, err := publicResourceResponse(gin.H{"track": track, "points": points})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, response)
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
58
backend/api/internal/logic/common/resource_test.go
Normal file
58
backend/api/internal/logic/common/resource_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
)
|
||||
|
||||
func TestFilterFieldsKeepsOnlyAllowedKeys(t *testing.T) {
|
||||
got := FilterFields(map[string]any{"name": "n", "password_hash": "x"}, []string{"name"})
|
||||
if len(got) != 1 || got["name"] != "n" {
|
||||
t.Fatalf("unexpected filtered fields: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceResponseStripsInternalIDsRecursively(t *testing.T) {
|
||||
got := ResourceResponse(map[string]any{
|
||||
"id": uint64(1), "identity": "root",
|
||||
"child": map[string]any{"gasorder_basic_id": uint64(2), "identity": "child"},
|
||||
}).(map[string]any)
|
||||
if _, exists := got["id"]; exists {
|
||||
t.Fatal("root database ID was exposed")
|
||||
}
|
||||
child := got["child"].(map[string]any)
|
||||
if _, exists := child["gasorder_basic_id"]; exists {
|
||||
t.Fatal("relation database ID was exposed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicFieldProtectionMasksGasorderContacts(t *testing.T) {
|
||||
value := map[string]any{"contact_name": "张三", "contact_phone": "13800138000"}
|
||||
ProtectPublicFields(value, false, false, false)
|
||||
if _, exists := value["contact_name"]; exists {
|
||||
t.Fatal("contact name remains public")
|
||||
}
|
||||
if _, exists := value["contact_phone"]; exists {
|
||||
t.Fatal("contact phone remains public")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceRelationOptionalEmptyIdentityClearsRelation(t *testing.T) {
|
||||
values, err := ResolveResourceRelations(
|
||||
map[string]any{"warehouse_identity": ""},
|
||||
nil,
|
||||
[]ResourceRelation{{Input: "warehouse_identity", Column: "warehouse_id", Model: &models.ProductWarehouse{}}},
|
||||
false,
|
||||
)
|
||||
if err != nil || values["warehouse_id"] != uint64(0) {
|
||||
t.Fatalf("optional relation clear = (%#v, %v)", values, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommonMethodModesRemainHTTPCompatible(t *testing.T) {
|
||||
if http.MethodGet == "" || http.MethodPost == "" {
|
||||
t.Fatal("standard HTTP methods unavailable")
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,16 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ListDeliveryBasic 查询配送点分页列表。
|
||||
func ListDeliveryBasic(ctx *gin.Context) { listPage[models.DeliveryBasic](ctx) }
|
||||
func ListDeliveryBasic(ctx *gin.Context) { common.ListPage[models.DeliveryBasic](ctx) }
|
||||
|
||||
// GetDeliveryBasic 查询一个配送点。
|
||||
func GetDeliveryBasic(ctx *gin.Context) { getByIdentity[models.DeliveryBasic](ctx) }
|
||||
func GetDeliveryBasic(ctx *gin.Context) { common.GetByIdentity[models.DeliveryBasic](ctx) }
|
||||
|
||||
// CreateDeliveryBasic 创建配送点档案。
|
||||
func CreateDeliveryBasic(ctx *gin.Context) {
|
||||
@@ -27,17 +28,17 @@ func CreateDeliveryBasic(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
gasBasicID, err := resolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
||||
gasBasicID, err := common.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}
|
||||
delivery := models.DeliveryBasic{Entity: common.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
|
||||
}
|
||||
respondCreatedResource(ctx, delivery)
|
||||
common.RespondCreatedResource(ctx, delivery)
|
||||
}
|
||||
|
||||
// UpdateDeliveryBasic 更新配送点基础资料。
|
||||
@@ -52,10 +53,10 @@ func UpdateDeliveryBasic(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
gasBasicID, err := resolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
||||
gasBasicID, err := common.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"})
|
||||
common.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"})
|
||||
}
|
||||
81
backend/api/internal/logic/platform/ec_category.go
Normal file
81
backend/api/internal/logic/platform/ec_category.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
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 := common.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 {
|
||||
common.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])
|
||||
}
|
||||
29
backend/api/internal/logic/platform/ec_order.go
Normal file
29
backend/api/internal/logic/platform/ec_order.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
var items []models.EcOrderItem
|
||||
if err := impl.DBService.Where("ec_order_id = ?", order.ID).Order("id asc").Find(&items).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := common.PublicResourceResponse(gin.H{"order": order, "items": items})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, response)
|
||||
}
|
||||
68
backend/api/internal/logic/platform/fin_settlement.go
Normal file
68
backend/api/internal/logic/platform/fin_settlement.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
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) { common.ListResource(ctx, &models.FinSettlement{}) },
|
||||
func(ctx *gin.Context) {
|
||||
if err := rewriteSettlementSubject(ctx); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
_, create, _, _ := common.ResourceHandlers(&models.FinSettlement{}, fields, fields)
|
||||
create(ctx)
|
||||
},
|
||||
func(ctx *gin.Context) { common.GetResource(ctx, &models.FinSettlement{}) },
|
||||
func(ctx *gin.Context) {
|
||||
if err := rewriteSettlementSubject(ctx); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
_, _, _, update := common.ResourceHandlers(&models.FinSettlement{}, fields, fields)
|
||||
update(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
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 := common.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
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -30,8 +31,8 @@ func passwordHash(password string) (string, error) {
|
||||
return string(hash), err
|
||||
}
|
||||
|
||||
func ListGasAccount(ctx *gin.Context) { listPage[models.GasAccount](ctx) }
|
||||
func GetGasAccount(ctx *gin.Context) { getByIdentity[models.GasAccount](ctx) }
|
||||
func ListGasAccount(ctx *gin.Context) { common.ListPage[models.GasAccount](ctx) }
|
||||
func GetGasAccount(ctx *gin.Context) { common.GetByIdentity[models.GasAccount](ctx) }
|
||||
|
||||
func CreateGasAccount(ctx *gin.Context) {
|
||||
var request accountRequest
|
||||
@@ -39,7 +40,7 @@ func CreateGasAccount(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
gasBasicID, err := resolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, true)
|
||||
gasBasicID, err := common.ResolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
@@ -49,12 +50,12 @@ func CreateGasAccount(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
account := models.GasAccount{Entity: newEntity("enabled"), GasBasicID: gasBasicID, Username: request.Username, DisplayName: request.DisplayName, PasswordHash: hash, RoleCode: request.RoleCode}
|
||||
account := models.GasAccount{Entity: common.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
|
||||
}
|
||||
respondCreatedResource(ctx, account)
|
||||
common.RespondCreatedResource(ctx, account)
|
||||
}
|
||||
|
||||
func UpdateGasAccount(ctx *gin.Context) {
|
||||
@@ -63,16 +64,16 @@ func UpdateGasAccount(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
gasBasicID, err := resolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, true)
|
||||
gasBasicID, err := common.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"})
|
||||
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 ListDeliveryAccount(ctx *gin.Context) { listPage[models.DeliveryAccount](ctx) }
|
||||
func GetDeliveryAccount(ctx *gin.Context) { getByIdentity[models.DeliveryAccount](ctx) }
|
||||
func ListDeliveryAccount(ctx *gin.Context) { common.ListPage[models.DeliveryAccount](ctx) }
|
||||
func GetDeliveryAccount(ctx *gin.Context) { common.GetByIdentity[models.DeliveryAccount](ctx) }
|
||||
|
||||
func CreateDeliveryAccount(ctx *gin.Context) {
|
||||
var request accountRequest
|
||||
@@ -80,7 +81,7 @@ func CreateDeliveryAccount(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
deliveryBasicID, err := resolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, true)
|
||||
deliveryBasicID, err := common.ResolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
@@ -90,12 +91,12 @@ func CreateDeliveryAccount(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
account := models.DeliveryAccount{Entity: newEntity("enabled"), DeliveryBasicID: deliveryBasicID, Username: request.Username, DisplayName: request.DisplayName, PasswordHash: hash, RoleCode: request.RoleCode}
|
||||
account := models.DeliveryAccount{Entity: common.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
|
||||
}
|
||||
respondCreatedResource(ctx, account)
|
||||
common.RespondCreatedResource(ctx, account)
|
||||
}
|
||||
|
||||
func UpdateDeliveryAccount(ctx *gin.Context) {
|
||||
@@ -104,10 +105,10 @@ func UpdateDeliveryAccount(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
deliveryBasicID, err := resolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, true)
|
||||
deliveryBasicID, err := common.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"})
|
||||
common.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"})
|
||||
}
|
||||
@@ -4,15 +4,16 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ListGasBasic 查询可燃气体站分页列表。
|
||||
func ListGasBasic(ctx *gin.Context) { listPage[models.GasBasic](ctx) }
|
||||
func ListGasBasic(ctx *gin.Context) { common.ListPage[models.GasBasic](ctx) }
|
||||
|
||||
// GetGasBasic 查询一个可燃气体站。
|
||||
func GetGasBasic(ctx *gin.Context) { getByIdentity[models.GasBasic](ctx) }
|
||||
func GetGasBasic(ctx *gin.Context) { common.GetByIdentity[models.GasBasic](ctx) }
|
||||
|
||||
// CreateGasBasic 创建可燃气体站档案。
|
||||
func CreateGasBasic(ctx *gin.Context) {
|
||||
@@ -21,12 +22,12 @@ func CreateGasBasic(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
request.Entity = newEntity("draft")
|
||||
request.Entity = common.NewEntity("draft")
|
||||
if err := impl.DBService.Create(&request).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
respondCreatedResource(ctx, request)
|
||||
common.RespondCreatedResource(ctx, request)
|
||||
}
|
||||
|
||||
// UpdateGasBasic 更新可燃气体站基础资料。
|
||||
@@ -43,5 +44,5 @@ func UpdateGasBasic(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.GasBasic{}, gin.H{"name": request.Name, "credit_code": request.CreditCode, "principal": request.Principal, "address": request.Address, "longitude": request.Longitude, "latitude": request.Latitude}, []string{"name", "credit_code", "principal", "address", "longitude", "latitude"})
|
||||
common.UpdateAllowedByIdentity(ctx, &models.GasBasic{}, gin.H{"name": request.Name, "credit_code": request.CreditCode, "principal": request.Principal, "address": request.Address, "longitude": request.Longitude, "latitude": request.Latitude}, []string{"name", "credit_code", "principal", "address", "longitude", "latitude"})
|
||||
}
|
||||
615
backend/api/internal/logic/platform/gasorder_workflow.go
Normal file
615
backend/api/internal/logic/platform/gasorder_workflow.go
Normal file
@@ -0,0 +1,615 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
var gasorderCreatorModels = map[string]any{
|
||||
"user": &models.UserAccount{}, "staff": &models.StaffAccount{},
|
||||
"delivery": &models.DeliveryBasic{}, "gas": &models.GasBasic{},
|
||||
}
|
||||
|
||||
func ListGasorderContract(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderContract{}) }
|
||||
func GetGasorderContract(ctx *gin.Context) { getGasorderContract(ctx) }
|
||||
func ListGasorderContractProduct(ctx *gin.Context) {
|
||||
common.ListResource(ctx, &models.GasorderContractProduct{})
|
||||
}
|
||||
func GetGasorderContractProduct(ctx *gin.Context) {
|
||||
common.GetResource(ctx, &models.GasorderContractProduct{})
|
||||
}
|
||||
func ListGasorderContractRevision(ctx *gin.Context) {
|
||||
common.ListResource(ctx, &models.GasorderContractRevision{})
|
||||
}
|
||||
func GetGasorderContractRevision(ctx *gin.Context) {
|
||||
common.GetResource(ctx, &models.GasorderContractRevision{})
|
||||
}
|
||||
func ListGasorderBasic(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderBasic{}) }
|
||||
func GetGasorderBasic(ctx *gin.Context) { getGasorderBasic(ctx) }
|
||||
func ListGasorderItem(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderItem{}) }
|
||||
func GetGasorderItem(ctx *gin.Context) { common.GetResource(ctx, &models.GasorderItem{}) }
|
||||
func ListGasorderAssign(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderAssign{}) }
|
||||
func GetGasorderAssign(ctx *gin.Context) { common.GetResource(ctx, &models.GasorderAssign{}) }
|
||||
func ListGasorderStatus(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderStatus{}) }
|
||||
func GetGasorderStatus(ctx *gin.Context) { common.GetResource(ctx, &models.GasorderStatus{}) }
|
||||
func ListGasorderTrack(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderTrack{}) }
|
||||
func GetGasorderTrack(ctx *gin.Context) { getGasorderTrack(ctx) }
|
||||
func ListGasorderTrackPoint(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderTrackPoint{}) }
|
||||
func GetGasorderTrackPoint(ctx *gin.Context) { common.GetResource(ctx, &models.GasorderTrackPoint{}) }
|
||||
func ListGasorderConfirm(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderConfirm{}) }
|
||||
func GetGasorderConfirm(ctx *gin.Context) { common.GetResource(ctx, &models.GasorderConfirm{}) }
|
||||
func ListGasorderPayment(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderPayment{}) }
|
||||
func GetGasorderPayment(ctx *gin.Context) { common.GetResource(ctx, &models.GasorderPayment{}) }
|
||||
|
||||
func getGasorderContract(ctx *gin.Context) {
|
||||
var contract models.GasorderContract
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&contract).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
var products []models.GasorderContractProduct
|
||||
var revisions []models.GasorderContractRevision
|
||||
if err := impl.DBService.Where("gasorder_contract_id = ?", contract.ID).Order("bound_at asc, id asc").Find(&products).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := impl.DBService.Where("gasorder_contract_id = ?", contract.ID).Order("occurred_at asc, id asc").Find(&revisions).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := common.PublicResourceResponse(gin.H{"contract": contract, "products": products, "revisions": revisions})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, common.ProtectPreciseLocation(ctx, &models.GasorderContract{}, response))
|
||||
}
|
||||
|
||||
func getGasorderBasic(ctx *gin.Context) {
|
||||
var order models.GasorderBasic
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
var items []models.GasorderItem
|
||||
var assignments []models.GasorderAssign
|
||||
var statuses []models.GasorderStatus
|
||||
var tracks []models.GasorderTrack
|
||||
var confirmations []models.GasorderConfirm
|
||||
var payments []models.GasorderPayment
|
||||
loaders := []struct {
|
||||
order string
|
||||
value any
|
||||
}{
|
||||
{"id asc", &items}, {"assigned_at asc, id asc", &assignments},
|
||||
{"occurred_at asc, id asc", &statuses}, {"attempt_no asc, id asc", &tracks},
|
||||
{"confirmed_at asc, id asc", &confirmations}, {"attempt_no asc, id asc", &payments},
|
||||
}
|
||||
for _, loader := range loaders {
|
||||
if err := impl.DBService.Where("gasorder_basic_id = ?", order.ID).Order(loader.order).Find(loader.value).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
response, err := common.PublicResourceResponse(gin.H{
|
||||
"order": order, "items": items, "assignments": assignments, "statuses": statuses,
|
||||
"tracks": tracks, "confirmations": confirmations, "payments": payments,
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, common.ProtectPreciseLocation(ctx, &models.GasorderBasic{}, response))
|
||||
}
|
||||
|
||||
func CreateGasorderContract(ctx *gin.Context) {
|
||||
var request struct {
|
||||
ContractNo string `json:"contract_no" binding:"required,max=64"`
|
||||
UserAccountIdentity string `json:"user_account_identity" binding:"required"`
|
||||
GasBasicIdentity string `json:"gas_basic_identity" binding:"required"`
|
||||
DeliveryIdentity string `json:"delivery_basic_identity"`
|
||||
Title string `json:"title" binding:"required,max=255"`
|
||||
Terms string `json:"terms"`
|
||||
FileURI string `json:"file_uri" binding:"max=512"`
|
||||
DefaultDeliveryFee int64 `json:"default_delivery_fee"`
|
||||
SignedAt time.Time `json:"signed_at" binding:"required"`
|
||||
EffectiveAt time.Time `json:"effective_at" binding:"required"`
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.DefaultDeliveryFee < 0 ||
|
||||
request.SignedAt.IsZero() || request.EffectiveAt.IsZero() ||
|
||||
(request.ExpiredAt != nil && !request.ExpiredAt.After(request.EffectiveAt)) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
userID, err := common.ResolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
gasID, err := common.ResolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
deliveryID, err := common.ResolveIdentityID(&models.DeliveryBasic{}, request.DeliveryIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
contract := models.GasorderContract{
|
||||
Entity: models.Entity{Identity: models.NewIdentity(), Status: "draft", Version: 1},
|
||||
ContractNo: request.ContractNo, UserAccountID: userID, GasBasicID: gasID, DeliveryBasicID: deliveryID,
|
||||
Title: request.Title, Terms: request.Terms, FileURI: request.FileURI, DefaultDeliveryFee: request.DefaultDeliveryFee,
|
||||
SignedAt: request.SignedAt, EffectiveAt: request.EffectiveAt, ExpiredAt: request.ExpiredAt,
|
||||
}
|
||||
if err := impl.DBService.Create(&contract).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, contract)
|
||||
}
|
||||
|
||||
func UpdateGasorderContract(ctx *gin.Context) {
|
||||
var request struct {
|
||||
DeliveryIdentity string `json:"delivery_basic_identity"`
|
||||
Title string `json:"title" binding:"required,max=255"`
|
||||
Terms string `json:"terms"`
|
||||
FileURI string `json:"file_uri" binding:"max=512"`
|
||||
DefaultDeliveryFee int64 `json:"default_delivery_fee"`
|
||||
SignedAt time.Time `json:"signed_at" binding:"required"`
|
||||
EffectiveAt time.Time `json:"effective_at" binding:"required"`
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.DefaultDeliveryFee < 0 ||
|
||||
(request.ExpiredAt != nil && !request.ExpiredAt.After(request.EffectiveAt)) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
deliveryID, err := common.ResolveIdentityID(&models.DeliveryBasic{}, request.DeliveryIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
result := impl.DBService.Model(&models.GasorderContract{}).
|
||||
Where("identity = ? AND status = ?", ctx.Param("identity"), "draft").
|
||||
Updates(map[string]any{"delivery_basic_id": deliveryID, "title": request.Title, "terms": request.Terms,
|
||||
"file_uri": request.FileURI, "default_delivery_fee": request.DefaultDeliveryFee,
|
||||
"signed_at": request.SignedAt, "effective_at": request.EffectiveAt, "expired_at": request.ExpiredAt})
|
||||
if result.Error != nil || result.RowsAffected != 1 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func ActivateGasorderContract(ctx *gin.Context) {
|
||||
changeGasorderContract(ctx, "activate", "active")
|
||||
}
|
||||
|
||||
func TerminateGasorderContract(ctx *gin.Context) {
|
||||
changeGasorderContract(ctx, "terminate", "terminated")
|
||||
}
|
||||
|
||||
func RenewGasorderContract(ctx *gin.Context) {
|
||||
var request struct {
|
||||
EffectiveAt time.Time `json:"effective_at" binding:"required"`
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil ||
|
||||
(request.ExpiredAt != nil && !request.ExpiredAt.After(request.EffectiveAt)) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
operatorIdentity, operatorName := walletOperator(ctx)
|
||||
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
var contract models.GasorderContract
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&contract).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if contract.Status != "active" && contract.Status != "expired" && contract.Status != "terminated" {
|
||||
return errors.New("contract cannot be renewed")
|
||||
}
|
||||
if err := tx.Model(&contract).Updates(map[string]any{"status": "active", "effective_at": request.EffectiveAt, "expired_at": request.ExpiredAt}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
contract.Status, contract.EffectiveAt, contract.ExpiredAt = "active", request.EffectiveAt, request.ExpiredAt
|
||||
return tx.Create(contractRevision(contract, "renew", request.Reason, operatorIdentity, operatorName)).Error
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func changeGasorderContract(ctx *gin.Context, action, target string) {
|
||||
var request struct {
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
operatorIdentity, operatorName := walletOperator(ctx)
|
||||
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
var contract models.GasorderContract
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&contract).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if action == "activate" && contract.Status != "draft" && contract.Status != "terminated" {
|
||||
return errors.New("contract cannot be activated")
|
||||
}
|
||||
if action == "terminate" && contract.Status != "active" {
|
||||
return errors.New("contract cannot be terminated")
|
||||
}
|
||||
if target == "active" {
|
||||
now := time.Now()
|
||||
if contract.EffectiveAt.After(now) || (contract.ExpiredAt != nil && !contract.ExpiredAt.After(now)) {
|
||||
return errors.New("contract outside effective period")
|
||||
}
|
||||
var productCount int64
|
||||
if err := tx.Model(&models.GasorderContractProduct{}).
|
||||
Where("gasorder_contract_id = ? AND unbound_at IS NULL", contract.ID).Count(&productCount).Error; err != nil || productCount == 0 {
|
||||
return errors.New("contract has no active product")
|
||||
}
|
||||
}
|
||||
if err := tx.Model(&contract).Update("status", target).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
contract.Status = target
|
||||
return tx.Create(contractRevision(contract, action, request.Reason, operatorIdentity, operatorName)).Error
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true, "status": target})
|
||||
}
|
||||
|
||||
func contractRevision(contract models.GasorderContract, action, reason, operatorIdentity, operatorName string) *models.GasorderContractRevision {
|
||||
return &models.GasorderContractRevision{
|
||||
Entity: models.Entity{Identity: models.NewIdentity(), Status: "recorded", Version: 1},
|
||||
GasorderContractID: contract.ID, Action: action, ContractStatus: contract.Status,
|
||||
EffectiveAt: contract.EffectiveAt, ExpiredAt: contract.ExpiredAt,
|
||||
OperatorIdentity: operatorIdentity, OperatorName: operatorName, OccurredAt: time.Now(), Reason: reason,
|
||||
}
|
||||
}
|
||||
|
||||
func BindGasorderContractProduct(ctx *gin.Context) {
|
||||
var request struct {
|
||||
ContractIdentity string `json:"gasorder_contract_identity" binding:"required"`
|
||||
ProductIdentity string `json:"product_info_identity" binding:"required"`
|
||||
UnitPrice int64 `json:"unit_price"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.UnitPrice < 0 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var contract models.GasorderContract
|
||||
if err := impl.DBService.Where("identity = ?", request.ContractIdentity).First(&contract).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
if contract.Status != "draft" && contract.Status != "active" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var product models.ProductInfo
|
||||
if err := impl.DBService.Where("identity = ?", request.ProductIdentity).First(&product).Error; err != nil ||
|
||||
!product.IsEnabled || product.Status == "scrapped" || product.UserAccountID != contract.UserAccountID {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var productType models.ProductType
|
||||
if err := impl.DBService.Where("id = ?", product.ProductTypeID).First(&productType).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
binding := models.GasorderContractProduct{
|
||||
Entity: models.Entity{Identity: models.NewIdentity(), Status: "bound", Version: 1},
|
||||
GasorderContractID: contract.ID, ProductInfoID: product.ID, ProductCode: product.Code,
|
||||
ProductTypeName: productType.Name, ProductParams: product.Params, UnitPrice: request.UnitPrice, BoundAt: time.Now(),
|
||||
}
|
||||
if err := impl.DBService.Create(&binding).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, binding)
|
||||
}
|
||||
|
||||
func UnbindGasorderContractProduct(ctx *gin.Context) {
|
||||
now := time.Now()
|
||||
result := impl.DBService.Model(&models.GasorderContractProduct{}).
|
||||
Where("identity = ? AND unbound_at IS NULL", ctx.Param("identity")).
|
||||
Updates(map[string]any{"status": "unbound", "unbound_at": &now})
|
||||
if result.Error != nil || result.RowsAffected != 1 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func CreateGasorderBasic(ctx *gin.Context) {
|
||||
var request struct {
|
||||
RequestNo string `json:"request_no" binding:"required,max=128"`
|
||||
ContractIdentity string `json:"gasorder_contract_identity" binding:"required"`
|
||||
CreatorType string `json:"creator_type" binding:"required"`
|
||||
CreatorIdentity string `json:"creator_identity" binding:"required"`
|
||||
UserAddressIdentity string `json:"user_address_identity" binding:"required"`
|
||||
ContractProductIdentities []string `json:"gasorder_contract_product_identities" binding:"required,min=1"`
|
||||
ContactName string `json:"contact_name" binding:"required,max=64"`
|
||||
ContactPhone string `json:"contact_phone" binding:"required,max=32"`
|
||||
DiscountAmount int64 `json:"discount_amount"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.DiscountAmount < 0 || gasorderCreatorModels[request.CreatorType] == nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
creatorID, err := common.ResolveIdentityID(gasorderCreatorModels[request.CreatorType], request.CreatorIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
operatorIdentity, operatorName := walletOperator(ctx)
|
||||
var order models.GasorderBasic
|
||||
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("request_no = ?", request.RequestNo).First(&order).Error; err == nil {
|
||||
return nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
var contract models.GasorderContract
|
||||
if err := tx.Where("identity = ?", request.ContractIdentity).First(&contract).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
if contract.Status != "active" || contract.EffectiveAt.After(now) || (contract.ExpiredAt != nil && !contract.ExpiredAt.After(now)) {
|
||||
return errors.New("contract is not active")
|
||||
}
|
||||
if request.CreatorType == "user" && creatorID != contract.UserAccountID {
|
||||
return errors.New("user cannot create an order for another user")
|
||||
}
|
||||
if request.CreatorType == "gas" && creatorID != contract.GasBasicID {
|
||||
return errors.New("gas station does not own the contract")
|
||||
}
|
||||
var address models.UserAddress
|
||||
if err := tx.Where("identity = ? AND user_account_id = ?", request.UserAddressIdentity, contract.UserAccountID).First(&address).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var bindings []models.GasorderContractProduct
|
||||
if err := tx.Where("identity IN ? AND gasorder_contract_id = ? AND unbound_at IS NULL",
|
||||
request.ContractProductIdentities, contract.ID).Find(&bindings).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(bindings) != len(request.ContractProductIdentities) {
|
||||
return errors.New("invalid contract product")
|
||||
}
|
||||
var productAmount int64
|
||||
for _, binding := range bindings {
|
||||
var product models.ProductInfo
|
||||
if err := tx.Where("id = ?", binding.ProductInfoID).First(&product).Error; err != nil ||
|
||||
!product.IsEnabled || product.Status == "scrapped" || product.UserAccountID != contract.UserAccountID {
|
||||
return errors.New("contract product is no longer eligible")
|
||||
}
|
||||
var activeOrderCount int64
|
||||
if err := tx.Table("gasorder_item").
|
||||
Joins("JOIN gasorder_basic ON gasorder_basic.id = gasorder_item.gasorder_basic_id").
|
||||
Where("gasorder_item.product_info_id = ? AND gasorder_basic.status NOT IN ?", binding.ProductInfoID, []string{"completed", "cancelled"}).
|
||||
Count(&activeOrderCount).Error; err != nil || activeOrderCount != 0 {
|
||||
return errors.New("contract product already has an active order")
|
||||
}
|
||||
productAmount += binding.UnitPrice
|
||||
}
|
||||
payable := productAmount + contract.DefaultDeliveryFee - request.DiscountAmount
|
||||
if payable <= 0 {
|
||||
return errors.New("invalid payable amount")
|
||||
}
|
||||
order = models.GasorderBasic{
|
||||
Entity: models.Entity{Identity: models.NewIdentity(), Status: "created", Version: 1},
|
||||
OrderNo: models.NewIdentity(), RequestNo: request.RequestNo, GasorderContractID: contract.ID,
|
||||
UserAccountID: contract.UserAccountID, CreatorType: request.CreatorType, CreatorID: creatorID,
|
||||
CreatorIdentity: request.CreatorIdentity, GasBasicID: contract.GasBasicID, DeliveryBasicID: contract.DeliveryBasicID,
|
||||
Address: address.Address, Longitude: address.Longitude, Latitude: address.Latitude,
|
||||
ContactName: request.ContactName, ContactPhone: request.ContactPhone,
|
||||
ProductAmount: productAmount, DeliveryFee: contract.DefaultDeliveryFee,
|
||||
DiscountAmount: request.DiscountAmount, PayableAmount: payable,
|
||||
OperatorIdentity: operatorIdentity, OperatorName: operatorName, Remark: request.Remark,
|
||||
}
|
||||
if err := tx.Create(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, binding := range bindings {
|
||||
item := models.GasorderItem{
|
||||
Entity: models.Entity{Identity: models.NewIdentity(), Status: "ordered", Version: 1},
|
||||
GasorderBasicID: order.ID, GasorderContractProductID: binding.ID, ProductInfoID: binding.ProductInfoID,
|
||||
ProductCode: binding.ProductCode, ProductTypeName: binding.ProductTypeName,
|
||||
ProductParams: binding.ProductParams, UnitPrice: binding.UnitPrice,
|
||||
}
|
||||
if err := tx.Create(&item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Create(gasorderStatusRecord(order.ID, "", "created", "order created", operatorIdentity, operatorName)).Error
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, order)
|
||||
}
|
||||
|
||||
func AssignGasorderBasic(ctx *gin.Context) {
|
||||
var request struct {
|
||||
DeliveryIdentity string `json:"delivery_basic_identity" binding:"required"`
|
||||
StaffIdentity string `json:"staff_account_identity" binding:"required"`
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
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 != "enabled" {
|
||||
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 != "enabled" || staff.WorkStatus == "off_duty" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
operatorIdentity, operatorName := walletOperator(ctx)
|
||||
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
var order models.GasorderBasic
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.Status != "created" && order.Status != "assigned" {
|
||||
return errors.New("order cannot be assigned")
|
||||
}
|
||||
previous := order.Status
|
||||
if err := tx.Model(&order).Updates(map[string]any{
|
||||
"delivery_basic_id": delivery.ID, "staff_account_id": staff.ID, "status": "assigned",
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
assignment := models.GasorderAssign{
|
||||
Entity: models.Entity{Identity: models.NewIdentity(), Status: "recorded", Version: 1},
|
||||
GasorderBasicID: order.ID, GasBasicID: order.GasBasicID, DeliveryBasicID: delivery.ID,
|
||||
StaffAccountID: staff.ID, AssignerIdentity: operatorIdentity, AssignerName: operatorName,
|
||||
AssignedAt: time.Now(), Reason: request.Reason,
|
||||
}
|
||||
if err := tx.Create(&assignment).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if previous != "assigned" {
|
||||
return tx.Create(gasorderStatusRecord(order.ID, previous, "assigned", request.Reason, operatorIdentity, operatorName)).Error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true, "status": "assigned"})
|
||||
}
|
||||
|
||||
func GasorderStartFilling(ctx *gin.Context) {
|
||||
transitionGasorder(ctx, "filling", map[string]bool{"assigned": true})
|
||||
}
|
||||
func GasorderReady(ctx *gin.Context) {
|
||||
transitionGasorder(ctx, "ready", map[string]bool{"filling": true})
|
||||
}
|
||||
func GasorderCancel(ctx *gin.Context) {
|
||||
transitionGasorder(ctx, "cancelled", map[string]bool{"created": true, "assigned": true})
|
||||
}
|
||||
|
||||
func GasorderException(ctx *gin.Context) {
|
||||
transitionGasorder(ctx, "exception", map[string]bool{"filling": true, "ready": true, "delivering": true, "awaiting_confirmation": true})
|
||||
}
|
||||
|
||||
func GasorderRecover(ctx *gin.Context) {
|
||||
var request struct {
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
operatorIdentity, operatorName := walletOperator(ctx)
|
||||
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
var order models.GasorderBasic
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.Status != "exception" || order.PreviousStatus == "" {
|
||||
return errors.New("order cannot recover")
|
||||
}
|
||||
target := order.PreviousStatus
|
||||
if err := tx.Model(&order).Updates(map[string]any{"status": target, "previous_status": ""}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(gasorderStatusRecord(order.ID, "exception", target, request.Reason, operatorIdentity, operatorName)).Error
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func transitionGasorder(ctx *gin.Context, target string, allowed map[string]bool) {
|
||||
var request struct {
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
operatorIdentity, operatorName := walletOperator(ctx)
|
||||
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
var order models.GasorderBasic
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if !allowed[order.Status] || (target == "filling" && order.DeliveryBasicID == 0) {
|
||||
return errors.New("invalid order transition")
|
||||
}
|
||||
updates := map[string]any{"status": target}
|
||||
if target == "exception" {
|
||||
updates["previous_status"] = order.Status
|
||||
}
|
||||
if err := tx.Model(&order).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(gasorderStatusRecord(order.ID, order.Status, target, request.Reason, operatorIdentity, operatorName)).Error
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true, "status": target})
|
||||
}
|
||||
|
||||
func gasorderStatusRecord(orderID uint64, from, to, reason, operatorIdentity, operatorName string) *models.GasorderStatus {
|
||||
return &models.GasorderStatus{
|
||||
Entity: models.Entity{Identity: models.NewIdentity(), Status: "recorded", Version: 1},
|
||||
GasorderBasicID: orderID, FromStatus: from, ToStatus: to,
|
||||
OperatorIdentity: operatorIdentity, OperatorName: operatorName, OccurredAt: time.Now(), Reason: reason,
|
||||
}
|
||||
}
|
||||
|
||||
func getGasorderTrack(ctx *gin.Context) {
|
||||
var track models.GasorderTrack
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&track).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
var points []models.GasorderTrackPoint
|
||||
if err := impl.DBService.Where("gasorder_track_id = ?", track.ID).Order("occurred_at asc, id asc").Find(&points).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := common.PublicResourceResponse(gin.H{"track": track, "points": points})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, common.ProtectPreciseLocation(ctx, &models.GasorderTrackPoint{}, response))
|
||||
}
|
||||
|
||||
func trimGasorderReason(value string) string { return strings.TrimSpace(value) }
|
||||
@@ -0,0 +1,54 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
)
|
||||
|
||||
func TestGasorderCreatorTypesCoverEveryConfirmedOrigin(t *testing.T) {
|
||||
for _, creatorType := range []string{"user", "staff", "delivery", "gas"} {
|
||||
if gasorderCreatorModels[creatorType] == nil {
|
||||
t.Fatalf("creator type %q is not supported", creatorType)
|
||||
}
|
||||
}
|
||||
if gasorderCreatorModels["ec"] != nil {
|
||||
t.Fatal("commerce order creator leaked into gasorder")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGasorderStatusRecordIsImmutableSnapshot(t *testing.T) {
|
||||
record := gasorderStatusRecord(7, "assigned", "filling", "start filling", "operator-a", "Operator")
|
||||
if record.GasorderBasicID != 7 || record.FromStatus != "assigned" || record.ToStatus != "filling" {
|
||||
t.Fatalf("unexpected status record: %#v", record)
|
||||
}
|
||||
if record.Status != "recorded" || record.OccurredAt.IsZero() {
|
||||
t.Fatalf("status record lacks immutable metadata: %#v", record)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContractRevisionKeepsSingleContractHistory(t *testing.T) {
|
||||
contract := models.GasorderContract{
|
||||
Entity: models.Entity{ID: 9, Status: "active"},
|
||||
}
|
||||
revision := contractRevision(contract, "renew", "annual renewal", "operator-a", "Operator")
|
||||
if revision.GasorderContractID != 9 || revision.Action != "renew" || revision.ContractStatus != "active" {
|
||||
t.Fatalf("unexpected contract revision: %#v", revision)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGasorderSensitiveContactsAreMasked(t *testing.T) {
|
||||
value := map[string]any{
|
||||
"contact_name": "张三",
|
||||
"contact_phone": "13800138000",
|
||||
"recipient_name": "李四",
|
||||
"recipient_phone": "13900139000",
|
||||
}
|
||||
common.ProtectPublicFields(value, false, false, false)
|
||||
for _, raw := range []string{"contact_name", "contact_phone", "recipient_name", "recipient_phone"} {
|
||||
if _, exists := value[raw]; exists {
|
||||
t.Fatalf("raw field %s remains in response: %#v", raw, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,8 @@ func platformMenuAllowsPath(menus []models.PlatformMenu, requestPath string) boo
|
||||
return false
|
||||
}
|
||||
relative := strings.Trim(requestPath[index+len(marker):], "/")
|
||||
domain := strings.Split(relative, "/")[0]
|
||||
resource := strings.Split(relative, "/")[0]
|
||||
domain := platformRouteDomain(resource)
|
||||
for _, menu := range menus {
|
||||
if menu.MenuCode == domain {
|
||||
return true
|
||||
@@ -53,6 +54,26 @@ func platformMenuAllowsPath(menus []models.PlatformMenu, requestPath string) boo
|
||||
return false
|
||||
}
|
||||
|
||||
func platformRouteDomain(resource string) string {
|
||||
prefix := strings.Split(resource, "_")[0]
|
||||
switch prefix {
|
||||
case "product":
|
||||
return "device"
|
||||
case "gasorder":
|
||||
return "delivery"
|
||||
case "fin":
|
||||
return "finance"
|
||||
case "cms":
|
||||
return "content"
|
||||
case "cs":
|
||||
return "customer_service"
|
||||
case "platfrom", "platform":
|
||||
return "platform"
|
||||
default:
|
||||
return prefix
|
||||
}
|
||||
}
|
||||
|
||||
// RequirePlatformMenuAccess enforces role-menu authorization after JWT authentication.
|
||||
func RequirePlatformMenuAccess() gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"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"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -15,10 +16,10 @@ import (
|
||||
var errSystemPlatformRole = errors.New("system platform roles cannot be modified")
|
||||
|
||||
// ListPlatformRole 查询平台角色分页列表。
|
||||
func ListPlatformRole(ctx *gin.Context) { listPage[models.PlatformRole](ctx) }
|
||||
func ListPlatformRole(ctx *gin.Context) { common.ListPage[models.PlatformRole](ctx) }
|
||||
|
||||
// GetPlatformRole 查询一个平台角色。
|
||||
func GetPlatformRole(ctx *gin.Context) { getByIdentity[models.PlatformRole](ctx) }
|
||||
func GetPlatformRole(ctx *gin.Context) { common.GetByIdentity[models.PlatformRole](ctx) }
|
||||
|
||||
// CreatePlatformRole 创建非内置平台角色。
|
||||
func CreatePlatformRole(ctx *gin.Context) {
|
||||
@@ -30,7 +31,7 @@ func CreatePlatformRole(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
request.Entity = newEntity("enabled")
|
||||
request.Entity = common.NewEntity("enabled")
|
||||
request.IsSystem = false
|
||||
if request.DataScope == "" {
|
||||
request.DataScope = "global"
|
||||
@@ -39,7 +40,7 @@ func CreatePlatformRole(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
respondCreatedResource(ctx, request)
|
||||
common.RespondCreatedResource(ctx, request)
|
||||
}
|
||||
|
||||
// UpdatePlatformRole 更新非内置平台角色。
|
||||
@@ -57,14 +58,14 @@ func UpdatePlatformRole(ctx *gin.Context) {
|
||||
}
|
||||
var role models.PlatformRole
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
if role.IsSystem {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.PlatformRole{}, gin.H{"name": request.Name, "data_scope": request.DataScope}, []string{"name", "data_scope"})
|
||||
common.UpdateAllowedByIdentity(ctx, &models.PlatformRole{}, gin.H{"name": request.Name, "data_scope": request.DataScope}, []string{"name", "data_scope"})
|
||||
}
|
||||
|
||||
type platformMenuRequest struct {
|
||||
@@ -102,7 +103,7 @@ func platformMenuViews(list []models.PlatformMenu) []platformMenuView {
|
||||
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)
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
list := []models.PlatformMenu{menu}
|
||||
@@ -125,17 +126,17 @@ func CreatePlatformMenu(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
parentID, err := resolveIdentityID(&models.PlatformMenu{}, request.ParentIdentity, false)
|
||||
parentID, err := common.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}
|
||||
menu := models.PlatformMenu{Entity: common.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
|
||||
}
|
||||
respondCreatedResource(ctx, menu)
|
||||
common.RespondCreatedResource(ctx, menu)
|
||||
}
|
||||
|
||||
func UpdatePlatformMenu(ctx *gin.Context) {
|
||||
@@ -147,26 +148,26 @@ func UpdatePlatformMenu(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
parentID, err := resolveIdentityID(&models.PlatformMenu{}, request.ParentIdentity, false)
|
||||
parentID, err := common.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"})
|
||||
common.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"})
|
||||
}
|
||||
|
||||
func UpdatePlatformMenuStatus(ctx *gin.Context) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
UpdateRecordStatus(ctx, &models.PlatformMenu{})
|
||||
common.UpdateRecordStatus(ctx, &models.PlatformMenu{})
|
||||
}
|
||||
|
||||
func ArchivePlatformMenu(ctx *gin.Context) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
ArchiveRecord(ctx, &models.PlatformMenu{})
|
||||
common.ArchiveRecord(ctx, &models.PlatformMenu{})
|
||||
}
|
||||
|
||||
type platformRoleMenusRequest struct {
|
||||
@@ -232,7 +233,7 @@ func ListPlatformRoleMenuIdentities(ctx *gin.Context) {
|
||||
}
|
||||
var role models.PlatformRole
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
var identities []string
|
||||
@@ -261,14 +262,14 @@ func UpdatePlatformRoleStatus(ctx *gin.Context) {
|
||||
}
|
||||
var role models.PlatformRole
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
if role.IsSystem {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.PlatformRole{}, gin.H{"status": request.Status}, []string{"status"})
|
||||
common.UpdateAllowedByIdentity(ctx, &models.PlatformRole{}, gin.H{"status": request.Status}, []string{"status"})
|
||||
}
|
||||
|
||||
// ArchivePlatformRole 归档非内置平台角色,系统角色始终受保护。
|
||||
@@ -278,14 +279,14 @@ func ArchivePlatformRole(ctx *gin.Context) {
|
||||
}
|
||||
var role models.PlatformRole
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
if role.IsSystem {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.PlatformRole{}, archiveValues(), []string{"status"})
|
||||
common.UpdateAllowedByIdentity(ctx, &models.PlatformRole{}, archiveValues(), []string{"status"})
|
||||
}
|
||||
|
||||
// ListPlatformMenu 返回菜单树构建所需的有序菜单列表。
|
||||
@@ -305,10 +306,10 @@ func ListPlatformMenu(ctx *gin.Context) {
|
||||
|
||||
// ListPlatfromAccount 查询平台账号列表,手机号在展示层脱敏。
|
||||
func ListPlatfromAccount(ctx *gin.Context) {
|
||||
page, size := pageSize(ctx)
|
||||
page, size := common.PageSize(ctx)
|
||||
var list []models.PlatfromAccount
|
||||
var total int64
|
||||
query := applyKeywordFilter(ctx, impl.DBService.Model(&models.PlatfromAccount{}), &models.PlatfromAccount{})
|
||||
query := common.ApplyKeywordFilter(ctx, impl.DBService.Model(&models.PlatfromAccount{}), &models.PlatfromAccount{})
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -320,7 +321,7 @@ func ListPlatfromAccount(ctx *gin.Context) {
|
||||
views := make([]map[string]any, 0, len(list))
|
||||
for _, item := range list {
|
||||
view := platformAccountView(item)
|
||||
protectPreciseLocation(ctx, &models.PlatfromAccount{}, view)
|
||||
common.ProtectPreciseLocation(ctx, &models.PlatfromAccount{}, view)
|
||||
views = append(views, view)
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": views})
|
||||
@@ -346,11 +347,11 @@ func platformAccountView(account models.PlatfromAccount) map[string]any {
|
||||
func GetPlatfromAccount(ctx *gin.Context) {
|
||||
var account models.PlatfromAccount
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&account).Error; err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
view := platformAccountView(account)
|
||||
infra.Response.Success(ctx, protectPreciseLocation(ctx, &models.PlatfromAccount{}, view))
|
||||
infra.Response.Success(ctx, common.ProtectPreciseLocation(ctx, &models.PlatfromAccount{}, view))
|
||||
}
|
||||
|
||||
func CreatePlatfromAccount(ctx *gin.Context) {
|
||||
@@ -371,13 +372,13 @@ func CreatePlatfromAccount(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
account := models.PlatfromAccount{Entity: newEntity("enabled"), Username: request.Username, DisplayName: request.DisplayName, Avatar: request.Avatar, PasswordHash: hash, PlatformRoleCode: request.PlatformRoleCode, Phone: request.Phone}
|
||||
account := models.PlatfromAccount{Entity: common.NewEntity("enabled"), Username: request.Username, DisplayName: request.DisplayName, Avatar: request.Avatar, PasswordHash: hash, PlatformRoleCode: request.PlatformRoleCode, Phone: request.Phone}
|
||||
if err := impl.DBService.Create(&account).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
view := platformAccountView(account)
|
||||
infra.Response.Success(ctx, protectPreciseLocation(ctx, &models.PlatfromAccount{}, view))
|
||||
infra.Response.Success(ctx, common.ProtectPreciseLocation(ctx, &models.PlatfromAccount{}, view))
|
||||
}
|
||||
|
||||
func UpdatePlatfromAccount(ctx *gin.Context) {
|
||||
@@ -402,7 +403,7 @@ func UpdatePlatfromAccount(ctx *gin.Context) {
|
||||
}
|
||||
values["platform_role_code"] = *request.PlatformRoleCode
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.PlatfromAccount{}, values, []string{"display_name", "avatar", "platform_role_code", "phone"})
|
||||
common.UpdateAllowedByIdentity(ctx, &models.PlatfromAccount{}, values, []string{"display_name", "avatar", "platform_role_code", "phone"})
|
||||
}
|
||||
|
||||
func isAssignablePlatformRole(roleCode string) bool {
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"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"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -23,16 +24,16 @@ var productLifecycleStatuses = map[string]bool{
|
||||
"pending": true, "in_stock": true, "in_transit": true, "in_use": true, "repairing": true, "scrapped": true,
|
||||
}
|
||||
|
||||
func ProductInfoHandlers(relations ...ResourceRelation) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
|
||||
func ProductInfoHandlers(relations ...common.ResourceRelation) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
|
||||
fields := []string{"code", "name", "params", "produced_at", "is_enabled", "status", "action", "reason", "remark"}
|
||||
return func(ctx *gin.Context) { listResource(ctx, &models.ProductInfo{}) },
|
||||
return func(ctx *gin.Context) { common.ListResource(ctx, &models.ProductInfo{}) },
|
||||
func(ctx *gin.Context) { createProductInfo(ctx, fields, relations) },
|
||||
func(ctx *gin.Context) { getResource(ctx, &models.ProductInfo{}) },
|
||||
func(ctx *gin.Context) { common.GetResource(ctx, &models.ProductInfo{}) },
|
||||
func(ctx *gin.Context) { updateProductInfo(ctx, fields, relations) }
|
||||
}
|
||||
|
||||
func createProductInfo(ctx *gin.Context, fields []string, relations []ResourceRelation) {
|
||||
values, err := prepareResourceValues(ctx, &models.ProductInfo{}, fields, relations)
|
||||
func createProductInfo(ctx *gin.Context, fields []string, relations []common.ResourceRelation) {
|
||||
values, err := common.PrepareResourceValues(ctx, &models.ProductInfo{}, fields, relations)
|
||||
if err != nil || !validParamsText(values["params"]) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
@@ -40,7 +41,7 @@ func createProductInfo(ctx *gin.Context, fields []string, relations []ResourceRe
|
||||
delete(values, "action")
|
||||
delete(values, "reason")
|
||||
delete(values, "remark")
|
||||
data := models.ProductInfo{Entity: newEntity("pending"), Params: "{}", IsEnabled: false}
|
||||
data := models.ProductInfo{Entity: common.NewEntity("pending"), Params: "{}", IsEnabled: false}
|
||||
if err := decodeValues(values, &data); err != nil || data.Code == "" || data.Name == "" || data.ProducedAt.IsZero() {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
@@ -62,16 +63,16 @@ func createProductInfo(ctx *gin.Context, fields []string, relations []ResourceRe
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
respondCreatedResource(ctx, &data)
|
||||
common.RespondCreatedResource(ctx, &data)
|
||||
}
|
||||
|
||||
func updateProductInfo(ctx *gin.Context, fields []string, relations []ResourceRelation) {
|
||||
func updateProductInfo(ctx *gin.Context, fields []string, relations []common.ResourceRelation) {
|
||||
var input map[string]any
|
||||
if err := ctx.ShouldBindJSON(&input); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
values, err := resolveResourceRelations(input, fields, relations, false)
|
||||
values, err := common.ResolveResourceRelations(input, fields, relations, false)
|
||||
if err != nil || len(values) == 0 || !validParamsText(values["params"]) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
@@ -124,24 +125,24 @@ func updateProductInfo(ctx *gin.Context, fields []string, relations []ResourceRe
|
||||
return tx.Create(newProductOwner(current, action, reason, remark, operatorIdentity, operatorName, time.Now())).Error
|
||||
})
|
||||
if err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func ProductOwnerHandlers(relations ...ResourceRelation) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
|
||||
func ProductOwnerHandlers(relations ...common.ResourceRelation) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
|
||||
fields := []string{"action", "occurred_at", "reason", "remark"}
|
||||
return listProductOwners,
|
||||
func(ctx *gin.Context) { createProductOwner(ctx, fields, relations) },
|
||||
func(ctx *gin.Context) { getResource(ctx, &models.ProductOwner{}) }
|
||||
func(ctx *gin.Context) { common.GetResource(ctx, &models.ProductOwner{}) }
|
||||
}
|
||||
|
||||
func listProductOwners(ctx *gin.Context) {
|
||||
page, size := pageSize(ctx)
|
||||
page, size := common.PageSize(ctx)
|
||||
var list []models.ProductOwner
|
||||
var total int64
|
||||
query := applyKeywordFilter(ctx, impl.DBService.Model(&models.ProductOwner{}), &models.ProductOwner{})
|
||||
query := common.ApplyKeywordFilter(ctx, impl.DBService.Model(&models.ProductOwner{}), &models.ProductOwner{})
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -150,7 +151,7 @@ func listProductOwners(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := publicResourceResponse(list)
|
||||
response, err := common.PublicResourceResponse(list)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -166,24 +167,24 @@ func UpdateProductInfoStatus(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.ProductInfo{}, gin.H{"status": request.Status}, []string{"status"})
|
||||
common.UpdateAllowedByIdentity(ctx, &models.ProductInfo{}, gin.H{"status": request.Status}, []string{"status"})
|
||||
}
|
||||
|
||||
func ProductRepairHandlers(relations ...ResourceRelation) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
|
||||
func ProductRepairHandlers(relations ...common.ResourceRelation) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
|
||||
fields := []string{"repair_no", "repair_type", "started_at", "completed_at", "result", "target_status", "content", "operator", "remark"}
|
||||
return func(ctx *gin.Context) { listResource(ctx, &models.ProductRepair{}) },
|
||||
return func(ctx *gin.Context) { common.ListResource(ctx, &models.ProductRepair{}) },
|
||||
func(ctx *gin.Context) { createProductRepair(ctx, fields, relations) },
|
||||
func(ctx *gin.Context) { getResource(ctx, &models.ProductRepair{}) },
|
||||
func(ctx *gin.Context) { common.GetResource(ctx, &models.ProductRepair{}) },
|
||||
func(ctx *gin.Context) { updateProductRepair(ctx, fields, relations) }
|
||||
}
|
||||
|
||||
func createProductRepair(ctx *gin.Context, fields []string, relations []ResourceRelation) {
|
||||
values, err := prepareResourceValues(ctx, &models.ProductRepair{}, fields, relations)
|
||||
func createProductRepair(ctx *gin.Context, fields []string, relations []common.ResourceRelation) {
|
||||
values, err := common.PrepareResourceValues(ctx, &models.ProductRepair{}, fields, relations)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
data := models.ProductRepair{Entity: newEntity("active"), Result: "pending"}
|
||||
data := models.ProductRepair{Entity: common.NewEntity("active"), Result: "pending"}
|
||||
if err := decodeValues(values, &data); err != nil || data.Result != "pending" || !validRepair(data) || data.ProductInfoID == 0 || data.RepairNo == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
@@ -198,16 +199,16 @@ func createProductRepair(ctx *gin.Context, fields []string, relations []Resource
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
respondCreatedResource(ctx, &data)
|
||||
common.RespondCreatedResource(ctx, &data)
|
||||
}
|
||||
|
||||
func updateProductRepair(ctx *gin.Context, fields []string, relations []ResourceRelation) {
|
||||
func updateProductRepair(ctx *gin.Context, fields []string, relations []common.ResourceRelation) {
|
||||
var input map[string]any
|
||||
if err := ctx.ShouldBindJSON(&input); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
values, err := resolveResourceRelations(input, fields, relations, false)
|
||||
values, err := common.ResolveResourceRelations(input, fields, relations, false)
|
||||
if err != nil || len(values) == 0 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
@@ -238,7 +239,7 @@ func updateProductRepair(ctx *gin.Context, fields []string, relations []Resource
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
@@ -259,8 +260,8 @@ func validRepair(repair models.ProductRepair) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func createProductOwner(ctx *gin.Context, fields []string, relations []ResourceRelation) {
|
||||
values, err := prepareResourceValues(ctx, &models.ProductOwner{}, fields, relations)
|
||||
func createProductOwner(ctx *gin.Context, fields []string, relations []common.ResourceRelation) {
|
||||
values, err := common.PrepareResourceValues(ctx, &models.ProductOwner{}, fields, relations)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
@@ -270,7 +271,7 @@ func createProductOwner(ctx *gin.Context, fields []string, relations []ResourceR
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
data := models.ProductOwner{Entity: newEntity("recorded")}
|
||||
data := models.ProductOwner{Entity: common.NewEntity("recorded")}
|
||||
if err := decodeValues(values, &data); err != nil || data.ProductInfoID == 0 || data.OccurredAt.IsZero() {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
@@ -280,7 +281,7 @@ func createProductOwner(ctx *gin.Context, fields []string, relations []ResourceR
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
respondCreatedResource(ctx, &data)
|
||||
common.RespondCreatedResource(ctx, &data)
|
||||
}
|
||||
|
||||
func validParamsText(value any) bool {
|
||||
@@ -14,6 +14,7 @@ const (
|
||||
ReadOnly ResourceMode = "readonly"
|
||||
AppendOnly ResourceMode = "append_only"
|
||||
Editable ResourceMode = "editable"
|
||||
Managed ResourceMode = "managed"
|
||||
)
|
||||
|
||||
// ResourceContract is the expected cross-layer representation of one resource.
|
||||
@@ -44,6 +45,8 @@ func (d ResourceDefinition) Allows(method string) bool {
|
||||
return method == http.MethodGet || method == http.MethodPost
|
||||
case Editable:
|
||||
return method == http.MethodGet || method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch
|
||||
case Managed:
|
||||
return method == http.MethodGet || method == http.MethodPost || method == http.MethodPut
|
||||
case Writable:
|
||||
return method == http.MethodGet || method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch || method == http.MethodDelete
|
||||
default:
|
||||
@@ -79,7 +82,9 @@ func ExpectedResources() []ResourceContract {
|
||||
resourceContract("user", "user_account", Writable, "list"), resourceContract("user", "user_address", Writable, "list"), resourceContract("user", "user_service_relation", 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", AppendOnly, "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", Writable, "list"), resourceContract("ec", "ec_order", Writable, "list"), resourceContract("ec", "ec_order_item", Writable, "list"), resourceContract("ec", "ec_review", Writable, "list"),
|
||||
resourceContract("delivery", "delivery_task", Writable, "list"), resourceContract("delivery", "delivery_track", Writable, "list"), resourceContract("delivery", "delivery_track_point", 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"),
|
||||
resourceContract("finance", "fin_payment", Writable, "list"), resourceContract("finance", "fin_settlement", Writable, "list"), resourceContract("finance", "fin_reconciliation", Writable, "list"),
|
||||
resourceContract("content", "cms_content", Writable, "list"), resourceContract("customer_service", "cs_ticket", Writable, "list"),
|
||||
resourceContract("platform", "platfrom_account", Writable, "list"), resourceContract("platform", "platform_role", Writable, "list"), resourceContract("platform", "platform_menu", Writable, "tree"),
|
||||
@@ -92,21 +97,5 @@ func resourceContract(domain, name string, mode ResourceMode, pageKind string) R
|
||||
}
|
||||
|
||||
func resourcePath(domain, name string) string {
|
||||
if domain == "product" || domain == "wallet" {
|
||||
return "/" + name
|
||||
}
|
||||
switch name {
|
||||
case "staff_account":
|
||||
return "/staff/account"
|
||||
case "staff_credential":
|
||||
return "/staff/credential"
|
||||
case "user_account":
|
||||
return "/user/account"
|
||||
case "user_address":
|
||||
return "/user/address"
|
||||
case "user_service_relation":
|
||||
return "/user/service_relation"
|
||||
default:
|
||||
return "/" + domain + "/" + name
|
||||
}
|
||||
}
|
||||
@@ -1,894 +0,0 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/types"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/gin-gonic/gin"
|
||||
"google.golang.org/grpc/status"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestExpectedResources(t *testing.T) {
|
||||
assertContract(t, ExpectedResources(), "gas", "gas_basic", Writable, "list")
|
||||
assertContract(t, ExpectedResources(), "ec", "ec_order_item", Writable, "list")
|
||||
assertContract(t, ExpectedResources(), "wallet", "wallet_record", ReadOnly, "list")
|
||||
assertContract(t, ExpectedResources(), "delivery", "delivery_track_point", ReadOnly, "list")
|
||||
assertContract(t, ExpectedResources(), "product", "product_info", Editable, "list")
|
||||
assertContract(t, ExpectedResources(), "product", "product_owner", AppendOnly, "list")
|
||||
}
|
||||
|
||||
func TestContentUsesCMSResourceAndRemovedDomainsAreAbsent(t *testing.T) {
|
||||
resources := ExpectedResources()
|
||||
assertContract(t, resources, "content", "cms_content", Writable, "list")
|
||||
for _, removed := range []string{"ntf_template", "report", "report_item", "report_metric_snapshot", "audit_operation_log", "audit_export_log", "audit_approval"} {
|
||||
for _, resource := range resources {
|
||||
if resource.Name == removed {
|
||||
t.Fatalf("removed resource %s remains registered", removed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafetyResourcesAreAbsent(t *testing.T) {
|
||||
removed := map[string]struct{}{
|
||||
"safe_rule": {}, "safe_event": {}, "safe_inspection": {}, "safe_event_disposal": {},
|
||||
}
|
||||
for _, contract := range ExpectedResources() {
|
||||
if _, exists := removed[contract.Name]; exists || contract.Domain == "safety" {
|
||||
t.Fatalf("removed safety resource %s/%s remains registered", contract.Domain, contract.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceDefinitionAllowsOnlySupportedMethods(t *testing.T) {
|
||||
if (ResourceDefinition{Mode: ReadOnly}).Allows(http.MethodPost) {
|
||||
t.Fatal("readonly allows POST")
|
||||
}
|
||||
if !(ResourceDefinition{Mode: AppendOnly}).Allows(http.MethodPost) {
|
||||
t.Fatal("append-only rejects POST")
|
||||
}
|
||||
if (ResourceDefinition{Mode: AppendOnly}).Allows(http.MethodDelete) {
|
||||
t.Fatal("append-only allows DELETE")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveValuesOnlyArchives(t *testing.T) {
|
||||
got := archiveValues()
|
||||
if len(got) != 1 || got["status"] != "archived" {
|
||||
t.Fatalf("unexpected archive values: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterFieldsKeepsOnlyAllowedKeys(t *testing.T) {
|
||||
got := filterFields(map[string]any{"name": "n", "password_hash": "x"}, []string{"name"})
|
||||
if len(got) != 1 || got["name"] != "n" {
|
||||
t.Fatalf("unexpected filtered fields: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceDefinitionAllowsOnlyMethodsForEachMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mode ResourceMode
|
||||
method string
|
||||
want bool
|
||||
}{
|
||||
{"readonly GET", ReadOnly, http.MethodGet, true},
|
||||
{"readonly POST", ReadOnly, http.MethodPost, false},
|
||||
{"append-only GET", AppendOnly, http.MethodGet, true},
|
||||
{"append-only POST", AppendOnly, http.MethodPost, true},
|
||||
{"append-only PUT", AppendOnly, http.MethodPut, false},
|
||||
{"editable GET", Editable, http.MethodGet, true},
|
||||
{"editable POST", Editable, http.MethodPost, true},
|
||||
{"editable PUT", Editable, http.MethodPut, true},
|
||||
{"editable PATCH", Editable, http.MethodPatch, true},
|
||||
{"editable DELETE", Editable, http.MethodDelete, false},
|
||||
{"writable GET", Writable, http.MethodGet, true},
|
||||
{"writable POST", Writable, http.MethodPost, true},
|
||||
{"writable PUT", Writable, http.MethodPut, true},
|
||||
{"writable PATCH", Writable, http.MethodPatch, true},
|
||||
{"writable DELETE", Writable, http.MethodDelete, true},
|
||||
{"writable OPTIONS", Writable, http.MethodOptions, false},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := (ResourceDefinition{Mode: test.mode}).Allows(test.method); got != test.want {
|
||||
t.Fatalf("Allows(%s) = %t, want %t", test.method, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAllowedByIdentityFiltersUnknownFieldsAndUsesIdentity(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec(regexp.QuoteMeta(`UPDATE "platform_role" SET "status"=$1,"updated_at"=$2 WHERE identity = $3`)).
|
||||
WithArgs("disabled", sqlmock.AnyArg(), "role-a").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectCommit()
|
||||
|
||||
ctx, recorder := updateContext(http.MethodPatch, "/roles/role-a", "role-a", nil)
|
||||
updateAllowedByIdentity(ctx, &models.PlatformRole{}, gin.H{"status": "disabled", "is_system": true}, []string{"status"})
|
||||
assertResponseCode(t, recorder, 0)
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestUpdateAllowedByIdentityReturnsNotFoundForZeroRows(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec(regexp.QuoteMeta(`UPDATE "platform_role" SET "status"=$1,"updated_at"=$2 WHERE identity = $3`)).
|
||||
WithArgs("disabled", sqlmock.AnyArg(), "missing").
|
||||
WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
mock.ExpectCommit()
|
||||
ctx, recorder := updateContext(http.MethodPatch, "/roles/missing", "missing", nil)
|
||||
|
||||
updateAllowedByIdentity(ctx, &models.PlatformRole{}, gin.H{"status": "disabled"}, []string{"status"})
|
||||
|
||||
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrRecordNotFound)))
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestUpdateAllowedByIdentityReturnsUniformResponseForDatabaseError(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec(regexp.QuoteMeta(`UPDATE "platform_role" SET "status"=$1,"updated_at"=$2 WHERE identity = $3`)).
|
||||
WithArgs("disabled", sqlmock.AnyArg(), "role-a").
|
||||
WillReturnError(errors.New("database unavailable"))
|
||||
mock.ExpectRollback()
|
||||
|
||||
ctx, recorder := updateContext(http.MethodPatch, "/roles/role-a", "role-a", nil)
|
||||
updateAllowedByIdentity(ctx, &models.PlatformRole{}, gin.H{"status": "disabled"}, []string{"status"})
|
||||
|
||||
var reply responseBody
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &reply); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if reply.Code != 500 || reply.Message == "" || string(reply.Details) != `""` {
|
||||
t.Fatalf("database error did not use uniform response: %s", recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformRoleStatusAndArchiveReturnNotFoundWhenUpdateAffectsZeroRows(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
handler gin.HandlerFunc
|
||||
method string
|
||||
body string
|
||||
status string
|
||||
}{
|
||||
{"status", UpdatePlatformRoleStatus, http.MethodPatch, `{"status":"disabled"}`, "disabled"},
|
||||
{"archive", ArchivePlatformRole, http.MethodDelete, "", "archived"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platform_role" WHERE identity = $1 ORDER BY "platform_role"."id" LIMIT $2`)).
|
||||
WithArgs("role-a", 1).
|
||||
WillReturnRows(platformRoleRows("role-a"))
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec(regexp.QuoteMeta(`UPDATE "platform_role" SET "status"=$1,"updated_at"=$2 WHERE identity = $3`)).
|
||||
WithArgs(test.status, sqlmock.AnyArg(), "role-a").
|
||||
WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
mock.ExpectCommit()
|
||||
|
||||
ctx, recorder := updateContext(test.method, "/roles/role-a", "role-a", []byte(test.body))
|
||||
ctx.Set("Auth", &types.JwtClaims{Role: "root"})
|
||||
test.handler(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrRecordNotFound)))
|
||||
assertMockExpectations(t, mock)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEcOrderReturnsOrderItems(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "ec_order" WHERE identity = $1 ORDER BY "ec_order"."id" LIMIT $2`)).
|
||||
WithArgs("order-a", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "order_no", "user_account_id", "gas_station_id", "delivery_point_id", "total_amount"}).
|
||||
AddRow(uint64(1), "order-a", nil, nil, "enabled", 1, "O-1", uint64(2), uint64(3), uint64(4), int64(500)))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "ec_order_item" WHERE ec_order_id = $1 ORDER BY id asc`)).
|
||||
WithArgs(uint64(1)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "ec_order_id", "ec_product_id", "product_snapshot", "quantity", "sale_amount"}).
|
||||
AddRow(uint64(2), "item-a", nil, nil, "enabled", 1, uint64(1), uint64(5), `{}`, 2, int64(500)))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id","identity" FROM "delivery_basic" WHERE id IN ($1)`)).
|
||||
WithArgs(uint64(4)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity"}).AddRow(uint64(4), "delivery-a"))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id","identity" FROM "ec_order" WHERE id IN ($1)`)).
|
||||
WithArgs(uint64(1)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity"}).AddRow(uint64(1), "order-a"))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id","identity" FROM "ec_product" WHERE id IN ($1)`)).
|
||||
WithArgs(uint64(5)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity"}).AddRow(uint64(5), "product-a"))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id","identity" FROM "gas_basic" WHERE id IN ($1)`)).
|
||||
WithArgs(uint64(3)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity"}).AddRow(uint64(3), "gas-a"))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id","identity" FROM "user_account" WHERE id IN ($1)`)).
|
||||
WithArgs(uint64(2)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity"}).AddRow(uint64(2), "user-a"))
|
||||
|
||||
ctx, recorder := updateContext(http.MethodGet, "/ec/ec_order/order-a", "order-a", nil)
|
||||
GetEcOrder(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, 0)
|
||||
if !strings.Contains(recorder.Body.String(), `"items"`) || !strings.Contains(recorder.Body.String(), `"item-a"`) || strings.Contains(recorder.Body.String(), `_id"`) {
|
||||
t.Fatalf("order detail omitted its items: %s", recorder.Body.String())
|
||||
}
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestPrepareResourceValuesResolvesRequiredIdentityRelationsAndRejectsInvalidPayloads(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id" FROM "ec_order" WHERE identity = $1 ORDER BY "ec_order"."id" LIMIT $2`)).
|
||||
WithArgs("order-a", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uint64(1)))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id" FROM "ec_product" WHERE identity = $1 ORDER BY "ec_product"."id" LIMIT $2`)).
|
||||
WithArgs("product-a", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uint64(2)))
|
||||
|
||||
ctx, _ := updateContext(http.MethodPost, "/ec/ec_order_item", "", []byte(`{"ec_order_identity":"order-a","ec_product_identity":"product-a","quantity":2}`))
|
||||
values, err := prepareResourceValues(ctx, &models.EcOrderItem{}, []string{"quantity"}, []ResourceRelation{
|
||||
{Input: "ec_order_identity", Column: "ec_order_id", Model: &models.EcOrder{}, Required: true},
|
||||
{Input: "ec_product_identity", Column: "ec_product_id", Model: &models.EcProduct{}, Required: true},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if values["ec_order_id"] != uint64(1) || values["ec_product_id"] != uint64(2) || values["quantity"] != float64(2) {
|
||||
t.Fatalf("unexpected resolved values: %#v", values)
|
||||
}
|
||||
assertMockExpectations(t, mock)
|
||||
|
||||
for _, body := range []string{`{}`, `{"ec_order_id":1}`, `{"quantity":2}`} {
|
||||
ctx, _ := updateContext(http.MethodPost, "/ec/ec_order_item", "", []byte(body))
|
||||
if _, err := prepareResourceValues(ctx, &models.EcOrderItem{}, []string{"quantity"}, []ResourceRelation{{Input: "ec_order_identity", Column: "ec_order_id", Model: &models.EcOrder{}, Required: true}}); err == nil {
|
||||
t.Fatalf("payload %s was accepted", body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareResourceValuesAcceptsArbitraryTextFields(t *testing.T) {
|
||||
t.Run("order item", func(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id" FROM "ec_order" WHERE identity = $1 ORDER BY "ec_order"."id" LIMIT $2`)).
|
||||
WithArgs("order-a", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uint64(1)))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id" FROM "ec_product" WHERE identity = $1 ORDER BY "ec_product"."id" LIMIT $2`)).
|
||||
WithArgs("product-a", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uint64(2)))
|
||||
|
||||
ctx, _ := updateContext(http.MethodPost, "/ec/ec_order_item", "", []byte(`{"ec_order_identity":"order-a","ec_product_identity":"product-a","product_snapshot":"arbitrary product snapshot text","quantity":2,"sale_amount":500}`))
|
||||
values, err := prepareResourceValues(ctx, &models.EcOrderItem{}, []string{"product_snapshot", "quantity", "sale_amount"}, []ResourceRelation{
|
||||
{Input: "ec_order_identity", Column: "ec_order_id", Model: &models.EcOrder{}, Required: true},
|
||||
{Input: "ec_product_identity", Column: "ec_product_id", Model: &models.EcProduct{}, Required: true},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if values["product_snapshot"] != `arbitrary product snapshot text` {
|
||||
t.Fatalf("product snapshot was not normalized to a string: %#v", values)
|
||||
}
|
||||
assertMockExpectations(t, mock)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
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()
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id","identity" FROM "gas_basic" WHERE id IN ($1)`)).
|
||||
WithArgs(uint64(8)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity"}).AddRow(uint64(8), "gas-a"))
|
||||
|
||||
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)
|
||||
body := recorder.Body.String()
|
||||
if !strings.Contains(body, `"identity"`) || !strings.Contains(body, `"gas_basic_identity":"gas-a"`) {
|
||||
t.Fatalf("create response omitted public identities: %s", body)
|
||||
}
|
||||
if strings.Contains(body, `"gas_basic_id"`) || strings.Contains(body, `"password_hash"`) {
|
||||
t.Fatalf("create response exposed internal or sensitive fields: %s", body)
|
||||
}
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestListGasAccountAppliesKeywordToCountAndRows(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
keywordWhere := ` WHERE (LOWER("username") LIKE $1 OR LOWER("display_name") LIKE $2 OR LOWER("role_code") LIKE $3)`
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "gas_account"`+keywordWhere)).
|
||||
WithArgs("%operator%", "%operator%", "%operator%").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "gas_account"`+keywordWhere+` ORDER BY created_at desc LIMIT $4`)).
|
||||
WithArgs("%operator%", "%operator%", "%operator%", 20).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "gas_basic_id", "username", "display_name", "password_hash", "role_code"}))
|
||||
|
||||
ctx, recorder := updateContext(http.MethodGet, "/gas/gas_account?keyword=Operator", "", nil)
|
||||
ListGasAccount(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, 0)
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestKeywordColumnsUseSafeTextAllowlist(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
model any
|
||||
want []string
|
||||
}{
|
||||
{"gas basic excludes sensitive fields", &models.GasBasic{}, []string{"code", "name"}},
|
||||
{"user address has no searchable safe text", &models.UserAddress{}, []string{}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := keywordColumns(test.model)
|
||||
if !reflect.DeepEqual(got, test.want) {
|
||||
t.Fatalf("keywordColumns(%T) = %#v, want %#v", test.model, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
ctx.Set("Auth", &types.JwtClaims{Role: "root"})
|
||||
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 TestListPlatformMenuReturnsOnlyMenusAssignedToNonRootRole(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platform_role" WHERE role_code = $1 AND status = $2 ORDER BY "platform_role"."id" LIMIT $3`)).
|
||||
WithArgs("finance_operator", "enabled", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "role_code", "name", "data_scope", "is_system"}).
|
||||
AddRow(uint64(7), "finance-role", nil, nil, "enabled", 1, "finance_operator", "Finance", "global", false))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT platform_menu.* FROM "platform_menu" JOIN platform_role_menu_relation ON platform_role_menu_relation.platform_menu_id = platform_menu.id WHERE platform_role_menu_relation.platform_role_id = $1 AND platform_menu.status = $2 ORDER BY sort_no asc, id asc`)).
|
||||
WithArgs(uint64(7), "enabled").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "parent_id", "menu_code", "name", "icon", "path", "sort_no"}).
|
||||
AddRow(uint64(9), "finance-menu", nil, nil, "enabled", 1, uint64(0), "finance", "Finance", "", "/finance/payment", 1))
|
||||
|
||||
ctx, recorder := updateContext(http.MethodGet, "/platform/platform_menu", "", nil)
|
||||
ctx.Set("Auth", &types.JwtClaims{Role: "finance_operator"})
|
||||
ListPlatformMenu(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, 0)
|
||||
body := recorder.Body.String()
|
||||
if !strings.Contains(body, `"identity":"finance-menu"`) || strings.Contains(body, `"gas-menu"`) {
|
||||
t.Fatalf("non-root menu response was not constrained: %s", body)
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, forbidden := range []string{`"id"`, `"ec_order_id"`, `"ec_product_id"`, `"delivery_task_id"`} {
|
||||
if strings.Contains(string(encoded), forbidden) {
|
||||
t.Fatalf("response exposed internal relation key %s: %s", forbidden, encoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatedResourceResponseUsesSafeAllowlist(t *testing.T) {
|
||||
response := maskCreatedSensitiveFields(map[string]any{
|
||||
"identity": "address-a",
|
||||
"user_account_identity": "user-a",
|
||||
"status": "draft",
|
||||
"version": float64(1),
|
||||
"phone": "13800138000",
|
||||
"real_name": "张三",
|
||||
"credential_no": "CERT-123456",
|
||||
"address": "敏感详细地址",
|
||||
"principal": "负责人",
|
||||
"credit_code": "CREDIT-123",
|
||||
"longitude": "120.123456",
|
||||
"latitude": "30.456789",
|
||||
})
|
||||
encoded, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := string(encoded)
|
||||
if !strings.Contains(body, `"identity":"address-a"`) || !strings.Contains(body, `"user_account_identity":"user-a"`) || !strings.Contains(body, `"status":"draft"`) {
|
||||
t.Fatalf("created response omitted safe public fields: %s", body)
|
||||
}
|
||||
for _, forbidden := range []string{`"phone"`, `"phone_masked"`, `"real_name"`, `"credential_no"`, `"address"`, `"principal"`, `"credit_code"`, `"longitude"`, `"latitude"`, "敏感详细地址", "负责人", "CREDIT-123", "120.123456", "30.456789"} {
|
||||
if strings.Contains(body, forbidden) {
|
||||
t.Fatalf("created response exposed sensitive field %s: %s", forbidden, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultResourceResponseMasksPIIAndCoordinates(t *testing.T) {
|
||||
ctx, _ := updateContext(http.MethodGet, "/user/account/user-a", "user-a", nil)
|
||||
response := protectPreciseLocation(ctx, &models.UserAccount{}, map[string]any{
|
||||
"identity": "user-a",
|
||||
"name": "张三",
|
||||
"phone": "13800138000",
|
||||
"avatar": "https://private.example/avatar.png",
|
||||
"address": "敏感详细地址",
|
||||
"longitude": "120.123456",
|
||||
"latitude": "30.456789",
|
||||
})
|
||||
encoded, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := string(encoded)
|
||||
if !strings.Contains(body, `"identity":"user-a"`) || !strings.Contains(body, `"phone_masked":"138****8000"`) || !strings.Contains(body, `"name_masked"`) {
|
||||
t.Fatalf("default response omitted safe identity or masked PII: %s", body)
|
||||
}
|
||||
for _, forbidden := range []string{`"phone":`, `"name":`, `"avatar":`, `"address":`, `"longitude":"120.123456"`, `"latitude":"30.456789"`, "张三", "敏感详细地址", "private.example"} {
|
||||
if strings.Contains(body, forbidden) {
|
||||
t.Fatalf("default response leaked %s: %s", forbidden, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExplicitPreciseScopeRetainsCoordinatesButStillMasksPII(t *testing.T) {
|
||||
ctx, _ := updateContext(http.MethodGet, "/user/address/address-a", "address-a", nil)
|
||||
ctx.Set("Auth", &types.JwtClaims{Extend: map[string]string{"location_scope": "precise"}})
|
||||
response := protectPreciseLocation(ctx, &models.UserAddress{}, map[string]any{
|
||||
"identity": "address-a",
|
||||
"address": "敏感详细地址",
|
||||
"longitude": "120.123456",
|
||||
"latitude": "30.456789",
|
||||
})
|
||||
encoded, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := string(encoded)
|
||||
if !strings.Contains(body, `"longitude":"120.123456"`) || !strings.Contains(body, `"latitude":"30.456789"`) {
|
||||
t.Fatalf("authorized response omitted precise coordinates: %s", body)
|
||||
}
|
||||
if strings.Contains(body, `"address":`) || strings.Contains(body, "敏感详细地址") {
|
||||
t.Fatalf("precise location scope leaked address PII: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicResponseProjectionRemovesCredentialAndAttachmentSecrets(t *testing.T) {
|
||||
ctx, _ := updateContext(http.MethodGet, "/staff/credential/credential-a", "credential-a", nil)
|
||||
ctx.Set("Auth", &types.JwtClaims{Extend: map[string]string{"location_scope": "precise"}})
|
||||
response := protectPreciseLocation(ctx, &models.StaffCredential{}, map[string]any{
|
||||
"identity": "credential-a",
|
||||
"credential_type": "installer",
|
||||
"credential_no": "CERT-123456",
|
||||
"evidence_uri": "private://evidence",
|
||||
"file_uri": "private://file",
|
||||
"attachment_uri": "private://attachment",
|
||||
})
|
||||
encoded, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := string(encoded)
|
||||
if !strings.Contains(body, `"identity":"credential-a"`) || !strings.Contains(body, `"credential_type":"installer"`) {
|
||||
t.Fatalf("safe credential fields were removed: %s", body)
|
||||
}
|
||||
for _, forbidden := range []string{"credential_no", "evidence_uri", "file_uri", "attachment_uri", "CERT-123456", "private://"} {
|
||||
if strings.Contains(body, forbidden) {
|
||||
t.Fatalf("credential response leaked %s: %s", forbidden, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformAccountDetailMasksDisplayNameAndAvatarWithPreciseScope(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platfrom_account" WHERE identity = $1 ORDER BY "platfrom_account"."id" LIMIT $2`)).
|
||||
WithArgs("account-a", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "username", "display_name", "avatar", "password_hash", "platform_role_code", "phone"}).
|
||||
AddRow(uint64(1), "account-a", nil, nil, "enabled", 1, "operator", "张三", "private://avatar", "hash", "finance_operator", "13800138000"))
|
||||
|
||||
ctx, recorder := updateContext(http.MethodGet, "/platform/platfrom_account/account-a", "account-a", nil)
|
||||
ctx.Set("Auth", &types.JwtClaims{Extend: map[string]string{"location_scope": "precise"}})
|
||||
GetPlatfromAccount(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, 0)
|
||||
body := recorder.Body.String()
|
||||
if !strings.Contains(body, `"display_name_masked"`) || !strings.Contains(body, `"phone_masked":"138****8000"`) {
|
||||
t.Fatalf("platform account detail omitted masked PII: %s", body)
|
||||
}
|
||||
for _, forbidden := range []string{`"display_name":`, `"avatar":`, "张三", "private://avatar"} {
|
||||
if strings.Contains(body, forbidden) {
|
||||
t.Fatalf("platform account detail leaked %s: %s", forbidden, body)
|
||||
}
|
||||
}
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestPlatformAccountListMasksDisplayNameAndAvatar(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "platfrom_account"`)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platfrom_account" ORDER BY created_at desc LIMIT $1`)).
|
||||
WithArgs(20).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "username", "display_name", "avatar", "password_hash", "platform_role_code", "phone"}).
|
||||
AddRow(uint64(1), "account-a", nil, nil, "enabled", 1, "operator", "张三", "private://avatar", "hash", "finance_operator", "13800138000"))
|
||||
|
||||
ctx, recorder := updateContext(http.MethodGet, "/platform/platfrom_account", "", nil)
|
||||
ListPlatfromAccount(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, 0)
|
||||
body := recorder.Body.String()
|
||||
if !strings.Contains(body, `"display_name_masked"`) || strings.Contains(body, `"display_name":`) || strings.Contains(body, `"avatar":`) {
|
||||
t.Fatalf("platform account list did not apply the masked projection: %s", body)
|
||||
}
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestNonRootCannotManagePlatformRolesOrMenus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
handler gin.HandlerFunc
|
||||
method string
|
||||
target string
|
||||
identity string
|
||||
body string
|
||||
}{
|
||||
{"create role", CreatePlatformRole, http.MethodPost, "/platform/platform_role", "", `{"role_code":"auditor","name":"Auditor"}`},
|
||||
{"create menu", CreatePlatformMenu, http.MethodPost, "/platform/platform_menu", "", `{"menu_code":"audit","name":"Audit","path":"/audit"}`},
|
||||
{"update menu status", UpdatePlatformMenuStatus, http.MethodPatch, "/platform/platform_menu/menu-a/status", "menu-a", `{"status":"disabled"}`},
|
||||
{"archive menu", ArchivePlatformMenu, http.MethodDelete, "/platform/platform_menu/menu-a", "menu-a", ``},
|
||||
{"replace role menus", ReplacePlatformRoleMenus, http.MethodPut, "/platform/platform_role/role-a/menu", "role-a", `{"menu_identities":[]}`},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
ctx, recorder := updateContext(test.method, test.target, test.identity, []byte(test.body))
|
||||
ctx.Set("Auth", &types.JwtClaims{Role: "platform_operator"})
|
||||
|
||||
test.handler(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrPermissionDenied)))
|
||||
assertMockExpectations(t, mock)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformMenuAllowsOnlyAssignedDomain(t *testing.T) {
|
||||
menus := []models.PlatformMenu{
|
||||
{MenuCode: "finance", Path: "/finance"},
|
||||
{MenuCode: "fin_payment", Path: "/finance/fin-payment"},
|
||||
}
|
||||
if !platformMenuAllowsPath(menus, "/heqi/platform/v1/finance/fin_payment") {
|
||||
t.Fatal("assigned finance domain should be allowed")
|
||||
}
|
||||
if platformMenuAllowsPath(menus, "/heqi/platform/v1/user/account") {
|
||||
t.Fatal("unassigned user domain should be denied")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePlatformAccountRequiresAssignableNonRootRole(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
}{
|
||||
{"missing role", `{"username":"operator","password":"secure-password"}`},
|
||||
{"root role", `{"username":"operator","password":"secure-password","platform_role_code":"root"}`},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
ctx, recorder := updateContext(http.MethodPost, "/platform/platfrom_account", "", []byte(test.body))
|
||||
ctx.Set("Auth", &types.JwtClaims{Role: "root"})
|
||||
|
||||
CreatePlatfromAccount(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrInvalidArgument)))
|
||||
assertMockExpectations(t, mock)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonRootCannotAssignPlatformAccountRole(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
handler gin.HandlerFunc
|
||||
method string
|
||||
identity string
|
||||
body string
|
||||
}{
|
||||
{"create account", CreatePlatfromAccount, http.MethodPost, "", `{"username":"operator","password":"secure-password","platform_role_code":"auditor"}`},
|
||||
{"change account role", UpdatePlatfromAccount, http.MethodPut, "account-a", `{"platform_role_code":"auditor"}`},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
ctx, recorder := updateContext(test.method, "/platform/platfrom_account/"+test.identity, test.identity, []byte(test.body))
|
||||
ctx.Set("Auth", &types.JwtClaims{Role: "platform_operator"})
|
||||
|
||||
test.handler(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrPermissionDenied)))
|
||||
assertMockExpectations(t, mock)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListGasAccountProjectsGasBasicIdentityAndNeverReturnsRelationID(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "gas_account"`)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "gas_account" ORDER BY created_at desc LIMIT $1`)).
|
||||
WithArgs(20).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "gas_basic_id", "username", "display_name", "password_hash", "role_code"}).
|
||||
AddRow(uint64(9), "account-a", nil, nil, "enabled", 1, uint64(7), "operator", "Operator", "hash", "admin"))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id","identity" FROM "gas_basic" WHERE id IN ($1)`)).
|
||||
WithArgs(uint64(7)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity"}).AddRow(uint64(7), "gas-a"))
|
||||
|
||||
ctx, recorder := updateContext(http.MethodGet, "/gas/gas_account", "", nil)
|
||||
ListGasAccount(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, 0)
|
||||
body := recorder.Body.String()
|
||||
if !strings.Contains(body, `"gas_basic_identity":"gas-a"`) || strings.Contains(body, `"gas_basic_id"`) || strings.Contains(body, `"id":`) {
|
||||
t.Fatalf("account list did not return the public relation shape: %s", body)
|
||||
}
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestListGasAccountPreloadsRelationIdentitiesInOneQuery(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "gas_account"`)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(2))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "gas_account" ORDER BY created_at desc LIMIT $1`)).
|
||||
WithArgs(20).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "gas_basic_id", "username", "display_name", "password_hash", "role_code"}).
|
||||
AddRow(uint64(9), "account-a", nil, nil, "enabled", 1, uint64(7), "operator-a", "Operator A", "hash", "admin").
|
||||
AddRow(uint64(10), "account-b", nil, nil, "enabled", 1, uint64(8), "operator-b", "Operator B", "hash", "admin"))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id","identity" FROM "gas_basic" WHERE id IN ($1,$2)`)).
|
||||
WithArgs(uint64(7), uint64(8)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity"}).AddRow(uint64(7), "gas-a").AddRow(uint64(8), "gas-b"))
|
||||
|
||||
ctx, recorder := updateContext(http.MethodGet, "/gas/gas_account", "", nil)
|
||||
ListGasAccount(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, 0)
|
||||
body := recorder.Body.String()
|
||||
if !strings.Contains(body, `"gas_basic_identity":"gas-a"`) || !strings.Contains(body, `"gas_basic_identity":"gas-b"`) {
|
||||
t.Fatalf("account list omitted preloaded relation identities: %s", body)
|
||||
}
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestListGasAccountFailsWhenRelationIdentityProjectionCannotLoad(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "gas_account"`)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "gas_account" ORDER BY created_at desc LIMIT $1`)).
|
||||
WithArgs(20).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "gas_basic_id", "username", "display_name", "password_hash", "role_code"}).
|
||||
AddRow(uint64(9), "account-a", nil, nil, "enabled", 1, uint64(7), "operator", "Operator", "hash", "admin"))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id","identity" FROM "gas_basic" WHERE id IN ($1)`)).
|
||||
WithArgs(uint64(7)).
|
||||
WillReturnError(errors.New("relation lookup unavailable"))
|
||||
|
||||
ctx, recorder := updateContext(http.MethodGet, "/gas/gas_account", "", nil)
|
||||
ListGasAccount(ctx)
|
||||
|
||||
if strings.Contains(recorder.Body.String(), `"code":0`) {
|
||||
t.Fatalf("relation projection failure was returned as success: %s", recorder.Body.String())
|
||||
}
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestGetDeliveryTrackOrdersAndMasksPointsWithoutPreciseLocationScope(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
now := time.Now().UTC()
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "delivery_track" WHERE identity = $1 ORDER BY "delivery_track"."id" LIMIT $2`)).
|
||||
WithArgs("track-a", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "delivery_task_id", "started_at", "completed_at"}).
|
||||
AddRow(uint64(7), "track-a", nil, nil, "enabled", 1, uint64(8), now, nil))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "delivery_track_point" WHERE delivery_track_id = $1 ORDER BY occurred_at asc`)).
|
||||
WithArgs(uint64(7)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "delivery_track_id", "point_type", "occurred_at", "longitude", "latitude"}).
|
||||
AddRow(uint64(9), "point-a", nil, nil, "enabled", 1, uint64(7), "arrival", now, "120.123", "30.456"))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id","identity" FROM "delivery_task" WHERE id IN ($1)`)).
|
||||
WithArgs(uint64(8)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity"}).AddRow(uint64(8), "task-a"))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id","identity" FROM "delivery_track" WHERE id IN ($1)`)).
|
||||
WithArgs(uint64(7)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity"}).AddRow(uint64(7), "track-a"))
|
||||
|
||||
ctx, recorder := updateContext(http.MethodGet, "/delivery/delivery_track/track-a", "track-a", nil)
|
||||
GetDeliveryTrack(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, 0)
|
||||
if strings.Contains(recorder.Body.String(), "120.123") || strings.Contains(recorder.Body.String(), "30.456") {
|
||||
t.Fatalf("unauthorized response exposed precise coordinates: %s", recorder.Body.String())
|
||||
}
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestListDeliveryTrackPointsMasksCoordinatesWithoutPreciseLocationScope(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
now := time.Now().UTC()
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "delivery_track_point"`)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "delivery_track_point" ORDER BY created_at desc LIMIT $1`)).
|
||||
WithArgs(20).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "delivery_track_id", "point_type", "occurred_at", "longitude", "latitude"}).
|
||||
AddRow(uint64(9), "point-a", now, now, "enabled", 1, uint64(7), "arrival", now, "120.123456", "30.456789"))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id","identity" FROM "delivery_track" WHERE id IN ($1)`)).
|
||||
WithArgs(uint64(7)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity"}).AddRow(uint64(7), "track-a"))
|
||||
|
||||
ctx, recorder := updateContext(http.MethodGet, "/delivery/delivery_track_point", "", nil)
|
||||
listResource(ctx, &models.DeliveryTrackPoint{})
|
||||
|
||||
assertResponseCode(t, recorder, 0)
|
||||
body := recorder.Body.String()
|
||||
if strings.Contains(body, "120.123456") || strings.Contains(body, "30.456789") {
|
||||
t.Fatalf("track-point list exposed precise coordinates without scope: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"delivery_track_identity":"track-a"`) {
|
||||
t.Fatalf("track-point list omitted its public relation identity: %s", body)
|
||||
}
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestGetDeliveryTrackPointReturnsCoordinatesWithPreciseLocationScope(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
now := time.Now().UTC()
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "delivery_track_point" WHERE identity = $1 ORDER BY "delivery_track_point"."id" LIMIT $2`)).
|
||||
WithArgs("point-a", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "delivery_track_id", "point_type", "occurred_at", "longitude", "latitude"}).
|
||||
AddRow(uint64(9), "point-a", now, now, "enabled", 1, uint64(7), "arrival", now, "120.123456", "30.456789"))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id","identity" FROM "delivery_track" WHERE id IN ($1)`)).
|
||||
WithArgs(uint64(7)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity"}).AddRow(uint64(7), "track-a"))
|
||||
|
||||
ctx, recorder := updateContext(http.MethodGet, "/delivery/delivery_track_point/point-a", "point-a", nil)
|
||||
ctx.Set("Auth", &types.JwtClaims{Extend: map[string]string{"location_scope": "precise"}})
|
||||
getResource(ctx, &models.DeliveryTrackPoint{})
|
||||
|
||||
assertResponseCode(t, recorder, 0)
|
||||
if !strings.Contains(recorder.Body.String(), "120.123456") || !strings.Contains(recorder.Body.String(), "30.456789") {
|
||||
t.Fatalf("authorized track-point detail omitted precise coordinates: %s", recorder.Body.String())
|
||||
}
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestReplacePlatformRoleMenusAllowsAnEmptySetToClearAssignmentsTransactionally(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platform_role" WHERE identity = $1 ORDER BY "platform_role"."id" LIMIT $2`)).
|
||||
WithArgs("role-a", 1).
|
||||
WillReturnRows(platformRoleRows("role-a"))
|
||||
mock.ExpectExec(regexp.QuoteMeta(`DELETE FROM "platform_role_menu_relation" WHERE platform_role_id = $1`)).
|
||||
WithArgs(uint64(1)).
|
||||
WillReturnResult(sqlmock.NewResult(0, 2))
|
||||
mock.ExpectCommit()
|
||||
|
||||
ctx, recorder := updateContext(http.MethodPut, "/roles/role-a/menus", "role-a", []byte(`{"menu_identities":[]}`))
|
||||
ctx.Set("Auth", &types.JwtClaims{Role: "root"})
|
||||
ReplacePlatformRoleMenus(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, 0)
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestReplacePlatformRoleMenusRejectsSystemRoleBeforeChangingRelations(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platform_role" WHERE identity = $1 ORDER BY "platform_role"."id" LIMIT $2`)).
|
||||
WithArgs("root-role", 1).
|
||||
WillReturnRows(platformSystemRoleRows("root-role"))
|
||||
mock.ExpectRollback()
|
||||
|
||||
ctx, recorder := updateContext(http.MethodPut, "/roles/root-role/menus", "root-role", []byte(`{"menu_identities":[]}`))
|
||||
ctx.Set("Auth", &types.JwtClaims{Role: "root"})
|
||||
ReplacePlatformRoleMenus(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrInvalidArgument)))
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
type responseBody struct {
|
||||
Code int32 `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details json.RawMessage `json:"details"`
|
||||
}
|
||||
|
||||
func setupPlatformRoleDatabase(t *testing.T) (*gorm.DB, sqlmock.Sqlmock) {
|
||||
t.Helper()
|
||||
previous := impl.DBService
|
||||
sqlDatabase, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
database, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDatabase}), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
impl.DBService = database
|
||||
t.Cleanup(func() {
|
||||
impl.DBService = previous
|
||||
_ = sqlDatabase.Close()
|
||||
})
|
||||
return database, mock
|
||||
}
|
||||
|
||||
func platformRoleRows(identity string) *sqlmock.Rows {
|
||||
return sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "role_code", "name", "data_scope", "is_system"}).
|
||||
AddRow(uint64(1), identity, nil, nil, "enabled", 1, identity, identity, "global", false)
|
||||
}
|
||||
|
||||
func platformSystemRoleRows(identity string) *sqlmock.Rows {
|
||||
return sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "role_code", "name", "data_scope", "is_system"}).
|
||||
AddRow(uint64(1), identity, nil, nil, "enabled", 1, identity, identity, "global", true)
|
||||
}
|
||||
|
||||
func updateContext(method, target, identity string, body []byte) (*gin.Context, *httptest.ResponseRecorder) {
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(method, target, bytes.NewReader(body))
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
ctx.Params = gin.Params{{Key: "identity", Value: identity}}
|
||||
return ctx, recorder
|
||||
}
|
||||
|
||||
func assertResponseCode(t *testing.T, recorder *httptest.ResponseRecorder, want int32) {
|
||||
t.Helper()
|
||||
var reply responseBody
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &reply); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if reply.Code != want {
|
||||
t.Fatalf("response code = %d, want %d: %s", reply.Code, want, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func assertMockExpectations(t *testing.T, mock sqlmock.Sqlmock) {
|
||||
t.Helper()
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertContract(t *testing.T, contracts []ResourceContract, domain, name string, mode ResourceMode, pageKind string) {
|
||||
t.Helper()
|
||||
for _, contract := range contracts {
|
||||
if contract.Domain == domain && contract.Name == name && contract.Mode == mode && contract.PageKind == pageKind {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("missing resource contract %s/%s with mode %q and page kind %q", domain, name, mode, pageKind)
|
||||
}
|
||||
@@ -4,15 +4,16 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ListStaff 查询服务人员分页列表。
|
||||
func ListStaff(ctx *gin.Context) { listPage[models.StaffAccount](ctx) }
|
||||
func ListStaff(ctx *gin.Context) { common.ListPage[models.StaffAccount](ctx) }
|
||||
|
||||
// GetStaff 查询一个服务人员档案。
|
||||
func GetStaff(ctx *gin.Context) { getByIdentity[models.StaffAccount](ctx) }
|
||||
func GetStaff(ctx *gin.Context) { common.GetByIdentity[models.StaffAccount](ctx) }
|
||||
|
||||
// CreateStaff 创建服务人员档案。
|
||||
func CreateStaff(ctx *gin.Context) {
|
||||
@@ -36,17 +37,17 @@ func CreateStaff(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
gasBasicID, err := resolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
||||
gasBasicID, err := common.ResolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
deliveryBasicID, err := resolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, false)
|
||||
deliveryBasicID, err := common.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}
|
||||
staff := models.StaffAccount{Entity: common.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"
|
||||
}
|
||||
@@ -54,7 +55,7 @@ func CreateStaff(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
respondCreatedResource(ctx, staff)
|
||||
common.RespondCreatedResource(ctx, staff)
|
||||
}
|
||||
|
||||
// UpdateStaff 更新服务人员档案。
|
||||
@@ -72,15 +73,15 @@ func UpdateStaff(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
gasBasicID, err := resolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
||||
gasBasicID, err := common.ResolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
deliveryBasicID, err := resolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, false)
|
||||
deliveryBasicID, err := common.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"})
|
||||
common.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"})
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -17,25 +18,25 @@ type staffCredentialRequest struct {
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
}
|
||||
|
||||
func ListStaffCredential(ctx *gin.Context) { listPage[models.StaffCredential](ctx) }
|
||||
func GetStaffCredential(ctx *gin.Context) { getByIdentity[models.StaffCredential](ctx) }
|
||||
func ListStaffCredential(ctx *gin.Context) { common.ListPage[models.StaffCredential](ctx) }
|
||||
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 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
staffAccountID, err := resolveIdentityID(&models.StaffAccount{}, request.StaffAccountIdentity, true)
|
||||
staffAccountID, err := common.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}
|
||||
credential := models.StaffCredential{Entity: common.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
|
||||
}
|
||||
respondCreatedResource(ctx, credential)
|
||||
common.RespondCreatedResource(ctx, credential)
|
||||
}
|
||||
func UpdateStaffCredential(ctx *gin.Context) {
|
||||
var request staffCredentialRequest
|
||||
@@ -43,12 +44,12 @@ func UpdateStaffCredential(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
staffAccountID, err := resolveIdentityID(&models.StaffAccount{}, request.StaffAccountIdentity, true)
|
||||
staffAccountID, err := common.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"})
|
||||
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"})
|
||||
}
|
||||
|
||||
type userAddressRequest struct {
|
||||
@@ -59,25 +60,25 @@ type userAddressRequest struct {
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
func ListUserAddress(ctx *gin.Context) { listPage[models.UserAddress](ctx) }
|
||||
func GetUserAddress(ctx *gin.Context) { getByIdentity[models.UserAddress](ctx) }
|
||||
func ListUserAddress(ctx *gin.Context) { common.ListPage[models.UserAddress](ctx) }
|
||||
func GetUserAddress(ctx *gin.Context) { common.GetByIdentity[models.UserAddress](ctx) }
|
||||
func CreateUserAddress(ctx *gin.Context) {
|
||||
var request userAddressRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
userAccountID, err := resolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
|
||||
userAccountID, err := common.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}
|
||||
address := models.UserAddress{Entity: common.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
|
||||
}
|
||||
respondCreatedResource(ctx, address)
|
||||
common.RespondCreatedResource(ctx, address)
|
||||
}
|
||||
func UpdateUserAddress(ctx *gin.Context) {
|
||||
var request userAddressRequest
|
||||
@@ -85,12 +86,12 @@ func UpdateUserAddress(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
userAccountID, err := resolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
|
||||
userAccountID, err := common.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"})
|
||||
common.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 {
|
||||
@@ -100,40 +101,40 @@ type userServiceRelationRequest struct {
|
||||
StaffAccountIdentity string `json:"staff_account_identity"`
|
||||
}
|
||||
|
||||
func ListUserServiceRelation(ctx *gin.Context) { listPage[models.UserServiceRelation](ctx) }
|
||||
func GetUserServiceRelation(ctx *gin.Context) { getByIdentity[models.UserServiceRelation](ctx) }
|
||||
func ListUserServiceRelation(ctx *gin.Context) { common.ListPage[models.UserServiceRelation](ctx) }
|
||||
func GetUserServiceRelation(ctx *gin.Context) { common.GetByIdentity[models.UserServiceRelation](ctx) }
|
||||
func CreateUserServiceRelation(ctx *gin.Context) {
|
||||
var request userServiceRelationRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
userAccountID, err := resolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
|
||||
userAccountID, err := common.ResolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
gasBasicID, err := resolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
||||
gasBasicID, err := common.ResolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
deliveryBasicID, err := resolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, false)
|
||||
deliveryBasicID, err := common.ResolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
staffAccountID, err := resolveIdentityID(&models.StaffAccount{}, request.StaffAccountIdentity, false)
|
||||
staffAccountID, err := common.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}
|
||||
relation := models.UserServiceRelation{Entity: common.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
|
||||
}
|
||||
respondCreatedResource(ctx, relation)
|
||||
common.RespondCreatedResource(ctx, relation)
|
||||
}
|
||||
func UpdateUserServiceRelation(ctx *gin.Context) {
|
||||
var request userServiceRelationRequest
|
||||
@@ -141,25 +142,25 @@ func UpdateUserServiceRelation(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
userAccountID, err := resolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
|
||||
userAccountID, err := common.ResolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
gasBasicID, err := resolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
||||
gasBasicID, err := common.ResolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
deliveryBasicID, err := resolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, false)
|
||||
deliveryBasicID, err := common.ResolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
staffAccountID, err := resolveIdentityID(&models.StaffAccount{}, request.StaffAccountIdentity, false)
|
||||
staffAccountID, err := common.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"})
|
||||
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"})
|
||||
}
|
||||
36
backend/api/internal/logic/platform/test_helpers_test.go
Normal file
36
backend/api/internal/logic/platform/test_helpers_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupPlatformRoleDatabase(t *testing.T) (*gorm.DB, sqlmock.Sqlmock) {
|
||||
t.Helper()
|
||||
previous := impl.DBService
|
||||
sqlDatabase, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
database, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDatabase}), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
impl.DBService = database
|
||||
t.Cleanup(func() {
|
||||
impl.DBService = previous
|
||||
_ = sqlDatabase.Close()
|
||||
})
|
||||
return database, mock
|
||||
}
|
||||
|
||||
func assertMockExpectations(t *testing.T, mock sqlmock.Sqlmock) {
|
||||
t.Helper()
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,16 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ListUser 查询业主客户分页列表。
|
||||
func ListUser(ctx *gin.Context) { listPage[models.UserAccount](ctx) }
|
||||
func ListUser(ctx *gin.Context) { common.ListPage[models.UserAccount](ctx) }
|
||||
|
||||
// GetUser 查询一个业主客户档案。
|
||||
func GetUser(ctx *gin.Context) { getByIdentity[models.UserAccount](ctx) }
|
||||
func GetUser(ctx *gin.Context) { common.GetByIdentity[models.UserAccount](ctx) }
|
||||
|
||||
// CreateUser 创建业主客户档案。
|
||||
func CreateUser(ctx *gin.Context) {
|
||||
@@ -33,12 +34,12 @@ func CreateUser(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
user := models.UserAccount{Entity: newEntity("enabled"), Username: request.Username, PasswordHash: hash, Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RealName: request.RealName}
|
||||
user := models.UserAccount{Entity: common.NewEntity("enabled"), Username: request.Username, PasswordHash: hash, Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RealName: request.RealName}
|
||||
if err := impl.DBService.Create(&user).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
respondCreatedResource(ctx, user)
|
||||
common.RespondCreatedResource(ctx, user)
|
||||
}
|
||||
|
||||
// UpdateUser 更新业主客户档案。
|
||||
@@ -53,5 +54,5 @@ func UpdateUser(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.UserAccount{}, gin.H{"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "real_name": request.RealName}, []string{"name", "phone", "avatar", "real_name"})
|
||||
common.UpdateAllowedByIdentity(ctx, &models.UserAccount{}, gin.H{"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "real_name": request.RealName}, []string{"name", "phone", "avatar", "real_name"})
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/middleware"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"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"
|
||||
@@ -39,11 +40,11 @@ func ListWalletApplyCash(ctx *gin.Context) { listWalletPage[models.WalletApplyCa
|
||||
func GetWalletApplyCash(ctx *gin.Context) { getWalletByIdentity[models.WalletApplyCash](ctx) }
|
||||
|
||||
func listWalletPage[T any](ctx *gin.Context) {
|
||||
page, size := pageSize(ctx)
|
||||
page, size := common.PageSize(ctx)
|
||||
var list []T
|
||||
var total int64
|
||||
model := new(T)
|
||||
query := applyKeywordFilter(ctx, impl.DBService.Model(model), model)
|
||||
query := common.ApplyKeywordFilter(ctx, impl.DBService.Model(model), model)
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -52,28 +53,28 @@ func listWalletPage[T any](ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := publicResourceResponse(list)
|
||||
response, err := common.PublicResourceResponse(list)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
protectWalletResponse(response, false)
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": protectPreciseLocation(ctx, model, response)})
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": common.ProtectPreciseLocation(ctx, model, response)})
|
||||
}
|
||||
|
||||
func getWalletByIdentity[T any](ctx *gin.Context) {
|
||||
var data T
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&data).Error; err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := publicResourceResponse(data)
|
||||
response, err := common.PublicResourceResponse(data)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
protectWalletResponse(response, true)
|
||||
infra.Response.Success(ctx, protectPreciseLocation(ctx, new(T), response))
|
||||
infra.Response.Success(ctx, common.ProtectPreciseLocation(ctx, new(T), response))
|
||||
}
|
||||
|
||||
func protectWalletResponse(value any, includeDetails bool) {
|
||||
@@ -89,7 +90,7 @@ func protectWalletResponse(value any, includeDetails bool) {
|
||||
delete(data, "card_no_last4")
|
||||
}
|
||||
if owner, ok := data["card_owner"].(string); ok && owner != "" {
|
||||
data["card_owner"] = maskPersonalNameValue(owner)
|
||||
data["card_owner"] = common.MaskPersonalNameValue(owner)
|
||||
}
|
||||
if !includeDetails {
|
||||
delete(data, "args")
|
||||
@@ -120,7 +121,7 @@ func GetOrCreateOwnerWallet(ctx *gin.Context) {
|
||||
ownerIdentity := strings.TrimSpace(ctx.Param("owner_identity"))
|
||||
ownerID, err := resolveWalletOwner(ownerType, ownerIdentity)
|
||||
if err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
var wallet models.WalletBasic
|
||||
@@ -145,12 +146,12 @@ func GetOrCreateOwnerWallet(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := publicResourceResponse(wallet)
|
||||
response, err := common.PublicResourceResponse(wallet)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, protectPreciseLocation(ctx, &models.WalletBasic{}, response))
|
||||
infra.Response.Success(ctx, common.ProtectPreciseLocation(ctx, &models.WalletBasic{}, response))
|
||||
}
|
||||
|
||||
func resolveWalletOwner(ownerType, ownerIdentity string) (uint64, error) {
|
||||
@@ -164,7 +165,7 @@ func resolveWalletOwner(ownerType, ownerIdentity string) (uint64, error) {
|
||||
if model == nil || ownerIdentity == "" {
|
||||
return 0, errors.New("invalid wallet owner")
|
||||
}
|
||||
return resolveIdentityID(model, ownerIdentity, true)
|
||||
return common.ResolveIdentityID(model, ownerIdentity, true)
|
||||
}
|
||||
|
||||
func UpdateWalletBasicStatus(ctx *gin.Context) {
|
||||
@@ -175,7 +176,7 @@ func UpdateWalletBasicStatus(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.WalletBasic{}, gin.H{"status": request.Status}, []string{"status"})
|
||||
common.UpdateAllowedByIdentity(ctx, &models.WalletBasic{}, gin.H{"status": request.Status}, []string{"status"})
|
||||
}
|
||||
|
||||
func RechargeWalletBasic(ctx *gin.Context) {
|
||||
@@ -238,10 +239,10 @@ func RechargeWalletBasic(ctx *gin.Context) {
|
||||
return tx.Create(&record).Error
|
||||
})
|
||||
if err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := publicResourceResponse(record)
|
||||
response, err := common.PublicResourceResponse(record)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -299,7 +300,7 @@ func reviewWalletApplyCash(ctx *gin.Context, targetStatus string) {
|
||||
}).Error
|
||||
})
|
||||
if err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true, "status": targetStatus})
|
||||
@@ -1,14 +0,0 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// DeliveryTask 对应 delivery_task,保存配送履约任务。
|
||||
type DeliveryTask struct {
|
||||
Entity // 公共实体字段
|
||||
EcOrderID uint64 `gorm:"column:ec_order_id;not null;index" json:"ec_order_id"` // ec_order_id 业务字段
|
||||
StaffAccountID uint64 `gorm:"column:staff_account_id;not null;default:0;index" json:"staff_account_id"` // staff_account_id 业务字段
|
||||
DeliveryPointID uint64 `gorm:"column:delivery_point_id;not null;index" json:"delivery_point_id"` // delivery_point_id 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&DeliveryTask{}) }
|
||||
func (table *DeliveryTask) TableName() string { return "delivery_task" }
|
||||
@@ -1,17 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DeliveryTrack 对应 delivery_track,保存配送轨迹摘要。
|
||||
type DeliveryTrack struct {
|
||||
Entity // 公共实体字段
|
||||
DeliveryTaskID uint64 `gorm:"column:delivery_task_id;not null;index" json:"delivery_task_id"` // delivery_task_id 业务字段
|
||||
StartedAt *time.Time `gorm:"column:started_at;type:timestamptz" json:"started_at"` // started_at 业务字段
|
||||
CompletedAt *time.Time `gorm:"column:completed_at;type:timestamptz" json:"completed_at"` // completed_at 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&DeliveryTrack{}) }
|
||||
func (table *DeliveryTrack) TableName() string { return "delivery_track" }
|
||||
@@ -1,19 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DeliveryTrackPoint 对应 delivery_track_point,保存配送节点和位置。
|
||||
type DeliveryTrackPoint struct {
|
||||
Entity // 公共实体字段
|
||||
DeliveryTrackID uint64 `gorm:"column:delivery_track_id;not null;index" json:"delivery_track_id"` // delivery_track_id 业务字段
|
||||
PointType string `gorm:"column:point_type;type:varchar(32);not null" json:"point_type"` // point_type 业务字段
|
||||
OccurredAt time.Time `gorm:"column:occurred_at;type:timestamptz;not null" json:"occurred_at"` // occurred_at 业务字段
|
||||
Longitude string `gorm:"column:longitude;type:varchar(32);not null;default:''" json:"longitude"` // longitude 业务字段
|
||||
Latitude string `gorm:"column:latitude;type:varchar(32);not null;default:''" json:"latitude"` // latitude 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&DeliveryTrackPoint{}) }
|
||||
func (table *DeliveryTrackPoint) TableName() string { return "delivery_track_point" }
|
||||
23
backend/api/internal/models/gasorder_assign.go
Normal file
23
backend/api/internal/models/gasorder_assign.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// GasorderAssign 对应 gasorder_assign,保存订单履约分配历史。
|
||||
type GasorderAssign struct {
|
||||
Entity // 公共实体字段
|
||||
GasorderBasicID uint64 `gorm:"column:gasorder_basic_id;not null;index" json:"gasorder_basic_id"` // 订单自增主键
|
||||
GasBasicID uint64 `gorm:"column:gas_basic_id;not null;index" json:"gas_basic_id"` // 气站自增主键
|
||||
DeliveryBasicID uint64 `gorm:"column:delivery_basic_id;not null;index" json:"delivery_basic_id"` // 配送点自增主键
|
||||
StaffAccountID uint64 `gorm:"column:staff_account_id;not null;index" json:"staff_account_id"` // 配送人员自增主键
|
||||
AssignerIdentity string `gorm:"column:assigner_identity;type:varchar(36);not null;default:''" json:"assigner_identity"` // 分配人标识
|
||||
AssignerName string `gorm:"column:assigner_name;type:varchar(64);not null;default:''" json:"assigner_name"` // 分配人姓名快照
|
||||
AssignedAt time.Time `gorm:"column:assigned_at;type:timestamptz;not null;index" json:"assigned_at"` // 分配时间
|
||||
Reason string `gorm:"column:reason;type:text;not null;default:''" json:"reason"` // 分配原因
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&GasorderAssign{}) }
|
||||
func (table *GasorderAssign) TableName() string { return "gasorder_assign" }
|
||||
34
backend/api/internal/models/gasorder_basic.go
Normal file
34
backend/api/internal/models/gasorder_basic.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// GasorderBasic 对应 gasorder_basic,保存气体配送订单当前快照。
|
||||
type GasorderBasic struct {
|
||||
Entity // 公共实体字段,Status 保存订单当前状态
|
||||
OrderNo string `gorm:"column:order_no;type:varchar(64);not null;uniqueIndex" json:"order_no"` // 订单编号
|
||||
RequestNo string `gorm:"column:request_no;type:varchar(128);not null;uniqueIndex" json:"request_no"` // 创建幂等号
|
||||
GasorderContractID uint64 `gorm:"column:gasorder_contract_id;not null;index" json:"gasorder_contract_id"` // 合同自增主键
|
||||
UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` // 服务用户自增主键
|
||||
CreatorType string `gorm:"column:creator_type;type:varchar(32);not null" json:"creator_type"` // 业务创建方类型
|
||||
CreatorID uint64 `gorm:"column:creator_id;not null;default:0;index" json:"creator_id"` // 业务创建方自增主键
|
||||
CreatorIdentity string `gorm:"column:creator_identity;type:varchar(36);not null;index" json:"creator_identity"` // 业务创建方标识
|
||||
GasBasicID uint64 `gorm:"column:gas_basic_id;not null;index" json:"gas_basic_id"` // 履约气站自增主键
|
||||
DeliveryBasicID uint64 `gorm:"column:delivery_basic_id;not null;default:0;index" json:"delivery_basic_id"` // 当前配送点自增主键
|
||||
StaffAccountID uint64 `gorm:"column:staff_account_id;not null;default:0;index" json:"staff_account_id"` // 当前配送人员自增主键
|
||||
Address string `gorm:"column:address;type:varchar(255);not null" json:"address"` // 配送地址快照
|
||||
Longitude string `gorm:"column:longitude;type:varchar(32);not null;default:''" json:"longitude"` // 配送经度快照
|
||||
Latitude string `gorm:"column:latitude;type:varchar(32);not null;default:''" json:"latitude"` // 配送纬度快照
|
||||
ContactName string `gorm:"column:contact_name;type:varchar(64);not null" json:"contact_name"` // 联系人快照
|
||||
ContactPhone string `gorm:"column:contact_phone;type:varchar(32);not null" json:"contact_phone"` // 联系电话快照
|
||||
ProductAmount int64 `gorm:"column:product_amount;not null;check:product_amount >= 0" json:"product_amount"` // 商品金额,单位分
|
||||
DeliveryFee int64 `gorm:"column:delivery_fee;not null;default:0;check:delivery_fee >= 0" json:"delivery_fee"` // 配送费,单位分
|
||||
DiscountAmount int64 `gorm:"column:discount_amount;not null;default:0;check:discount_amount >= 0" json:"discount_amount"` // 优惠金额,单位分
|
||||
PayableAmount int64 `gorm:"column:payable_amount;not null;check:payable_amount > 0" json:"payable_amount"` // 应付金额,单位分
|
||||
PreviousStatus string `gorm:"column:previous_status;type:varchar(32);not null;default:''" json:"previous_status"` // 异常前状态
|
||||
OperatorIdentity string `gorm:"column:operator_identity;type:varchar(36);not null;default:''" json:"operator_identity"` // 建单操作人标识
|
||||
OperatorName string `gorm:"column:operator_name;type:varchar(64);not null;default:''" json:"operator_name"` // 建单操作人姓名快照
|
||||
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 订单备注
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&GasorderBasic{}) }
|
||||
func (table *GasorderBasic) TableName() string { return "gasorder_basic" }
|
||||
22
backend/api/internal/models/gasorder_confirm.go
Normal file
22
backend/api/internal/models/gasorder_confirm.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// GasorderConfirm 对应 gasorder_confirm,保存用户签收确认。
|
||||
type GasorderConfirm struct {
|
||||
Entity // 公共实体字段
|
||||
GasorderBasicID uint64 `gorm:"column:gasorder_basic_id;not null;uniqueIndex" json:"gasorder_basic_id"` // 订单自增主键
|
||||
ConfirmType string `gorm:"column:confirm_type;type:varchar(32);not null" json:"confirm_type"` // 签收类型
|
||||
RecipientName string `gorm:"column:recipient_name;type:varchar(64);not null" json:"recipient_name"` // 签收人姓名快照
|
||||
RecipientPhone string `gorm:"column:recipient_phone;type:varchar(32);not null;default:''" json:"recipient_phone"` // 签收人手机号快照
|
||||
ProofURI string `gorm:"column:proof_uri;type:varchar(512);not null;default:''" json:"proof_uri"` // 签名或凭证地址
|
||||
ConfirmedAt time.Time `gorm:"column:confirmed_at;type:timestamptz;not null;index" json:"confirmed_at"` // 确认时间
|
||||
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 签收备注
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&GasorderConfirm{}) }
|
||||
func (table *GasorderConfirm) TableName() string { return "gasorder_confirm" }
|
||||
26
backend/api/internal/models/gasorder_contract.go
Normal file
26
backend/api/internal/models/gasorder_contract.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// GasorderContract 对应 gasorder_contract,保存用户与气站唯一供气合同。
|
||||
type GasorderContract struct {
|
||||
Entity // 公共实体字段
|
||||
ContractNo string `gorm:"column:contract_no;type:varchar(64);not null;uniqueIndex" json:"contract_no"` // 合同编号
|
||||
UserAccountID uint64 `gorm:"column:user_account_id;not null;uniqueIndex:idx_gasorder_contract_party" json:"user_account_id"` // 签约用户自增主键
|
||||
GasBasicID uint64 `gorm:"column:gas_basic_id;not null;uniqueIndex:idx_gasorder_contract_party" json:"gas_basic_id"` // 签约气站自增主键
|
||||
DeliveryBasicID uint64 `gorm:"column:delivery_basic_id;not null;default:0;index" json:"delivery_basic_id"` // 默认配送点自增主键
|
||||
Title string `gorm:"column:title;type:varchar(255);not null" json:"title"` // 合同标题
|
||||
Terms string `gorm:"column:terms;type:text;not null;default:''" json:"terms"` // 合同条款
|
||||
FileURI string `gorm:"column:file_uri;type:varchar(512);not null;default:''" json:"file_uri"` // 合同文件地址
|
||||
DefaultDeliveryFee int64 `gorm:"column:default_delivery_fee;not null;default:0;check:default_delivery_fee >= 0" json:"default_delivery_fee"` // 默认配送费,单位分
|
||||
SignedAt time.Time `gorm:"column:signed_at;type:timestamptz;not null" json:"signed_at"` // 签订时间
|
||||
EffectiveAt time.Time `gorm:"column:effective_at;type:timestamptz;not null" json:"effective_at"` // 生效时间
|
||||
ExpiredAt *time.Time `gorm:"column:expired_at;type:timestamptz" json:"expired_at"` // 到期时间
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&GasorderContract{}) }
|
||||
func (table *GasorderContract) TableName() string { return "gasorder_contract" }
|
||||
23
backend/api/internal/models/gasorder_contract_product.go
Normal file
23
backend/api/internal/models/gasorder_contract_product.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// GasorderContractProduct 对应 gasorder_contract_product,保存合同气瓶绑定历史。
|
||||
type GasorderContractProduct struct {
|
||||
Entity // 公共实体字段
|
||||
GasorderContractID uint64 `gorm:"column:gasorder_contract_id;not null;index" json:"gasorder_contract_id"` // 合同自增主键
|
||||
ProductInfoID uint64 `gorm:"column:product_info_id;not null;index;uniqueIndex:idx_active_contract_product,where:unbound_at IS NULL" json:"product_info_id"` // 实体气瓶自增主键
|
||||
ProductCode string `gorm:"column:product_code;type:varchar(64);not null" json:"product_code"` // 产品标识快照
|
||||
ProductTypeName string `gorm:"column:product_type_name;type:varchar(128);not null" json:"product_type_name"` // 产品类型名称快照
|
||||
ProductParams string `gorm:"column:product_params;type:text;not null;default:'{}'" json:"product_params"` // 产品规格参数快照
|
||||
UnitPrice int64 `gorm:"column:unit_price;not null;check:unit_price >= 0" json:"unit_price"` // 约定充装单价,单位分
|
||||
BoundAt time.Time `gorm:"column:bound_at;type:timestamptz;not null" json:"bound_at"` // 绑定时间
|
||||
UnboundAt *time.Time `gorm:"column:unbound_at;type:timestamptz;index" json:"unbound_at"` // 解绑时间
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&GasorderContractProduct{}) }
|
||||
func (table *GasorderContractProduct) TableName() string { return "gasorder_contract_product" }
|
||||
24
backend/api/internal/models/gasorder_contract_revision.go
Normal file
24
backend/api/internal/models/gasorder_contract_revision.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// GasorderContractRevision 对应 gasorder_contract_revision,保存唯一合同的不可变变更快照。
|
||||
type GasorderContractRevision struct {
|
||||
Entity // 公共实体字段
|
||||
GasorderContractID uint64 `gorm:"column:gasorder_contract_id;not null;index" json:"gasorder_contract_id"` // 合同自增主键
|
||||
Action string `gorm:"column:action;type:varchar(32);not null;index" json:"action"` // 合同变更动作
|
||||
ContractStatus string `gorm:"column:contract_status;type:varchar(32);not null" json:"contract_status"` // 合同状态快照
|
||||
EffectiveAt time.Time `gorm:"column:effective_at;type:timestamptz;not null" json:"effective_at"` // 生效时间快照
|
||||
ExpiredAt *time.Time `gorm:"column:expired_at;type:timestamptz" json:"expired_at"` // 到期时间快照
|
||||
OperatorIdentity string `gorm:"column:operator_identity;type:varchar(36);not null;default:''" json:"operator_identity"` // 操作人标识
|
||||
OperatorName string `gorm:"column:operator_name;type:varchar(64);not null;default:''" json:"operator_name"` // 操作人姓名快照
|
||||
OccurredAt time.Time `gorm:"column:occurred_at;type:timestamptz;not null;index" json:"occurred_at"` // 发生时间
|
||||
Reason string `gorm:"column:reason;type:text;not null;default:''" json:"reason"` // 变更原因
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&GasorderContractRevision{}) }
|
||||
func (table *GasorderContractRevision) TableName() string { return "gasorder_contract_revision" }
|
||||
18
backend/api/internal/models/gasorder_item.go
Normal file
18
backend/api/internal/models/gasorder_item.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// GasorderItem 对应 gasorder_item,保存订单气瓶与规格价格快照。
|
||||
type GasorderItem struct {
|
||||
Entity // 公共实体字段
|
||||
GasorderBasicID uint64 `gorm:"column:gasorder_basic_id;not null;index" json:"gasorder_basic_id"` // 订单自增主键
|
||||
GasorderContractProductID uint64 `gorm:"column:gasorder_contract_product_id;not null;index" json:"gasorder_contract_product_id"` // 合同气瓶绑定自增主键
|
||||
ProductInfoID uint64 `gorm:"column:product_info_id;not null;index" json:"product_info_id"` // 实体气瓶自增主键
|
||||
ProductCode string `gorm:"column:product_code;type:varchar(64);not null" json:"product_code"` // 产品标识快照
|
||||
ProductTypeName string `gorm:"column:product_type_name;type:varchar(128);not null" json:"product_type_name"` // 产品类型名称快照
|
||||
ProductParams string `gorm:"column:product_params;type:text;not null;default:'{}'" json:"product_params"` // 产品规格参数快照
|
||||
UnitPrice int64 `gorm:"column:unit_price;not null;check:unit_price >= 0" json:"unit_price"` // 成交单价,单位分
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&GasorderItem{}) }
|
||||
func (table *GasorderItem) TableName() string { return "gasorder_item" }
|
||||
15
backend/api/internal/models/gasorder_payment.go
Normal file
15
backend/api/internal/models/gasorder_payment.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// GasorderPayment 对应 gasorder_payment,保存订单支付尝试与统一钱包支付关联。
|
||||
type GasorderPayment struct {
|
||||
Entity // 公共实体字段,Status 保存支付尝试状态
|
||||
GasorderBasicID uint64 `gorm:"column:gasorder_basic_id;not null;index;uniqueIndex:idx_gasorder_payment_attempt" json:"gasorder_basic_id"` // 订单自增主键
|
||||
WalletPaymentID uint64 `gorm:"column:wallet_payment_id;not null;uniqueIndex" json:"wallet_payment_id"` // 钱包支付记录自增主键
|
||||
AttemptNo int `gorm:"column:attempt_no;not null;uniqueIndex:idx_gasorder_payment_attempt" json:"attempt_no"` // 支付尝试序号
|
||||
Amount int64 `gorm:"column:amount;not null;check:amount > 0" json:"amount"` // 支付金额,单位分
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&GasorderPayment{}) }
|
||||
func (table *GasorderPayment) TableName() string { return "gasorder_payment" }
|
||||
22
backend/api/internal/models/gasorder_status.go
Normal file
22
backend/api/internal/models/gasorder_status.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// GasorderStatus 对应 gasorder_status,保存不可变订单状态历史。
|
||||
type GasorderStatus struct {
|
||||
Entity // 公共实体字段
|
||||
GasorderBasicID uint64 `gorm:"column:gasorder_basic_id;not null;index" json:"gasorder_basic_id"` // 订单自增主键
|
||||
FromStatus string `gorm:"column:from_status;type:varchar(32);not null;default:''" json:"from_status"` // 原状态
|
||||
ToStatus string `gorm:"column:to_status;type:varchar(32);not null;index" json:"to_status"` // 新状态
|
||||
OperatorIdentity string `gorm:"column:operator_identity;type:varchar(36);not null;default:''" json:"operator_identity"` // 操作人标识
|
||||
OperatorName string `gorm:"column:operator_name;type:varchar(64);not null;default:''" json:"operator_name"` // 操作人姓名快照
|
||||
OccurredAt time.Time `gorm:"column:occurred_at;type:timestamptz;not null;index" json:"occurred_at"` // 发生时间
|
||||
Reason string `gorm:"column:reason;type:text;not null;default:''" json:"reason"` // 变更原因
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&GasorderStatus{}) }
|
||||
func (table *GasorderStatus) TableName() string { return "gasorder_status" }
|
||||
20
backend/api/internal/models/gasorder_track.go
Normal file
20
backend/api/internal/models/gasorder_track.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// GasorderTrack 对应 gasorder_track,保存一次配送尝试的轨迹摘要。
|
||||
type GasorderTrack struct {
|
||||
Entity // 公共实体字段
|
||||
GasorderBasicID uint64 `gorm:"column:gasorder_basic_id;not null;index;uniqueIndex:idx_gasorder_track_attempt" json:"gasorder_basic_id"` // 订单自增主键
|
||||
StaffAccountID uint64 `gorm:"column:staff_account_id;not null;index" json:"staff_account_id"` // 配送人员自增主键
|
||||
AttemptNo int `gorm:"column:attempt_no;not null;uniqueIndex:idx_gasorder_track_attempt" json:"attempt_no"` // 配送尝试序号
|
||||
StartedAt time.Time `gorm:"column:started_at;type:timestamptz;not null;index" json:"started_at"` // 开始时间
|
||||
CompletedAt *time.Time `gorm:"column:completed_at;type:timestamptz" json:"completed_at"` // 到达时间
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&GasorderTrack{}) }
|
||||
func (table *GasorderTrack) TableName() string { return "gasorder_track" }
|
||||
21
backend/api/internal/models/gasorder_track_point.go
Normal file
21
backend/api/internal/models/gasorder_track_point.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// GasorderTrackPoint 对应 gasorder_track_point,保存不可变配送位置点。
|
||||
type GasorderTrackPoint struct {
|
||||
Entity // 公共实体字段
|
||||
GasorderTrackID uint64 `gorm:"column:gasorder_track_id;not null;index" json:"gasorder_track_id"` // 轨迹自增主键
|
||||
Longitude string `gorm:"column:longitude;type:varchar(32);not null" json:"longitude"` // 经度
|
||||
Latitude string `gorm:"column:latitude;type:varchar(32);not null" json:"latitude"` // 纬度
|
||||
OccurredAt time.Time `gorm:"column:occurred_at;type:timestamptz;not null;index" json:"occurred_at"` // 定位发生时间
|
||||
Source string `gorm:"column:source;type:varchar(32);not null;default:'gps'" json:"source"` // 定位来源
|
||||
Accuracy string `gorm:"column:accuracy;type:varchar(32);not null;default:''" json:"accuracy"` // 定位精度
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&GasorderTrackPoint{}) }
|
||||
func (table *GasorderTrackPoint) TableName() string { return "gasorder_track_point" }
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/middleware"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -25,6 +26,7 @@ func RegisterPlatform(serviceKey string, engine *gin.Engine) {
|
||||
|
||||
registerGasRoute(protected)
|
||||
registerDeliveryRoute(protected)
|
||||
registerGasorderRoute(protected)
|
||||
registerStaffRoute(protected)
|
||||
registerUserRoute(protected)
|
||||
registerProductRoute(protected)
|
||||
@@ -36,26 +38,57 @@ func RegisterPlatform(serviceKey string, engine *gin.Engine) {
|
||||
}
|
||||
|
||||
func registerGasRoute(group *gin.RouterGroup) {
|
||||
registerWritableResource(group, "/gas/gas_basic", platform.ListGasBasic, platform.CreateGasBasic, platform.GetGasBasic, platform.UpdateGasBasic, &models.GasBasic{})
|
||||
registerWritableResource(group, "/gas/gas_account", platform.ListGasAccount, platform.CreateGasAccount, platform.GetGasAccount, platform.UpdateGasAccount, &models.GasAccount{})
|
||||
registerWritableResource(group, "/gas_basic", platform.ListGasBasic, platform.CreateGasBasic, platform.GetGasBasic, platform.UpdateGasBasic, &models.GasBasic{})
|
||||
registerWritableResource(group, "/gas_account", platform.ListGasAccount, platform.CreateGasAccount, platform.GetGasAccount, platform.UpdateGasAccount, &models.GasAccount{})
|
||||
}
|
||||
|
||||
func registerDeliveryRoute(group *gin.RouterGroup) {
|
||||
registerWritableResource(group, "/delivery/delivery_basic", platform.ListDeliveryBasic, platform.CreateDeliveryBasic, platform.GetDeliveryBasic, platform.UpdateDeliveryBasic, &models.DeliveryBasic{})
|
||||
registerWritableResource(group, "/delivery/delivery_account", platform.ListDeliveryAccount, platform.CreateDeliveryAccount, platform.GetDeliveryAccount, platform.UpdateDeliveryAccount, &models.DeliveryAccount{})
|
||||
registerRestrictedWritableResource(group, "/delivery/delivery_task", &models.DeliveryTask{}, nil,
|
||||
requiredRelation("ec_order_identity", "ec_order_id", &models.EcOrder{}), optionalRelation("staff_account_identity", "staff_account_id", &models.StaffAccount{}), requiredRelation("delivery_basic_identity", "delivery_point_id", &models.DeliveryBasic{}))
|
||||
trackRelations := []platform.ResourceRelation{requiredRelation("delivery_task_identity", "delivery_task_id", &models.DeliveryTask{})}
|
||||
list, create, _, update := platform.ResourceHandlers(&models.DeliveryTrack{}, []string{"started_at", "completed_at"}, []string{"started_at", "completed_at"}, trackRelations...)
|
||||
registerWritableResource(group, "/delivery/delivery_track", list, create, platform.GetDeliveryTrack, update, &models.DeliveryTrack{})
|
||||
registerReadOnlyResource(group, "/delivery/delivery_track_point", &models.DeliveryTrackPoint{})
|
||||
registerWritableResource(group, "/delivery_basic", platform.ListDeliveryBasic, platform.CreateDeliveryBasic, platform.GetDeliveryBasic, platform.UpdateDeliveryBasic, &models.DeliveryBasic{})
|
||||
registerWritableResource(group, "/delivery_account", platform.ListDeliveryAccount, platform.CreateDeliveryAccount, platform.GetDeliveryAccount, platform.UpdateDeliveryAccount, &models.DeliveryAccount{})
|
||||
}
|
||||
|
||||
func registerGasorderRoute(group *gin.RouterGroup) {
|
||||
contract := group.Group("/gasorder_contract")
|
||||
contract.GET("", platform.ListGasorderContract)
|
||||
contract.POST("", platform.CreateGasorderContract)
|
||||
contract.GET("/:identity", platform.GetGasorderContract)
|
||||
contract.PUT("/:identity", platform.UpdateGasorderContract)
|
||||
contract.POST("/:identity/activate", platform.ActivateGasorderContract)
|
||||
contract.POST("/:identity/renew", platform.RenewGasorderContract)
|
||||
contract.POST("/:identity/terminate", platform.TerminateGasorderContract)
|
||||
|
||||
contractProduct := group.Group("/gasorder_contract_product")
|
||||
contractProduct.GET("", platform.ListGasorderContractProduct)
|
||||
contractProduct.POST("", platform.BindGasorderContractProduct)
|
||||
contractProduct.GET("/:identity", platform.GetGasorderContractProduct)
|
||||
contractProduct.POST("/:identity/unbind", platform.UnbindGasorderContractProduct)
|
||||
registerReadOnlyHandlers(group, "/gasorder_contract_revision", platform.ListGasorderContractRevision, platform.GetGasorderContractRevision)
|
||||
|
||||
order := group.Group("/gasorder_basic")
|
||||
order.GET("", platform.ListGasorderBasic)
|
||||
order.POST("", platform.CreateGasorderBasic)
|
||||
order.GET("/:identity", platform.GetGasorderBasic)
|
||||
order.POST("/:identity/assign", platform.AssignGasorderBasic)
|
||||
order.POST("/:identity/filling", platform.GasorderStartFilling)
|
||||
order.POST("/:identity/ready", platform.GasorderReady)
|
||||
order.POST("/:identity/exception", platform.GasorderException)
|
||||
order.POST("/:identity/recover", platform.GasorderRecover)
|
||||
order.POST("/:identity/cancel", platform.GasorderCancel)
|
||||
|
||||
registerReadOnlyHandlers(group, "/gasorder_item", platform.ListGasorderItem, platform.GetGasorderItem)
|
||||
registerReadOnlyHandlers(group, "/gasorder_assign", platform.ListGasorderAssign, platform.GetGasorderAssign)
|
||||
registerReadOnlyHandlers(group, "/gasorder_status", platform.ListGasorderStatus, platform.GetGasorderStatus)
|
||||
registerReadOnlyHandlers(group, "/gasorder_track", platform.ListGasorderTrack, platform.GetGasorderTrack)
|
||||
registerReadOnlyHandlers(group, "/gasorder_track_point", platform.ListGasorderTrackPoint, platform.GetGasorderTrackPoint)
|
||||
registerReadOnlyHandlers(group, "/gasorder_confirm", platform.ListGasorderConfirm, platform.GetGasorderConfirm)
|
||||
registerReadOnlyHandlers(group, "/gasorder_payment", platform.ListGasorderPayment, platform.GetGasorderPayment)
|
||||
}
|
||||
|
||||
func registerProductRoute(group *gin.RouterGroup) {
|
||||
registerRestrictedNoDeleteResource(group, "/product_type", &models.ProductType{}, []string{"code", "name"})
|
||||
registerRestrictedNoDeleteResource(group, "/product_warehouse", &models.ProductWarehouse{}, []string{"code", "name", "address", "manager", "phone", "is_enabled"})
|
||||
|
||||
infoRelations := []platform.ResourceRelation{
|
||||
infoRelations := []common.ResourceRelation{
|
||||
requiredRelation("product_type_identity", "product_type_id", &models.ProductType{}),
|
||||
optionalRelation("warehouse_identity", "warehouse_id", &models.ProductWarehouse{}),
|
||||
optionalRelation("gas_basic_identity", "gas_basic_id", &models.GasBasic{}),
|
||||
@@ -120,34 +153,34 @@ func registerWalletRoute(group *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
func registerCommerceRoute(group *gin.RouterGroup) {
|
||||
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{}))
|
||||
registerRestrictedWritableResource(group, "/ec/ec_cart", &models.EcCart{}, []string{"quantity", "selected"}, requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}), requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}))
|
||||
orderRelations := []platform.ResourceRelation{requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}), optionalRelation("gas_basic_identity", "gas_station_id", &models.GasBasic{}), optionalRelation("delivery_basic_identity", "delivery_point_id", &models.DeliveryBasic{})}
|
||||
list, create, _, update := platform.ResourceHandlers(&models.EcOrder{}, []string{"order_no", "total_amount"}, []string{"total_amount"}, orderRelations...)
|
||||
registerWritableResource(group, "/ec/ec_order", list, create, platform.GetEcOrder, update, &models.EcOrder{})
|
||||
registerRestrictedWritableResource(group, "/ec/ec_order_item", &models.EcOrderItem{}, []string{"product_snapshot", "quantity", "sale_amount"}, requiredRelation("ec_order_identity", "ec_order_id", &models.EcOrder{}), requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}))
|
||||
registerRestrictedWritableResource(group, "/ec/ec_review", &models.EcReview{}, []string{"score", "content"}, requiredRelation("ec_order_identity", "ec_order_id", &models.EcOrder{}), requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}), requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}))
|
||||
categoryRelations := []common.ResourceRelation{optionalRelation("parent_identity", "parent_id", &models.EcCategory{})}
|
||||
_, categoryCreate, _, categoryUpdate := common.ResourceHandlers(&models.EcCategory{}, []string{"name", "sort_no"}, []string{"name", "sort_no"}, categoryRelations...)
|
||||
registerWritableResource(group, "/ec_category", platform.ListEcCategory, categoryCreate, platform.GetEcCategory, categoryUpdate, &models.EcCategory{})
|
||||
registerRestrictedWritableResource(group, "/ec_product", &models.EcProduct{}, []string{"product_code", "name", "price_amount", "stock_quantity"}, requiredRelation("ec_category_identity", "ec_category_id", &models.EcCategory{}))
|
||||
registerRestrictedWritableResource(group, "/ec_product_attribute", &models.EcProductAttribute{}, []string{"name", "value", "sort_no"}, requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}))
|
||||
registerRestrictedWritableResource(group, "/ec_product_image", &models.EcProductImage{}, []string{"image_uri", "sort_no", "is_cover"}, requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}))
|
||||
registerRestrictedWritableResource(group, "/ec_cart", &models.EcCart{}, []string{"quantity", "selected"}, requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}), requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}))
|
||||
orderRelations := []common.ResourceRelation{requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}), optionalRelation("gas_basic_identity", "gas_station_id", &models.GasBasic{}), optionalRelation("delivery_basic_identity", "delivery_point_id", &models.DeliveryBasic{})}
|
||||
list, create, _, update := common.ResourceHandlers(&models.EcOrder{}, []string{"order_no", "total_amount"}, []string{"total_amount"}, orderRelations...)
|
||||
registerWritableResource(group, "/ec_order", list, create, platform.GetEcOrder, update, &models.EcOrder{})
|
||||
registerRestrictedWritableResource(group, "/ec_order_item", &models.EcOrderItem{}, []string{"product_snapshot", "quantity", "sale_amount"}, requiredRelation("ec_order_identity", "ec_order_id", &models.EcOrder{}), requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}))
|
||||
registerRestrictedWritableResource(group, "/ec_review", &models.EcReview{}, []string{"score", "content"}, requiredRelation("ec_order_identity", "ec_order_id", &models.EcOrder{}), requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}), requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}))
|
||||
}
|
||||
|
||||
func registerStaffRoute(group *gin.RouterGroup) {
|
||||
registerWritableResource(group, "/staff/account", platform.ListStaff, platform.CreateStaff, platform.GetStaff, platform.UpdateStaff, &models.StaffAccount{})
|
||||
registerWritableResource(group, "/staff/credential", platform.ListStaffCredential, platform.CreateStaffCredential, platform.GetStaffCredential, platform.UpdateStaffCredential, &models.StaffCredential{})
|
||||
registerWritableResource(group, "/staff_account", platform.ListStaff, platform.CreateStaff, platform.GetStaff, platform.UpdateStaff, &models.StaffAccount{})
|
||||
registerWritableResource(group, "/staff_credential", platform.ListStaffCredential, platform.CreateStaffCredential, platform.GetStaffCredential, platform.UpdateStaffCredential, &models.StaffCredential{})
|
||||
}
|
||||
|
||||
func registerUserRoute(group *gin.RouterGroup) {
|
||||
registerWritableResource(group, "/user/account", platform.ListUser, platform.CreateUser, platform.GetUser, platform.UpdateUser, &models.UserAccount{})
|
||||
registerWritableResource(group, "/user/address", platform.ListUserAddress, platform.CreateUserAddress, platform.GetUserAddress, platform.UpdateUserAddress, &models.UserAddress{})
|
||||
registerWritableResource(group, "/user/service_relation", platform.ListUserServiceRelation, platform.CreateUserServiceRelation, platform.GetUserServiceRelation, platform.UpdateUserServiceRelation, &models.UserServiceRelation{})
|
||||
registerWritableResource(group, "/user_account", platform.ListUser, platform.CreateUser, platform.GetUser, platform.UpdateUser, &models.UserAccount{})
|
||||
registerWritableResource(group, "/user_address", platform.ListUserAddress, platform.CreateUserAddress, platform.GetUserAddress, platform.UpdateUserAddress, &models.UserAddress{})
|
||||
registerWritableResource(group, "/user_service_relation", platform.ListUserServiceRelation, platform.CreateUserServiceRelation, platform.GetUserServiceRelation, platform.UpdateUserServiceRelation, &models.UserServiceRelation{})
|
||||
}
|
||||
|
||||
func registerPlatformRoute(group *gin.RouterGroup) {
|
||||
registerWritableResource(group, "/platform/platfrom_account", platform.ListPlatfromAccount, platform.CreatePlatfromAccount, platform.GetPlatfromAccount, platform.UpdatePlatfromAccount, &models.PlatfromAccount{})
|
||||
role := group.Group("/platform/platform_role")
|
||||
registerWritableResource(group, "/platfrom_account", platform.ListPlatfromAccount, platform.CreatePlatfromAccount, platform.GetPlatfromAccount, platform.UpdatePlatfromAccount, &models.PlatfromAccount{})
|
||||
role := group.Group("/platform_role")
|
||||
role.GET("", platform.ListPlatformRole)
|
||||
role.POST("", platform.CreatePlatformRole)
|
||||
role.GET("/:identity", platform.GetPlatformRole)
|
||||
@@ -157,7 +190,7 @@ func registerPlatformRoute(group *gin.RouterGroup) {
|
||||
role.GET("/:identity/menu", platform.ListPlatformRoleMenuIdentities)
|
||||
role.PUT("/:identity/menu", platform.ReplacePlatformRoleMenus)
|
||||
role.PUT("/:identity/menus", platform.ReplacePlatformRoleMenus)
|
||||
menu := group.Group("/platform/platform_menu")
|
||||
menu := group.Group("/platform_menu")
|
||||
menu.GET("", platform.ListPlatformMenu)
|
||||
menu.POST("", platform.CreatePlatformMenu)
|
||||
menu.GET("/:identity", platform.GetPlatformMenu)
|
||||
@@ -167,16 +200,16 @@ 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, "/fin_payment", &models.FinPayment{}, []string{"channel", "amount", "paid_at"}, requiredRelation("ec_order_identity", "ec_order_id", &models.EcOrder{}))
|
||||
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"})
|
||||
registerWritableResource(group, "/fin_settlement", settlementList, settlementCreate, settlementGet, settlementUpdate, &models.FinSettlement{})
|
||||
registerRestrictedWritableResource(group, "/fin_reconciliation", &models.FinReconciliation{}, []string{"channel", "bill_date", "difference_amount"})
|
||||
|
||||
}
|
||||
|
||||
func registerContentRoute(group *gin.RouterGroup) {
|
||||
registerRestrictedWritableResource(group, "/content/cms_content", &models.CmsContent{}, []string{"content_type", "title", "body", "version_no", "publish_status"})
|
||||
registerRestrictedWritableResource(group, "/customer_service/cs_ticket", &models.CsTicket{}, []string{"ticket_no", "category", "priority"}, requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}))
|
||||
registerRestrictedWritableResource(group, "/cms_content", &models.CmsContent{}, []string{"content_type", "title", "body", "version_no", "publish_status"})
|
||||
registerRestrictedWritableResource(group, "/cs_ticket", &models.CsTicket{}, []string{"ticket_no", "category", "priority"}, requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}))
|
||||
}
|
||||
|
||||
func registerWritableResource(group *gin.RouterGroup, path string, list, create, get, update gin.HandlerFunc, model any) {
|
||||
@@ -185,12 +218,12 @@ func registerWritableResource(group *gin.RouterGroup, path string, list, create,
|
||||
resource.POST("", create)
|
||||
resource.GET("/:identity", get)
|
||||
resource.PUT("/:identity", update)
|
||||
resource.PATCH("/:identity/status", func(ctx *gin.Context) { platform.UpdateRecordStatus(ctx, model) })
|
||||
resource.DELETE("/:identity", func(ctx *gin.Context) { platform.ArchiveRecord(ctx, model) })
|
||||
resource.PATCH("/:identity/status", func(ctx *gin.Context) { common.UpdateRecordStatus(ctx, model) })
|
||||
resource.DELETE("/:identity", func(ctx *gin.Context) { common.ArchiveRecord(ctx, model) })
|
||||
}
|
||||
|
||||
func registerRestrictedWritableResource(group *gin.RouterGroup, path string, model any, fields []string, relations ...platform.ResourceRelation) {
|
||||
list, create, get, update := platform.ResourceHandlers(model, fields, fields, relations...)
|
||||
func registerRestrictedWritableResource(group *gin.RouterGroup, path string, model any, fields []string, relations ...common.ResourceRelation) {
|
||||
list, create, get, update := common.ResourceHandlers(model, fields, fields, relations...)
|
||||
registerWritableResource(group, path, list, create, get, update, model)
|
||||
}
|
||||
|
||||
@@ -200,25 +233,31 @@ func registerNoDeleteResource(group *gin.RouterGroup, path string, list, create,
|
||||
resource.POST("", create)
|
||||
resource.GET("/:identity", get)
|
||||
resource.PUT("/:identity", update)
|
||||
resource.PATCH("/:identity/status", func(ctx *gin.Context) { platform.UpdateRecordStatus(ctx, model) })
|
||||
resource.PATCH("/:identity/status", func(ctx *gin.Context) { common.UpdateRecordStatus(ctx, model) })
|
||||
}
|
||||
|
||||
func registerRestrictedNoDeleteResource(group *gin.RouterGroup, path string, model any, fields []string, relations ...platform.ResourceRelation) {
|
||||
list, create, get, update := platform.ResourceHandlers(model, fields, fields, relations...)
|
||||
func registerRestrictedNoDeleteResource(group *gin.RouterGroup, path string, model any, fields []string, relations ...common.ResourceRelation) {
|
||||
list, create, get, update := common.ResourceHandlers(model, fields, fields, relations...)
|
||||
registerNoDeleteResource(group, path, list, create, get, update, model)
|
||||
}
|
||||
|
||||
func registerReadOnlyResource(group *gin.RouterGroup, path string, model any) {
|
||||
list, _, get, _ := platform.ResourceHandlers(model, nil, nil)
|
||||
list, _, get, _ := common.ResourceHandlers(model, nil, nil)
|
||||
resource := group.Group(path)
|
||||
resource.GET("", list)
|
||||
resource.GET("/:identity", get)
|
||||
}
|
||||
|
||||
func requiredRelation(input, column string, model any) platform.ResourceRelation {
|
||||
return platform.ResourceRelation{Input: input, Column: column, Model: model, Required: true}
|
||||
func registerReadOnlyHandlers(group *gin.RouterGroup, path string, list, get gin.HandlerFunc) {
|
||||
resource := group.Group(path)
|
||||
resource.GET("", list)
|
||||
resource.GET("/:identity", get)
|
||||
}
|
||||
|
||||
func optionalRelation(input, column string, model any) platform.ResourceRelation {
|
||||
return platform.ResourceRelation{Input: input, Column: column, Model: model}
|
||||
func requiredRelation(input, column string, model any) common.ResourceRelation {
|
||||
return common.ResourceRelation{Input: input, Column: column, Model: model, Required: true}
|
||||
}
|
||||
|
||||
func optionalRelation(input, column string, model any) common.ResourceRelation {
|
||||
return common.ResourceRelation{Input: input, Column: column, Model: model}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,11 @@ func TestEveryContractHasRegisteredRoute(t *testing.T) {
|
||||
assertRouteMethods(t, routes, path+"/:identity/status", http.MethodPatch)
|
||||
assertNoRouteMethods(t, routes, path, http.MethodDelete)
|
||||
assertNoRouteMethods(t, routes, path+"/:identity", http.MethodDelete)
|
||||
case platform.Managed:
|
||||
assertRouteMethods(t, routes, path, http.MethodGet, http.MethodPost)
|
||||
assertRouteMethods(t, routes, path+"/:identity", http.MethodGet, http.MethodPut)
|
||||
assertNoRouteMethods(t, routes, path, http.MethodDelete)
|
||||
assertNoRouteMethods(t, routes, path+"/:identity", http.MethodPatch, http.MethodDelete)
|
||||
default:
|
||||
assertRouteMethods(t, routes, path, http.MethodGet, http.MethodPost)
|
||||
assertRouteMethods(t, routes, path+"/:identity", http.MethodGet, http.MethodPut, http.MethodDelete)
|
||||
@@ -51,7 +56,7 @@ func TestPlatformGasRouteUsesGasBasic(t *testing.T) {
|
||||
if route.Path == "/heqi/platform/v1/gas/gas_station" {
|
||||
t.Fatal("gas station route must use gas_basic as its resource name")
|
||||
}
|
||||
if route.Method == "GET" && route.Path == "/heqi/platform/v1/gas/gas_basic" {
|
||||
if route.Method == "GET" && route.Path == "/heqi/platform/v1/gas_basic" {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -71,26 +76,26 @@ func TestPlatformOrganizationAndAccountRoutesExposeResourceCRUD(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, resource := range []string{
|
||||
"/gas/gas_basic",
|
||||
"/gas/gas_account",
|
||||
"/delivery/delivery_basic",
|
||||
"/delivery/delivery_account",
|
||||
"/staff/account",
|
||||
"/staff/credential",
|
||||
"/user/account",
|
||||
"/user/address",
|
||||
"/user/service_relation",
|
||||
"/platform/platfrom_account",
|
||||
"/platform/platform_role",
|
||||
"/platform/platform_menu",
|
||||
"/gas_basic",
|
||||
"/gas_account",
|
||||
"/delivery_basic",
|
||||
"/delivery_account",
|
||||
"/staff_account",
|
||||
"/staff_credential",
|
||||
"/user_account",
|
||||
"/user_address",
|
||||
"/user_service_relation",
|
||||
"/platfrom_account",
|
||||
"/platform_role",
|
||||
"/platform_menu",
|
||||
} {
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource, http.MethodGet, http.MethodPost)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource+"/:identity", http.MethodGet, http.MethodPut, http.MethodDelete)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource+"/:identity/status", http.MethodPatch)
|
||||
}
|
||||
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/platform/platform_role/:identity/menu", http.MethodGet, http.MethodPut)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/platform/platform_role/:identity/menus", http.MethodPut)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/platform_role/:identity/menu", http.MethodGet, http.MethodPut)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/platform_role/:identity/menus", http.MethodPut)
|
||||
}
|
||||
|
||||
func TestPlatformProductCommerceAndDeliveryRoutesFollowTheirContracts(t *testing.T) {
|
||||
@@ -106,20 +111,30 @@ func TestPlatformProductCommerceAndDeliveryRoutesFollowTheirContracts(t *testing
|
||||
}
|
||||
|
||||
for _, resource := range []string{
|
||||
"/ec/ec_category", "/ec/ec_product", "/ec/ec_product_attribute", "/ec/ec_product_image", "/ec/ec_cart", "/ec/ec_order", "/ec/ec_order_item", "/ec/ec_review",
|
||||
"/delivery/delivery_task", "/delivery/delivery_track",
|
||||
"/ec_category", "/ec_product", "/ec_product_attribute", "/ec_product_image", "/ec_cart", "/ec_order", "/ec_order_item", "/ec_review",
|
||||
} {
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource, http.MethodGet, http.MethodPost)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource+"/:identity", http.MethodGet, http.MethodPut, http.MethodDelete)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource+"/:identity/status", http.MethodPatch)
|
||||
}
|
||||
|
||||
trackPoint := "/heqi/platform/v1/delivery/delivery_track_point"
|
||||
assertRouteMethods(t, routes, trackPoint, http.MethodGet)
|
||||
assertRouteMethods(t, routes, trackPoint+"/:identity", http.MethodGet)
|
||||
assertNoRouteMethods(t, routes, trackPoint, http.MethodPost)
|
||||
assertNoRouteMethods(t, routes, trackPoint+"/:identity", http.MethodPut, http.MethodDelete)
|
||||
assertNoRouteMethods(t, routes, trackPoint+"/:identity/status", http.MethodPatch)
|
||||
for _, resource := range []string{"/gasorder_item", "/gasorder_assign", "/gasorder_status", "/gasorder_track", "/gasorder_track_point", "/gasorder_confirm", "/gasorder_payment", "/gasorder_contract_revision"} {
|
||||
path := "/heqi/platform/v1" + resource
|
||||
assertRouteMethods(t, routes, path, http.MethodGet)
|
||||
assertRouteMethods(t, routes, path+"/:identity", http.MethodGet)
|
||||
assertNoRouteMethods(t, routes, path, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
|
||||
assertNoRouteMethods(t, routes, path+"/:identity", http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
|
||||
}
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/gasorder_contract", http.MethodGet, http.MethodPost)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/gasorder_contract/:identity", http.MethodGet, http.MethodPut)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/gasorder_basic", http.MethodGet, http.MethodPost)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/gasorder_basic/:identity", http.MethodGet)
|
||||
for _, action := range []string{"assign", "filling", "ready", "exception", "recover", "cancel"} {
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/gasorder_basic/:identity/"+action, http.MethodPost)
|
||||
}
|
||||
for _, old := range []string{"/delivery/delivery_task", "/delivery/delivery_track", "/delivery/delivery_track_point"} {
|
||||
assertNoRouteMethods(t, routes, "/heqi/platform/v1"+old, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
|
||||
}
|
||||
|
||||
for _, resource := range []string{"/product_type", "/product_warehouse", "/product_info", "/product_repair"} {
|
||||
path := "/heqi/platform/v1" + resource
|
||||
@@ -157,8 +172,8 @@ func TestPlatformFinanceContentRoutesFollowTheirContracts(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, resource := range []string{
|
||||
"/finance/fin_payment", "/finance/fin_settlement", "/finance/fin_reconciliation",
|
||||
"/content/cms_content", "/customer_service/cs_ticket",
|
||||
"/fin_payment", "/fin_settlement", "/fin_reconciliation",
|
||||
"/cms_content", "/cs_ticket",
|
||||
} {
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource, http.MethodGet, http.MethodPost)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource+"/:identity", http.MethodGet, http.MethodPut, http.MethodDelete)
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
set -euo pipefail
|
||||
mkdir -p build
|
||||
GOARCH=amd64 GOOS=linux go build -o ./build/platform-api ./cmd/main/main.go
|
||||
GOARCH=amd64 GOOS=linux go build -o ./build/platform-cli ./cmd/cli/main.go
|
||||
GOARCH=amd64 GOOS=linux go build -o ./build/platform-cli ./cmd/cli
|
||||
|
||||
Reference in New Issue
Block a user