// Package platform 提供平台总后台的同步 HTTP 业务逻辑。 package common import ( "errors" "reflect" "strings" "git.apinb.com/bsm-sdk/core/errcode" "git.apinb.com/bsm-sdk/core/infra" "git.apinb.com/bsm-sdk/core/utils" "git.apinb.com/heqiapp/platforms/backend/api/internal/impl" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // 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 { Status string `json:"status" binding:"required,max=32"` } if err := ctx.ShouldBindJSON(&request); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } UpdateAllowedByIdentity(ctx, model, gin.H{"status": request.Status}, []string{"status"}) } // ArchiveRecord 通过 archived 状态实现逻辑删除,不物理删除主数据。 func ArchiveRecord(ctx *gin.Context, model any) { UpdateAllowedByIdentity(ctx, model, gin.H{"status": "archived"}, []string{"status"}) } 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) var list []T var total int64 model := new(T) databaseQuery := ApplyKeywordFilter(ctx, impl.DBService.Model(model), model) if err := databaseQuery.Count(&total).Error; err != nil { infra.Response.Error(ctx, err) return } if err := databaseQuery.Order("created_at desc").Offset((page - 1) * size).Limit(size).Find(&list).Error; err != nil { infra.Response.Error(ctx, err) return } 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)}) } var keywordSafeColumns = map[string]bool{ "code": true, "name": true, "username": true, "display_name": true, "role_code": true, "delivery_code": true, "work_status": true, "credential_type": true, "device_no": true, "model": true, "online_status": true, "rule_code": true, "action": true, "event_code": true, "title": true, "result": true, "product_code": true, "value": true, "order_no": true, "channel": true, "settlement_no": true, "subject_type": true, "content_type": true, "publish_status": true, "template_code": true, "ticket_no": true, "category": true, "priority": true, "platform_role_code": true, "data_scope": true, "menu_code": true, "path": true, "resource_type": true, "owner_type": true, "owner_identity": true, "payment_no": true, "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 { keyword := strings.ToLower(strings.TrimSpace(ctx.Query("keyword"))) if keyword == "" { return query } columns := keywordColumns(model) if len(columns) == 0 { return query } conditions := make([]string, 0, len(columns)) arguments := make([]any, 0, len(columns)) for _, column := range columns { conditions = append(conditions, `LOWER("`+column+`") LIKE ?`) arguments = append(arguments, "%"+keyword+"%") } return query.Where("("+strings.Join(conditions, " OR ")+")", arguments...) } func keywordColumns(model any) []string { modelType := reflect.TypeOf(model) for modelType.Kind() == reflect.Pointer { modelType = modelType.Elem() } columns := make([]string, 0) for index := 0; index < modelType.NumField(); index++ { field := modelType.Field(index) if field.Anonymous || field.Type.Kind() != reflect.String { continue } column := gormColumn(field.Tag.Get("gorm")) if keywordSafeColumns[column] && !isSensitiveKeywordColumn(model, column) { columns = append(columns, column) } } return columns } func isSensitiveKeywordColumn(model any, column string) bool { switch model.(type) { case *models.UserAccount, *models.StaffAccount: return column == "name" default: return false } } func gormColumn(tag string) string { for _, part := range strings.Split(tag, ";") { if strings.HasPrefix(part, "column:") { return strings.TrimPrefix(part, "column:") } } return "" } 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) return } response, err := PublicResourceResponse(data) if err != nil { infra.Response.Error(ctx, err) return } 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) if len(values) == 0 { infra.Response.Success(ctx, gin.H{"updated": false}) return } result := impl.DBService.Model(model).Where("identity = ?", ctx.Param("identity")).Updates(values) if result.Error != nil { infra.Response.Error(ctx, result.Error) return } if result.RowsAffected == 0 { infra.Response.Error(ctx, errcode.ErrRecordNotFound) return } infra.Response.Success(ctx, gin.H{"updated": true}) } 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) return } if result.RowsAffected == 0 { infra.Response.Error(ctx, errcode.ErrRecordNotFound) return } infra.Response.Success(ctx, gin.H{"updated": true}) } func RespondRecordError(ctx *gin.Context, err error) { if errors.Is(err, gorm.ErrRecordNotFound) { infra.Response.Error(ctx, errcode.ErrRecordNotFound) return } infra.Response.Error(ctx, err) } func PageSize(ctx *gin.Context) (int, int) { page := utils.String2Int(ctx.DefaultQuery("page", "1")) size := utils.String2Int(ctx.DefaultQuery("size", "20")) if page < 1 { page = 1 } if size < 1 || size > 100 { size = 20 } return page, size } func MaskPhone(phone string) string { if len(phone) < 7 { return "***" } return phone[:3] + "****" + phone[len(phone)-4:] }