749 lines
23 KiB
Go
749 lines
23 KiB
Go
package platform
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.apinb.com/bsm-sdk/core/errcode"
|
|
"git.apinb.com/bsm-sdk/core/infra"
|
|
"git.apinb.com/bsm-sdk/core/middleware"
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ResourceRelation accepts a stable external identity while retaining the
|
|
// relation's database ID as an internal persistence detail.
|
|
type ResourceRelation struct {
|
|
Input string
|
|
Column string
|
|
Model any
|
|
Required bool
|
|
}
|
|
|
|
// 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) },
|
|
func(ctx *gin.Context) { createResource(ctx, model, createFields, relations) },
|
|
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)
|
|
list := reflect.New(reflect.SliceOf(reflect.TypeOf(model).Elem()))
|
|
var total int64
|
|
query := applyKeywordFilter(ctx, impl.DBService.Model(model), model)
|
|
if err := query.Count(&total).Error; err != nil {
|
|
infra.Response.Error(ctx, err)
|
|
return
|
|
}
|
|
if err := query.Order("created_at desc").Offset((page - 1) * size).Limit(size).Find(list.Interface()).Error; err != nil {
|
|
infra.Response.Error(ctx, err)
|
|
return
|
|
}
|
|
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)})
|
|
}
|
|
|
|
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)
|
|
return
|
|
}
|
|
response, err := publicResourceResponse(data.Interface())
|
|
if err != nil {
|
|
infra.Response.Error(ctx, err)
|
|
return
|
|
}
|
|
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)
|
|
if err != nil {
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
return
|
|
}
|
|
encoded, err := json.Marshal(values)
|
|
if err != nil {
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
return
|
|
}
|
|
data := reflect.New(reflect.TypeOf(model).Elem())
|
|
if err := json.Unmarshal(encoded, data.Interface()); err != nil {
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
return
|
|
}
|
|
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())
|
|
}
|
|
|
|
func respondCreatedResource(ctx *gin.Context, value any) {
|
|
response, err := publicResourceResponse(value)
|
|
if err != nil {
|
|
infra.Response.Error(ctx, err)
|
|
return
|
|
}
|
|
infra.Response.Success(ctx, maskCreatedSensitiveFields(response))
|
|
}
|
|
|
|
func maskCreatedSensitiveFields(value any) any {
|
|
switch data := value.(type) {
|
|
case map[string]any:
|
|
safe := make(map[string]any)
|
|
for key, item := range data {
|
|
if isCreatedResponseField(key) {
|
|
safe[key] = maskCreatedSensitiveFields(item)
|
|
}
|
|
}
|
|
return safe
|
|
case []any:
|
|
safe := make([]any, len(data))
|
|
for index, item := range data {
|
|
safe[index] = maskCreatedSensitiveFields(item)
|
|
}
|
|
return safe
|
|
}
|
|
return value
|
|
}
|
|
|
|
func isCreatedResponseField(key string) bool {
|
|
switch key {
|
|
case "identity", "status", "version", "created_at", "updated_at":
|
|
return true
|
|
default:
|
|
return strings.HasSuffix(key, "_identity")
|
|
}
|
|
}
|
|
|
|
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))
|
|
return value
|
|
}
|
|
|
|
var sensitiveResponseFields = map[string]bool{
|
|
"avatar": true, "address": true, "credential_no": true,
|
|
"evidence_uri": true, "evidence_url": true, "file_uri": true,
|
|
"attachment_uri": true, "attachment_url": true,
|
|
"certificate_uri": true, "certificate_url": true,
|
|
"credential_uri": true, "credential_url": true,
|
|
}
|
|
|
|
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)
|
|
}
|
|
delete(data, "phone")
|
|
for key := range sensitiveResponseFields {
|
|
delete(data, key)
|
|
}
|
|
if maskPersonalName {
|
|
if name, ok := data["name"].(string); ok && name != "" {
|
|
data["name_masked"] = maskPersonalNameValue(name)
|
|
}
|
|
delete(data, "name")
|
|
if name, ok := data["real_name"].(string); ok && 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)
|
|
}
|
|
delete(data, "display_name")
|
|
}
|
|
if !retainCoordinates {
|
|
delete(data, "longitude")
|
|
delete(data, "latitude")
|
|
}
|
|
for _, item := range data {
|
|
protectPublicFields(item, maskPersonalName, maskDisplayName, retainCoordinates)
|
|
}
|
|
case []any:
|
|
for _, item := range data {
|
|
protectPublicFields(item, maskPersonalName, maskDisplayName, retainCoordinates)
|
|
}
|
|
}
|
|
}
|
|
|
|
func maskPersonalNameValue(name string) string {
|
|
runes := []rune(name)
|
|
if len(runes) == 0 {
|
|
return ""
|
|
}
|
|
if len(runes) == 1 {
|
|
return "*"
|
|
}
|
|
return string(runes[0]) + strings.Repeat("*", len(runes)-1)
|
|
}
|
|
|
|
func hasPreciseLocationScope(ctx *gin.Context) bool {
|
|
claims, err := middleware.ParseAuth(ctx)
|
|
return err == nil && claims.Extend["location_scope"] == "precise"
|
|
}
|
|
|
|
func updateResource(ctx *gin.Context, model any, allowedFields []string, relations []ResourceRelation) {
|
|
var input map[string]any
|
|
if err := ctx.ShouldBindJSON(&input); err != nil {
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
return
|
|
}
|
|
values, err := resolveResourceRelations(input, allowedFields, relations, false)
|
|
if err != nil || normalizeStringJSONBFields(model, values) != nil {
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
return
|
|
}
|
|
if len(values) == 0 {
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
return
|
|
}
|
|
updateAllowedByIdentity(ctx, model, values, append(allowedFields, relationColumns(relations)...))
|
|
}
|
|
|
|
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)
|
|
if err != nil || normalizeStringJSONBFields(model, values) != nil || len(values) == 0 {
|
|
return nil, errors.New("invalid resource payload")
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
func normalizeStringJSONBFields(model any, values map[string]any) error {
|
|
modelType := reflect.TypeOf(model)
|
|
for modelType.Kind() == reflect.Pointer {
|
|
modelType = modelType.Elem()
|
|
}
|
|
for index := 0; index < modelType.NumField(); index++ {
|
|
field := modelType.Field(index)
|
|
if field.Type.Kind() != reflect.String || !strings.Contains(field.Tag.Get("gorm"), "type:jsonb") {
|
|
continue
|
|
}
|
|
column := gormColumn(field.Tag.Get("gorm"))
|
|
value, exists := values[column]
|
|
if !exists {
|
|
continue
|
|
}
|
|
if text, ok := value.(string); ok {
|
|
if !json.Valid([]byte(text)) {
|
|
return errors.New("invalid jsonb string")
|
|
}
|
|
continue
|
|
}
|
|
encoded, err := json.Marshal(value)
|
|
if err != nil || !json.Valid(encoded) {
|
|
return errors.New("invalid jsonb value")
|
|
}
|
|
values[column] = string(encoded)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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 {
|
|
if requireRelations && relation.Required {
|
|
return nil, errors.New("missing required relation")
|
|
}
|
|
continue
|
|
}
|
|
identity, ok := raw.(string)
|
|
if !ok || strings.TrimSpace(identity) == "" {
|
|
return nil, errors.New("invalid relation identity")
|
|
}
|
|
id, err := resolveIdentityID(relation.Model, identity, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
values[relation.Column] = id
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
// resolveIdentityID is the only boundary that converts a public identity to a
|
|
// persistence-only numeric key. Callers must never bind a client supplied ID.
|
|
func resolveIdentityID(model any, identity string, required bool) (uint64, error) {
|
|
identity = strings.TrimSpace(identity)
|
|
if identity == "" {
|
|
if required {
|
|
return 0, errors.New("missing required relation")
|
|
}
|
|
return 0, nil
|
|
}
|
|
var related struct{ ID uint64 }
|
|
if err := impl.DBService.Model(model).Select("id").Where("identity = ?", identity).First(&related).Error; err != nil {
|
|
return 0, err
|
|
}
|
|
return related.ID, nil
|
|
}
|
|
|
|
func relationColumns(relations []ResourceRelation) []string {
|
|
columns := make([]string, 0, len(relations))
|
|
for _, relation := range relations {
|
|
columns = append(columns, relation.Column)
|
|
}
|
|
return columns
|
|
}
|
|
|
|
// resourceResponse strips database surrogate IDs from API data. Business
|
|
// identities are the only public relation keys accepted or returned.
|
|
func resourceResponse(value any) any {
|
|
encoded, err := json.Marshal(value)
|
|
if err != nil {
|
|
return value
|
|
}
|
|
var decoded any
|
|
if err := json.Unmarshal(encoded, &decoded); err != nil {
|
|
return value
|
|
}
|
|
return stripInternalIDs(decoded)
|
|
}
|
|
|
|
// 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) {
|
|
encoded, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var decoded any
|
|
if err := json.Unmarshal(encoded, &decoded); err != nil {
|
|
return nil, err
|
|
}
|
|
return projectRelationIdentities(decoded)
|
|
}
|
|
|
|
var relationIdentityModels = map[string]any{
|
|
"gas_basic_id": &models.GasBasic{},
|
|
"gas_station_id": &models.GasBasic{},
|
|
"delivery_basic_id": &models.DeliveryBasic{},
|
|
"delivery_point_id": &models.DeliveryBasic{},
|
|
"user_account_id": &models.UserAccount{},
|
|
"staff_account_id": &models.StaffAccount{},
|
|
"smart_cylinder_valve_id": &models.DevSmartCylinderValve{},
|
|
"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{},
|
|
"platform_role_id": &models.PlatformRole{},
|
|
"platform_menu_id": &models.PlatformMenu{},
|
|
"report_id": &models.Report{},
|
|
"wallet_id": &models.Wallet{},
|
|
}
|
|
|
|
var relationIdentityKeys = map[string]string{
|
|
"gas_station_id": "gas_basic_identity",
|
|
"delivery_point_id": "delivery_basic_identity",
|
|
}
|
|
|
|
type relationIdentityReference struct {
|
|
target map[string]any
|
|
identityKey string
|
|
id uint64
|
|
}
|
|
|
|
type relationIdentityGroup struct {
|
|
model any
|
|
ids []uint64
|
|
seen map[uint64]struct{}
|
|
references []relationIdentityReference
|
|
}
|
|
|
|
type relationIdentityRecord struct {
|
|
ID uint64
|
|
Identity string
|
|
}
|
|
|
|
func projectRelationIdentities(value any) (any, error) {
|
|
groups := map[string]*relationIdentityGroup{}
|
|
collectRelationIdentityReferences(value, groups)
|
|
groupKeys := make([]string, 0, len(groups))
|
|
for key := range groups {
|
|
groupKeys = append(groupKeys, key)
|
|
}
|
|
sort.Strings(groupKeys)
|
|
for _, key := range groupKeys {
|
|
group := groups[key]
|
|
var rows []relationIdentityRecord
|
|
if err := impl.DBService.Model(group.model).Select("id", "identity").Where("id IN ?", group.ids).Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
identities := make(map[uint64]string, len(rows))
|
|
for _, row := range rows {
|
|
identities[row.ID] = row.Identity
|
|
}
|
|
for _, reference := range group.references {
|
|
identity, found := identities[reference.id]
|
|
if !found {
|
|
return nil, errors.New("related identity not found")
|
|
}
|
|
reference.target[reference.identityKey] = identity
|
|
}
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func collectRelationIdentityReferences(value any, groups map[string]*relationIdentityGroup) {
|
|
switch data := value.(type) {
|
|
case map[string]any:
|
|
for key, item := range data {
|
|
if key == "id" {
|
|
delete(data, key)
|
|
continue
|
|
}
|
|
if strings.HasSuffix(key, "_id") {
|
|
identityKey := strings.TrimSuffix(key, "_id") + "_identity"
|
|
if alias := relationIdentityKeys[key]; alias != "" {
|
|
identityKey = alias
|
|
}
|
|
model := relationIdentityModels[key]
|
|
if key == "subject_id" {
|
|
model = settlementSubjectModel(data["subject_type"])
|
|
identityKey = "subject_identity"
|
|
}
|
|
if model != nil {
|
|
if id, ok := responseRelationID(item); ok && id != 0 {
|
|
key := reflect.TypeOf(model).String()
|
|
group := groups[key]
|
|
if group == nil {
|
|
group = &relationIdentityGroup{model: model, seen: map[uint64]struct{}{}}
|
|
groups[key] = group
|
|
}
|
|
if _, found := group.seen[id]; !found {
|
|
group.ids = append(group.ids, id)
|
|
group.seen[id] = struct{}{}
|
|
}
|
|
group.references = append(group.references, relationIdentityReference{target: data, identityKey: identityKey, id: id})
|
|
} else {
|
|
data[identityKey] = ""
|
|
}
|
|
}
|
|
delete(data, key)
|
|
continue
|
|
}
|
|
collectRelationIdentityReferences(item, groups)
|
|
}
|
|
case []any:
|
|
for _, item := range data {
|
|
collectRelationIdentityReferences(item, groups)
|
|
}
|
|
}
|
|
}
|
|
|
|
func responseRelationID(value any) (uint64, bool) {
|
|
var id uint64
|
|
switch raw := value.(type) {
|
|
case float64:
|
|
id = uint64(raw)
|
|
case uint64:
|
|
id = raw
|
|
case int:
|
|
id = uint64(raw)
|
|
default:
|
|
return 0, false
|
|
}
|
|
return id, true
|
|
}
|
|
|
|
func settlementSubjectModel(value any) any {
|
|
subjectType, _ := value.(string)
|
|
switch subjectType {
|
|
case "gas", "gas_basic":
|
|
return &models.GasBasic{}
|
|
case "delivery", "delivery_basic":
|
|
return &models.DeliveryBasic{}
|
|
case "staff", "staff_account":
|
|
return &models.StaffAccount{}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func stripInternalIDs(value any) any {
|
|
switch data := value.(type) {
|
|
case map[string]any:
|
|
for key, item := range data {
|
|
if key == "id" || strings.HasSuffix(key, "_id") {
|
|
delete(data, key)
|
|
continue
|
|
}
|
|
data[key] = stripInternalIDs(item)
|
|
}
|
|
case []any:
|
|
for index := range data {
|
|
data[index] = stripInternalIDs(data[index])
|
|
}
|
|
}
|
|
return value
|
|
}
|
|
|
|
// DisposeSafetyEvent atomically updates an event and appends its operator-owned
|
|
// action record. Disposal records deliberately have no update or delete route.
|
|
func ListSafetyEventDisposals(ctx *gin.Context) {
|
|
page, size := pageSize(ctx)
|
|
var list []models.SafeEventDisposal
|
|
query := impl.DBService.Model(&models.SafeEventDisposal{}).Where("safe_event_identity = ?", ctx.Param("identity"))
|
|
var total int64
|
|
if err := query.Count(&total).Error; err != nil {
|
|
infra.Response.Error(ctx, err)
|
|
return
|
|
}
|
|
if err := query.Order("created_at asc").Offset((page - 1) * size).Limit(size).Find(&list).Error; err != nil {
|
|
infra.Response.Error(ctx, err)
|
|
return
|
|
}
|
|
infra.Response.Success(ctx, gin.H{"total": total, "list": resourceResponse(list)})
|
|
}
|
|
|
|
func DisposeSafetyEvent(ctx *gin.Context) {
|
|
claims, err := middleware.ParseAuth(ctx)
|
|
if err != nil {
|
|
infra.Response.Error(ctx, err)
|
|
return
|
|
}
|
|
var request struct {
|
|
Action string `json:"action" binding:"required,max=64"`
|
|
Reason string `json:"reason" binding:"max=2000"`
|
|
Status string `json:"status" binding:"max=32"`
|
|
}
|
|
if err := ctx.ShouldBindJSON(&request); err != nil {
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
return
|
|
}
|
|
if request.Status == "" {
|
|
request.Status = "disposed"
|
|
}
|
|
var disposal models.SafeEventDisposal
|
|
err = impl.DBService.Transaction(func(transaction *gorm.DB) error {
|
|
var event models.SafeEvent
|
|
if err := transaction.Where("identity = ?", ctx.Param("identity")).First(&event).Error; err != nil {
|
|
return err
|
|
}
|
|
if result := transaction.Model(&models.SafeEvent{}).Where("identity = ?", event.Identity).Update("status", request.Status); result.Error != nil {
|
|
return result.Error
|
|
} else if result.RowsAffected == 0 {
|
|
return gorm.ErrRecordNotFound
|
|
}
|
|
disposal = models.SafeEventDisposal{
|
|
Entity: newEntity("enabled"),
|
|
SafeEventIdentity: event.Identity,
|
|
Action: request.Action,
|
|
Reason: request.Reason,
|
|
OperatorIdentity: claims.Identity,
|
|
}
|
|
return transaction.Create(&disposal).Error
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
|
return
|
|
}
|
|
infra.Response.Error(ctx, err)
|
|
return
|
|
}
|
|
infra.Response.Success(ctx, resourceResponse(disposal))
|
|
}
|
|
|
|
// 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])
|
|
}
|