Browse Source

✨ feat(auth): 添加小程序静默登录功能

- 新增静默登录mutation,应用启动时自动尝试登录
- 添加小程序登录用户类型定义
- 使用useLaunch在小程序启动时执行静默登录
- 优化登录状态管理,减少不必要的用户信息查询

♻️ refactor(client): 重构hono客户端实现

- 新增hono客户端相关文件,包括client.js、utils.js等
- 移除全局polyfill注册,避免环境冲突
- 优化API基础URL配置,默认使用8080端口
- 改进fetch响应处理,增强内容类型检测

📦 build(payment): 新增支付工具模块

- 创建完整的支付工具函数库
- 实现微信支付参数验证、安全检查和状态管理
- 添加支付频率限制和重试逻辑
- 提供支付状态同步和超时处理功能

🐛 fix(fetch): 修复响应内容类型检测

- 改进content-type头部检测逻辑,支持大小写不敏感
- 优化204状态码处理,正确处理无内容响应
yourname 1 month ago
parent
commit
1b15ef6d74

+ 71 - 2
mini/src/utils/auth.tsx

@@ -1,11 +1,12 @@
 import { createContext, useContext, PropsWithChildren } from 'react'
 import { createContext, useContext, PropsWithChildren } from 'react'
-import Taro from '@tarojs/taro'
+import Taro, { useLaunch } from '@tarojs/taro'
 import { authClient } from '../api'
 import { authClient } from '../api'
 import { InferResponseType, InferRequestType } from 'hono'
 import { InferResponseType, InferRequestType } from 'hono'
 import { QueryClient, useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
 import { QueryClient, useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
 
 
 // 用户类型定义
 // 用户类型定义
 export type User = InferResponseType<typeof authClient.me.$get, 200>
 export type User = InferResponseType<typeof authClient.me.$get, 200>
+export type MiniLoginUser = InferResponseType<typeof authClient['mini-login']['$post'], 200>['user']
 type LoginRequest = InferRequestType<typeof authClient.login.$post>['json']
 type LoginRequest = InferRequestType<typeof authClient.login.$post>['json']
 type RegisterRequest = InferRequestType<typeof authClient.register.$post>['json']
 type RegisterRequest = InferRequestType<typeof authClient.register.$post>['json']
 
 
@@ -52,6 +53,53 @@ export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
     refetchOnReconnect: false, // 网络重连不重新获取
     refetchOnReconnect: false, // 网络重连不重新获取
   })
   })
 
 
