feat(validation): 添加表单验证功能并集成到名称处理器

- 新增基于结构体标签的验证器实现
- 实现必填和最小值验证规则- 在名称处理器中集成验证逻辑
- 添加验证错误页面模板- 注册默认验证服务
- 创建名称输入表单模板
- 为 NewName 结构体添加验证标签
This commit is contained in:
2025-11-06 21:58:21 +08:00
parent afcc82c274
commit 61ea4a3f35
6 changed files with 161 additions and 1 deletions

View File

@@ -0,0 +1,15 @@
{{ layout "layout.html" }}
<form method="post" action="{{ . }}">
<div style="padding:5px">
<label for="name">Name:</label>
<input id="name" name="name"/>
</div>
<div style="padding:5px">
<label for="insertatstart">Insert at Front:</label>
<input id="insertatstart" name="insertatstart" type="checkbox" value="true"/>
</div>
<div style="padding:5px">
<input type="submit" value="Submit"/>
</div>
</form>

View File

@@ -5,6 +5,7 @@ import (
"platform/http/actionresults"
"platform/http/handing"
"platform/logging"
"platform/validation"
)
var names = []string{"Alice", "Bob", "charlie", "Dora"}
@@ -12,10 +13,11 @@ var names = []string{"Alice", "Bob", "charlie", "Dora"}
type NameHandler struct {
logging.Logger
handing.URLGenerator
validation.Validator
}
type NewName struct {
Name string
Name string `validation:"required,min:3"`
InsertAtStart bool
}
@@ -35,8 +37,16 @@ func (n NameHandler) GetNames() actionresults.ActionResult {
return actionresults.NewTemplateAction("simple_message.html", names)
}
func (n NameHandler) GetFOrm() actionresults.ActionResult {
postUrl, _ := n.URLGenerator.GenerateUrl(NameHandler.PostName)
return actionresults.NewTemplateAction("name_form.html", postUrl)
}
func (n NameHandler) PostName(new NewName) actionresults.ActionResult {
n.Logger.Debugf("PostName method invoked with argument: %v", new)
if ok, errs := n.Validator.Validate(&new); !ok {
return actionresults.NewTemplateAction("validation_errors.html", errs)
}
if new.InsertAtStart {
names = append([]string{new.Name}, names...)
} else {

View File

@@ -0,0 +1,8 @@
{{ layout "layout.html" }}
<h3>Validation Errors</h3>
<ul>
{{ range . }}
<li>{{ .FieldName }}:{{ .Error }}</li>
{{ end }}
</ul>

View File

@@ -4,6 +4,7 @@ import (
"platform/config"
"platform/logging"
"platform/templates"
"platform/validation"
)
func RegisterDefaultService() {
@@ -39,4 +40,12 @@ func RegisterDefaultService() {
if err != nil {
panic(err)
}
// 注册验证服务
err = AddSingleton(func() validation.Validator {
return validation.NewDefaultValidator()
})
if err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,62 @@
package validation
import (
"reflect"
"strings"
)
func NewDefaultValidator() Validator {
return NewValidator(DefaultValidators())
}
func NewValidator(validators map[string]ValidatorFunc) Validator {
return &TagValidator{
validators: validators,
}
}
type TagValidator struct {
validators map[string]ValidatorFunc
}
func (tv *TagValidator) Validate(data interface{}) (ok bool, errs []ValidatorError) {
errs = []ValidatorError{}
dataVal := reflect.ValueOf(data)
if dataVal.Kind() == reflect.Ptr {
dataVal = dataVal.Elem()
}
if dataVal.Kind() != reflect.Struct {
panic("Only structs can be validated")
}
for i := 0; i < dataVal.NumField(); i++ {
fieldType := dataVal.Type().Field(i)
validationTag, found := fieldType.Tag.Lookup("validate")
if !found {
continue
}
for _, v := range strings.Split(validationTag, ",") {
var name, arg string = "", ""
if strings.Contains(v, ":") {
nameAndArgs := strings.SplitN(v, ":", 2)
name = nameAndArgs[0]
arg = nameAndArgs[1]
} else {
name = v
}
if validator, ok := tv.validators[name]; ok {
valid, err := validator(fieldType.Name, dataVal.Field(i).Interface(), arg)
if !valid {
errs = append(errs, ValidatorError{
FieldName: fieldType.Name,
Error: err,
})
} else {
panic("Unknow validator:" + name)
}
}
}
}
ok = len(errs) == 0
return
}

View File

@@ -0,0 +1,56 @@
package validation
import (
"fmt"
"strconv"
)
type Validator interface {
Validate(data interface{}) (ok bool, errs []ValidatorError)
}
// ValidatorError 验证错误
type ValidatorError struct {
FieldName string
Error error
}
type ValidatorFunc func(fieldName string, value interface{}, arg string) (bool, error)
func DefaultValidators() map[string]ValidatorFunc {
return map[string]ValidatorFunc{
"required": required,
"min": minValue,
}
}
func required(fieldName string, value interface{}, arg string) (valid bool, err error) {
if str, ok := value.(string); ok {
valid = str != ""
err = fmt.Errorf("字段 %v 不能为空", fieldName)
return
} else {
err = fmt.Errorf("字段 %v 必须为字符串", fieldName)
}
return
}
func minValue(fieldName string, value interface{}, arg string) (valid bool, err error) {
minVal, err := strconv.Atoi(arg)
if err != nil {
err = fmt.Errorf("字段 %v 的配置参数 %v 必须为数字", fieldName, arg)
return
}
if iVal, iValOk := value.(int); iValOk {
valid = iVal >= minVal
err = fmt.Errorf("字段 %v 的值不能小于 %v", fieldName, minVal)
} else if fVal, fValOk := value.(float64); fValOk {
valid = fVal >= float64(minVal)
err = fmt.Errorf("字段 %v 的值不能小于 %v", fieldName, minVal)
} else if sVal, sValOk := value.(string); sValOk {
err = fmt.Errorf("字段 %v 的值长度不能小于 %v", fieldName, minVal)
valid = len(sVal) >= minVal
} else {
err = fmt.Errorf("字段 %v 的值类型必须为int,float,string", fieldName)
}
return
}