refactor platform logic and add gasorder domain

This commit is contained in:
david
2026-07-28 10:42:26 +08:00
parent 8c6d52cf8c
commit 899fc6186b
44 changed files with 1636 additions and 1446 deletions

View File

@@ -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>")
}

View File

@@ -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})
}

View File

@@ -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 "***"
}

View File

@@ -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])
}

View 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")
}
}

View File

@@ -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"})
}

View 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])
}

View 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)
}

View 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
}

View File

@@ -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"})
}

View File

@@ -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"})
}

View 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) }

View File

@@ -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)
}
}
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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"})
}

View File

@@ -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"})
}

View 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)
}
}

View File

@@ -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"})
}

View File

@@ -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})

View File

@@ -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" }

View File

@@ -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" }

View File

@@ -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" }

View 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" }

View 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" }

View 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" }

View 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" }

View 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" }

View 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" }

View 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" }

View 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" }

View 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" }

View 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" }

View 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" }

View File

@@ -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}
}

View File

@@ -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)

View File

@@ -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