diff --git a/code/base-project/base-app/eslint.config.mjs b/code/base-project/base-app/eslint.config.mjs index 6125f118..f69e566e 100644 --- a/code/base-project/base-app/eslint.config.mjs +++ b/code/base-project/base-app/eslint.config.mjs @@ -12,5 +12,6 @@ export default antfu({ 'no-console': 'off', // 关闭 console 报错 'node/prefer-global/process': 'off', '@typescript-eslint/no-require-imports': 'off', + 'no-useless-catch': 'off', }, }) diff --git a/code/base-project/base-app/src/app/_layout.tsx b/code/base-project/base-app/src/app/_layout.tsx index a9eb8566..d1c0789f 100644 --- a/code/base-project/base-app/src/app/_layout.tsx +++ b/code/base-project/base-app/src/app/_layout.tsx @@ -5,7 +5,7 @@ import { Stack, useRouter } from 'expo-router' import * as SplashScreen from 'expo-splash-screen' import { StatusBar } from 'expo-status-bar' import { RefreshCw } from 'lucide-react-native' -import { useEffect, useState } from 'react' +import { useEffect } from 'react' import EmptyState from '@/components/common/EmptyStatus' import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider' import { useUserStore } from '@/store/user' @@ -33,33 +33,23 @@ export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) { export default function RootLayout() { const router = useRouter() - - const token = useUserStore(state => state.token) + const userStore = useUserStore() const [fontsLoaded] = useFonts({ SpaceMono: require('@/static/fonts/SpaceMono-Regular.ttf'), }) - const [storeLoaded, setStoreLoaded] = useState(false) - - // 等待 store 恢复数据 - useEffect(() => { - if (token !== undefined) { - setStoreLoaded(true) - } - }, [token]) - // splash screen 隐藏 & 登录跳转 useEffect(() => { - if (fontsLoaded && storeLoaded) { + if (fontsLoaded) { SplashScreen.hideAsync() // 如果没有 token,则跳转登录页 - if (token === null) { + if (!userStore.isLoggedIn()) { router.replace('/auth') } } - }, [fontsLoaded, storeLoaded, token]) + }, [fontsLoaded]) return ( diff --git a/code/base-project/base-app/src/app/auth/_layout.tsx b/code/base-project/base-app/src/app/auth/_layout.tsx index a39b7384..212f2238 100644 --- a/code/base-project/base-app/src/app/auth/_layout.tsx +++ b/code/base-project/base-app/src/app/auth/_layout.tsx @@ -1,6 +1,9 @@ import type { StepLayoutItem } from '@/components/custom/StepLayout' -import { Slot } from 'expo-router' +import { Slot, Stack, useRouter } from 'expo-router' +import { Image, Text, View } from 'react-native' +import { SafeAreaView } from 'react-native-safe-area-context' import StepLayout from '@/components/custom/StepLayout' +import CustomTabbar from '@/components/custom/Tabbar' const steps: StepLayoutItem[] = [ { @@ -22,5 +25,6 @@ export default function AuthLayout() { + ) } diff --git a/code/base-project/base-app/src/app/auth/login.tsx b/code/base-project/base-app/src/app/auth/login.tsx index c474d54b..9550c01a 100644 --- a/code/base-project/base-app/src/app/auth/login.tsx +++ b/code/base-project/base-app/src/app/auth/login.tsx @@ -36,17 +36,20 @@ export default function AuthLogin() { setForm(prev => ({ ...prev, [field]: value })) } const defaultLogin = () => { - alovaInstance.Post('/auth/login/default', { + alovaInstance.Post('/auth/test', { title: 'foo', body: 'bar', userId: 1, - }).then((res) => { - console.log(res) }) + .then((res) => { + console.log('成功返回:', res) + }) + .catch((err) => { + console.log('请求失败或未授权:', err) + }) } return ( - - + {headerVisible && ( @@ -44,10 +46,7 @@ export default function StepLayout({ steps, children, showHearder = true }: Step onPress={canGoBack ? (currentStep.onBack ?? (() => router.back())) : undefined} style={{ opacity: canGoBack ? 1 : 0 }} > - + diff --git a/code/base-project/base-app/src/request/api/auth.ts b/code/base-project/base-app/src/request/api/auth.ts new file mode 100644 index 00000000..e69de29b diff --git a/code/base-project/base-app/src/request/index.ts b/code/base-project/base-app/src/request/index.ts index 6b979b19..8fcb7ae8 100644 --- a/code/base-project/base-app/src/request/index.ts +++ b/code/base-project/base-app/src/request/index.ts @@ -1,21 +1,110 @@ +import type { Method } from 'alova' +import type { AxiosError } from 'axios' import { axiosRequestAdapter } from '@alova/adapter-axios' import { createAlova } from 'alova' -import { createServerTokenAuthentication } from 'alova/client' -import adapterFetch from 'alova/fetch' -import { useToast } from '@/utils/toast' +import { router } from 'expo-router' +import { userStore } from '@/store/user' +import { toast } from '@/utils/toast' -const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication({ - // ... -}) +enum HttpCode { + Success = 200, + Unauthorized = 401, + RefreshToken = 402, + Fail = 500, +} + +const WhiteList = [ + '/auth/login/**', + '/auth/register/**', + '/auth/refresh/token', +] + +function isWhiteListed(url: string) { + return WhiteList.some((pattern) => { + if (pattern.endsWith('/**')) { + const base = pattern.slice(0, -3) + return url.startsWith(base) + } + return url === pattern + }) +} const alovaInstance = createAlova({ - // baseURL: 'http://192.168.1.136:8085/app', - baseURL: 'http://192.168.5.6:8085/app', + baseURL: 'http://192.168.1.136:8085/app', requestAdapter: axiosRequestAdapter(), - responded(response) { - // response自动被推断为AxiosResponse类型 - - return response.data + responded: { + onSuccess: async (response, method: Method) => { + const resData = response.data as ResponseData + + if (resData.code === HttpCode.Success) { + return resData.data + } + + // Access Token 失效 → 重新登录 + if (resData.code === HttpCode.Unauthorized) { + console.log('===到这一步了===') + router.replace('/auth/login') + return + } + + // Access Token 过期但 Refresh Token 有效 → 刷新 token 并重试 + if (resData.code === HttpCode.RefreshToken) { + try { + const refreshToken = userStore.getState().userInfo?.refreshToken + if (!refreshToken) { + router.replace('/auth/login') + return + } + + // 调用刷新接口 + const res = await alovaInstance.Get>('/auth/refresh/token', { + headers: { + refreshToken, + }, + }) + const newToken = res.data + + // 更新 store + userStore.getState().setUserInfo({ + ...userStore.getState().userInfo!, + token: newToken, + }) + + // 重试原请求 + method.config.headers.token = newToken + await method.send() + + return + } + catch (err) { + console.log('===到这一步了===') + router.replace('/auth/login') + return Promise.reject(err) + } + } + + // 其他错误 + return Promise.reject(resData.message || '请求失败') + }, + + onError: (error: AxiosError, method) => { + console.log(error.status) + return Promise.reject(error) + }, + }, + beforeRequest(method) { + if (isWhiteListed(method.url)) + return + + if (!userStore.getState().isLoggedIn()) { + toast.show({ + title: '尚未登录', + }) + router.replace('/auth/login') + return + } + + method.config.headers.token = userStore.getState().userInfo?.token ?? '' }, }) diff --git a/code/base-project/base-app/src/store/user.ts b/code/base-project/base-app/src/store/user.ts index 79f40036..9381a9e8 100644 --- a/code/base-project/base-app/src/store/user.ts +++ b/code/base-project/base-app/src/store/user.ts @@ -4,26 +4,22 @@ import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' interface UserState { - userInfo: { userId: string, username: string } | null - token: string | null - setUserInfo: (userInfo: UserState['userInfo']) => void - setToken: (token: string) => void + userInfo: UserInfo | null + setUserInfo: (userInfo: UserInfo | null) => void logout: () => void isLoggedIn: () => boolean } -export const useUserStore = create()( +const useUserStore = create()( persist( (set, get) => ({ userInfo: null, - token: null, setUserInfo: userInfo => set({ userInfo }), - setToken: token => set({ token }), logout: () => { - console.log('当前 token:', get().token) - set({ userInfo: null, token: null }) + console.log('当前 token:', get().userInfo?.token) + set({ userInfo: null }) }, - isLoggedIn: () => !!get().token, + isLoggedIn: () => !!get().userInfo?.token, }), { name: 'user-storage', @@ -31,3 +27,6 @@ export const useUserStore = create()( }, ), ) +const userStore = useUserStore + +export { userStore, useUserStore } diff --git a/code/base-project/base-app/src/types/global.d.ts b/code/base-project/base-app/src/types/global.d.ts new file mode 100644 index 00000000..996114a6 --- /dev/null +++ b/code/base-project/base-app/src/types/global.d.ts @@ -0,0 +1,20 @@ +export {} + +declare global { + interface ResponseData { + trackId: string + code: number + message: string + data: T + } + + interface UserInfo { + account: string + id: string + seqNo: number + username: string + gender: number + token?: string + refreshToken?: string + } +} diff --git a/code/base-project/base-app/src/utils/toast.tsx b/code/base-project/base-app/src/utils/toast.tsx index 7228559c..44675211 100644 --- a/code/base-project/base-app/src/utils/toast.tsx +++ b/code/base-project/base-app/src/utils/toast.tsx @@ -1,5 +1,5 @@ import type { IToastProps } from '@gluestack-ui/core/lib/esm/toast/creator/types' -import React, { useRef } from 'react' +import React, { createContext, useContext, useRef } from 'react' import { Toast, ToastDescription, ToastTitle, useToast as useGluestackToast } from '@/components/ui/toast' type ToastVariant = 'solid' | 'outline' @@ -12,28 +12,74 @@ interface ShowToastOptions extends IToastProps { action?: ToastAction } +// 创建全局 toast 实例 +let globalToast: any = null + +// 创建 Context 用于在组件内部使用 +const ToastContext = createContext | null>(null) + export function ToastProviderWrapper({ children }: { children: React.ReactNode }) { const toast = useGluestackToast() const toastRef = useRef(toast) toastRef.current = toast - return <>{children} + // 设置全局实例 + React.useEffect(() => { + globalToast = { + show: (options: ShowToastOptions) => { + const id = Math.random().toString() + toast.show({ + id, + placement: options.placement ?? 'top', + duration: options.duration !== undefined ? options.duration : 3000, + avoidKeyboard: options.avoidKeyboard ?? false, + containerStyle: options.containerStyle, + onCloseComplete: options.onCloseComplete, + render: options.render + ? () => options.render!({ id }) + : ({ id }) => ( + + {options.title && {options.title}} + {options.description && {options.description}} + + ), + }) + return id + }, + close: (id: string) => { + toast.close(id) + }, + closeAll: () => { + toast.closeAll() + }, + isActive: (id: string) => toast.isActive(id), + } + }, [toast]) + + return ( + + {children} + + ) } /** - * 自定义 Hook,用法: - * const toast = useToast(); - * toast.show({ ... }); - * toast.close(id); - * toast.closeAll(); + * 自定义 Hook,用于 React 组件内部 */ export function useToast() { - const toast = useGluestackToast() + const toast = useContext(ToastContext) + + if (!toast) { + throw new Error('useToast must be used within a ToastProviderWrapper') + } return { show: (options: ShowToastOptions) => { const id = Math.random().toString() - toast.show({ id, placement: options.placement ?? 'top', @@ -54,18 +100,40 @@ export function useToast() { ), }) - return id }, - close: (id: string) => { toast.close(id) }, - closeAll: () => { toast.closeAll() }, - isActive: (id: string) => toast.isActive(id), } } + +/** + * 全局 toast 实例,可在组件外部使用 + */ +export const toast = { + show: (options: ShowToastOptions) => { + if (!globalToast) { + console.warn('Toast not initialized. Make sure ToastProviderWrapper is rendered.') + return '' + } + return globalToast.show(options) + }, + close: (id: string) => { + if (globalToast) { + globalToast.close(id) + } + }, + closeAll: () => { + if (globalToast) { + globalToast.closeAll() + } + }, + isActive: (id: string) => { + return globalToast ? globalToast.isActive(id) : false + }, +} diff --git a/code/base-project/base-app/tsconfig.json b/code/base-project/base-app/tsconfig.json index 4ffc7b86..5d7a381c 100644 --- a/code/base-project/base-app/tsconfig.json +++ b/code/base-project/base-app/tsconfig.json @@ -12,7 +12,7 @@ "include": [ "./**/*.ts", "./**/*.tsx", - "./src/types/*.d.ts", + "src/**/*.d.ts", "nativewind-env.d.ts" ], "exclude": [ 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 985a1cd8..a1abbe14 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 @@ -13,18 +13,48 @@ func InitAuthRouter(r *router.Router) { group := r.Group("auth") group.POST("/login/default", loginDefault) group.POST("/register/default", registerDefault) + group.POST("/refresh/token", RefreshAccessToken) } -//func RefreshToken(c *router.Context) error { -// _, err := c.GetBodyWithRequired("accessToken", "refreshToken") -// if err != nil { -// return err -// } -// //accessToken := bodyData.GetString("accessToken") -// //refreshToken := bodyData.GetString("refreshToken") -//} +func RefreshAccessToken(c *router.Context) error { + // 从 Header 获取 Refresh Token + refreshToken := c.Header("refreshToken") + if refreshToken == "" { + return response.Fail(c). + Code(response.HttpCode.Unauthorized). + Message("未提供 Token"). + Send() + } + + // 验证 Refresh Token + tokenResult := jwtx.VerifyToken(refreshToken) + + if !tokenResult.IsValid || tokenResult.IsExpired { + // Refresh Token 无效或过期,需要重新登录 + return response.Fail(c). + Code(response.HttpCode.Unauthorized). + Message("登录已过期,请重新登录"). + Send() + } + + userID := tokenResult.Claims.UserID + + // 生成新的 Access Token + newAccessToken, err := jwtx.CreateAccessToken(userID) + if err != nil { + return err + } + + // 返回新的 Access Token + respData := map[string]string{ + "accessToken": newAccessToken, + } + + return response.Success(c). + Data(respData). + Send() +} -// 默认登录 // 默认登录 func loginDefault(c *router.Context) error { // 获取请求体并校验必填字段 @@ -43,13 +73,12 @@ func loginDefault(c *router.Context) error { ) user, err := db.FindOne("user_info", userQuery) if err != nil { - return response.Success(c).Message("查询用户失败,请稍后重试").IsSuccess(false).Send() + return response.Success(c).Message("查询用户失败,请稍后重试").Send() } if user == nil { // 账号或密码错误 - return response.Success(c). - IsSuccess(false). + return response.Fail(c). Message("账号或密码错误"). Send() } @@ -99,7 +128,7 @@ func registerDefault(c *router.Context) error { } if len(existingUsers) > 0 { - return response.Error(c).IsSuccess(false).Message("该账号已被注册").Send() + return response.Fail(c).Message("该账号已被注册").Send() } // 构造新用户数据 @@ -115,7 +144,7 @@ func registerDefault(c *router.Context) error { return err } if insertedRows != 1 { - return response.Success(c).IsSuccess(false).Message("注册失败").Send() + return response.Fail(c).Message("注册失败").Send() } return response.Success(c).Message("注册成功").Send() diff --git a/code/base-project/base-go-v2/internal/api/middleware/auth.go b/code/base-project/base-go-v2/internal/api/middleware/auth.go new file mode 100644 index 00000000..faddcfb9 --- /dev/null +++ b/code/base-project/base-go-v2/internal/api/middleware/auth.go @@ -0,0 +1,39 @@ +package middleware + +import ( + response "base-go-v2/internal/reponse" + "base-go-v2/internal/router" + "base-go-v2/internal/utils/jwtx" +) + +func AuthMiddleware() router.HandlerFunc { + return func(c *router.Context) error { + token := c.Header("token") + if token == "" { + c.Abort() + return response.Fail(c). + Code(response.HttpCode.Unauthorized). + Message("未提供 Token"). + Send() + } + + tokenVerify := jwtx.VerifyToken(token) + + // Access Token 有效 + if tokenVerify.IsValid { + c.Set("user_id", tokenVerify.Claims.UserID) + return nil + } + + // Access Token 失效 → 返回刷新提示 + if !tokenVerify.IsValid { + c.Abort() + return response.Fail(c). + Code(response.HttpCode.RefreshToken). + Message("Access Token 已过期,请刷新"). + Send() + } + + return c.Next() + } +} 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 bd9fabd0..ee32d157 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 @@ -28,7 +28,7 @@ func ErrorMiddleware() router.HandlerFunc { // be 是 nil 指针类型 if errors.As(err, &be) { // 业务错误,不记录日志 - return response.Error(c).Message(err.Error()).Send() + return response.Fail(c).Message(err.Error()).Send() } } @@ -40,6 +40,6 @@ func ErrorMiddleware() router.HandlerFunc { logx.String("error", err.Error()), logx.String("stack", stack), ) - return response.Error(c).Message("服务器内部错误").Send() + return response.Fail(c).Message("服务器内部错误").Send() } } diff --git a/code/base-project/base-go-v2/internal/api/middleware/recovery.go b/code/base-project/base-go-v2/internal/api/middleware/recovery.go index aa4ff01c..5e3d7c75 100644 --- a/code/base-project/base-go-v2/internal/api/middleware/recovery.go +++ b/code/base-project/base-go-v2/internal/api/middleware/recovery.go @@ -21,7 +21,7 @@ func RecoveryMiddleware() router.HandlerFunc { ) // 返回统一 JSON 响应 - _ = response.Error(c).Message("系统 Panic").Send() + _ = response.Fail(c).Message("系统 Panic").Send() } }() diff --git a/code/base-project/base-go-v2/internal/api/router.go b/code/base-project/base-go-v2/internal/api/router.go index 4bbd160c..dccc8c0c 100644 --- a/code/base-project/base-go-v2/internal/api/router.go +++ b/code/base-project/base-go-v2/internal/api/router.go @@ -14,6 +14,7 @@ func InitAppRouter() *router.Router { r.Use(middleware.CORSMiddleware()) r.Use(middleware.LoggerMiddleware()) r.Use(middleware.ErrorMiddleware()) + r.Use(middleware.AuthMiddleware()).ExcludePaths("/auth/**") auth.InitAuthRouter(r) return r diff --git a/code/base-project/base-go-v2/internal/reponse/response.go b/code/base-project/base-go-v2/internal/reponse/response.go index 0e0208bf..8324d2f7 100644 --- a/code/base-project/base-go-v2/internal/reponse/response.go +++ b/code/base-project/base-go-v2/internal/reponse/response.go @@ -8,128 +8,71 @@ import ( "net/http" ) -// Result 是最终响应结构体 type Result struct { - IsSuccess bool `json:"isSuccess"` // 是否成功 - Code string `json:"code"` // 状态码 - Message string `json:"message"` // 提示信息 - Data interface{} `json:"data,omitempty"` // 数据 - TrackId string `json:"trackId,omitempty"` // 请求追踪ID + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` + TrackId string `json:"trackId,omitempty"` } -// Code 定义状态码 -type Code struct { - Value string - Message string +var HttpCode = struct { + Success int + Unauthorized int + Fail int + RefreshToken int +}{ + Success: 200, + Unauthorized: 401, + Fail: 500, + RefreshToken: 402, } -// 常用状态码 -var ( - CodeSuccess = Code{"0000", "请求成功"} - CodeFail = Code{"0001", "请求失败"} - CodeNoLogin = Code{"1001", "未登录"} - CodeLoginExpired = Code{"1002", "登录已过期"} - CodeInvalidToken = Code{"1003", "Token 无效"} - CodeInvalidAccount = Code{"1005", "账号或密码错误"} -) - -const defaultMessage = "未知错误" - -// Builder 用于构建响应 type Builder struct { c *router.Context result Result } -// ---------------- 构造函数 ---------------- - -// Success 创建成功响应 func Success(c *router.Context) *Builder { return &Builder{ c: c, result: Result{ - IsSuccess: true, - Code: CodeSuccess.Value, - Message: CodeSuccess.Message, + Code: HttpCode.Success, + Message: "请求成功", }, } } -// Error 创建失败响应 -func Error(c *router.Context) *Builder { +func Fail(c *router.Context) *Builder { return &Builder{ c: c, result: Result{ - IsSuccess: false, - Code: CodeFail.Value, - Message: CodeFail.Message, + Code: HttpCode.Fail, + Message: "请求失败", }, } } -// ---------------- 链式方法 ---------------- - -// 设置 IsSuccess 字段 -func (b *Builder) IsSuccess(IsSuccess bool) *Builder { - b.result.IsSuccess = IsSuccess - return b -} - -// 设置 Code,并根据 Code 自动设置 IsSuccess -func (b *Builder) Code(code string) *Builder { +func (b *Builder) Code(code int) *Builder { b.result.Code = code - b.result.Message = GetMessage(code) - b.result.IsSuccess = code == CodeSuccess.Value return b } -// 设置 Code 对象,并自动设置 IsSuccess -func (b *Builder) CodeObj(code Code) *Builder { - b.result.Code = code.Value - b.result.Message = code.Message - b.result.IsSuccess = code.Value == CodeSuccess.Value - return b -} - -// 自定义提示信息 func (b *Builder) Message(msg string) *Builder { b.result.Message = msg return b } -// 设置返回数据 func (b *Builder) Data(data interface{}) *Builder { b.result.Data = data return b } -// 发送响应 func (b *Builder) Send() error { - // 默认 Data 为空 if b.result.Data == nil { b.result.Data = "" } - - // 从 routinex 获取 trackId - trackId := routinex.Get(logx.TrackID) - if trackId != nil { + if trackId := routinex.Get(logx.TrackID); trackId != nil { b.result.TrackId = strutil.ToString(trackId) } - return b.c.JSON(http.StatusOK, b.result) } - -// ---------------- 辅助函数 ---------------- - -// 根据 Code 获取默认提示信息 -func GetMessage(value string) string { - for _, c := range []Code{ - CodeSuccess, CodeFail, CodeNoLogin, - CodeLoginExpired, CodeInvalidToken, CodeInvalidAccount, - } { - if c.Value == value { - return c.Message - } - } - return defaultMessage -} diff --git a/code/base-project/base-go-v2/internal/router/router.go b/code/base-project/base-go-v2/internal/router/router.go index edaba7a7..d3da0a1d 100644 --- a/code/base-project/base-go-v2/internal/router/router.go +++ b/code/base-project/base-go-v2/internal/router/router.go @@ -67,8 +67,24 @@ func (n *node) search(parts []string, height int, params map[string]string) *nod // 中间件条目 type middlewareEntry struct { - handler HandlerFunc - excludePaths map[string]struct{} + handler HandlerFunc + excludePaths map[string]struct{} + excludePrefix []string +} + +// 判断路径是否被排除 +func (m *middlewareEntry) IsExcluded(path string) bool { + if m.excludePaths != nil { + if _, ok := m.excludePaths[path]; ok { + return true + } + } + for _, prefix := range m.excludePrefix { + if strings.HasPrefix(path, prefix) { + return true + } + } + return false } // Router 主结构 @@ -78,7 +94,7 @@ type Router struct { basePath string } -// NewRouter 创建 +// 创建 Router func NewRouter(basePath ...string) *Router { path := "/" if len(basePath) > 0 && basePath[0] != "" { @@ -90,11 +106,13 @@ func NewRouter(basePath ...string) *Router { } } +// MiddlewareHandle 用于链式排除路径 type MiddlewareHandle struct { router *Router entryIdx int } +// Use 注册中间件 func (r *Router) Use(handler HandlerFunc) *MiddlewareHandle { entry := middlewareEntry{handler: handler} r.middleware = append(r.middleware, entry) @@ -104,15 +122,27 @@ func (r *Router) Use(handler HandlerFunc) *MiddlewareHandle { } } +// ExcludePaths 支持 /** 前缀匹配 func (mh *MiddlewareHandle) ExcludePaths(paths ...string) *Router { - excludeMap := make(map[string]struct{}, len(paths)) + excludeMap := make(map[string]struct{}, 0) + excludePrefix := make([]string, 0) + for _, p := range paths { - excludeMap[p] = struct{}{} + if strings.HasSuffix(p, "/**") { + base := strings.TrimSuffix(p, "/**") + excludePrefix = append(excludePrefix, base) + } else { + excludeMap[p] = struct{}{} + } } + mh.router.middleware[mh.entryIdx].excludePaths = excludeMap + mh.router.middleware[mh.entryIdx].excludePrefix = excludePrefix + return mh.router } +// Group 创建子路由组 func (r *Router) Group(prefix string, m ...HandlerFunc) *Router { newMiddleware := make([]middlewareEntry, len(r.middleware)) copy(newMiddleware, r.middleware) @@ -127,6 +157,7 @@ func (r *Router) Group(prefix string, m ...HandlerFunc) *Router { } } +// GET/POST func (r *Router) GET(path string, handler HandlerFunc) *Router { return r.handle("GET", path, handler) } @@ -145,7 +176,7 @@ func (r *Router) handle(method, path string, handler HandlerFunc) *Router { return r } -// Handler 入口函数(fasthttp) +// Handler 入口(fasthttp) func (r *Router) Handler(ctx *fasthttp.RequestCtx) { method := string(ctx.Method()) root := r.roots[method] @@ -166,10 +197,8 @@ func (r *Router) Handler(ctx *fasthttp.RequestCtx) { handlers := make([]HandlerFunc, 0) path := string(ctx.Path()) for _, m := range r.middleware { - if m.excludePaths != nil { - if _, excluded := m.excludePaths[path]; excluded { - continue - } + if m.IsExcluded(path) { + continue } handlers = append(handlers, m.handler) } @@ -185,6 +214,7 @@ func (r *Router) Handler(ctx *fasthttp.RequestCtx) { _ = c.Next() } +// 辅助函数 func parsePattern(pattern string) []string { vs := strings.Split(strings.Trim(pattern, "/"), "/") parts := make([]string, 0, len(vs)) diff --git a/code/base-project/base-go-v2/internal/utils/jwtx/jwt.go b/code/base-project/base-go-v2/internal/utils/jwtx/jwt.go index 8cac9ef9..8810529f 100644 --- a/code/base-project/base-go-v2/internal/utils/jwtx/jwt.go +++ b/code/base-project/base-go-v2/internal/utils/jwtx/jwt.go @@ -15,9 +15,9 @@ type CustomClaims struct { // TokenResult 用于返回验证结果 type TokenResult struct { - Claims *CustomClaims - Valid bool - Expired bool + Claims *CustomClaims + IsValid bool + IsExpired bool } // CreateAccessToken 创建 Access Token,只返回 token 和 error @@ -61,7 +61,7 @@ func createToken(userID int, expiry time.Duration) (string, error) { // VerifyToken 验证 token 并返回结构化结果 func VerifyToken(tokenString string) TokenResult { - result := TokenResult{Valid: false} + result := TokenResult{IsValid: false, IsExpired: false} token, _ := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) { return []byte(config.App.JWT.Secret), nil @@ -73,9 +73,9 @@ func VerifyToken(tokenString string) TokenResult { } result.Claims = claims - result.Valid = true + result.IsValid = true if claims.ExpiresAt != nil && time.Now().After(claims.ExpiresAt.Time) { - result.Expired = true + result.IsExpired = true } return result 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 index fdc172a2..982cb986 100644 --- a/code/base-project/base-go-v2/internal/utils/validate/validate.go +++ b/code/base-project/base-go-v2/internal/utils/validate/validate.go @@ -36,25 +36,25 @@ func NotEmpty(data map[string]interface{}, fields ...string) error { // collectErrors 递归收集字段错误 func collectErrors(v interface{}, path string, errs map[string]string) { if v == nil { - errs[path] = fmt.Sprintf("字段 %s 不能为空", path) + errs[path] = "字段不能为空" return } switch val := v.(type) { case string: if strings.TrimSpace(val) == "" { - errs[path] = fmt.Sprintf("字段 %s 不能为空", path) + errs[path] = "字段不能为空" } case []interface{}: if len(val) == 0 { - errs[path] = fmt.Sprintf("字段 %s 不能为空", path) + errs[path] = "字段不能为空" } for i, elem := range val { collectErrors(elem, fmt.Sprintf("%s[%d]", path, i), errs) } case map[string]interface{}: if len(val) == 0 { - errs[path] = fmt.Sprintf("字段 %s 不能为空", path) + errs[path] = "字段不能为空" } for k, elem := range val { collectErrors(elem, fmt.Sprintf("%s.%s", path, k), errs) diff --git a/code/base-project/base-go-v2/logs/error/error-2025-11-06.log b/code/base-project/base-go-v2/logs/error/error-2025-11-06.log new file mode 100644 index 00000000..e69de29b diff --git a/code/base-project/base-go-v2/logs/info/info-2025-11-06.log b/code/base-project/base-go-v2/logs/info/info-2025-11-06.log new file mode 100644 index 00000000..85257910 --- /dev/null +++ b/code/base-project/base-go-v2/logs/info/info-2025-11-06.log @@ -0,0 +1,69 @@ +{"level":"INFO","ts":"2025-11-06 13:11:55.116","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0.0005349,"trackId":"376961877109899264"} +{"level":"INFO","ts":"2025-11-06 13:14:17.628","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0.0005502,"trackId":"376962474844356608"} +{"level":"INFO","ts":"2025-11-06 13:15:48.248","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0.0009768,"trackId":"376962854932185088"} +{"level":"INFO","ts":"2025-11-06 13:19:48.605","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0.0005207,"trackId":"376963863062511616"} +{"level":"INFO","ts":"2025-11-06 13:21:27.160","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0.0005053,"trackId":"376964276432142336"} +{"level":"INFO","ts":"2025-11-06 13:22:45.690","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376964605815029760"} +{"level":"INFO","ts":"2025-11-06 13:24:24.423","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376965019931246592"} +{"level":"INFO","ts":"2025-11-06 13:24:44.251","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376965103095906304"} +{"level":"INFO","ts":"2025-11-06 13:26:19.417","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0.0006208,"trackId":"376965502251040768"} +{"level":"INFO","ts":"2025-11-06 13:31:17.803","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376966753772634112"} +{"level":"INFO","ts":"2025-11-06 13:31:49.685","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376966887495434240"} +{"level":"INFO","ts":"2025-11-06 13:31:51.226","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376966893958856704"} +{"level":"INFO","ts":"2025-11-06 13:31:52.059","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/login/default","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376966897448517632"} +{"level":"INFO","ts":"2025-11-06 13:38:42.256","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0008604,"trackId":"376968617939435520"} +{"level":"INFO","ts":"2025-11-06 13:49:29.911","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0006377,"trackId":"376971334405586944"} +{"level":"INFO","ts":"2025-11-06 14:03:51.212","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0009761,"trackId":"376974946959622144"} +{"level":"INFO","ts":"2025-11-06 14:06:06.794","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376975515635941376"} +{"level":"INFO","ts":"2025-11-06 14:18:02.722","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376978518455615488"} +{"level":"INFO","ts":"2025-11-06 14:18:38.073","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0008201,"trackId":"376978666724261888"} +{"level":"INFO","ts":"2025-11-06 14:19:30.070","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0002887,"trackId":"376978884819681280"} +{"level":"INFO","ts":"2025-11-06 14:20:01.974","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0008341,"trackId":"376979018630561792"} +{"level":"INFO","ts":"2025-11-06 14:20:12.293","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979061915779072"} +{"level":"INFO","ts":"2025-11-06 14:22:02.141","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0009975,"trackId":"376979522647490560"} +{"level":"INFO","ts":"2025-11-06 14:22:19.654","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0009764,"trackId":"376979596102336512"} +{"level":"INFO","ts":"2025-11-06 14:22:58.685","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979759810215936"} +{"level":"INFO","ts":"2025-11-06 14:22:59.290","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979762351964160"} +{"level":"INFO","ts":"2025-11-06 14:22:59.524","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979763333431296"} +{"level":"INFO","ts":"2025-11-06 14:22:59.723","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979764168097792"} +{"level":"INFO","ts":"2025-11-06 14:22:59.923","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979765006958592"} +{"level":"INFO","ts":"2025-11-06 14:23:00.125","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0001014,"trackId":"376979765854208000"} +{"level":"INFO","ts":"2025-11-06 14:23:08.297","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979800130060288"} +{"level":"INFO","ts":"2025-11-06 14:23:09.077","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0010337,"trackId":"376979803397423104"} +{"level":"INFO","ts":"2025-11-06 14:23:12.741","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979818765352960"} +{"level":"INFO","ts":"2025-11-06 14:23:13.561","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979822208876544"} +{"level":"INFO","ts":"2025-11-06 14:23:14.337","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979825463656448"} +{"level":"INFO","ts":"2025-11-06 14:23:15.086","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979828605190144"} +{"level":"INFO","ts":"2025-11-06 14:23:15.826","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979831708975104"} +{"level":"INFO","ts":"2025-11-06 14:23:16.041","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979832610750464"} +{"level":"INFO","ts":"2025-11-06 14:23:16.207","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979833307004928"} +{"level":"INFO","ts":"2025-11-06 14:23:16.410","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979834158448640"} +{"level":"INFO","ts":"2025-11-06 14:23:16.624","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0009752,"trackId":"376979835051835392"} +{"level":"INFO","ts":"2025-11-06 14:23:16.824","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979835894890496"} +{"level":"INFO","ts":"2025-11-06 14:23:17.023","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979836729556992"} +{"level":"INFO","ts":"2025-11-06 14:23:17.222","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0004907,"trackId":"376979837564223488"} +{"level":"INFO","ts":"2025-11-06 14:23:17.443","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0002058,"trackId":"376979838491164672"} +{"level":"INFO","ts":"2025-11-06 14:23:17.622","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0009347,"trackId":"376979839241945088"} +{"level":"INFO","ts":"2025-11-06 14:23:17.841","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979840160497664"} +{"level":"INFO","ts":"2025-11-06 14:23:18.041","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0002851,"trackId":"376979840999358464"} +{"level":"INFO","ts":"2025-11-06 14:23:18.259","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979841913716736"} +{"level":"INFO","ts":"2025-11-06 14:23:18.457","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.000401,"trackId":"376979842739994624"} +{"level":"INFO","ts":"2025-11-06 14:23:18.673","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979843650158592"} +{"level":"INFO","ts":"2025-11-06 14:23:18.891","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979844564516864"} +{"level":"INFO","ts":"2025-11-06 14:23:19.110","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0006448,"trackId":"376979845478875136"} +{"level":"INFO","ts":"2025-11-06 14:23:19.309","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979846317735936"} +{"level":"INFO","ts":"2025-11-06 14:23:19.525","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979847223705600"} +{"level":"INFO","ts":"2025-11-06 14:23:19.740","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0007353,"trackId":"376979848121286656"} +{"level":"INFO","ts":"2025-11-06 14:23:19.939","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979848960147456"} +{"level":"INFO","ts":"2025-11-06 14:23:20.140","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.000381,"trackId":"376979849803202560"} +{"level":"INFO","ts":"2025-11-06 14:23:20.376","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0005992,"trackId":"376979850793058304"} +{"level":"INFO","ts":"2025-11-06 14:23:20.591","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376979851694833664"} +{"level":"INFO","ts":"2025-11-06 14:23:20.773","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0005447,"trackId":"376979852454002688"} +{"level":"INFO","ts":"2025-11-06 14:23:21.190","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0003462,"trackId":"376979854207221760"} +{"level":"INFO","ts":"2025-11-06 14:25:18.653","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376980346882752512"} +{"level":"INFO","ts":"2025-11-06 14:25:20.423","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376980354306670592"} +{"level":"INFO","ts":"2025-11-06 14:25:33.356","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.000976,"trackId":"376980408547409920"} +{"level":"INFO","ts":"2025-11-06 14:25:48.802","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376980473336823808"} +{"level":"INFO","ts":"2025-11-06 14:25:49.654","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0009762,"trackId":"376980476906176512"} +{"level":"INFO","ts":"2025-11-06 14:26:01.687","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0,"trackId":"376980527380430848"} +{"level":"INFO","ts":"2025-11-06 16:01:53.899","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/auth/test","method":"POST","ip":"192.168.1.136","elapsed":0.0009752,"trackId":"377004653902036992"} diff --git a/code/base-project/base-go-v2/logs/warn/warn-2025-11-06.log b/code/base-project/base-go-v2/logs/warn/warn-2025-11-06.log new file mode 100644 index 00000000..e69de29b