Эх сурвалжийг харах

feat(validator): 添加验证引擎和CEL表达式支持

- 新增业务验证函数,包括手机号、身份证、姓名、邮编、社会信用代码和银行卡号验证
- 实现CEL表达式执行器,支持表达式缓存和多种求值方式
- 添加验证上下文管理,支持验证组、策略、语言和角色控制- 创建验证引擎,集成字段、消息和oneof验证逻辑- 支持基于protobuf扩展的声明式验证规则
- 实现错误收集和国际化支持
- 添加并发安全的表达式缓存机制
- 支持快速失败和最大错误数限制策略
gms 1 сар өмнө
parent
commit
a76125a5e2

+ 7 - 2
go.mod

@@ -1,10 +1,10 @@
 module git.ikuban.com/server/kratos-utils/v2
 
-go 1.23.0
+go 1.23.2
 
 require (
 	dario.cat/mergo v1.0.0
-	git.ikuban.com/server/kubanapis v1.0.4
+	git.ikuban.com/server/kubanapis v1.0.4-0.20251030015030-bf45b22fe12b
 	git.ikuban.com/server/yaml v0.0.0-20220411094446-ff9c47c8eeaf
 	github.com/bytedance/sonic v1.13.3
 	github.com/dcsunny/gocrypt v0.0.0-20200828060317-4dec5212cc15
@@ -13,6 +13,7 @@ require (
 	github.com/elliotchance/orderedmap/v3 v3.1.0
 	github.com/go-kratos/kratos/v2 v2.8.4
 	github.com/go-resty/resty/v2 v2.7.0
+	github.com/google/cel-go v0.26.1
 	github.com/google/gnostic v0.7.0
 	github.com/google/uuid v1.6.0
 	github.com/jhump/protoreflect v1.17.0
@@ -27,6 +28,8 @@ require (
 )
 
 require (
+	cel.dev/expr v0.24.0 // indirect
+	github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
 	github.com/bufbuild/protocompile v0.14.1 // indirect
 	github.com/bytedance/sonic/loader v0.2.4 // indirect
 	github.com/cloudwego/base64x v0.1.5 // indirect
@@ -50,6 +53,7 @@ require (
 	github.com/lestrrat-go/option v1.0.0 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/spf13/cast v1.7.1 // indirect
+	github.com/stoewer/go-strcase v1.2.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
 	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
@@ -59,6 +63,7 @@ require (
 	go.uber.org/zap v1.27.0 // indirect
 	golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
 	golang.org/x/crypto v0.36.0 // indirect
+	golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
 	golang.org/x/net v0.38.0 // indirect
 	golang.org/x/sys v0.31.0 // indirect
 	golang.org/x/text v0.23.0 // indirect

+ 11 - 4
go.sum

@@ -1,5 +1,5 @@
-cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4=
-cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
+cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
+cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -597,8 +597,8 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
 dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
-git.ikuban.com/server/kubanapis v1.0.4 h1:FC+G0LZTb0ldFL5rBO5gHaHDbABz3gQit4gswGzX2fE=
-git.ikuban.com/server/kubanapis v1.0.4/go.mod h1:9qkIT289Wy0d7QQH1sooflxEmfYYXYv1rso8kvfYuIo=
+git.ikuban.com/server/kubanapis v1.0.4-0.20251030015030-bf45b22fe12b h1:OWHfu5WtIlfF3Ow0PuZbieadpxGnSB4UTNp/M6LJlew=
+git.ikuban.com/server/kubanapis v1.0.4-0.20251030015030-bf45b22fe12b/go.mod h1:b5gSB2UCDTDEKb7+nqSCONPt6Cmo7/2N+d7puBD8bos=
 git.ikuban.com/server/yaml v0.0.0-20220411094446-ff9c47c8eeaf h1:fqbBaasBkDOOUIGVe1ikz/dHzTLP9e0HayM4NhvhvXs=
 git.ikuban.com/server/yaml v0.0.0-20220411094446-ff9c47c8eeaf/go.mod h1:eovuz52s0DRORmHHZrvkUpRUwDMzFCeKQEm6GwiHOwM=
 git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
@@ -612,6 +612,8 @@ github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3
 github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
 github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
+github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
 github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
 github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI=
 github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
@@ -770,6 +772,8 @@ github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
+github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
 github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
 github.com/google/gnostic v0.7.0 h1:d7EpuFp8vVdML+y0JJJYiKeOLjKTdH/GvVkLOBWqJpw=
 github.com/google/gnostic v0.7.0/go.mod h1:IAcUyMl6vtC95f60EZ8oXyqTsOersP6HbwjeG7EyDPM=
@@ -923,6 +927,7 @@ github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z
 github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
 github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
 github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
 github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -1023,6 +1028,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
+golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
+golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=

+ 106 - 0
transport/middleware/validator/middleware.go

@@ -0,0 +1,106 @@
+package validator
+
+import (
+	"context"
+
+	"github.com/go-kratos/kratos/v2/middleware"
+	"google.golang.org/protobuf/proto"
+
+	"git.ikuban.com/server/kratos-utils/v2/validator"
+)
+
+// Validator 是验证器中间件选项
+type Option func(*options)
+
+type options struct {
+	validator validator.Validator
+	groups    []string
+}
+
+// WithValidator 设置验证器
+func WithValidator(v validator.Validator) Option {
+	return func(o *options) {
+		o.validator = v
+	}
+}
+
+// WithGroups 设置验证组
+func WithGroups(groups ...string) Option {
+	return func(o *options) {
+		o.groups = groups
+	}
+}
+
+// Server 是服务端验证中间件
+func Server(opts ...Option) middleware.Middleware {
+	o := &options{}
+	for _, opt := range opts {
+		opt(o)
+	}
+
+	// 如果没有指定验证器,创建默认验证器
+	if o.validator == nil {
+		v, err := validator.New()
+		if err != nil {
+			panic(err)
+		}
+		o.validator = v
+	}
+
+	return func(handler middleware.Handler) middleware.Handler {
+		return func(ctx context.Context, req interface{}) (interface{}, error) {
+			// 检查请求是否为 proto.Message
+			if msg, ok := req.(proto.Message); ok {
+				// 如果设置了验证组,使用验证组验证
+				if len(o.groups) > 0 {
+					ctx = validator.WithGroups(ctx, o.groups...)
+				}
+
+				// 执行验证
+				if err := o.validator.Validate(ctx, msg); err != nil {
+					return nil, err
+				}
+			}
+
+			// 继续处理请求
+			return handler(ctx, req)
+		}
+	}
+}
+
+// Client 是客户端验证中间件
+func Client(opts ...Option) middleware.Middleware {
+	o := &options{}
+	for _, opt := range opts {
+		opt(o)
+	}
+
+	// 如果没有指定验证器,创建默认验证器
+	if o.validator == nil {
+		v, err := validator.New()
+		if err != nil {
+			panic(err)
+		}
+		o.validator = v
+	}
+
+	return func(handler middleware.Handler) middleware.Handler {
+		return func(ctx context.Context, req interface{}) (interface{}, error) {
+			// 检查请求是否为 proto.Message
+			if msg, ok := req.(proto.Message); ok {
+				// 如果设置了验证组,使用验证组验证
+				if len(o.groups) > 0 {
+					ctx = validator.WithGroups(ctx, o.groups...)
+				}
+
+				// 执行验证
+				if err := o.validator.Validate(ctx, msg); err != nil {
+					return nil, err
+				}
+			}
+
+			// 继续处理请求
+			return handler(ctx, req)
+		}
+	}
+}

+ 161 - 0
validator/business.go

@@ -0,0 +1,161 @@
+package validator
+
+import (
+	"regexp"
+	"strconv"
+	"strings"
+	"unicode"
+)
+
+// isValidChineseMobile 验证中国手机号
+func isValidChineseMobile(mobile string) bool {
+	// 中国手机号: 1 + (3-9) + 9位数字
+	pattern := `^1[3-9]\d{9}$`
+	matched, _ := regexp.MatchString(pattern, mobile)
+	return matched
+}
+
+// isValidChineseIDCard 验证中国身份证号
+func isValidChineseIDCard(idCard string) bool {
+	// 支持15位和18位
+	if len(idCard) != 15 && len(idCard) != 18 {
+		return false
+	}
+
+	if len(idCard) == 18 {
+		return isValid18IDCard(idCard)
+	}
+
+	// 15位身份证验证(旧版)
+	pattern := `^\d{15}$`
+	matched, _ := regexp.MatchString(pattern, idCard)
+	return matched
+}
+
+// isValid18IDCard 验证18位身份证
+func isValid18IDCard(idCard string) bool {
+	// 基本格式验证: 6位地区码 + 8位出生日期 + 3位顺序码 + 1位校验码
+	pattern := `^\d{17}[\dXx]$`
+	matched, _ := regexp.MatchString(pattern, idCard)
+	if !matched {
+		return false
+	}
+
+	// 校验位验证(简化版)
+	// 权重因子
+	weights := []int{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}
+	// 校验码
+	checkCodes := []string{"1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"}
+
+	sum := 0
+	for i := 0; i < 17; i++ {
+		digit, _ := strconv.Atoi(string(idCard[i]))
+		sum += digit * weights[i]
+	}
+
+	checkCode := checkCodes[sum%11]
+	lastChar := strings.ToUpper(string(idCard[17]))
+
+	return checkCode == lastChar
+}
+
+// isValidChineseName 验证中文姓名
+func isValidChineseName(name string) bool {
+	if name == "" {
+		return false
+	}
+
+	// 长度检查: 2-20个字符
+	runeCount := len([]rune(name))
+	if runeCount < 2 || runeCount > 20 {
+		return false
+	}
+
+	// 检查是否包含中文字符
+	hasChinese := false
+	for _, r := range name {
+		if unicode.Is(unicode.Han, r) {
+			hasChinese = true
+		}
+	}
+
+	if !hasChinese {
+		return false
+	}
+
+	// 允许中文、·(间隔号,用于少数民族姓名)
+	pattern := `^[\p{Han}·]+$`
+	matched, _ := regexp.MatchString(pattern, name)
+	return matched
+}
+
+// isValidChinesePostcode 验证中国邮政编码
+func isValidChinesePostcode(postcode string) bool {
+	// 中国邮政编码: 6位数字
+	pattern := `^\d{6}$`
+	matched, _ := regexp.MatchString(pattern, postcode)
+	return matched
+}
+
+// isValidUSCC 验证统一社会信用代码
+func isValidUSCC(uscc string) bool {
+	// 统一社会信用代码: 18位
+	if len(uscc) != 18 {
+		return false
+	}
+
+	// 基本格式验证
+	pattern := `^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$`
+	matched, _ := regexp.MatchString(pattern, uscc)
+	if !matched {
+		return false
+	}
+
+	// TODO: 添加校验位算法验证
+
+	return true
+}
+
+// isValidBankCard 验证银行卡号(Luhn 算法)
+func isValidBankCard(cardNumber string) bool {
+	// 移除空格
+	cardNumber = strings.ReplaceAll(cardNumber, " ", "")
+
+	// 长度检查: 13-19位
+	if len(cardNumber) < 13 || len(cardNumber) > 19 {
+		return false
+	}
+
+	// 必须全是数字
+	for _, r := range cardNumber {
+		if !unicode.IsDigit(r) {
+			return false
+		}
+	}
+
+	// Luhn 算法验证
+	return luhnCheck(cardNumber)
+}
+
+// luhnCheck Luhn 算法校验
+func luhnCheck(cardNumber string) bool {
+	sum := 0
+	alternate := false
+
+	// 从右往左遍历
+	for i := len(cardNumber) - 1; i >= 0; i-- {
+		digit, _ := strconv.Atoi(string(cardNumber[i]))
+
+		if alternate {
+			digit *= 2
+			if digit > 9 {
+				digit -= 9
+			}
+		}
+
+		sum += digit
+		alternate = !alternate
+	}
+
+	return sum%10 == 0
+}

+ 226 - 0
validator/cel_executor.go

@@ -0,0 +1,226 @@
+package validator
+
+import (
+	"fmt"
+	"sync"
+
+	"github.com/google/cel-go/cel"
+	"github.com/google/cel-go/common/types"
+	"github.com/google/cel-go/common/types/ref"
+	"google.golang.org/protobuf/proto"
+)
+
+// CELExecutor CEL 表达式执行器
+type CELExecutor struct {
+	mu    sync.RWMutex
+	cache map[string]cel.Program // 表达式缓存 (cel.Program 是接口,不需要指针)
+	env   *cel.Env               // CEL 环境
+}
+
+// NewCELExecutor 创建 CEL 执行器
+func NewCELExecutor() (*CELExecutor, error) {
+	// 创建 CEL 环境 (v0.26+ 不需要显式注册基础类型)
+	env, err := cel.NewEnv()
+	if err != nil {
+		return nil, fmt.Errorf("failed to create CEL environment: %w", err)
+	}
+
+	return &CELExecutor{
+		cache: make(map[string]cel.Program),
+		env:   env,
+	}, nil
+}
+
+// Evaluate 执行 CEL 表达式
+func (e *CELExecutor) Evaluate(expression string, data map[string]interface{}) (bool, error) {
+	// 获取或编译程序
+	prog, err := e.getProgram(expression)
+	if err != nil {
+		return false, fmt.Errorf("failed to compile CEL expression: %w", err)
+	}
+
+	// 执行表达式
+	result, _, err := prog.Eval(data)
+	if err != nil {
+		return false, fmt.Errorf("failed to evaluate CEL expression: %w", err)
+	}
+
+	// 检查结果类型
+	boolResult, ok := result.Value().(bool)
+	if !ok {
+		return false, fmt.Errorf("CEL expression must return boolean, got %T", result.Value())
+	}
+
+	return boolResult, nil
+}
+
+// EvaluateWithProto 使用 Proto 消息执行 CEL 表达式
+func (e *CELExecutor) EvaluateWithProto(expression string, msg proto.Message) (bool, error) {
+	// 获取或编译程序
+	prog, err := e.getProgram(expression)
+	if err != nil {
+		return false, fmt.Errorf("failed to compile CEL expression: %w", err)
+	}
+
+	// 将 Proto 消息转换为 CEL 变量
+	data := map[string]interface{}{
+		"this": msg,
+	}
+
+	// 执行表达式
+	result, _, err := prog.Eval(data)
+	if err != nil {
+		return false, fmt.Errorf("failed to evaluate CEL expression: %w", err)
+	}
+
+	// 检查结果类型
+	boolResult, ok := result.Value().(bool)
+	if !ok {
+		return false, fmt.Errorf("CEL expression must return boolean, got %T", result.Value())
+	}
+
+	return boolResult, nil
+}
+
+// EvaluateFieldRule 执行字段级 CEL 规则
+func (e *CELExecutor) EvaluateFieldRule(expression string, fieldValue interface{}, parent proto.Message) (bool, error) {
+	// 获取或编译程序
+	prog, err := e.getProgram(expression)
+	if err != nil {
+		return false, fmt.Errorf("failed to compile CEL expression: %w", err)
+	}
+
+	// 准备数据
+	data := map[string]interface{}{
+		"this": fieldValue,
+	}
+
+	// 如果有父消息,也添加进去(用于跨字段验证)
+	if parent != nil {
+		data["parent"] = parent
+	}
+
+	// 执行表达式
+	result, _, err := prog.Eval(data)
+	if err != nil {
+		return false, fmt.Errorf("failed to evaluate CEL expression: %w", err)
+	}
+
+	// 检查结果类型
+	boolResult, ok := result.Value().(bool)
+	if !ok {
+		return false, fmt.Errorf("CEL expression must return boolean, got %T", result.Value())
+	}
+
+	return boolResult, nil
+}
+
+// EvaluateString 执行返回字符串的 CEL 表达式(用于动态错误消息)
+func (e *CELExecutor) EvaluateString(expression string, data map[string]interface{}) (string, error) {
+	// 获取或编译程序
+	prog, err := e.getProgram(expression)
+	if err != nil {
+		return "", fmt.Errorf("failed to compile CEL expression: %w", err)
+	}
+
+	// 执行表达式
+	result, _, err := prog.Eval(data)
+	if err != nil {
+		return "", fmt.Errorf("failed to evaluate CEL expression: %w", err)
+	}
+
+	// 检查结果类型
+	strResult, ok := result.Value().(string)
+	if !ok {
+		return "", fmt.Errorf("CEL expression must return string, got %T", result.Value())
+	}
+
+	return strResult, nil
+}
+
+// getProgram 获取或编译程序(带缓存)
+func (e *CELExecutor) getProgram(expression string) (cel.Program, error) {
+	// 先尝试从缓存读取
+	e.mu.RLock()
+	if prog, ok := e.cache[expression]; ok {
+		e.mu.RUnlock()
+		return prog, nil
+	}
+	e.mu.RUnlock()
+
+	// 编译表达式
+	ast, issues := e.env.Compile(expression)
+	if issues != nil && issues.Err() != nil {
+		return nil, fmt.Errorf("CEL compilation error: %w", issues.Err())
+	}
+
+	// 创建程序
+	prog, err := e.env.Program(ast)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create CEL program: %w", err)
+	}
+
+	// 存入缓存
+	e.mu.Lock()
+	e.cache[expression] = prog
+	e.mu.Unlock()
+
+	return prog, nil
+}
+
+// ClearCache 清空表达式缓存
+func (e *CELExecutor) ClearCache() {
+	e.mu.Lock()
+	e.cache = make(map[string]cel.Program)
+	e.mu.Unlock()
+}
+
+// CacheSize 获取缓存大小
+func (e *CELExecutor) CacheSize() int {
+	e.mu.RLock()
+	defer e.mu.RUnlock()
+	return len(e.cache)
+}
+
+// ValidateCELExpression 验证 CEL 表达式语法(不执行)
+func (e *CELExecutor) ValidateCELExpression(expression string) error {
+	_, issues := e.env.Compile(expression)
+	if issues != nil && issues.Err() != nil {
+		return fmt.Errorf("invalid CEL expression: %w", issues.Err())
+	}
+	return nil
+}
+
+// Helper 函数
+
+// celBool 将 ref.Val 转换为 bool
+func celBool(val ref.Val) (bool, bool) {
+	if val.Type() == types.BoolType {
+		return val.Value().(bool), true
+	}
+	return false, false
+}
+
+// celString 将 ref.Val 转换为 string
+func celString(val ref.Val) (string, bool) {
+	if val.Type() == types.StringType {
+		return val.Value().(string), true
+	}
+	return "", false
+}
+
+// celInt 将 ref.Val 转换为 int64
+func celInt(val ref.Val) (int64, bool) {
+	if val.Type() == types.IntType {
+		return val.Value().(int64), true
+	}
+	return 0, false
+}
+
+// celDouble 将 ref.Val 转换为 float64
+func celDouble(val ref.Val) (float64, bool) {
+	if val.Type() == types.DoubleType {
+		return val.Value().(float64), true
+	}
+	return 0, false
+}

+ 209 - 0
validator/context.go

@@ -0,0 +1,209 @@
+package validator
+
+import (
+	"context"
+)
+
+// contextKey 上下文键类型
+type contextKey string
+
+const (
+	// 验证组
+	contextKeyGroups contextKey = "validator:groups"
+	// 验证策略
+	contextKeyStrategy contextKey = "validator:strategy"
+	// 语言
+	contextKeyLanguage contextKey = "validator:language"
+	// 用户角色
+	contextKeyRoles contextKey = "validator:roles"
+	// 是否管理员
+	contextKeyIsAdmin contextKey = "validator:is_admin"
+)
+
+// ValidationContext 验证上下文
+type ValidationContext struct {
+	// 激活的验证组
+	Groups []string
+	// 验证策略
+	Strategy *ValidationStrategy
+	// 语言偏好
+	Language string
+	// 用户角色
+	Roles []string
+	// 是否管理员
+	IsAdmin bool
+	// 自定义元数据
+	Metadata map[string]interface{}
+}
+
+// ValidationStrategy 验证策略
+type ValidationStrategy struct {
+	// 验证模式
+	Mode ValidationMode
+	// 是否快速失败(遇到第一个错误就停止)
+	FailFast bool
+	// 是否收集警告
+	CollectWarnings bool
+	// 最大错误数(超过后停止验证)
+	MaxErrors int
+	// 是否启用缓存
+	EnableCache bool
+}
+
+// ValidationMode 验证模式
+type ValidationMode int
+
+const (
+	// 验证所有规则
+	ValidationModeAll ValidationMode = iota
+	// 仅验证必填项
+	ValidationModeRequiredOnly
+	// 仅验证修改的字段
+	ValidationModeChangedOnly
+	// 自定义(根据组选择)
+	ValidationModeCustom
+)
+
+// NewContext 创建验证上下文
+func NewContext() *ValidationContext {
+	return &ValidationContext{
+		Groups:   make([]string, 0),
+		Metadata: make(map[string]interface{}),
+		Strategy: DefaultStrategy(),
+	}
+}
+
+// DefaultStrategy 默认验证策略
+func DefaultStrategy() *ValidationStrategy {
+	return &ValidationStrategy{
+		Mode:            ValidationModeAll,
+		FailFast:        false,
+		CollectWarnings: true,
+		MaxErrors:       100,
+		EnableCache:     true,
+	}
+}
+
+// WithGroups 设置验证组到上下文
+func WithGroups(ctx context.Context, groups ...string) context.Context {
+	return context.WithValue(ctx, contextKeyGroups, groups)
+}
+
+// GetGroups 从上下文获取验证组
+func GetGroups(ctx context.Context) []string {
+	if groups, ok := ctx.Value(contextKeyGroups).([]string); ok {
+		return groups
+	}
+	return nil
+}
+
+// WithStrategy 设置验证策略到上下文
+func WithStrategy(ctx context.Context, strategy *ValidationStrategy) context.Context {
+	return context.WithValue(ctx, contextKeyStrategy, strategy)
+}
+
+// GetStrategy 从上下文获取验证策略
+func GetStrategy(ctx context.Context) *ValidationStrategy {
+	if strategy, ok := ctx.Value(contextKeyStrategy).(*ValidationStrategy); ok {
+		return strategy
+	}
+	return DefaultStrategy()
+}
+
+// WithLanguage 设置语言到上下文
+func WithLanguage(ctx context.Context, lang string) context.Context {
+	return context.WithValue(ctx, contextKeyLanguage, lang)
+}
+
+// GetLanguage 从上下文获取语言
+func GetLanguage(ctx context.Context) string {
+	if lang, ok := ctx.Value(contextKeyLanguage).(string); ok {
+		return lang
+	}
+	return "zh-CN" // 默认中文
+}
+
+// WithRoles 设置用户角色到上下文
+func WithRoles(ctx context.Context, roles ...string) context.Context {
+	return context.WithValue(ctx, contextKeyRoles, roles)
+}
+
+// GetRoles 从上下文获取用户角色
+func GetRoles(ctx context.Context) []string {
+	if roles, ok := ctx.Value(contextKeyRoles).([]string); ok {
+		return roles
+	}
+	return nil
+}
+
+// WithAdmin 设置管理员标志到上下文
+func WithAdmin(ctx context.Context, isAdmin bool) context.Context {
+	return context.WithValue(ctx, contextKeyIsAdmin, isAdmin)
+}
+
+// IsAdmin 从上下文判断是否管理员
+func IsAdmin(ctx context.Context) bool {
+	if isAdmin, ok := ctx.Value(contextKeyIsAdmin).(bool); ok {
+		return isAdmin
+	}
+	return false
+}
+
+// FromContext 从上下文创建 ValidationContext
+func FromContext(ctx context.Context) *ValidationContext {
+	vc := NewContext()
+	vc.Groups = GetGroups(ctx)
+	vc.Strategy = GetStrategy(ctx)
+	vc.Language = GetLanguage(ctx)
+	vc.Roles = GetRoles(ctx)
+	vc.IsAdmin = IsAdmin(ctx)
+	return vc
+}
+
+// HasGroup 检查是否包含指定组
+func (vc *ValidationContext) HasGroup(group string) bool {
+	if len(vc.Groups) == 0 {
+		// 没有指定组时,默认验证所有规则
+		return true
+	}
+	for _, g := range vc.Groups {
+		if g == group {
+			return true
+		}
+	}
+	return false
+}
+
+// HasAnyGroup 检查是否包含任一指定组
+func (vc *ValidationContext) HasAnyGroup(groups []string) bool {
+	if len(vc.Groups) == 0 {
+		return true
+	}
+	if len(groups) == 0 {
+		return true
+	}
+	for _, g := range groups {
+		if vc.HasGroup(g) {
+			return true
+		}
+	}
+	return false
+}
+
+// ShouldFailFast 是否应该快速失败
+func (vc *ValidationContext) ShouldFailFast() bool {
+	return vc.Strategy != nil && vc.Strategy.FailFast
+}
+
+// ShouldCollectWarnings 是否应该收集警告
+func (vc *ValidationContext) ShouldCollectWarnings() bool {
+	return vc.Strategy == nil || vc.Strategy.CollectWarnings
+}
+
+// MaxErrorsReached 是否达到最大错误数
+func (vc *ValidationContext) MaxErrorsReached(errorCount int) bool {
+	if vc.Strategy == nil {
+		return false
+	}
+	return vc.Strategy.MaxErrors > 0 && errorCount >= vc.Strategy.MaxErrors
+}

+ 490 - 0
validator/engine.go

@@ -0,0 +1,490 @@
+package validator
+
+import (
+	"fmt"
+
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/reflect/protoreflect"
+
+	validate "git.ikuban.com/server/kubanapis/kuban/api/validate"
+)
+
+// Engine 验证引擎
+type Engine struct {
+	celExecutor *CELExecutor
+}
+
+// NewEngine 创建验证引擎
+func NewEngine(celExecutor *CELExecutor) *Engine {
+	return &Engine{
+		celExecutor: celExecutor,
+	}
+}
+
+// Validate 验证消息
+func (e *Engine) Validate(vctx *ValidationContext, msg proto.Message) error {
+	if msg == nil {
+		return nil
+	}
+
+	errors := &ValidationErrors{}
+
+	// 获取消息的反射对象
+	msgReflect := msg.ProtoReflect()
+	msgDescriptor := msgReflect.Descriptor()
+
+	// 1. 验证消息级别规则
+	if err := e.validateMessageRules(vctx, msgReflect, msgDescriptor, errors); err != nil {
+		return err
+	}
+
+	// 2. 验证字段
+	if err := e.validateFields(vctx, msgReflect, msgDescriptor, errors); err != nil {
+		return err
+	}
+
+	// 3. 验证 oneof
+	if err := e.validateOneofs(vctx, msgReflect, msgDescriptor, errors); err != nil {
+		return err
+	}
+
+	// 返回错误(如果有)
+	if errors.HasErrors() {
+		return errors.ToKratosError()
+	}
+
+	return nil
+}
+
+// validateMessageRules 验证消息级别规则
+func (e *Engine) validateMessageRules(
+	vctx *ValidationContext,
+	msgReflect protoreflect.Message,
+	msgDescriptor protoreflect.MessageDescriptor,
+	errors *ValidationErrors,
+) error {
+	// 获取消息选项
+	opts := msgDescriptor.Options()
+	if opts == nil {
+		return nil
+	}
+
+	// 检查是否有验证规则扩展
+	if !proto.HasExtension(opts, validate.E_Message) {
+		return nil
+	}
+
+	// 获取消息验证规则
+	msgRules := proto.GetExtension(opts, validate.E_Message).(*validate.MessageRules)
+	if msgRules == nil {
+		return nil
+	}
+
+	// 检查是否被禁用
+	if msgRules.Disabled {
+		return nil
+	}
+
+	// 检查验证组
+	if !vctx.HasAnyGroup(msgRules.Groups) {
+		return nil
+	}
+
+	// 执行 CEL 规则
+	for _, celRule := range msgRules.Cel {
+		if err := e.executeCELRule(vctx, celRule, msgReflect.Interface(), "", errors); err != nil {
+			if vctx.ShouldFailFast() {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+// validateFields 验证所有字段
+func (e *Engine) validateFields(
+	vctx *ValidationContext,
+	msgReflect protoreflect.Message,
+	msgDescriptor protoreflect.MessageDescriptor,
+	errors *ValidationErrors,
+) error {
+	fields := msgDescriptor.Fields()
+
+	for i := 0; i < fields.Len(); i++ {
+		field := fields.Get(i)
+		fieldPath := string(field.Name())
+
+		// 检查是否达到最大错误数
+		if vctx.MaxErrorsReached(errors.Count()) {
+			break
+		}
+
+		// 验证字段
+		if err := e.validateField(vctx, msgReflect, field, fieldPath, errors); err != nil {
+			if vctx.ShouldFailFast() {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+// validateField 验证单个字段
+func (e *Engine) validateField(
+	vctx *ValidationContext,
+	msgReflect protoreflect.Message,
+	field protoreflect.FieldDescriptor,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	// 获取字段选项
+	opts := field.Options()
+	if opts == nil {
+		return nil
+	}
+
+	// 检查是否有验证规则
+	if !proto.HasExtension(opts, validate.E_Field) {
+		return nil
+	}
+
+	// 获取字段验证规则
+	fieldRules := proto.GetExtension(opts, validate.E_Field).(*validate.FieldRules)
+	if fieldRules == nil {
+		return nil
+	}
+
+	// 检查验证组
+	if !vctx.HasAnyGroup(fieldRules.Groups) {
+		return nil
+	}
+
+	// 获取字段值
+	fieldValue := msgReflect.Get(field)
+	hasValue := msgReflect.Has(field)
+
+	// 检查忽略策略
+	if e.shouldIgnoreField(fieldRules, hasValue, fieldValue, field) {
+		return nil
+	}
+
+	// 验证 required
+	if fieldRules.Required {
+		if !hasValue {
+			defaultMsg := fmt.Sprintf("字段 %s 是必填的", fieldPath)
+			message := e.getErrorMessage(fieldRules, vctx, "required", defaultMsg)
+			errors.Add(NewValidationError(
+				fieldPath,
+				ErrCodeRequired,
+				message,
+			))
+			if vctx.ShouldFailFast() {
+				return errors
+			}
+		}
+	}
+
+	// 如果字段未设置且不是 required,跳过后续验证
+	if !hasValue && !fieldRules.Required {
+		return nil
+	}
+
+	// 执行 CEL 规则
+	for _, celRule := range fieldRules.Cel {
+		if err := e.executeCELFieldRule(
+			vctx,
+			celRule,
+			fieldValue.Interface(),
+			msgReflect.Interface(),
+			fieldPath,
+			errors,
+		); err != nil {
+			if vctx.ShouldFailFast() {
+				return err
+			}
+		}
+	}
+
+	// 根据字段类型执行类型特定验证
+	return e.validateFieldType(vctx, fieldRules, fieldValue, field, fieldPath, errors)
+}
+
+// validateFieldType 根据字段类型执行验证
+func (e *Engine) validateFieldType(
+	vctx *ValidationContext,
+	fieldRules *validate.FieldRules,
+	fieldValue protoreflect.Value,
+	field protoreflect.FieldDescriptor,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	// 根据规则类型分发
+	switch rule := fieldRules.Type.(type) {
+	case *validate.FieldRules_String_:
+		return e.validateString(vctx, fieldRules, rule.String_, fieldValue.String(), fieldPath, errors)
+	case *validate.FieldRules_Int32:
+		return e.validateInt32(vctx, fieldRules, rule.Int32, int32(fieldValue.Int()), fieldPath, errors)
+	case *validate.FieldRules_Int64:
+		return e.validateInt64(vctx, fieldRules, rule.Int64, fieldValue.Int(), fieldPath, errors)
+	case *validate.FieldRules_Uint32:
+		return e.validateUInt32(vctx, fieldRules, rule.Uint32, uint32(fieldValue.Uint()), fieldPath, errors)
+	case *validate.FieldRules_Uint64:
+		return e.validateUInt64(vctx, fieldRules, rule.Uint64, fieldValue.Uint(), fieldPath, errors)
+	case *validate.FieldRules_Sint32:
+		return e.validateInt32(vctx, fieldRules, (*validate.Int32Rules)(rule.Sint32), int32(fieldValue.Int()), fieldPath, errors)
+	case *validate.FieldRules_Sint64:
+		return e.validateInt64(vctx, fieldRules, (*validate.Int64Rules)(rule.Sint64), fieldValue.Int(), fieldPath, errors)
+	case *validate.FieldRules_Fixed32:
+		return e.validateUInt32(vctx, fieldRules, (*validate.UInt32Rules)(rule.Fixed32), uint32(fieldValue.Uint()), fieldPath, errors)
+	case *validate.FieldRules_Fixed64:
+		return e.validateUInt64(vctx, fieldRules, (*validate.UInt64Rules)(rule.Fixed64), fieldValue.Uint(), fieldPath, errors)
+	case *validate.FieldRules_Sfixed32:
+		return e.validateInt32(vctx, fieldRules, (*validate.Int32Rules)(rule.Sfixed32), int32(fieldValue.Int()), fieldPath, errors)
+	case *validate.FieldRules_Sfixed64:
+		return e.validateInt64(vctx, fieldRules, (*validate.Int64Rules)(rule.Sfixed64), fieldValue.Int(), fieldPath, errors)
+	case *validate.FieldRules_Float:
+		return e.validateFloat(vctx, fieldRules, rule.Float, float32(fieldValue.Float()), fieldPath, errors)
+	case *validate.FieldRules_Double:
+		return e.validateDouble(vctx, fieldRules, rule.Double, fieldValue.Float(), fieldPath, errors)
+	case *validate.FieldRules_Bool:
+		return e.validateBool(vctx, fieldRules, rule.Bool, fieldValue.Bool(), fieldPath, errors)
+	case *validate.FieldRules_Bytes:
+		return e.validateBytes(vctx, fieldRules, rule.Bytes, fieldValue.Bytes(), fieldPath, errors)
+	case *validate.FieldRules_Enum:
+		return e.validateEnum(vctx, fieldRules, rule.Enum, fieldValue.Enum(), field, fieldPath, errors)
+	case *validate.FieldRules_Repeated:
+		return e.validateRepeated(vctx, fieldRules, rule.Repeated, fieldValue.List(), field, fieldPath, errors)
+	case *validate.FieldRules_Map:
+		return e.validateMap(vctx, fieldRules, rule.Map, fieldValue.Map(), field, fieldPath, errors)
+	}
+
+	return nil
+}
+
+// shouldIgnoreField 检查是否应该忽略字段验证
+func (e *Engine) shouldIgnoreField(
+	rules *validate.FieldRules,
+	hasValue bool,
+	value protoreflect.Value,
+	field protoreflect.FieldDescriptor,
+) bool {
+	switch rules.Ignore {
+	case validate.IgnoreRule_IGNORE_ALWAYS:
+		return true
+	case validate.IgnoreRule_IGNORE_IF_ZERO:
+		return !hasValue || isZeroValue(value, field)
+	case validate.IgnoreRule_IGNORE_NEVER:
+		return false
+	default:
+		// IGNORE_UNSPECIFIED: 未设置时不验证
+		return !hasValue
+	}
+}
+
+// isZeroValue 检查是否为零值
+func isZeroValue(value protoreflect.Value, field protoreflect.FieldDescriptor) bool {
+	switch field.Kind() {
+	case protoreflect.BoolKind:
+		return !value.Bool()
+	case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind,
+		protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
+		return value.Int() == 0
+	case protoreflect.Uint32Kind, protoreflect.Fixed32Kind,
+		protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
+		return value.Uint() == 0
+	case protoreflect.FloatKind, protoreflect.DoubleKind:
+		return value.Float() == 0.0
+	case protoreflect.StringKind:
+		return value.String() == ""
+	case protoreflect.BytesKind:
+		return len(value.Bytes()) == 0
+	case protoreflect.MessageKind:
+		return !value.Message().IsValid()
+	default:
+		return false
+	}
+}
+
+// validateOneofs 验证所有 oneof
+func (e *Engine) validateOneofs(
+	vctx *ValidationContext,
+	msgReflect protoreflect.Message,
+	msgDescriptor protoreflect.MessageDescriptor,
+	errors *ValidationErrors,
+) error {
+	oneofs := msgDescriptor.Oneofs()
+
+	for i := 0; i < oneofs.Len(); i++ {
+		oneof := oneofs.Get(i)
+
+		// 获取 oneof 选项
+		opts := oneof.Options()
+		if opts == nil {
+			continue
+		}
+
+		// 检查是否有验证规则
+		if !proto.HasExtension(opts, validate.E_Oneof) {
+			continue
+		}
+
+		// 获取 oneof 验证规则
+		oneofRules := proto.GetExtension(opts, validate.E_Oneof).(*validate.OneofRules)
+		if oneofRules == nil {
+			continue
+		}
+
+		// 验证 required
+		if oneofRules.Required {
+			hasField := false
+			fields := oneof.Fields()
+			for j := 0; j < fields.Len(); j++ {
+				if msgReflect.Has(fields.Get(j)) {
+					hasField = true
+					break
+				}
+			}
+
+			if !hasField {
+				errors.Add(NewValidationError(
+					string(oneof.Name()),
+					ErrCodeRequired,
+					fmt.Sprintf("oneof %s 必须选择一个字段", oneof.Name()),
+				))
+				if vctx.ShouldFailFast() {
+					return errors
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// executeCELRule 执行 CEL 规则
+func (e *Engine) executeCELRule(
+	vctx *ValidationContext,
+	rule *validate.Rule,
+	msg proto.Message,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	// 执行表达式
+	result, err := e.celExecutor.EvaluateWithProto(rule.Expression, msg)
+	if err != nil {
+		// CEL 执行错误
+		errors.Add(&ValidationError{
+			FieldPath: fieldPath,
+			RuleID:    rule.Id,
+			Message:   fmt.Sprintf("CEL 表达式执行失败: %v", err),
+		})
+		return nil
+	}
+
+	// 如果验证失败
+	if !result {
+		message := rule.Message
+		if message == "" && vctx.Language == "en-US" && rule.MessageEn != "" {
+			message = rule.MessageEn
+		}
+		if message == "" {
+			message = fmt.Sprintf("验证规则 %s 失败", rule.Id)
+		}
+
+		errors.Add(&ValidationError{
+			FieldPath: fieldPath,
+			RuleID:    rule.Id,
+			Message:   message,
+			MessageEN: rule.MessageEn,
+		})
+	}
+
+	return nil
+}
+
+// executeCELFieldRule 执行字段级 CEL 规则
+func (e *Engine) executeCELFieldRule(
+	vctx *ValidationContext,
+	rule *validate.Rule,
+	fieldValue interface{},
+	parent proto.Message,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	// 执行表达式
+	result, err := e.celExecutor.EvaluateFieldRule(rule.Expression, fieldValue, parent)
+	if err != nil {
+		// CEL 执行错误
+		errors.Add(&ValidationError{
+			FieldPath: fieldPath,
+			RuleID:    rule.Id,
+			Message:   fmt.Sprintf("CEL 表达式执行失败: %v", err),
+		})
+		return nil
+	}
+
+	// 如果验证失败
+	if !result {
+		message := rule.Message
+		if message == "" && vctx.Language == "en-US" && rule.MessageEn != "" {
+			message = rule.MessageEn
+		}
+		if message == "" {
+			message = fmt.Sprintf("验证规则 %s 失败", rule.Id)
+		}
+
+		errors.Add(&ValidationError{
+			FieldPath: fieldPath,
+			RuleID:    rule.Id,
+			Message:   message,
+			MessageEN: rule.MessageEn,
+			Value:     fieldValue,
+		})
+	}
+
+	return nil
+}
+
+// getErrorMessage 获取错误消息,按优先级返回
+// 优先级: 特定消息(required_message) > 通用消息(error_message) > 默认消息
+func (e *Engine) getErrorMessage(
+	fieldRules *validate.FieldRules,
+	vctx *ValidationContext,
+	ruleType string,
+	defaultMessage string,
+) string {
+	// 1. 检查是否有特定规则的自定义消息 (如 required_message)
+	if ruleType == "required" {
+		if vctx.Language == "en-US" && fieldRules.RequiredMessageEn != "" {
+			return fieldRules.RequiredMessageEn
+		}
+		if fieldRules.RequiredMessage != "" {
+			return fieldRules.RequiredMessage
+		}
+	}
+
+	// 2. 检查通用自定义消息 (error_message)
+	if vctx.Language == "en-US" && fieldRules.ErrorMessageEn != "" {
+		return fieldRules.ErrorMessageEn
+	}
+	if fieldRules.ErrorMessage != "" {
+		return fieldRules.ErrorMessage
+	}
+
+	// 3. 返回默认消息
+	return defaultMessage
+}
+
+// ValidateField 验证单个字段(外部接口)
+func (e *Engine) ValidateField(vctx *ValidationContext, fieldPath string, value interface{}, rules interface{}) error {
+	errors := &ValidationErrors{}
+
+	// TODO: 实现单字段验证逻辑
+
+	if errors.HasErrors() {
+		return errors.ToKratosError()
+	}
+	return nil
+}

+ 252 - 0
validator/engine_extended.go

@@ -0,0 +1,252 @@
+package validator
+
+import (
+	"fmt"
+	"time"
+
+	"google.golang.org/protobuf/reflect/protoreflect"
+	"google.golang.org/protobuf/types/known/durationpb"
+	"google.golang.org/protobuf/types/known/timestamppb"
+
+	validate "git.ikuban.com/server/kubanapis/kuban/api/validate"
+)
+
+// validateEnum 验证枚举
+func (e *Engine) validateEnum(
+	vctx *ValidationContext,
+	fieldRules *validate.FieldRules,
+	rules *validate.EnumRules,
+	value protoreflect.EnumNumber,
+	field protoreflect.FieldDescriptor,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	intValue := int32(value)
+
+	if rules.Const != nil && intValue != *rules.Const {
+		defaultMsg := fmt.Sprintf("字段 %s 必须等于 %d", fieldPath, *rules.Const)
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+			e.getErrorMessage(fieldRules, vctx, "const", defaultMsg)))
+	}
+
+	// 验证是否为已定义的枚举值
+	if rules.DefinedOnly != nil && *rules.DefinedOnly {
+		enumValues := field.Enum().Values()
+		found := false
+		for i := 0; i < enumValues.Len(); i++ {
+			if enumValues.Get(i).Number() == value {
+				found = true
+				break
+			}
+		}
+		if !found {
+			defaultMsg := fmt.Sprintf("字段 %s 的枚举值未定义", fieldPath)
+			errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+				e.getErrorMessage(fieldRules, vctx, "defined_only", defaultMsg)))
+		}
+	}
+
+	// in 验证
+	if len(rules.In) > 0 {
+		found := false
+		for _, v := range rules.In {
+			if intValue == v {
+				found = true
+				break
+			}
+		}
+		if !found {
+			defaultMsg := fmt.Sprintf("字段 %s 的值不在允许的列表中", fieldPath)
+			errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+				e.getErrorMessage(fieldRules, vctx, "in", defaultMsg)))
+		}
+	}
+
+	// not_in 验证
+	if len(rules.NotIn) > 0 {
+		for _, v := range rules.NotIn {
+			if intValue == v {
+				defaultMsg := fmt.Sprintf("字段 %s 的值在禁止列表中", fieldPath)
+				errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+					e.getErrorMessage(fieldRules, vctx, "not_in", defaultMsg)))
+				break
+			}
+		}
+	}
+
+	return nil
+}
+
+// validateBytes 验证字节数组
+func (e *Engine) validateBytes(
+	vctx *ValidationContext,
+	fieldRules *validate.FieldRules,
+	rules *validate.BytesRules,
+	value []byte,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	// const 验证
+	if rules.Const != nil && string(value) != string(rules.Const) {
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+			fmt.Sprintf("字段 %s 必须等于指定值", fieldPath)))
+	}
+
+	// 长度验证
+	length := uint64(len(value))
+	if rules.Len != nil && length != *rules.Len {
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidLength,
+			fmt.Sprintf("字段 %s 长度必须为 %d 字节", fieldPath, *rules.Len)))
+	}
+	if rules.MinLen != nil && length < *rules.MinLen {
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidLength,
+			fmt.Sprintf("字段 %s 长度至少为 %d 字节", fieldPath, *rules.MinLen)))
+	}
+	if rules.MaxLen != nil && length > *rules.MaxLen {
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidLength,
+			fmt.Sprintf("字段 %s 长度最多为 %d 字节", fieldPath, *rules.MaxLen)))
+	}
+
+	// TODO: pattern, prefix, suffix, contains 验证
+
+	// in 验证
+	if len(rules.In) > 0 {
+		found := false
+		for _, allowed := range rules.In {
+			if string(value) == string(allowed) {
+				found = true
+				break
+			}
+		}
+		if !found {
+			errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+				fmt.Sprintf("字段 %s 的值不在允许的列表中", fieldPath)))
+		}
+	}
+
+	// not_in 验证
+	if len(rules.NotIn) > 0 {
+		for _, disallowed := range rules.NotIn {
+			if string(value) == string(disallowed) {
+				errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+					fmt.Sprintf("字段 %s 的值在禁止列表中", fieldPath)))
+				break
+			}
+		}
+	}
+
+	return nil
+}
+
+// validateDuration 验证 Duration
+func (e *Engine) validateDuration(
+	vctx *ValidationContext,
+	rules *validate.DurationRules,
+	value *durationpb.Duration,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	if value == nil {
+		if rules.Required != nil && *rules.Required {
+			errors.Add(NewValidationError(fieldPath, ErrCodeRequired,
+				fmt.Sprintf("字段 %s 是必填的", fieldPath)))
+		}
+		return nil
+	}
+
+	seconds := value.Seconds
+
+	// const 验证
+	if rules.ConstSeconds != nil && seconds != *rules.ConstSeconds {
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+			fmt.Sprintf("字段 %s 必须等于 %d 秒", fieldPath, *rules.ConstSeconds)))
+	}
+
+	// 范围验证
+	if rules.LtSeconds != nil && seconds >= *rules.LtSeconds {
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			fmt.Sprintf("字段 %s 必须小于 %d 秒", fieldPath, *rules.LtSeconds)))
+	}
+	if rules.LteSeconds != nil && seconds > *rules.LteSeconds {
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			fmt.Sprintf("字段 %s 必须小于等于 %d 秒", fieldPath, *rules.LteSeconds)))
+	}
+	if rules.GtSeconds != nil && seconds <= *rules.GtSeconds {
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			fmt.Sprintf("字段 %s 必须大于 %d 秒", fieldPath, *rules.GtSeconds)))
+	}
+	if rules.GteSeconds != nil && seconds < *rules.GteSeconds {
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			fmt.Sprintf("字段 %s 必须大于等于 %d 秒", fieldPath, *rules.GteSeconds)))
+	}
+
+	return nil
+}
+
+// validateTimestamp 验证 Timestamp
+func (e *Engine) validateTimestamp(
+	vctx *ValidationContext,
+	rules *validate.TimestampRules,
+	value *timestamppb.Timestamp,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	if value == nil {
+		if rules.Required != nil && *rules.Required {
+			errors.Add(NewValidationError(fieldPath, ErrCodeRequired,
+				fmt.Sprintf("字段 %s 是必填的", fieldPath)))
+		}
+		return nil
+	}
+
+	timestamp := value.Seconds
+
+	// const 验证
+	if rules.Const != nil && timestamp != *rules.Const {
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+			fmt.Sprintf("字段 %s 必须等于指定时间", fieldPath)))
+	}
+
+	// 范围验证
+	if rules.Lt != nil && timestamp >= *rules.Lt {
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			fmt.Sprintf("字段 %s 必须早于指定时间", fieldPath)))
+	}
+	if rules.Lte != nil && timestamp > *rules.Lte {
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			fmt.Sprintf("字段 %s 必须早于或等于指定时间", fieldPath)))
+	}
+	if rules.Gt != nil && timestamp <= *rules.Gt {
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			fmt.Sprintf("字段 %s 必须晚于指定时间", fieldPath)))
+	}
+	if rules.Gte != nil && timestamp < *rules.Gte {
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			fmt.Sprintf("字段 %s 必须晚于或等于指定时间", fieldPath)))
+	}
+
+	// 相对时间验证
+	now := time.Now().Unix()
+	if rules.LtNow != nil && *rules.LtNow && timestamp >= now {
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			fmt.Sprintf("字段 %s 必须在当前时间之前", fieldPath)))
+	}
+	if rules.GtNow != nil && *rules.GtNow && timestamp <= now {
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			fmt.Sprintf("字段 %s 必须在当前时间之后", fieldPath)))
+	}
+
+	// 时间范围验证
+	if rules.WithinSeconds != nil {
+		diff := timestamp - now
+		if diff < 0 {
+			diff = -diff
+		}
+		if diff > *rules.WithinSeconds {
+			errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+				fmt.Sprintf("字段 %s 必须在当前时间的 %d 秒内", fieldPath, *rules.WithinSeconds)))
+		}
+	}
+
+	return nil
+}

