diff --git a/code/base-project/base-app/src/app/auth/register.tsx b/code/base-project/base-app/src/app/auth/register.tsx index bc864bfa..12b64352 100644 --- a/code/base-project/base-app/src/app/auth/register.tsx +++ b/code/base-project/base-app/src/app/auth/register.tsx @@ -1,10 +1,149 @@ -import { Text, View } from 'react-native' +import type { FormWrapperRef } from '@/components/common/FormWrapper' +import { Lock, UserRound } from 'lucide-react-native' +import { useRef, useState } from 'react' +import { Image, View } from 'react-native' +import FormWrapper from '@/components/common/FormWrapper' +import { Button, ButtonSpinner, ButtonText } from '@/components/ui/button' +import { useToast } from '@/utils/toast' + +interface RegisterForm { + username: string + password: string + confirmPassword: string + a: string + b: string + c: string +} + +export default function AuthLogin() { + const toast = useToast() + const [form, setForm] = useState({ + username: '', + password: '', + confirmPassword: '', + a: '', + b: '', + c: '', + }) + const [loading, setLoading] = useState(false) + const formRef = useRef(null) + + const handleInputChange = (field: keyof RegisterForm, value: string) => { + setForm(prev => ({ ...prev, [field]: value })) + } + + const defaultLogin = () => { + if (formRef.current?.validate()) { + setLoading(true) + toast.show({ title: '校验通过,开始注册', action: 'success' }) + // 模拟注册过程 + setTimeout(() => { + setLoading(false) + }, 2000) + } + } -export default function Register() { return ( - - 测试内容 - + + + + + + handleInputChange('username', text)} + /> + + + val && !/\d{6}/.test(val) ? '请输入6位数字密码' : null, + ]} + > + handleInputChange('password', text)} + /> + + + val !== form.password ? '两次密码不一致' : null, + ]} + > + handleInputChange('confirmPassword', text)} + /> + + + handleInputChange('a', text)} + /> + + + handleInputChange('b', text)} + /> + + + handleInputChange('c', text)} + /> + + + + ) } diff --git a/code/base-project/base-app/src/components/common/FormWrapper.tsx b/code/base-project/base-app/src/components/common/FormWrapper.tsx new file mode 100644 index 00000000..51334bc5 --- /dev/null +++ b/code/base-project/base-app/src/components/common/FormWrapper.tsx @@ -0,0 +1,210 @@ +// FormWrapper.tsx +import React, { createContext, forwardRef, useContext, useEffect, useImperativeHandle, useRef, useState } from 'react' +import { + FormControl, + FormControlError, + FormControlErrorIcon, + FormControlErrorText, + FormControlLabel, + FormControlLabelText, +} from '@/components/ui/form-control' +import { AlertCircleIcon, EyeIcon, EyeOffIcon } from '@/components/ui/icon' +import { InputField, InputIcon, InputSlot, Input as NativeBaseInput } from '@/components/ui/input' + +export interface FormWrapperRef { + validate: () => boolean + getValues: () => Record +} + +// 创建 FormContext 来传递表单数据和验证器注册函数 +interface FormContextType { + form: T + registerValidator: (name: string, validator: () => boolean) => void + unregisterValidator: (name: string) => void +} + +const FormContext = createContext(undefined) + +// ---------------- FormItem ---------------- +export interface FormItemProps = any> { + name: keyof T + label?: string + required?: boolean + rules?: ((val: any) => string | null)[] + children: React.ReactNode +} + +export function FormItem>({ + name, + label, + required, + rules, + children, +}: FormItemProps) { + const formContext = useContext(FormContext) + if (!formContext) { + throw new Error('FormItem must be used within FormWrapper') + } + + const { form, registerValidator, unregisterValidator } = formContext + const [error, setError] = useState('') + + // 从 form 中根据 name 获取对应的值 + const value = form[name] + + const validate = (): boolean => { + let err = '' + + if (required && (value === undefined || value === '' || value === null)) { + err = `${label || String(name)} 不能为空` + } + else if (rules) { + for (const rule of rules) { + const msg = rule(value) + if (msg) { + err = msg + break + } + } + } + setError(err) + return !err + } + + // 注册验证函数到父组件 + useEffect(() => { + registerValidator(String(name), validate) + return () => unregisterValidator(String(name)) + }, [validate, registerValidator, unregisterValidator, name]) + + return ( + + {label && ( + + {label} + + )} + {children} + {error && ( + + + {error} + + )} + + ) +} + +// ---------------- FormInput ---------------- +export interface FormInputProps { + placeholder?: string + leftIcon?: React.ComponentType + isPassword?: boolean + value?: string + onChangeText?: (text: string) => void +} + +export function FormInput({ + placeholder, + leftIcon, + isPassword, + value, + onChangeText, +}: FormInputProps) { + const [showPassword, setShowPassword] = useState(false) + + return ( + + {leftIcon && ( + + + + )} + + {isPassword && ( + setShowPassword(prev => !prev)}> + + + )} + + ) +} + +// ---------------- FormWrapper ---------------- +interface FormWrapperProps> { + children: React.ReactNode + form: T +} + +// 定义组件接口 +interface FormWrapperComponent extends React.ForwardRefExoticComponent< + FormWrapperProps & React.RefAttributes +> { + FormItem: typeof FormItem + Input: typeof FormInput +} + +// 内部组件实现 +function FormWrapperInner>( + props: FormWrapperProps, + ref: React.Ref, +) { + const { children, form } = props + const validators = useRef boolean>>(new Map()) + + const registerValidator = (name: string, validator: () => boolean) => { + validators.current.set(name, validator) + } + + const unregisterValidator = (name: string) => { + validators.current.delete(name) + } + + const validate = (): boolean => { + let isValid = true + + // 然后调用所有验证函数 + validators.current.forEach((validator) => { + if (!validator()) { + isValid = false + } + }) + + return isValid + } + + const getValues = (): Record => form + + useImperativeHandle(ref, () => ({ validate, getValues })) + + const contextValue: FormContextType = { + form, + registerValidator, + unregisterValidator, + } + + return ( + + {children} + + ) +} + +// 创建基础组件 +const BaseFormWrapper = forwardRef(FormWrapperInner) as >( + props: FormWrapperProps & { ref?: React.Ref }, +) => React.JSX.Element + +// 创建带有静态属性的组件 +const FormWrapper = BaseFormWrapper as FormWrapperComponent + +// 添加静态属性 +FormWrapper.FormItem = FormItem +FormWrapper.Input = FormInput + +export default FormWrapper diff --git a/code/base-project/base-go-v2/go.mod b/code/base-project/base-go-v2/go.mod index aaf2ffdb..b5f458ce 100644 --- a/code/base-project/base-go-v2/go.mod +++ b/code/base-project/base-go-v2/go.mod @@ -4,8 +4,10 @@ go 1.25.0 require ( github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.10.9 github.com/spf13/viper v1.21.0 + github.com/valyala/fasthttp v1.68.0 go.uber.org/zap v1.27.0 ) @@ -13,7 +15,6 @@ require ( github.com/andybalholm/brotli v1.2.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/jmoiron/sqlx v1.4.0 // indirect github.com/klauspost/compress v1.18.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect @@ -23,7 +24,6 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.68.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/sys v0.37.0 // indirect diff --git a/code/base-project/base-go-v2/internal/api/auth/index.go b/code/base-project/base-go-v2/internal/api/auth/index.go index b87d2261..d9cc0685 100644 --- a/code/base-project/base-go-v2/internal/api/auth/index.go +++ b/code/base-project/base-go-v2/internal/api/auth/index.go @@ -1,13 +1,17 @@ package auth import ( + "base-go-v2/internal/db" response "base-go-v2/internal/reponse" "base-go-v2/internal/router" + "base-go-v2/internal/utils/mapx" + "base-go-v2/internal/utils/uid" ) func InitAuthRouter(r *router.Router) { group := r.Group("auth") group.POST("/login/default", loginDefault) + group.POST("/register/default", registerDefault) } // 默认登录 @@ -15,13 +19,41 @@ func loginDefault(c *router.Context) error { return response.Success(c).Send() } -// 注册 func registerDefault(c *router.Context) error { - var req struct { - Email string `json:"email"` - Password string `json:"password"` + params, err := c.GetBodyWithRequired("account", "password") + if err != nil { + return err + } + account := params.GetString("account") + password := params.GetString("password") + + // 构造查询条件,检查账号是否已存在 + accountQuery := mapx.New().SetKV("account", account) + + existingUsers, err := db.Find("user_info", accountQuery) + if err != nil { + return err } - if err := c.BindJSON(&req); err != nil { + if len(existingUsers) > 0 { + return response.Error(c).Message("该账号已被注册").Send() } + + // 构造新用户数据 + newUserData := mapx.New().SetKV( + "id", uid.NextID(), + "account", account, + "password", password, + ) + + // 插入新用户 + insertedRows, err := db.InsertOne("user_info", newUserData) + if err != nil { + return err + } + if insertedRows != 1 { + return response.Error(c).Message("注册失败").Send() + } + + return response.Success(c).Message("注册成功").Send() } diff --git a/code/base-project/base-go-v2/internal/api/middleware/error.go b/code/base-project/base-go-v2/internal/api/middleware/error.go index 90146122..bd9fabd0 100644 --- a/code/base-project/base-go-v2/internal/api/middleware/error.go +++ b/code/base-project/base-go-v2/internal/api/middleware/error.go @@ -1,29 +1,45 @@ package middleware import ( + "base-go-v2/internal/errs" "base-go-v2/internal/logx" "base-go-v2/internal/reponse" "base-go-v2/internal/router" + "errors" "runtime/debug" ) func ErrorMiddleware() router.HandlerFunc { + // 可扩展的业务错误类型列表 + businessErrors := []interface{}{ + (*errs.ValidationError)(nil), + // (*errs.BusinessError)(nil), + // (*errs.AuthError)(nil), + } + return func(c *router.Context) error { err := c.Next() - - if err != nil { - stack := string(debug.Stack()) - - logx.Logger.Error("请求错误", - logx.String("path", string(c.RequestCtx.Path())), - logx.String("method", string(c.RequestCtx.Method())), - logx.String("error", err.Error()), - logx.String("stack", stack), - ) - - return response.Error(c).Message(err.Error()).Send() + if err == nil { + return nil } - return nil + // 判断是否属于业务错误 + for _, be := range businessErrors { + // be 是 nil 指针类型 + if errors.As(err, &be) { + // 业务错误,不记录日志 + return response.Error(c).Message(err.Error()).Send() + } + } + + // 系统错误,记录日志 + stack := string(debug.Stack()) + logx.Logger.Error("请求错误", + logx.String("path", string(c.RequestCtx.Path())), + logx.String("method", string(c.RequestCtx.Method())), + logx.String("error", err.Error()), + logx.String("stack", stack), + ) + return response.Error(c).Message("服务器内部错误").Send() } } diff --git a/code/base-project/base-go-v2/internal/errs/validation.go b/code/base-project/base-go-v2/internal/errs/validation.go new file mode 100644 index 00000000..43bde8b8 --- /dev/null +++ b/code/base-project/base-go-v2/internal/errs/validation.go @@ -0,0 +1,19 @@ +package errs + +import "fmt" + +type ValidationError struct { + field string + message string +} + +func (e *ValidationError) Error() string { + return fmt.Sprintf("字段 %s: %s", e.field, e.message) +} + +func NewValidationError(field, message string) error { + return &ValidationError{ + field: field, + message: message, + } +} diff --git a/code/base-project/base-go-v2/internal/router/context.go b/code/base-project/base-go-v2/internal/router/context.go index 89159d66..5fbc6a2a 100644 --- a/code/base-project/base-go-v2/internal/router/context.go +++ b/code/base-project/base-go-v2/internal/router/context.go @@ -1,6 +1,8 @@ package router import ( + "base-go-v2/internal/utils/mapx" + "base-go-v2/internal/utils/validate" "encoding/json" "sync" @@ -72,6 +74,37 @@ func (c *Context) BindJSON(obj interface{}) error { return json.Unmarshal(body, obj) } +// BodyMap 将请求体解析为 mapx.M +func (c *Context) BodyMap() (mapx.M, error) { + body, err := c.Body() // 使用 BodyCache 读取请求体 + if err != nil { + return nil, err + } + + m := mapx.New() + if err := json.Unmarshal(body, &m); err != nil { + return nil, err + } + + return m, nil +} + +func (c *Context) GetBodyWithRequired(keys ...string) (mapx.M, error) { + // 解析请求体 + req, err := c.BodyMap() + if err != nil { + return nil, err + } + + // 校验必填字段 + if err := validate.ValidateNotEmpty(req, keys...); err != nil { + return nil, err + } + + // 返回整个请求体(包含所有字段) + return req, nil +} + // PostForm 获取表单参数 func (c *Context) PostForm(key string) string { return string(c.RequestCtx.FormValue(key)) diff --git a/code/base-project/base-go-v2/internal/utils/mapx/index.go b/code/base-project/base-go-v2/internal/utils/mapx/index.go new file mode 100644 index 00000000..1dac82b5 --- /dev/null +++ b/code/base-project/base-go-v2/internal/utils/mapx/index.go @@ -0,0 +1,109 @@ +package mapx + +// M 是 map[string]interface{} 的别名 +type M map[string]interface{} + +// New 创建一个空 M +func New() M { + return M{} +} + +// Set 设置单个键值,支持链式调用 +func (m M) Set(key string, value interface{}) M { + m[key] = value + return m +} + +// SetKV 批量设置键值,可变参数形式 +func (m M) SetKV(kvs ...interface{}) M { + if len(kvs)%2 != 0 { + panic("SetKV 参数必须成对出现") + } + for i := 0; i < len(kvs); i += 2 { + key, ok := kvs[i].(string) + if !ok { + panic("SetKV 键必须是字符串") + } + m[key] = kvs[i+1] + } + return m +} + +// Get 获取值 +func (m M) Get(key string) interface{} { + return m[key] +} + +// GetString 获取字符串类型 +func (m M) GetString(key string) string { + if v, ok := m[key]; ok { + if s, ok := v.(string); ok { + return s + } + } + return "" +} + +// GetInt 获取整数类型 +func (m M) GetInt(key string) int { + if v, ok := m[key]; ok { + switch val := v.(type) { + case int: + return val + case int8: + return int(val) + case int16: + return int(val) + case int32: + return int(val) + case int64: + return int(val) + case float32: + return int(val) + case float64: + return int(val) + } + } + return 0 +} + +// GetFloat 获取浮点数类型 +func (m M) GetFloat(key string) float64 { + if v, ok := m[key]; ok { + switch val := v.(type) { + case float32: + return float64(val) + case float64: + return val + case int: + return float64(val) + case int8: + return float64(val) + case int16: + return float64(val) + case int32: + return float64(val) + case int64: + return float64(val) + } + } + return 0 +} + +// GetBool 获取布尔类型 +func (m M) GetBool(key string) bool { + if v, ok := m[key]; ok { + if b, ok := v.(bool); ok { + return b + } + } + return false +} + +// Merge 合并另一个 M,后者会覆盖前者同名字段 +func (m M) Merge(other M) M { + for k, v := range other { + m[k] = v + } + return m +} diff --git a/code/base-project/base-go-v2/internal/utils/validate/validate.go b/code/base-project/base-go-v2/internal/utils/validate/validate.go new file mode 100644 index 00000000..015dd282 --- /dev/null +++ b/code/base-project/base-go-v2/internal/utils/validate/validate.go @@ -0,0 +1,42 @@ +package validate + +import ( + "base-go-v2/internal/errs" + "errors" + "reflect" + "strings" +) + +// ValidateNotEmpty 校验结构体中指定字段不能为空 +func ValidateNotEmpty(s interface{}, fields ...string) error { + v := reflect.ValueOf(s) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + for _, field := range fields { + f := v.FieldByName(field) + if !f.IsValid() { + return errors.New("field " + field + " does not exist") + } + + switch f.Kind() { + case reflect.String: + if strings.TrimSpace(f.String()) == "" { + return errs.NewValidationError(field, "不能为空") + } + case reflect.Slice, reflect.Array, reflect.Map: + if f.Len() == 0 { + return errs.NewValidationError(field, "不能为空") + } + case reflect.Ptr, reflect.Interface: + if f.IsNil() { + return errs.NewValidationError(field, "不能为空") + } + default: + continue + } + } + + return nil +}