+  // 静默登录mutation - 应用启动时自动尝试登录
+  const silentLoginMutation = useMutation<MiniLoginUser | null, Error, void>({
+    mutationFn: async () => {
+      try {
+        // 尝试静默登录
+        const loginRes = await Taro.login()
+        if (!loginRes.code) {
+          return null // 静默登录失败,但不抛出错误
+        }
+
+        // 使用小程序code进行静默登录
+        const response = await authClient['mini-login'].$post({
+          json: {
+            code: loginRes.code
+            // 静默登录不请求用户信息
+          }
+        })
+
+        if (response.status === 200) {
+          const { token, user } = await response.json()
+          Taro.setStorageSync('mini_token', token)
+          Taro.setStorageSync('userInfo', JSON.stringify(user))
+          return user
+        }
+
+        return null // 静默登录失败
+      } catch (error) {
+        // 静默登录失败不抛出错误,不影响用户体验
+        console.debug('静默登录失败:', error)
+        return null
+      }
+    },
+    onSuccess: (user) => {
+      if (user) {
+        queryClient.setQueryData(['currentUser'], user)
+      }
+    }
+  })
+
+  
+
+  // 在小程序启动时执行静默登录,刷新token和sessionKey
+  useLaunch(() => {
+    silentLoginMutation.mutate()
+  })
+
+
   const loginMutation = useMutation<User, Error, LoginRequest>({
   const loginMutation = useMutation<User, Error, LoginRequest>({
     mutationFn: async (data) => {
     mutationFn: async (data) => {
       const response = await authClient.login.$post({ json: data })
       const response = await authClient.login.$post({ json: data })
@@ -154,13 +202,34 @@ export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
 
 
   const updateUser = updateUserMutation.mutateAsync
   const updateUser = updateUserMutation.mutateAsync
 
 
+  // // 使用React Query获取用户信息
+  // const { data: user } = useQuery<User | null>({
+  //   queryKey: ['currentUser'],
+  //   queryFn: async () => {
+  //     // 直接从本地存储获取用户信息
+  //     const userInfoStr = Taro.getStorageSync('userInfo')
+  //     if (userInfoStr) {
+  //       try {
+  //         return JSON.parse(userInfoStr)
+  //       } catch {
+  //         return null
+  //       }
+  //     }
+  //     return null
+  //   },
+  //   staleTime: Infinity, // 用户信息不常变动,设为无限期
+  //   refetchOnWindowFocus: false, // 失去焦点不重新获取
+  //   refetchOnReconnect: false, // 网络重连不重新获取
+  //   enabled: false, // 不自动执行,由静默登录和登录mutation更新
+  // })
+
   const value = {
   const value = {
     user: user || null,
     user: user || null,
     login: loginMutation.mutateAsync,
     login: loginMutation.mutateAsync,
     logout: logoutMutation.mutateAsync,
     logout: logoutMutation.mutateAsync,
     register: registerMutation.mutateAsync,
     register: registerMutation.mutateAsync,
     updateUser,
     updateUser,
-    isLoading: isLoading || loginMutation.isPending || registerMutation.isPending || logoutMutation.isPending,
+    isLoading: loginMutation.isPending || registerMutation.isPending || logoutMutation.isPending || silentLoginMutation.isPending,
     isLoggedIn: !!user,
     isLoggedIn: !!user,
   }
   }
 
 

+ 4 - 4
mini/src/utils/headers-polyfill.js

@@ -71,9 +71,9 @@ class Headers {
     }
     }
   }
   }
   
   
-  // 全局注册(如果需要)
-  if (typeof globalThis.Headers === 'undefined') {
-    globalThis.Headers = Headers;
-  }
+  // // 全局注册(如果需要)
+  // if (typeof globalThis !== 'undefined' && typeof globalThis.Headers === 'undefined') {
+  //   globalThis.Headers = Headers;
+  // }
   
   
   export default Headers;
   export default Headers;

+ 3 - 0
mini/src/utils/hono-client/client/client.d.ts

@@ -0,0 +1,3 @@
+// Re-export types from official hono/client
+export type { ClientRequestOptions, InferResponseType, InferRequestType, Fetch } from 'hono/client';
+export { hc } from 'hono/client';

+ 170 - 0
mini/src/utils/hono-client/client/client.js

@@ -0,0 +1,170 @@
+// src/client/client.ts
+import { serialize } from "../utils/cookie.js";
+import {
+  buildSearchParams,
+  deepMerge,
+  mergePath,
+  removeIndexString,
+  replaceUrlParam,
+  replaceUrlProtocol
+} from "./utils.js";
+import Headers from "../../headers-polyfill.js";
+var createProxy = (callback, path) => {
+  const proxy = new Proxy(() => {
+  }, {
+    get(_obj, key) {
+      if (typeof key !== "string" || key === "then") {
+        return void 0;
+      }
+      return createProxy(callback, [...path, key]);
+    },
+    apply(_1, _2, args) {
+      return callback({
+        path,
+        args
+      });
+    }
+  });
+  return proxy;
+};
+var ClientRequestImpl = class {
+  url;
+  method;
+  queryParams = void 0;
+  pathParams = {};
+  rBody;
+  cType = void 0;
+  constructor(url, method) {
+    this.url = url;
+    this.method = method;
+  }
+  fetch = async (args, opt) => {
+    if (args) {
+      if (args.query) {
+        this.queryParams = buildSearchParams(args.query);
+      }
+      if (args.form) {
+        const form = new FormData();
+        for (const [k, v] of Object.entries(args.form)) {
+          if (Array.isArray(v)) {
+            for (const v2 of v) {
+              form.append(k, v2);
+            }
+          } else {
+            form.append(k, v);
+          }
+        }
+        this.rBody = form;
+      }
+      if (args.json) {
+        this.rBody = JSON.stringify(args.json);
+        this.cType = "application/json";
+      }
+      if (args.param) {
+        this.pathParams = args.param;
+      }
+    }
+    let methodUpperCase = this.method.toUpperCase();
+    const headerValues = {
+      ...args?.header,
+      ...typeof opt?.headers === "function" ? await opt.headers() : opt?.headers
+    };
+    if (args?.cookie) {
+      const cookies = [];
+      for (const [key, value] of Object.entries(args.cookie)) {
+        cookies.push(serialize(key, value, { path: "/" }));
+      }
+      headerValues["Cookie"] = cookies.join(",");
+    }
+    if (this.cType) {
+      headerValues["Content-Type"] = this.cType;
+    }
+    const headers = new Headers(headerValues ?? void 0);
+    let url = this.url;
+    url = removeIndexString(url);
+    url = replaceUrlParam(url, this.pathParams);
+    if (this.queryParams) {
+      url = url + "?" + this.queryParams.toString();
+    }
+    methodUpperCase = this.method.toUpperCase();
+    const setBody = !(methodUpperCase === "GET" || methodUpperCase === "HEAD");
+    return (opt?.fetch || fetch)(url, {
+      body: setBody ? this.rBody : void 0,
+      method: methodUpperCase,
+      headers,
+      ...opt?.init
+    });
+  };
+};
+var hc = (baseUrl, options) => createProxy(function proxyCallback(opts) {
+  const parts = [...opts.path];
+  const lastParts = parts.slice(-3).reverse();
+  if (lastParts[0] === "toString") {
+    if (lastParts[1] === "name") {
+      return lastParts[2] || "";
+    }
+    return proxyCallback.toString();
+  }
+  if (lastParts[0] === "valueOf") {
+    if (lastParts[1] === "name") {
+      return lastParts[2] || "";
+    }
+    return proxyCallback;
+  }
+  let method = "";
+  if (/^\$/.test(lastParts[0])) {
+    const last = parts.pop();
+    if (last) {
+      method = last.replace(/^\$/, "");
+    }
+  }
+  const path = parts.join("/");
+  const url = mergePath(baseUrl, path);
+  if (method === "url") {
+    let result = url;
+    if (opts.args[0]) {
+      if (opts.args[0].param) {
+        result = replaceUrlParam(url, opts.args[0].param);
+      }
+      if (opts.args[0].query) {
+        result = result + "?" + buildSearchParams(opts.args[0].query).toString();
+      }
+    }
+    result = removeIndexString(result);
+    return new URL(result);
+  }
+  if (method === "ws") {
+    const webSocketUrl = replaceUrlProtocol(
+      opts.args[0] && opts.args[0].param ? replaceUrlParam(url, opts.args[0].param) : url,
+      "ws"
+    );
+    const targetUrl = new URL(webSocketUrl);
+    const queryParams = opts.args[0]?.query;
+    if (queryParams) {
+      Object.entries(queryParams).forEach(([key, value]) => {
+        if (Array.isArray(value)) {
+          value.forEach((item) => targetUrl.searchParams.append(key, item));
+        } else {
+          targetUrl.searchParams.set(key, value);
+        }
+      });
+    }
+    const establishWebSocket = (...args) => {
+      if (options?.webSocket !== void 0 && typeof options.webSocket === "function") {
+        return options.webSocket(...args);
+      }
+      return new WebSocket(...args);
+    };
+    return establishWebSocket(targetUrl.toString());
+  }
+  const req = new ClientRequestImpl(url, method);
+  if (method) {
+    options ??= {};
+    const args = deepMerge(options, { ...opts.args[1] });
+    return req.fetch(opts.args[0], args);
+  }
+  return req;
+}, []);
+export {
+  hc
+};

+ 5 - 0
mini/src/utils/hono-client/client/index.js

@@ -0,0 +1,5 @@
+// src/client/index.ts
+import { hc } from "./client.js";
+export {
+  hc
+};

+ 70 - 0
mini/src/utils/hono-client/client/utils.js

@@ -0,0 +1,70 @@
+// src/client/utils.ts
+var mergePath = (base, path) => {
+  base = base.replace(/\/+$/, "");
+  base = base + "/";
+  path = path.replace(/^\/+/, "");
+  return base + path;
+};
+var replaceUrlParam = (urlString, params) => {
+  for (const [k, v] of Object.entries(params)) {
+    const reg = new RegExp("/:" + k + "(?:{[^/]+})?\\??");
+    urlString = urlString.replace(reg, v ? `/${v}` : "");
+  }
+  return urlString;
+};
+var buildSearchParams = (query) => {
+  const searchParams = new URLSearchParams();
+  for (const [k, v] of Object.entries(query)) {
+    if (v === void 0) {
+      continue;
+    }
+    if (Array.isArray(v)) {
+      for (const v2 of v) {
+        searchParams.append(k, v2);
+      }
+    } else {
+      searchParams.set(k, v);
+    }
+  }
+  return searchParams;
+};
+var replaceUrlProtocol = (urlString, protocol) => {
+  switch (protocol) {
+    case "ws":
+      return urlString.replace(/^http/, "ws");
+    case "http":
+      return urlString.replace(/^ws/, "http");
+  }
+};
+var removeIndexString = (urlSting) => {
+  if (/^https?:\/\/[^\/]+?\/index$/.test(urlSting)) {
+    return urlSting.replace(/\/index$/, "/");
+  }
+  return urlSting.replace(/\/index$/, "");
+};
+function isObject(item) {
+  return typeof item === "object" && item !== null && !Array.isArray(item);
+}
+function deepMerge(target, source) {
+  if (!isObject(target) && !isObject(source)) {
+    return source;
+  }
+  const merged = { ...target };
+  for (const key in source) {
+    const value = source[key];
+    if (isObject(merged[key]) && isObject(value)) {
+      merged[key] = deepMerge(merged[key], value);
+    } else {
+      merged[key] = value;
+    }
+  }
+  return merged;
+}
+export {
+  buildSearchParams,
+  deepMerge,
+  mergePath,
+  removeIndexString,
+  replaceUrlParam,
+  replaceUrlProtocol
+};

+ 147 - 0
mini/src/utils/hono-client/utils/cookie.js

@@ -0,0 +1,147 @@
+// src/utils/cookie.ts
+import { decodeURIComponent_, tryDecode } from "./url.js";
+var algorithm = { name: "HMAC", hash: "SHA-256" };
+var getCryptoKey = async (secret) => {
+  const secretBuf = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
+  return await crypto.subtle.importKey("raw", secretBuf, algorithm, false, ["sign", "verify"]);
+};
+var makeSignature = async (value, secret) => {
+  const key = await getCryptoKey(secret);
+  const signature = await crypto.subtle.sign(algorithm.name, key, new TextEncoder().encode(value));
+  return btoa(String.fromCharCode(...new Uint8Array(signature)));
+};
+var verifySignature = async (base64Signature, value, secret) => {
+  try {
+    const signatureBinStr = atob(base64Signature);
+    const signature = new Uint8Array(signatureBinStr.length);
+    for (let i = 0, len = signatureBinStr.length; i < len; i++) {
+      signature[i] = signatureBinStr.charCodeAt(i);
+    }
+    return await crypto.subtle.verify(algorithm, secret, signature, new TextEncoder().encode(value));
+  } catch {
+    return false;
+  }
+};
+var validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/;
+var validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/;
+var parse = (cookie, name) => {
+  if (name && cookie.indexOf(name) === -1) {
+    return {};
+  }
+  const pairs = cookie.trim().split(";");
+  const parsedCookie = {};
+  for (let pairStr of pairs) {
+    pairStr = pairStr.trim();
+    const valueStartPos = pairStr.indexOf("=");
+    if (valueStartPos === -1) {
+      continue;
+    }
+    const cookieName = pairStr.substring(0, valueStartPos).trim();
+    if (name && name !== cookieName || !validCookieNameRegEx.test(cookieName)) {
+      continue;
+    }
+    let cookieValue = pairStr.substring(valueStartPos + 1).trim();
+    if (cookieValue.startsWith('"') && cookieValue.endsWith('"')) {
+      cookieValue = cookieValue.slice(1, -1);
+    }
+    if (validCookieValueRegEx.test(cookieValue)) {
+      parsedCookie[cookieName] = cookieValue.indexOf("%") !== -1 ? tryDecode(cookieValue, decodeURIComponent_) : cookieValue;
+      if (name) {
+        break;
+      }
+    }
+  }
+  return parsedCookie;
+};
+var parseSigned = async (cookie, secret, name) => {
+  const parsedCookie = {};
+  const secretKey = await getCryptoKey(secret);
+  for (const [key, value] of Object.entries(parse(cookie, name))) {
+    const signatureStartPos = value.lastIndexOf(".");
+    if (signatureStartPos < 1) {
+      continue;
+    }
+    const signedValue = value.substring(0, signatureStartPos);
+    const signature = value.substring(signatureStartPos + 1);
+    if (signature.length !== 44 || !signature.endsWith("=")) {
+      continue;
+    }
+    const isVerified = await verifySignature(signature, signedValue, secretKey);
+    parsedCookie[key] = isVerified ? signedValue : false;
+  }
+  return parsedCookie;
+};
+var _serialize = (name, value, opt = {}) => {
+  let cookie = `${name}=${value}`;
+  if (name.startsWith("__Secure-") && !opt.secure) {
+    throw new Error("__Secure- Cookie must have Secure attributes");
+  }
+  if (name.startsWith("__Host-")) {
+    if (!opt.secure) {
+      throw new Error("__Host- Cookie must have Secure attributes");
+    }
+    if (opt.path !== "/") {
+      throw new Error('__Host- Cookie must have Path attributes with "/"');
+    }
+    if (opt.domain) {
+      throw new Error("__Host- Cookie must not have Domain attributes");
+    }
+  }
+  if (opt && typeof opt.maxAge === "number" && opt.maxAge >= 0) {
+    if (opt.maxAge > 3456e4) {
+      throw new Error(
+        "Cookies Max-Age SHOULD NOT be greater than 400 days (34560000 seconds) in duration."
+      );
+    }
+    cookie += `; Max-Age=${opt.maxAge | 0}`;
+  }
+  if (opt.domain && opt.prefix !== "host") {
+    cookie += `; Domain=${opt.domain}`;
+  }
+  if (opt.path) {
+    cookie += `; Path=${opt.path}`;
+  }
+  if (opt.expires) {
+    if (opt.expires.getTime() - Date.now() > 3456e7) {
+      throw new Error(
+        "Cookies Expires SHOULD NOT be greater than 400 days (34560000 seconds) in the future."
+      );
+    }
+    cookie += `; Expires=${opt.expires.toUTCString()}`;
+  }
+  if (opt.httpOnly) {
+    cookie += "; HttpOnly";
+  }
+  if (opt.secure) {
+    cookie += "; Secure";
+  }
+  if (opt.sameSite) {
+    cookie += `; SameSite=${opt.sameSite.charAt(0).toUpperCase() + opt.sameSite.slice(1)}`;
+  }
+  if (opt.priority) {
+    cookie += `; Priority=${opt.priority}`;
+  }
+  if (opt.partitioned) {
+    if (!opt.secure) {
+      throw new Error("Partitioned Cookie must have Secure attributes");
+    }
+    cookie += "; Partitioned";
+  }
+  return cookie;
+};
+var serialize = (name, value, opt) => {
+  value = encodeURIComponent(value);
+  return _serialize(name, value, opt);
+};
+var serializeSigned = async (name, value, secret, opt = {}) => {
+  const signature = await makeSignature(value, secret);
+  value = `${value}.${signature}`;
+  value = encodeURIComponent(value);
+  return _serialize(name, value, opt);
+};
+export {
+  parse,
+  parseSigned,
+  serialize,
+  serializeSigned
+};

+ 219 - 0
mini/src/utils/hono-client/utils/url.js

@@ -0,0 +1,219 @@
+// src/utils/url.ts
+var splitPath = (path) => {
+  const paths = path.split("/");
+  if (paths[0] === "") {
+    paths.shift();
+  }
+  return paths;
+};
+var splitRoutingPath = (routePath) => {
+  const { groups, path } = extractGroupsFromPath(routePath);
+  const paths = splitPath(path);
+  return replaceGroupMarks(paths, groups);
+};
+var extractGroupsFromPath = (path) => {
+  const groups = [];
+  path = path.replace(/\{[^}]+\}/g, (match, index) => {
+    const mark = `@${index}`;
+    groups.push([mark, match]);
+    return mark;
+  });
+  return { groups, path };
+};
+var replaceGroupMarks = (paths, groups) => {
+  for (let i = groups.length - 1; i >= 0; i--) {
+    const [mark] = groups[i];
+    for (let j = paths.length - 1; j >= 0; j--) {
+      if (paths[j].includes(mark)) {
+        paths[j] = paths[j].replace(mark, groups[i][1]);
+        break;
+      }
+    }
+  }
+  return paths;
+};
+var patternCache = {};
+var getPattern = (label, next) => {
+  if (label === "*") {
+    return "*";
+  }
+  const match = label.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
+  if (match) {
+    const cacheKey = `${label}#${next}`;
+    if (!patternCache[cacheKey]) {
+      if (match[2]) {
+        patternCache[cacheKey] = next && next[0] !== ":" && next[0] !== "*" ? [cacheKey, match[1], new RegExp(`^${match[2]}(?=/${next})`)] : [label, match[1], new RegExp(`^${match[2]}$`)];
+      } else {
+        patternCache[cacheKey] = [label, match[1], true];
+      }
+    }
+    return patternCache[cacheKey];
+  }
+  return null;
+};
+var tryDecode = (str, decoder) => {
+  try {
+    return decoder(str);
+  } catch {
+    return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => {
+      try {
+        return decoder(match);
+      } catch {
+        return match;
+      }
+    });
+  }
+};
+var tryDecodeURI = (str) => tryDecode(str, decodeURI);
+var getPath = (request) => {
+  const url = request.url;
+  const start = url.indexOf(
+    "/",
+    url.charCodeAt(9) === 58 ? 13 : 8
+  );
+  let i = start;
+  for (; i < url.length; i++) {
+    const charCode = url.charCodeAt(i);
+    if (charCode === 37) {
+      const queryIndex = url.indexOf("?", i);
+      const path = url.slice(start, queryIndex === -1 ? void 0 : queryIndex);
+      return tryDecodeURI(path.includes("%25") ? path.replace(/%25/g, "%2525") : path);
+    } else if (charCode === 63) {
+      break;
+    }
+  }
+  return url.slice(start, i);
+};
+var getQueryStrings = (url) => {
+  const queryIndex = url.indexOf("?", 8);
+  return queryIndex === -1 ? "" : "?" + url.slice(queryIndex + 1);
+};
+var getPathNoStrict = (request) => {
+  const result = getPath(request);
+  return result.length > 1 && result.at(-1) === "/" ? result.slice(0, -1) : result;
+};
+var mergePath = (base, sub, ...rest) => {
+  if (rest.length) {
+    sub = mergePath(sub, ...rest);
+  }
+  return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`;
+};
+var checkOptionalParameter = (path) => {
+  if (path.charCodeAt(path.length - 1) !== 63 || !path.includes(":")) {
+    return null;
+  }
+  const segments = path.split("/");
+  const results = [];
+  let basePath = "";
+  segments.forEach((segment) => {
+    if (segment !== "" && !/\:/.test(segment)) {
+      basePath += "/" + segment;
+    } else if (/\:/.test(segment)) {
+      if (/\?/.test(segment)) {
+        if (results.length === 0 && basePath === "") {
+          results.push("/");
+        } else {
+          results.push(basePath);
+        }
+        const optionalSegment = segment.replace("?", "");
+        basePath += "/" + optionalSegment;
+        results.push(basePath);
+      } else {
+        basePath += "/" + segment;
+      }
+    }
+  });
+  return results.filter((v, i, a) => a.indexOf(v) === i);
+};
+var _decodeURI = (value) => {
+  if (!/[%+]/.test(value)) {
+    return value;
+  }
+  if (value.indexOf("+") !== -1) {
+    value = value.replace(/\+/g, " ");
+  }
+  return value.indexOf("%") !== -1 ? tryDecode(value, decodeURIComponent_) : value;
+};
+var _getQueryParam = (url, key, multiple) => {
+  let encoded;
+  if (!multiple && key && !/[%+]/.test(key)) {
+    let keyIndex2 = url.indexOf(`?${key}`, 8);
+    if (keyIndex2 === -1) {
+      keyIndex2 = url.indexOf(`&${key}`, 8);
+    }
+    while (keyIndex2 !== -1) {
+      const trailingKeyCode = url.charCodeAt(keyIndex2 + key.length + 1);
+      if (trailingKeyCode === 61) {
+        const valueIndex = keyIndex2 + key.length + 2;
+        const endIndex = url.indexOf("&", valueIndex);
+        return _decodeURI(url.slice(valueIndex, endIndex === -1 ? void 0 : endIndex));
+      } else if (trailingKeyCode == 38 || isNaN(trailingKeyCode)) {
+        return "";
+      }
+      keyIndex2 = url.indexOf(`&${key}`, keyIndex2 + 1);
+    }
+    encoded = /[%+]/.test(url);
+    if (!encoded) {
+      return void 0;
+    }
+  }
+  const results = {};
+  encoded ??= /[%+]/.test(url);
+  let keyIndex = url.indexOf("?", 8);
+  while (keyIndex !== -1) {
+    const nextKeyIndex = url.indexOf("&", keyIndex + 1);
+    let valueIndex = url.indexOf("=", keyIndex);
+    if (valueIndex > nextKeyIndex && nextKeyIndex !== -1) {
+      valueIndex = -1;
+    }
+    let name = url.slice(
+      keyIndex + 1,
+      valueIndex === -1 ? nextKeyIndex === -1 ? void 0 : nextKeyIndex : valueIndex
+    );
+    if (encoded) {
+      name = _decodeURI(name);
+    }
+    keyIndex = nextKeyIndex;
+    if (name === "") {
+      continue;
+    }
+    let value;
+    if (valueIndex === -1) {
+      value = "";
+    } else {
+      value = url.slice(valueIndex + 1, nextKeyIndex === -1 ? void 0 : nextKeyIndex);
+      if (encoded) {
+        value = _decodeURI(value);
+      }
+    }
+    if (multiple) {
+      if (!(results[name] && Array.isArray(results[name]))) {
+        results[name] = [];
+      }
+      ;
+      results[name].push(value);
+    } else {
+      results[name] ??= value;
+    }
+  }
+  return key ? results[key] : results;
+};
+var getQueryParam = _getQueryParam;
+var getQueryParams = (url, key) => {
+  return _getQueryParam(url, key, true);
+};
+var decodeURIComponent_ = decodeURIComponent;
+export {
+  checkOptionalParameter,
+  decodeURIComponent_,
+  getPath,
+  getPathNoStrict,
+  getPattern,
+  getQueryParam,
+  getQueryParams,
+  getQueryStrings,
+  mergePath,
+  splitPath,
+  splitRoutingPath,
+  tryDecode
+};

+ 500 - 0
mini/src/utils/payment.ts

@@ -0,0 +1,500 @@
+/**
+ * 支付工具函数
+ * 封装微信支付相关逻辑
+ */
+
+import Taro from '@tarojs/taro'
+
+/**
+ * 微信支付参数类型
+ */
+export interface WechatPaymentParams {
+  timeStamp: string
+  nonceStr: string
+  package: string
+  signType: string
+  paySign: string
+}
+
+/**
+ * 支付结果类型
+ */
+export interface PaymentResult {
+  success: boolean
+  type?: 'success' | 'cancel' | 'fail' | 'error'
+  message?: string
+  result?: any
+}
+
+/**
+ * 调用微信支付
+ * @param paymentData 支付参数
+ * @returns 支付结果
+ */
+export const requestWechatPayment = async (paymentData: WechatPaymentParams): Promise<PaymentResult> => {
+  try {
+    const result = await Taro.requestPayment({
+      timeStamp: paymentData.timeStamp,
+      nonceStr: paymentData.nonceStr,
+      package: paymentData.package,
+      signType: paymentData.signType as any, // Taro类型定义问题,使用any绕过
+      paySign: paymentData.paySign
+    })
+
+    return {
+      success: true,
+      type: 'success',
+      result
+    }
+  } catch (error: any) {
+    console.error('微信支付调用失败:', error)
+
+    // 根据错误码处理不同场景
+    if (error.errMsg?.includes('cancel')) {
+      return {
+        success: false,
+        type: 'cancel',
+        message: '用户取消支付'
+      }
+    } else if (error.errMsg?.includes('fail')) {
+      return {
+        success: false,
+        type: 'fail',
+        message: '支付失败'
+      }
+    } else {
+      return {
+        success: false,
+        type: 'error',
+        message: error.errMsg || '支付异常'
+      }
+    }
+  }
+}
+
+/**
+ * 验证支付参数
+ * @param paymentData 支付参数
+ * @returns 验证结果
+ */
+export const validatePaymentParams = (paymentData: WechatPaymentParams): { valid: boolean; errors: string[] } => {
+  const errors: string[] = []
+
+  if (!paymentData.timeStamp) {
+    errors.push('时间戳不能为空')
+  }
+
+  if (!paymentData.nonceStr) {
+    errors.push('随机字符串不能为空')
+  }
+
+  if (!paymentData.package) {
+    errors.push('预支付ID不能为空')
+  }
+
+  if (!paymentData.signType) {
+    errors.push('签名类型不能为空')
+  }
+
+  if (!paymentData.paySign) {
+    errors.push('签名不能为空')
+  }
+
+  return {
+    valid: errors.length === 0,
+    errors
+  }
+}
+
+/**
+ * 处理支付金额
+ * @param amount 金额(元)
+ * @returns 金额(分)
+ */
+export const formatPaymentAmount = (amount: number): number => {
+  // 微信支付金额单位为分,需要乘以100
+  return Math.round(amount * 100)
+}
+
+/**
+ * 检查支付环境
+ * @returns 是否支持微信支付
+ */
+export const checkPaymentEnvironment = async (): Promise<boolean> => {
+  try {
+    // 检查是否在微信小程序环境中
+    if (typeof Taro.requestPayment === 'undefined') {
+      console.error('当前环境不支持微信支付')
+      return false
+    }
+
+    // 可以添加更多环境检查逻辑
+    return true
+  } catch (error) {
+    console.error('检查支付环境失败:', error)
+    return false
+  }
+}
+
+/**
+ * 支付安全验证
+ * @param orderId 订单ID
+ * @param amount 支付金额
+ * @param paymentParams 支付参数
+ * @returns 验证结果
+ */
+export const validatePaymentSecurity = (
+  orderId: number,
+  amount: number,
+  paymentParams: WechatPaymentParams
+): { valid: boolean; reason?: string } => {
+  // 验证订单ID
+  if (!orderId || orderId <= 0) {
+    return { valid: false, reason: '订单ID无效' }
+  }
+
+  // 验证金额
+  if (!amount || amount <= 0) {
+    return { valid: false, reason: '支付金额无效' }
+  }
+
+  // 验证支付参数
+  const paramValidation = validatePaymentParams(paymentParams)
+  if (!paramValidation.valid) {
+    return {
+      valid: false,
+      reason: `支付参数错误: ${paramValidation.errors.join(', ')}`
+    }
+  }
+
+  // 时间戳验证(防止重放攻击)
+  const timestamp = parseInt(paymentParams.timeStamp)
+  const currentTime = Math.floor(Date.now() / 1000)
+  const timeDiff = Math.abs(currentTime - timestamp)
+
+  // 时间戳应该在5分钟内有效
+  if (timeDiff > 300) {
+    return { valid: false, reason: '支付参数已过期,请重新发起支付' }
+  }
+
+  // 随机字符串长度验证 - 放宽限制以适应微信支付实际返回
+  if (!paymentParams.nonceStr || paymentParams.nonceStr.length < 8) {
+    return { valid: false, reason: '随机字符串长度无效' }
+  }
+
+  // 签名类型验证 - 放宽限制以适应微信支付实际返回
+  if (!paymentParams.signType) {
+    return { valid: false, reason: '签名类型不能为空' }
+  }
+
+  // 验证签名类型是否支持
+  const supportedSignTypes = ['RSA', 'HMAC-SHA256']
+  if (!supportedSignTypes.includes(paymentParams.signType)) {
+    return { valid: false, reason: '签名类型不支持' }
+  }
+
+  // 预支付ID格式验证 - 放宽限制以适应微信支付实际返回
+  if (!paymentParams.package) {
+    return { valid: false, reason: '预支付ID不能为空' }
+  }
+
+  // 签名长度验证 - 放宽限制以适应微信支付实际返回
+  if (!paymentParams.paySign || paymentParams.paySign.length < 16) {
+    return { valid: false, reason: '签名长度过短' }
+  }
+
+  return { valid: true }
+}
+
+/**
+ * 生成支付参数哈希(用于防篡改验证)
+ * @param paymentParams 支付参数
+ * @returns 参数哈希
+ */
+export const generatePaymentParamsHash = (paymentParams: WechatPaymentParams): string => {
+  const paramsString = [
+    paymentParams.timeStamp,
+    paymentParams.nonceStr,
+    paymentParams.package,
+    paymentParams.signType
+  ].join('&')
+
+  // 在实际项目中,这里应该使用更安全的哈希算法
+  // 这里使用简单的哈希作为示例
+  let hash = 0
+  for (let i = 0; i < paramsString.length; i++) {
+    const char = paramsString.charCodeAt(i)
+    hash = ((hash << 5) - hash) + char
+    hash = hash & hash // 转换为32位整数
+  }
+  return Math.abs(hash).toString(16)
+}
+
+/**
+ * 验证支付参数完整性
+ * @param originalParams 原始支付参数
+ * @param receivedParams 接收到的支付参数
+ * @returns 验证结果
+ */
+export const verifyPaymentParamsIntegrity = (
+  originalParams: WechatPaymentParams,
+  receivedParams: WechatPaymentParams
+): { valid: boolean; reason?: string } => {
+  const originalHash = generatePaymentParamsHash(originalParams)
+  const receivedHash = generatePaymentParamsHash(receivedParams)
+
+  if (originalHash !== receivedHash) {
+    return { valid: false, reason: '支付参数被篡改' }
+  }
+
+  return { valid: true }
+}
+
+/**
+ * 支付金额一致性验证
+ * @param expectedAmount 预期金额(元)
+ * @param paymentAmount 支付金额(分)
+ * @returns 验证结果
+ */
+export const validateAmountConsistency = (
+  expectedAmount: number,
+  paymentAmount: number
+): { valid: boolean; reason?: string } => {
+  const expectedInFen = Math.round(expectedAmount * 100)
+
+  if (expectedInFen !== paymentAmount) {
+    return {
+      valid: false,
+      reason: `金额不一致: 预期 ${expectedInFen} 分,实际 ${paymentAmount} 分`
+    }
+  }
+
+  return { valid: true }
+}
+
+/**
+ * 支付频率限制检查
+ */
+export class PaymentRateLimiter {
+  private static instance: PaymentRateLimiter
+  private attempts: Map<number, number[]> = new Map()
+  private readonly MAX_ATTEMPTS = 5
+  private readonly TIME_WINDOW = 60000 // 1分钟
+
+  private constructor() {}
+
+  static getInstance(): PaymentRateLimiter {
+    if (!PaymentRateLimiter.instance) {
+      PaymentRateLimiter.instance = new PaymentRateLimiter()
+    }
+    return PaymentRateLimiter.instance
+  }
+
+  /**
+   * 检查是否超过支付频率限制
+   */
+  isRateLimited(orderId: number): { limited: boolean; remainingTime?: number } {
+    const now = Date.now()
+    const attempts = this.attempts.get(orderId) || []
+
+    // 清理过期的尝试记录
+    const recentAttempts = attempts.filter(time => now - time < this.TIME_WINDOW)
+    this.attempts.set(orderId, recentAttempts)
+
+    if (recentAttempts.length >= this.MAX_ATTEMPTS) {
+      const oldestAttempt = Math.min(...recentAttempts)
+      const remainingTime = this.TIME_WINDOW - (now - oldestAttempt)
+      return { limited: true, remainingTime }
+    }
+
+    return { limited: false }
+  }
+
+  /**
+   * 记录支付尝试
+   */
+  recordAttempt(orderId: number): void {
+    const attempts = this.attempts.get(orderId) || []
+    attempts.push(Date.now())
+    this.attempts.set(orderId, attempts)
+  }
+
+  /**
+   * 清除支付尝试记录
+   */
+  clearAttempts(orderId: number): void {
+    this.attempts.delete(orderId)
+  }
+}
+
+/**
+ * 支付重试逻辑
+ * @param paymentFn 支付函数
+ * @param maxRetries 最大重试次数
+ * @param delay 重试延迟(毫秒)
+ * @returns 支付结果
+ */
+export const retryPayment = async (
+  paymentFn: () => Promise<PaymentResult>,
+  maxRetries: number = 3,
+  delay: number = 1000
+): Promise<PaymentResult> => {
+  let lastError: any = null
+
+  for (let attempt = 1; attempt <= maxRetries; attempt++) {
+    try {
+      const result = await paymentFn()
+
+      if (result.success) {
+        return result
+      }
+
+      // 如果是用户取消,不重试
+      if (result.type === 'cancel') {
+        return result
+      }
+
+      lastError = result
+
+      if (attempt < maxRetries) {
+        console.log(`支付失败,第${attempt}次重试...`)
+        await new Promise(resolve => setTimeout(resolve, delay))
+      }
+    } catch (error) {
+      lastError = error
+
+      if (attempt < maxRetries) {
+        console.log(`支付异常,第${attempt}次重试...`)
+        await new Promise(resolve => setTimeout(resolve, delay))
+      }
+    }
+  }
+
+  return {
+    success: false,
+    type: 'error',
+    message: `支付失败,已重试${maxRetries}次: ${lastError?.message || '未知错误'}`
+  }
+}
+
+/**
+ * 支付状态枚举
+ */
+export enum PaymentStatus {
+  PENDING = '待支付',
+  PROCESSING = '支付中',
+  SUCCESS = '支付成功',
+  FAILED = '支付失败',
+  REFUNDED = '已退款',
+  CLOSED = '已关闭'
+}
+
+/**
+ * 支付状态管理类
+ */
+export class PaymentStateManager {
+  private static instance: PaymentStateManager
+  private state: Map<number, PaymentStatus> = new Map()
+
+  private constructor() {}
+
+  static getInstance(): PaymentStateManager {
+    if (!PaymentStateManager.instance) {
+      PaymentStateManager.instance = new PaymentStateManager()
+    }
+    return PaymentStateManager.instance
+  }
+
+  /**
+   * 设置支付状态
+   */
+  setPaymentState(orderId: number, status: PaymentStatus): void {
+    this.state.set(orderId, status)
+    console.log(`订单 ${orderId} 支付状态更新为: ${status}`)
+  }
+
+  /**
+   * 获取支付状态
+   */
+  getPaymentState(orderId: number): PaymentStatus | undefined {
+    return this.state.get(orderId)
+  }
+
+  /**
+   * 检查是否重复支付
+   */
+  isDuplicatePayment(orderId: number): boolean {
+    const currentStatus = this.getPaymentState(orderId)
+    return currentStatus === PaymentStatus.PROCESSING || currentStatus === PaymentStatus.SUCCESS
+  }
+
+  /**
+   * 清除支付状态
+   */
+  clearPaymentState(orderId: number): void {
+    this.state.delete(orderId)
+  }
+
+  /**
+   * 获取所有支付状态
+   */
+  getAllPaymentStates(): Map<number, PaymentStatus> {
+    return new Map(this.state)
+  }
+}
+
+/**
+ * 支付超时处理
+ * @param orderId 订单ID
+ * @param timeout 超时时间(毫秒)
+ * @returns 超时Promise
+ */
+export const createPaymentTimeout = (timeout: number = 30000): Promise<PaymentResult> => {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve({
+        success: false,
+        type: 'error',
+        message: '支付超时,请检查网络或重试'
+      })
+    }, timeout)
+  })
+}
+
+/**
+ * 支付状态同步
+ * @param orderId 订单ID
+ * @param expectedStatus 期望状态
+ * @param maxAttempts 最大尝试次数
+ * @param interval 检查间隔(毫秒)
+ * @returns 同步结果
+ */
+export const syncPaymentStatus = async (
+  orderId: number,
+  expectedStatus: PaymentStatus,
+  maxAttempts: number = 10,
+  interval: number = 2000
+): Promise<{ synced: boolean; currentStatus?: PaymentStatus }> => {
+  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+    try {
+      // 这里可以调用后端API查询实际支付状态
+      // 暂时使用状态管理器模拟
+      const stateManager = PaymentStateManager.getInstance()
+      const currentStatus = stateManager.getPaymentState(orderId)
+
+      if (currentStatus === expectedStatus) {
+        return { synced: true, currentStatus }
+      }
+
+      console.log(`支付状态同步中... 第${attempt}次检查,当前状态: ${currentStatus}`)
+
+      if (attempt < maxAttempts) {
+        await new Promise(resolve => setTimeout(resolve, interval))
+      }
+    } catch (error) {
+      console.error(`支付状态同步失败,第${attempt}次尝试:`, error)
+    }
+  }
+
+  return { synced: false }
+}

+ 4 - 4
mini/src/utils/response-polyfill.ts

@@ -80,9 +80,9 @@ class ResponsePolyfill {
     }
     }
   }
   }
   
   
-  // 全局注册(如果需要)
-  if (typeof globalThis.Response === 'undefined') {
-    globalThis.Response = ResponsePolyfill as any
-  }
+  // // 全局注册(如果需要)
+  // if (typeof globalThis !== 'undefined' && typeof globalThis.Response === 'undefined') {
+  //   globalThis.Response = ResponsePolyfill as any
+  // }
   
   
   export default ResponsePolyfill
   export default ResponsePolyfill

+ 5 - 4
mini/src/utils/rpc-client.ts

@@ -1,9 +1,9 @@
 import Taro from '@tarojs/taro'
 import Taro from '@tarojs/taro'
-import { hc } from 'hono/client'
 import ResponsePolyfill from './response-polyfill'
 import ResponsePolyfill from './response-polyfill'
+import { hc } from './hono-client/client'
 
 
 // API配置
 // API配置
-const API_BASE_URL = process.env.TARO_APP_API_BASE_URL || 'http://localhost:3000'
+const API_BASE_URL = process.env.TARO_APP_API_BASE_URL || 'http://localhost:8080'
 
 
 // 完整的API地址
 // 完整的API地址
 // const BASE_URL = `${API_BASE_URL}/api/${API_VERSION}`
 // const BASE_URL = `${API_BASE_URL}/api/${API_VERSION}`
@@ -41,7 +41,7 @@ const taroFetch: any = async (input, init) => {
     // const response = await Taro.request(options)
     // const response = await Taro.request(options)
     console.log('options', options)
     console.log('options', options)
     const response = await Taro.request(options)
     const response = await Taro.request(options)
-
+    console.log('response', response)
     const responseHeaders = response.header;
     const responseHeaders = response.header;
     // if (response.header) {
     // if (response.header) {
     //   for (const [key, value] of Object.entries(response.header)) {
     //   for (const [key, value] of Object.entries(response.header)) {
@@ -50,9 +50,10 @@ const taroFetch: any = async (input, init) => {
     // }
     // }
 
 
       // 处理204 No Content响应,不设置body
       // 处理204 No Content响应,不设置body
+    const contentType = responseHeaders['content-type'] || responseHeaders['Content-Type'];
     const body = response.statusCode === 204
     const body = response.statusCode === 204
     ? null
     ? null
-    : responseHeaders['content-type']!.includes('application/json')
+    : (contentType && contentType.includes('application/json'))
       ? JSON.stringify(response.data)
       ? JSON.stringify(response.data)
       : response.data;
       : response.data;