+ 300 - 0
validator/engine_string.go

@@ -0,0 +1,300 @@
+package validator
+
+import (
+	"fmt"
+	"regexp"
+	"strings"
+	"unicode/utf8"
+
+	validate "git.ikuban.com/server/kubanapis/kuban/api/validate"
+)
+
+// validateString 验证字符串
+func (e *Engine) validateString(
+	vctx *ValidationContext,
+	fieldRules *validate.FieldRules,
+	rules *validate.StringRules,
+	value string,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	// const 验证
+	if rules.Const != nil && value != *rules.Const {
+		defaultMsg := fmt.Sprintf("字段 %s 必须等于 '%s'", fieldPath, *rules.Const)
+		errors.Add(NewValidationError(
+			fieldPath,
+			ErrCodeInvalidValue,
+			e.getErrorMessage(fieldRules, vctx, "const", defaultMsg),
+		))
+		if vctx.ShouldFailFast() {
+			return errors
+		}
+	}
+
+	// 长度验证(字符数)
+	runeCount := utf8.RuneCountInString(value)
+	if rules.Len != nil && uint64(runeCount) != *rules.Len {
+		defaultMsg := fmt.Sprintf("字段 %s 长度必须为 %d 个字符", fieldPath, *rules.Len)
+		errors.Add(NewValidationError(
+			fieldPath,
+			ErrCodeInvalidLength,
+			e.getErrorMessage(fieldRules, vctx, "len", defaultMsg),
+		))
+	}
+	if rules.MinLen != nil && uint64(runeCount) < *rules.MinLen {
+		defaultMsg := fmt.Sprintf("字段 %s 长度至少为 %d 个字符", fieldPath, *rules.MinLen)
+		errors.Add(NewValidationError(
+			fieldPath,
+			ErrCodeInvalidLength,
+			e.getErrorMessage(fieldRules, vctx, "min_len", defaultMsg),
+		))
+	}
+	if rules.MaxLen != nil && uint64(runeCount) > *rules.MaxLen {
+		defaultMsg := fmt.Sprintf("字段 %s 长度最多为 %d 个字符", fieldPath, *rules.MaxLen)
+		errors.Add(NewValidationError(
+			fieldPath,
+			ErrCodeInvalidLength,
+			e.getErrorMessage(fieldRules, vctx, "max_len", defaultMsg),
+		))
+	}
+
+	// 字节长度验证
+	byteLen := len(value)
+	if rules.LenBytes != nil && uint64(byteLen) != *rules.LenBytes {
+		errors.Add(NewValidationError(
+			fieldPath,
+			ErrCodeInvalidLength,
+			fmt.Sprintf("字段 %s 字节长度必须为 %d", fieldPath, *rules.LenBytes),
+		))
+	}
+	if rules.MinBytes != nil && uint64(byteLen) < *rules.MinBytes {
+		errors.Add(NewValidationError(
+			fieldPath,
+			ErrCodeInvalidLength,
+			fmt.Sprintf("字段 %s 字节长度至少为 %d", fieldPath, *rules.MinBytes),
+		))
+	}
+	if rules.MaxBytes != nil && uint64(byteLen) > *rules.MaxBytes {
+		errors.Add(NewValidationError(
+			fieldPath,
+			ErrCodeInvalidLength,
+			fmt.Sprintf("字段 %s 字节长度最多为 %d", fieldPath, *rules.MaxBytes),
+		))
+	}
+
+	// 模式验证(正则表达式)
+	if rules.Pattern != nil {
+		matched, err := regexp.MatchString(*rules.Pattern, value)
+		if err != nil {
+			errors.Add(NewValidationError(
+				fieldPath,
+				ErrCodeInvalidPattern,
+				fmt.Sprintf("正则表达式错误: %v", err),
+			))
+		} else if !matched {
+			errors.Add(NewValidationError(
+				fieldPath,
+				ErrCodeInvalidPattern,
+				fmt.Sprintf("字段 %s 格式不正确", fieldPath),
+			))
+		}
+	}
+
+	// 前缀/后缀验证
+	if rules.Prefix != nil && !strings.HasPrefix(value, *rules.Prefix) {
+		errors.Add(NewValidationError(
+			fieldPath,
+			ErrCodeInvalidValue,
+			fmt.Sprintf("字段 %s 必须以 '%s' 开头", fieldPath, *rules.Prefix),
+		))
+	}
+	if rules.Suffix != nil && !strings.HasSuffix(value, *rules.Suffix) {
+		errors.Add(NewValidationError(
+			fieldPath,
+			ErrCodeInvalidValue,
+			fmt.Sprintf("字段 %s 必须以 '%s' 结尾", fieldPath, *rules.Suffix),
+		))
+	}
+
+	// 包含/不包含验证
+	if rules.Contains != nil && !strings.Contains(value, *rules.Contains) {
+		errors.Add(NewValidationError(
+			fieldPath,
+			ErrCodeInvalidValue,
+			fmt.Sprintf("字段 %s 必须包含 '%s'", fieldPath, *rules.Contains),
+		))
+	}
+	if rules.NotContains != nil && strings.Contains(value, *rules.NotContains) {
+		errors.Add(NewValidationError(
+			fieldPath,
+			ErrCodeInvalidValue,
+			fmt.Sprintf("字段 %s 不能包含 '%s'", fieldPath, *rules.NotContains),
+		))
+	}
+
+	// in 验证
+	if len(rules.In) > 0 {
+		found := false
+		for _, allowed := range rules.In {
+			if value == allowed {
+				found = true
+				break
+			}
+		}
+		if !found {
+			errors.Add(NewValidationError(
+				fieldPath,
+				ErrCodeInvalidValue,
+				fmt.Sprintf("字段 %s 的值不在允许的列表中", fieldPath),
+			))
+		}
+	}
+
+	// not_in 验证
+	if len(rules.NotIn) > 0 {
+		for _, disallowed := range rules.NotIn {
+			if value == disallowed {
+				errors.Add(NewValidationError(
+					fieldPath,
+					ErrCodeInvalidValue,
+					fmt.Sprintf("字段 %s 的值在禁止列表中", fieldPath),
+				))
+				break
+			}
+		}
+	}
+
+	// Well-known 格式验证
+	switch rules.WellKnown.(type) {
+	case *validate.StringRules_Email:
+		if !isValidEmail(value) {
+			errors.Add(NewValidationError(
+				fieldPath,
+				ErrCodeInvalidFormat,
+				fmt.Sprintf("字段 %s 不是有效的邮箱地址", fieldPath),
+			))
+		}
+	case *validate.StringRules_Uuid:
+		if !isValidUUID(value) {
+			errors.Add(NewValidationError(
+				fieldPath,
+				ErrCodeInvalidFormat,
+				fmt.Sprintf("字段 %s 不是有效的 UUID", fieldPath),
+			))
+		}
+	case *validate.StringRules_Uri:
+		if !isValidURI(value) {
+			errors.Add(NewValidationError(
+				fieldPath,
+				ErrCodeInvalidFormat,
+				fmt.Sprintf("字段 %s 不是有效的 URI", fieldPath),
+			))
+		}
+	case *validate.StringRules_ChineseMobile:
+		if !isValidChineseMobile(value) {
+			errors.Add(NewValidationError(
+				fieldPath,
+				ErrCodeInvalidFormat,
+				fmt.Sprintf("字段 %s 不是有效的中国手机号", fieldPath),
+			))
+		}
+	case *validate.StringRules_ChineseIdCard:
+		if !isValidChineseIDCard(value) {
+			errors.Add(NewValidationError(
+				fieldPath,
+				ErrCodeInvalidFormat,
+				fmt.Sprintf("字段 %s 不是有效的中国身份证号", fieldPath),
+			))
+		}
+	case *validate.StringRules_ChineseName:
+		if !isValidChineseName(value) {
+			errors.Add(NewValidationError(
+				fieldPath,
+				ErrCodeInvalidFormat,
+				fmt.Sprintf("字段 %s 不是有效的中文姓名", fieldPath),
+			))
+		}
+	case *validate.StringRules_ChinesePostcode:
+		if !isValidChinesePostcode(value) {
+			errors.Add(NewValidationError(
+				fieldPath,
+				ErrCodeInvalidFormat,
+				fmt.Sprintf("字段 %s 不是有效的中国邮政编码", fieldPath),
+			))
+		}
+	}
+
+	// 字符类型验证
+	if rules.Numeric != nil && *rules.Numeric {
+		if !regexp.MustCompile(`^\d+$`).MatchString(value) {
+			errors.Add(NewValidationError(
+				fieldPath,
+				ErrCodeInvalidFormat,
+				fmt.Sprintf("字段 %s 必须只包含数字", fieldPath),
+			))
+		}
+	}
+
+	if rules.Alpha != nil && *rules.Alpha {
+		if !regexp.MustCompile(`^[a-zA-Z]+$`).MatchString(value) {
+			errors.Add(NewValidationError(
+				fieldPath,
+				ErrCodeInvalidFormat,
+				fmt.Sprintf("字段 %s 必须只包含字母", fieldPath),
+			))
+		}
+	}
+
+	if rules.Alphanumeric != nil && *rules.Alphanumeric {
+		if !regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(value) {
+			errors.Add(NewValidationError(
+				fieldPath,
+				ErrCodeInvalidFormat,
+				fmt.Sprintf("字段 %s 必须只包含字母和数字", fieldPath),
+			))
+		}
+	}
+
+	// 大小写验证
+	if rules.Lowercase != nil && *rules.Lowercase {
+		if value != strings.ToLower(value) {
+			errors.Add(NewValidationError(
+				fieldPath,
+				ErrCodeInvalidFormat,
+				fmt.Sprintf("字段 %s 必须是小写", fieldPath),
+			))
+		}
+	}
+
+	if rules.Uppercase != nil && *rules.Uppercase {
+		if value != strings.ToUpper(value) {
+			errors.Add(NewValidationError(
+				fieldPath,
+				ErrCodeInvalidFormat,
+				fmt.Sprintf("字段 %s 必须是大写", fieldPath),
+			))
+		}
+	}
+
+	return nil
+}
+
+// Helper functions for format validation
+
+func isValidEmail(email string) bool {
+	// 简化的邮箱验证正则
+	pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
+	matched, _ := regexp.MatchString(pattern, email)
+	return matched
+}
+
+func isValidUUID(uuid string) bool {
+	pattern := `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`
+	matched, _ := regexp.MatchString(pattern, uuid)
+	return matched
+}
+
+func isValidURI(uri string) bool {
+	// 简化的 URI 验证
+	return strings.Contains(uri, "://") || strings.HasPrefix(uri, "/")
+}

