Files
platforms/backend/api/internal/logic/common/base.go
2026-08-11 13:37:44 +08:00

277 lines
8.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package common provides shared HTTP and persistence helpers for business logic modules.
package common
import (
"errors"
"reflect"
"strings"
"unicode/utf8"
"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"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// AccountPasswordMinLength 是账号密码允许的最低字符数。
const AccountPasswordMinLength = 6
// IsValidAccountPassword 仅校验账号密码的最低字符长度。
func IsValidAccountPassword(password string) bool {
return utf8.RuneCountInString(password) >= AccountPasswordMinLength
}
// PasswordHash creates the shared password representation used by account modules.
func PasswordHash(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(hash), err
}
// 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 int `json:"status" binding:"required"`
}
if err := ctx.ShouldBindJSON(&request); err != nil || !IsGenericRecordStatus(request.Status) {
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": StatusArchived}, []string{"status"})
}
func NewEntity(status int) models.Entity {
return models.Entity{Identity: models.NewIdentity(), Status: status}
}
// IsGenericRecordStatus reports whether status belongs to the shared record lifecycle.
func IsGenericRecordStatus(status int) bool {
switch status {
case StatusDraft, StatusEnable, StatusDisable, StatusArchived, StatusFrozen:
return true
default:
return false
}
}
// ActiveRecords excludes logically archived records from operational queries.
func ActiveRecords(query *gorm.DB) *gorm.DB {
return query.Where(clause.Neq{
Column: clause.Column{Table: clause.CurrentTable, Name: "status"},
Value: StatusArchived,
})
}
func ListPage[T any](ctx *gin.Context) {
ListPageFiltered[T](ctx, nil)
}
// ListPageFiltered applies a resource-specific exact filter before pagination.
func ListPageFiltered[T any](ctx *gin.Context, filter func(*gorm.DB) *gorm.DB) {
page, size := PageSize(ctx)
var list []T
var total int64
model := new(T)
databaseQuery := ApplyKeywordFilter(ctx, ActiveRecords(impl.DBService.Model(model)), model)
if filter != nil {
databaseQuery = filter(databaseQuery)
}
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, "location_scope": true, "group_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,
"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 := ActiveRecords(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))
}
// UpdateAllowedByIdentity 按白名单更新资源,并保持原有数据库错误响应行为。
func UpdateAllowedByIdentity(ctx *gin.Context, model any, values map[string]any, allowedFields []string) {
UpdateAllowedByIdentityWithError(ctx, model, values, allowedFields, nil)
}
// UpdateAllowedByIdentityWithError 按白名单更新资源,并允许调用方转换数据库写入错误。
// 参数transformError 为空时保持原有错误响应,非空时仅转换数据库更新错误。
// 返回值:无,处理结果通过统一 HTTP 响应写入。
func UpdateAllowedByIdentityWithError(ctx *gin.Context, model any, values map[string]any, allowedFields []string, transformError func(error) error) {
values = FilterFields(values, allowedFields)
if len(values) == 0 {
infra.Response.Success(ctx, gin.H{"updated": false})
return
}
result := ActiveRecords(impl.DBService.Model(model)).Where("identity = ?", ctx.Param("identity")).Updates(values)
if result.Error != nil {
if transformError != nil {
result.Error = transformError(result.Error)
}
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 := ActiveRecords(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:]
}