+ 440 - 0
validator/engine_types.go

@@ -0,0 +1,440 @@
+package validator
+
+import (
+	"fmt"
+	"google.golang.org/protobuf/reflect/protoreflect"
+
+	validate "git.ikuban.com/server/kubanapis/kuban/api/validate"
+)
+
+// validateInt32 验证 int32
+func (e *Engine) validateInt32(
+	vctx *ValidationContext,
+	fieldRules *validate.FieldRules,
+	rules *validate.Int32Rules,
+	value int32,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	if rules.Const != nil && value != *rules.Const {
+		defaultMsg := fmt.Sprintf("字段 %s 必须等于 %d", fieldPath, *rules.Const)
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+			e.getErrorMessage(fieldRules, vctx, "const", defaultMsg)))
+	}
+	if rules.Lt != nil && value >= *rules.Lt {
+		defaultMsg := fmt.Sprintf("字段 %s 必须小于 %d", fieldPath, *rules.Lt)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Lte != nil && value > *rules.Lte {
+		defaultMsg := fmt.Sprintf("字段 %s 必须小于等于 %d", fieldPath, *rules.Lte)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Gt != nil && value <= *rules.Gt {
+		defaultMsg := fmt.Sprintf("字段 %s 必须大于 %d", fieldPath, *rules.Gt)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Gte != nil && value < *rules.Gte {
+		defaultMsg := fmt.Sprintf("字段 %s 必须大于等于 %d", fieldPath, *rules.Gte)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	return e.validateInt32In(value, rules, fieldPath, errors, fieldRules, vctx)
+}
+
+func (e *Engine) validateInt32In(value int32, rules *validate.Int32Rules, fieldPath string, errors *ValidationErrors, fieldRules *validate.FieldRules, vctx *ValidationContext) error {
+	if len(rules.In) > 0 {
+		found := false
+		for _, v := range rules.In {
+			if value == v {
+				found = true
+				break
+			}
+		}
+		if !found {
+			defaultMsg := fmt.Sprintf("字段 %s 的值不在允许的列表中", fieldPath)
+			errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+				e.getErrorMessage(fieldRules, vctx, "in", defaultMsg)))
+		}
+	}
+	if len(rules.NotIn) > 0 {
+		for _, v := range rules.NotIn {
+			if value == v {
+				defaultMsg := fmt.Sprintf("字段 %s 的值在禁止列表中", fieldPath)
+				errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+					e.getErrorMessage(fieldRules, vctx, "not_in", defaultMsg)))
+				break
+			}
+		}
+	}
+	return nil
+}
+
+// validateInt64 验证 int64
+func (e *Engine) validateInt64(
+	vctx *ValidationContext,
+	fieldRules *validate.FieldRules,
+	rules *validate.Int64Rules,
+	value int64,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	if rules.Const != nil && value != *rules.Const {
+		defaultMsg := fmt.Sprintf("字段 %s 必须等于 %d", fieldPath, *rules.Const)
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+			e.getErrorMessage(fieldRules, vctx, "const", defaultMsg)))
+	}
+	if rules.Lt != nil && value >= *rules.Lt {
+		defaultMsg := fmt.Sprintf("字段 %s 必须小于 %d", fieldPath, *rules.Lt)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Lte != nil && value > *rules.Lte {
+		defaultMsg := fmt.Sprintf("字段 %s 必须小于等于 %d", fieldPath, *rules.Lte)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Gt != nil && value <= *rules.Gt {
+		defaultMsg := fmt.Sprintf("字段 %s 必须大于 %d", fieldPath, *rules.Gt)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Gte != nil && value < *rules.Gte {
+		defaultMsg := fmt.Sprintf("字段 %s 必须大于等于 %d", fieldPath, *rules.Gte)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	return e.validateInt64In(value, rules, fieldPath, errors, fieldRules, vctx)
+}
+
+func (e *Engine) validateInt64In(value int64, rules *validate.Int64Rules, fieldPath string, errors *ValidationErrors, fieldRules *validate.FieldRules, vctx *ValidationContext) error {
+	if len(rules.In) > 0 {
+		found := false
+		for _, v := range rules.In {
+			if value == v {
+				found = true
+				break
+			}
+		}
+		if !found {
+			defaultMsg := fmt.Sprintf("字段 %s 的值不在允许的列表中", fieldPath)
+			errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+				e.getErrorMessage(fieldRules, vctx, "in", defaultMsg)))
+		}
+	}
+	if len(rules.NotIn) > 0 {
+		for _, v := range rules.NotIn {
+			if value == v {
+				defaultMsg := fmt.Sprintf("字段 %s 的值在禁止列表中", fieldPath)
+				errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+					e.getErrorMessage(fieldRules, vctx, "not_in", defaultMsg)))
+				break
+			}
+		}
+	}
+	return nil
+}
+
+// validateUInt32 验证 uint32
+func (e *Engine) validateUInt32(
+	vctx *ValidationContext,
+	fieldRules *validate.FieldRules,
+	rules *validate.UInt32Rules,
+	value uint32,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	if rules.Const != nil && value != *rules.Const {
+		defaultMsg := fmt.Sprintf("字段 %s 必须等于 %d", fieldPath, *rules.Const)
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+			e.getErrorMessage(fieldRules, vctx, "const", defaultMsg)))
+	}
+	if rules.Lt != nil && value >= *rules.Lt {
+		defaultMsg := fmt.Sprintf("字段 %s 必须小于 %d", fieldPath, *rules.Lt)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Lte != nil && value > *rules.Lte {
+		defaultMsg := fmt.Sprintf("字段 %s 必须小于等于 %d", fieldPath, *rules.Lte)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Gt != nil && value <= *rules.Gt {
+		defaultMsg := fmt.Sprintf("字段 %s 必须大于 %d", fieldPath, *rules.Gt)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Gte != nil && value < *rules.Gte {
+		defaultMsg := fmt.Sprintf("字段 %s 必须大于等于 %d", fieldPath, *rules.Gte)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	return e.validateUInt32In(value, rules, fieldPath, errors, fieldRules, vctx)
+}
+
+func (e *Engine) validateUInt32In(value uint32, rules *validate.UInt32Rules, fieldPath string, errors *ValidationErrors, fieldRules *validate.FieldRules, vctx *ValidationContext) error {
+	if len(rules.In) > 0 {
+		found := false
+		for _, v := range rules.In {
+			if value == v {
+				found = true
+				break
+			}
+		}
+		if !found {
+			defaultMsg := fmt.Sprintf("字段 %s 的值不在允许的列表中", fieldPath)
+			errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+				e.getErrorMessage(fieldRules, vctx, "in", defaultMsg)))
+		}
+	}
+	if len(rules.NotIn) > 0 {
+		for _, v := range rules.NotIn {
+			if value == v {
+				defaultMsg := fmt.Sprintf("字段 %s 的值在禁止列表中", fieldPath)
+				errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+					e.getErrorMessage(fieldRules, vctx, "not_in", defaultMsg)))
+				break
+			}
+		}
+	}
+	return nil
+}
+
+// validateUInt64 验证 uint64
+func (e *Engine) validateUInt64(
+	vctx *ValidationContext,
+	fieldRules *validate.FieldRules,
+	rules *validate.UInt64Rules,
+	value uint64,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	if rules.Const != nil && value != *rules.Const {
+		defaultMsg := fmt.Sprintf("字段 %s 必须等于 %d", fieldPath, *rules.Const)
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+			e.getErrorMessage(fieldRules, vctx, "const", defaultMsg)))
+	}
+	if rules.Lt != nil && value >= *rules.Lt {
+		defaultMsg := fmt.Sprintf("字段 %s 必须小于 %d", fieldPath, *rules.Lt)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Lte != nil && value > *rules.Lte {
+		defaultMsg := fmt.Sprintf("字段 %s 必须小于等于 %d", fieldPath, *rules.Lte)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Gt != nil && value <= *rules.Gt {
+		defaultMsg := fmt.Sprintf("字段 %s 必须大于 %d", fieldPath, *rules.Gt)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Gte != nil && value < *rules.Gte {
+		defaultMsg := fmt.Sprintf("字段 %s 必须大于等于 %d", fieldPath, *rules.Gte)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	return e.validateUInt64In(value, rules, fieldPath, errors, fieldRules, vctx)
+}
+
+func (e *Engine) validateUInt64In(value uint64, rules *validate.UInt64Rules, fieldPath string, errors *ValidationErrors, fieldRules *validate.FieldRules, vctx *ValidationContext) error {
+	if len(rules.In) > 0 {
+		found := false
+		for _, v := range rules.In {
+			if value == v {
+				found = true
+				break
+			}
+		}
+		if !found {
+			defaultMsg := fmt.Sprintf("字段 %s 的值不在允许的列表中", fieldPath)
+			errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+				e.getErrorMessage(fieldRules, vctx, "in", defaultMsg)))
+		}
+	}
+	if len(rules.NotIn) > 0 {
+		for _, v := range rules.NotIn {
+			if value == v {
+				defaultMsg := fmt.Sprintf("字段 %s 的值在禁止列表中", fieldPath)
+				errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+					e.getErrorMessage(fieldRules, vctx, "not_in", defaultMsg)))
+				break
+			}
+		}
+	}
+	return nil
+}
+
+// validateFloat 验证 float32
+func (e *Engine) validateFloat(
+	vctx *ValidationContext,
+	fieldRules *validate.FieldRules,
+	rules *validate.FloatRules,
+	value float32,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	if rules.Const != nil && value != *rules.Const {
+		defaultMsg := fmt.Sprintf("字段 %s 必须等于 %f", fieldPath, *rules.Const)
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+			e.getErrorMessage(fieldRules, vctx, "const", defaultMsg)))
+	}
+	if rules.Lt != nil && value >= *rules.Lt {
+		defaultMsg := fmt.Sprintf("字段 %s 必须小于 %f", fieldPath, *rules.Lt)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Lte != nil && value > *rules.Lte {
+		defaultMsg := fmt.Sprintf("字段 %s 必须小于等于 %f", fieldPath, *rules.Lte)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Gt != nil && value <= *rules.Gt {
+		defaultMsg := fmt.Sprintf("字段 %s 必须大于 %f", fieldPath, *rules.Gt)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Gte != nil && value < *rules.Gte {
+		defaultMsg := fmt.Sprintf("字段 %s 必须大于等于 %f", fieldPath, *rules.Gte)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	return nil
+}
+
+// validateDouble 验证 float64
+func (e *Engine) validateDouble(
+	vctx *ValidationContext,
+	fieldRules *validate.FieldRules,
+	rules *validate.DoubleRules,
+	value float64,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	if rules.Const != nil && value != *rules.Const {
+		defaultMsg := fmt.Sprintf("字段 %s 必须等于 %f", fieldPath, *rules.Const)
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+			e.getErrorMessage(fieldRules, vctx, "const", defaultMsg)))
+	}
+	if rules.Lt != nil && value >= *rules.Lt {
+		defaultMsg := fmt.Sprintf("字段 %s 必须小于 %f", fieldPath, *rules.Lt)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Lte != nil && value > *rules.Lte {
+		defaultMsg := fmt.Sprintf("字段 %s 必须小于等于 %f", fieldPath, *rules.Lte)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Gt != nil && value <= *rules.Gt {
+		defaultMsg := fmt.Sprintf("字段 %s 必须大于 %f", fieldPath, *rules.Gt)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	if rules.Gte != nil && value < *rules.Gte {
+		defaultMsg := fmt.Sprintf("字段 %s 必须大于等于 %f", fieldPath, *rules.Gte)
+		errors.Add(NewValidationError(fieldPath, ErrCodeOutOfRange,
+			e.getErrorMessage(fieldRules, vctx, "range", defaultMsg)))
+	}
+	return nil
+}
+
+// validateBool 验证 bool
+func (e *Engine) validateBool(
+	vctx *ValidationContext,
+	fieldRules *validate.FieldRules,
+	rules *validate.BoolRules,
+	value bool,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	if rules.Const != nil && value != *rules.Const {
+		defaultMsg := fmt.Sprintf("字段 %s 必须等于 %t", fieldPath, *rules.Const)
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+			e.getErrorMessage(fieldRules, vctx, "const", defaultMsg)))
+	}
+	return nil
+}
+
+// validateRepeated 验证 repeated 字段
+func (e *Engine) validateRepeated(
+	vctx *ValidationContext,
+	fieldRules *validate.FieldRules,
+	rules *validate.RepeatedRules,
+	list protoreflect.List,
+	field protoreflect.FieldDescriptor,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	if list == nil {
+		return nil
+	}
+
+	size := list.Len()
+
+	// 验证元素数量
+	if rules.MinItems != nil && uint64(size) < *rules.MinItems {
+		defaultMsg := fmt.Sprintf("字段 %s 至少需要 %d 个元素", fieldPath, *rules.MinItems)
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidLength,
+			e.getErrorMessage(fieldRules, vctx, "min_items", defaultMsg)))
+	}
+	if rules.MaxItems != nil && uint64(size) > *rules.MaxItems {
+		defaultMsg := fmt.Sprintf("字段 %s 最多允许 %d 个元素", fieldPath, *rules.MaxItems)
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidLength,
+			e.getErrorMessage(fieldRules, vctx, "max_items", defaultMsg)))
+	}
+
+	// 验证唯一性
+	if rules.Unique != nil && *rules.Unique {
+		seen := make(map[interface{}]bool)
+		for i := 0; i < size; i++ {
+			item := list.Get(i).Interface()
+			if seen[item] {
+				defaultMsg := fmt.Sprintf("字段 %s 包含重复元素", fieldPath)
+				errors.Add(NewValidationError(fieldPath, ErrCodeInvalidValue,
+					e.getErrorMessage(fieldRules, vctx, "unique", defaultMsg)))
+				break
+			}
+			seen[item] = true
+		}
+	}
+
+	// TODO: 验证每个元素(需要递归调用validateFieldType)
+
+	return nil
+}
+
+// validateMap 验证 map 字段
+func (e *Engine) validateMap(
+	vctx *ValidationContext,
+	fieldRules *validate.FieldRules,
+	rules *validate.MapRules,
+	mapValue protoreflect.Map,
+	field protoreflect.FieldDescriptor,
+	fieldPath string,
+	errors *ValidationErrors,
+) error {
+	if mapValue == nil {
+		return nil
+	}
+
+	size := mapValue.Len()
+
+	// 验证键值对数量
+	if rules.MinPairs != nil && uint64(size) < *rules.MinPairs {
+		defaultMsg := fmt.Sprintf("字段 %s 至少需要 %d 个键值对", fieldPath, *rules.MinPairs)
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidLength,
+			e.getErrorMessage(fieldRules, vctx, "min_pairs", defaultMsg)))
+	}
+	if rules.MaxPairs != nil && uint64(size) > *rules.MaxPairs {
+		defaultMsg := fmt.Sprintf("字段 %s 最多允许 %d 个键值对", fieldPath, *rules.MaxPairs)
+		errors.Add(NewValidationError(fieldPath, ErrCodeInvalidLength,
+			e.getErrorMessage(fieldRules, vctx, "max_pairs", defaultMsg)))
+	}
+
+	// TODO: 验证 keys 和 values
+
+	return nil
+}

+ 136 - 0
validator/errors.go

@@ -0,0 +1,136 @@
+package validator
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/go-kratos/kratos/v2/errors"
+)
+
+// ValidationError 验证错误
+type ValidationError struct {
+	FieldPath string            // 字段路径 如 "user.email"
+	RuleID    string            // 规则ID
+	Message   string            // 错误消息(中文)
+	MessageEN string            // 错误消息(英文)
+	Value     interface{}       // 失败的值
+	Metadata  map[string]string // 附加信息
+}
+
+// Error 实现 error 接口
+func (e *ValidationError) Error() string {
+	if e.FieldPath != "" {
+		return fmt.Sprintf("%s: %s", e.FieldPath, e.Message)
+	}
+	return e.Message
+}
+
+// ValidationErrors 多个验证错误的集合
+type ValidationErrors struct {
+	Errors   []*ValidationError // 错误列表
+	Warnings []*ValidationError // 警告列表(非阻塞)
+}
+
+// Error 实现 error 接口
+func (e *ValidationErrors) Error() string {
+	if len(e.Errors) == 0 {
+		return ""
+	}
+
+	var messages []string
+	for _, err := range e.Errors {
+		messages = append(messages, err.Error())
+	}
+	return strings.Join(messages, "; ")
+}
+
+// Add 添加错误
+func (e *ValidationErrors) Add(err *ValidationError) {
+	if e.Errors == nil {
+		e.Errors = make([]*ValidationError, 0)
+	}
+	e.Errors = append(e.Errors, err)
+}
+
+// AddWarning 添加警告
+func (e *ValidationErrors) AddWarning(err *ValidationError) {
+	if e.Warnings == nil {
+		e.Warnings = make([]*ValidationError, 0)
+	}
+	e.Warnings = append(e.Warnings, err)
+}
+
+// HasErrors 是否有错误
+func (e *ValidationErrors) HasErrors() bool {
+	return len(e.Errors) > 0
+}
+
+// IsEmpty 是否为空
+func (e *ValidationErrors) IsEmpty() bool {
+	return len(e.Errors) == 0 && len(e.Warnings) == 0
+}
+
+// Count 错误数量
+func (e *ValidationErrors) Count() int {
+	return len(e.Errors)
+}
+
+// ToKratosError 转换为 Kratos 错误
+func (e *ValidationErrors) ToKratosError() error {
+	if !e.HasErrors() {
+		return nil
+	}
+
+	// 构建详细的错误消息
+	var messages []string
+	for _, err := range e.Errors {
+		messages = append(messages, err.Error())
+	}
+
+	// 使用 Kratos 的 BadRequest 错误
+	kratosErr := errors.BadRequest(
+		"VALIDATION_FAILED",
+		strings.Join(messages, "; "),
+	)
+
+	// 添加错误元数据
+	metadata := make(map[string]string)
+	for i, err := range e.Errors {
+		prefix := fmt.Sprintf("error_%d", i)
+		metadata[prefix+"_field"] = err.FieldPath
+		metadata[prefix+"_rule"] = err.RuleID
+		metadata[prefix+"_message"] = err.Message
+	}
+
+	return kratosErr.WithMetadata(metadata)
+}
+
+// NewValidationError 创建验证错误
+func NewValidationError(fieldPath, ruleID, message string) *ValidationError {
+	return &ValidationError{
+		FieldPath: fieldPath,
+		RuleID:    ruleID,
+		Message:   message,
+	}
+}
+
+// NewValidationErrorWithValue 创建带值的验证错误
+func NewValidationErrorWithValue(fieldPath, ruleID, message string, value interface{}) *ValidationError {
+	return &ValidationError{
+		FieldPath: fieldPath,
+		RuleID:    ruleID,
+		Message:   message,
+		Value:     value,
+	}
+}
+
+// 预定义错误代码
+const (
+	ErrCodeInvalidValue   = "INVALID_VALUE"
+	ErrCodeRequired       = "REQUIRED_FIELD"
+	ErrCodeInvalidFormat  = "INVALID_FORMAT"
+	ErrCodeOutOfRange     = "OUT_OF_RANGE"
+	ErrCodeInvalidLength  = "INVALID_LENGTH"
+	ErrCodeInvalidPattern = "INVALID_PATTERN"
+	ErrCodeCELExecution   = "CEL_EXECUTION_ERROR"
+)

+ 210 - 0
validator/example_test.go

@@ -0,0 +1,210 @@
+package validator_test
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"git.ikuban.com/server/kratos-utils/v2/validator"
+	// 引入你的 proto 包
+	// pb "your-project/api/example"
+)
+
+// 示例 1: 基本使用
+func ExampleValidator_basic() {
+	// 创建验证器
+	_, err := validator.New()
+	if err != nil {
+		panic(err)
+	}
+
+	// 创建要验证的消息
+	// msg := &pb.UserRequest{
+	// 	Username: "test",
+	// 	Email:    "test@example.com",
+	// }
+
+	// 执行验证
+	// err = v.Validate(ctx, msg)
+	// if err != nil {
+	// 	fmt.Println("验证失败:", err)
+	// 	return
+	// }
+
+	fmt.Println("验证通过")
+	// Output: 验证通过
+}
+
+// 示例 2: 使用验证组
+func ExampleValidator_withGroups() {
+	ctx := context.Background()
+
+	// 设置验证组
+	ctx = validator.WithGroups(ctx, "create", "strict")
+
+	// 创建消息
+	// msg := &pb.UserRequest{...}
+
+	// 使用默认验证器验证(会应用 create 和 strict 组的规则)
+	// err := validator.Validate(ctx, msg)
+
+	fmt.Println("使用验证组验证")
+}
+
+// 示例 3: 设置验证策略
+func ExampleValidator_withStrategy() {
+	ctx := context.Background()
+
+	// 创建自定义验证策略
+	strategy := &validator.ValidationStrategy{
+		Mode:            validator.ValidationModeAll,
+		FailFast:        true, // 快速失败
+		CollectWarnings: true, // 收集警告
+		MaxErrors:       10,   // 最多收集10个错误
+		EnableCache:     true, // 启用缓存
+	}
+
+	ctx = validator.WithStrategy(ctx, strategy)
+
+	// 创建消息
+	// msg := &pb.UserRequest{...}
+
+	// 执行验证
+	// err := validator.Validate(ctx, msg)
+
+	fmt.Println("使用自定义策略验证")
+}
+
+// 示例 4: 设置语言
+func ExampleValidator_withLanguage() {
+	ctx := context.Background()
+
+	// 设置英文错误消息
+	ctx = validator.WithLanguage(ctx, "en-US")
+
+	// 创建消息
+	// msg := &pb.UserRequest{...}
+
+	// 执行验证(错误消息会使用英文)
+	// err := validator.Validate(ctx, msg)
+
+	fmt.Println("使用英文错误消息")
+}
+
+// 示例 5: 组合使用
+func ExampleValidator_combined() {
+	ctx := context.Background()
+
+	// 组合多个上下文设置
+	ctx = validator.WithGroups(ctx, "create")
+	ctx = validator.WithLanguage(ctx, "zh-CN")
+	ctx = validator.WithAdmin(ctx, false)
+
+	strategy := &validator.ValidationStrategy{
+		FailFast:  true,
+		MaxErrors: 5,
+	}
+	ctx = validator.WithStrategy(ctx, strategy)
+
+	// 创建消息
+	// msg := &pb.UserRegistrationRequest{
+	// 	Username: "testuser",
+	// 	Password: "Test@123",
+	// 	Mobile:   "13800138000",
+	// 	Email:    "test@example.com",
+	// }
+
+	// 执行验证
+	// err := validator.Validate(ctx, msg)
+	// if err != nil {
+	// 	fmt.Println("验证失败:", err)
+	// 	return
+	// }
+
+	fmt.Println("组合验证完成")
+}
+
+// 测试: 中国手机号验证
+func TestChineseMobileValidation(t *testing.T) {
+	testCases := []struct {
+		mobile string
+		valid  bool
+	}{
+		{"13800138000", true},     // 移动
+		{"18912345678", true},     // 电信
+		{"15612345678", true},     // 联通
+		{"12345678901", false},    // 无效前缀
+		{"1380013800", false},     // 长度不足
+		{"abc13800138000", false}, // 包含字母
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.mobile, func(t *testing.T) {
+			// 这里应该创建包含手机号字段的 proto 消息并验证
+			// msg := &pb.UserRequest{Mobile: tc.mobile}
+			// err := validator.Validate(context.Background(), msg)
+			//
+			// if tc.valid && err != nil {
+			// 	t.Errorf("期望验证通过,但失败了: %v", err)
+			// }
+			// if !tc.valid && err == nil {
+			// 	t.Error("期望验证失败,但通过了")
+			// }
+		})
+	}
+}
+
+// 测试: 中国身份证验证
+func TestChineseIDCardValidation(t *testing.T) {
+	testCases := []struct {
+		idCard string
+		valid  bool
+	}{
+		{"110101199003074218", true},  // 有效的18位身份证
+		{"320301199001011234", false}, // 校验位错误
+		{"12345678901234567", false},  // 格式错误
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.idCard, func(t *testing.T) {
+			// 实际验证逻辑
+		})
+	}
+}
+
+// 基准测试: 验证性能
+func BenchmarkValidator(b *testing.B) {
+	v, _ := validator.New()
+	ctx := context.Background()
+
+	// msg := &pb.UserRequest{
+	// 	Username: "testuser",
+	// 	Email:    "test@example.com",
+	// 	Mobile:   "13800138000",
+	// }
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		// _ = v.Validate(ctx, msg)
+		_ = ctx
+		_ = v
+	}
+}
+
+// 基准测试: CEL 表达式性能
+func BenchmarkCELValidation(b *testing.B) {
+	v, _ := validator.New()
+	ctx := context.Background()
+
+	// 包含 CEL 规则的消息
+	// msg := &pb.CELExample{
+	// 	Password: "Test@12345",
+	// }
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		// _ = v.Validate(ctx, msg)
+		_ = ctx
+		_ = v
+	}
+}

+ 123 - 0
validator/validator.go

@@ -0,0 +1,123 @@
+package validator
+
+import (
+	"context"
+
+	"google.golang.org/protobuf/proto"
+)
+
+// Validator 验证器接口
+type Validator interface {
+	// Validate 验证消息
+	Validate(ctx context.Context, msg proto.Message) error
+
+	// ValidateWithGroups 使用指定组验证消息
+	ValidateWithGroups(ctx context.Context, msg proto.Message, groups ...string) error
+
+	// ValidateField 验证单个字段
+	ValidateField(ctx context.Context, fieldPath string, value interface{}, rules interface{}) error
+}
+
+// validator 默认验证器实现
+type validator struct {
+	engine      *Engine
+	celExecutor *CELExecutor
+}
+
+// New 创建新的验证器
+func New(opts ...Option) (Validator, error) {
+	// 创建 CEL 执行器
+	celExecutor, err := NewCELExecutor()
+	if err != nil {
+		return nil, err
+	}
+
+	// 创建验证引擎
+	engine := NewEngine(celExecutor)
+
+	v := &validator{
+		engine:      engine,
+		celExecutor: celExecutor,
+	}
+
+	// 应用选项
+	for _, opt := range opts {
+		opt(v)
+	}
+
+	return v, nil
+}
+
+// Validate 验证消息
+func (v *validator) Validate(ctx context.Context, msg proto.Message) error {
+	if msg == nil {
+		return nil
+	}
+
+	// 从上下文创建验证上下文
+	vctx := FromContext(ctx)
+
+	// 执行验证
+	return v.engine.Validate(vctx, msg)
+}
+
+// ValidateWithGroups 使用指定组验证消息
+func (v *validator) ValidateWithGroups(ctx context.Context, msg proto.Message, groups ...string) error {
+	if msg == nil {
+		return nil
+	}
+
+	// 创建验证上下文并设置组
+	vctx := FromContext(ctx)
+	vctx.Groups = groups
+
+	// 执行验证
+	return v.engine.Validate(vctx, msg)
+}
+
+// ValidateField 验证单个字段
+func (v *validator) ValidateField(ctx context.Context, fieldPath string, value interface{}, rules interface{}) error {
+	vctx := FromContext(ctx)
+	return v.engine.ValidateField(vctx, fieldPath, value, rules)
+}
+
+// Option 验证器选项
+type Option func(*validator)
+
+// WithCELFunctions 添加自定义 CEL 函数(暂时保留接口,未来扩展)
+func WithCELFunctions() Option {
+	return func(v *validator) {
+		// TODO: 实现自定义 CEL 函数注册
+	}
+}
+
+// MustNew 创建验证器,如果失败则 panic
+func MustNew(opts ...Option) Validator {
+	v, err := New(opts...)
+	if err != nil {
+		panic(err)
+	}
+	return v
+}
+
+// 全局默认验证器
+var defaultValidator Validator
+
+// init 初始化默认验证器
+func init() {
+	var err error
+	defaultValidator, err = New()
+	if err != nil {
+		panic(err)
+	}
+}
+
+// Validate 使用默认验证器验证消息
+func Validate(ctx context.Context, msg proto.Message) error {
+	return defaultValidator.Validate(ctx, msg)
+}
+
+// ValidateWithGroups 使用默认验证器和指定组验证消息
+func ValidateWithGroups(ctx context.Context, msg proto.Message, groups ...string) error {
+	return defaultValidator.ValidateWithGroups(ctx, msg, groups...)
+}