From e893ef2e48bcf3c04d1ddc5e49685ad5f3821973 Mon Sep 17 00:00:00 2001 From: oneao Date: Thu, 16 Apr 2026 21:29:38 +0800 Subject: [PATCH] u --- code/allapp/allapp-go-v3/configs/config.yaml | 4 + code/allapp/allapp-go-v3/go.mod | 4 +- .../allapp-go-v3/internal/bootstrap/server.go | 2 +- .../allapp-go-v3/internal/config/config.go | 11 +- .../allapp-go-v3/internal/config/loader.go | 1 + .../allapp-go-v3/internal/handle/auth.go | 201 ++++++++++++++++++ .../allapp-go-v3/internal/handle/data_save.go | 5 +- .../allapp-go-v3/internal/router/router.go | 14 +- .../allapp-go-v3/internal/types/auth.go | 18 ++ code/allapp/allapp-go-v3/pkg/common/common.go | 45 ++++ code/allapp/allapp-go-v3/pkg/db/select.go | 24 ++- .../allapp-go-v3/pkg/requestx/requestx.go | 99 +++++++++ code/allapp/allapp-go-v3/pkg/wechat/wechat.go | 102 +++++++++ code/app/app-rn4/app.json | 27 ++- code/app/app-rn4/credentials/credentials.json | 17 ++ .../credentials/oneao-android.keystore | Bin 0 -> 2253 bytes code/app/app-rn4/package.json | 3 + .../app-rn4/plugins/withAndroidSignature.js | 95 +++++++++ code/app/app-rn4/pnpm-lock.yaml | 47 ++++ code/app/app-rn4/src/api/index.ts | 12 -- code/app/app-rn4/src/app/_layout.tsx | 3 + .../src/app/money/(tabs)/home/index.tsx | 15 +- code/app/app-rn4/src/request/api.ts | 32 +++ code/app/app-rn4/src/request/index.ts | 81 +++++++ code/app/app-rn4/src/store/user.ts | 17 ++ code/app/app-rn4/src/types/auth.ts | 16 ++ code/app/app-rn4/src/types/global.d.ts | 8 + 27 files changed, 870 insertions(+), 33 deletions(-) create mode 100644 code/allapp/allapp-go-v3/internal/handle/auth.go create mode 100644 code/allapp/allapp-go-v3/internal/types/auth.go create mode 100644 code/allapp/allapp-go-v3/pkg/common/common.go create mode 100644 code/allapp/allapp-go-v3/pkg/requestx/requestx.go create mode 100644 code/allapp/allapp-go-v3/pkg/wechat/wechat.go create mode 100644 code/app/app-rn4/credentials/credentials.json create mode 100644 code/app/app-rn4/credentials/oneao-android.keystore create mode 100644 code/app/app-rn4/plugins/withAndroidSignature.js delete mode 100644 code/app/app-rn4/src/api/index.ts create mode 100644 code/app/app-rn4/src/request/api.ts create mode 100644 code/app/app-rn4/src/request/index.ts create mode 100644 code/app/app-rn4/src/types/auth.ts diff --git a/code/allapp/allapp-go-v3/configs/config.yaml b/code/allapp/allapp-go-v3/configs/config.yaml index 9be48ad0..a1f67426 100644 --- a/code/allapp/allapp-go-v3/configs/config.yaml +++ b/code/allapp/allapp-go-v3/configs/config.yaml @@ -1,3 +1,7 @@ app: name: allapp port: 8080 + base_url: /app +wechat: + app_id: wxdab3e21a1f7e392f + app_secret: 0895443c6fedfc21cb92c9cd60de2a26 \ No newline at end of file diff --git a/code/allapp/allapp-go-v3/go.mod b/code/allapp/allapp-go-v3/go.mod index 430219b0..a080261e 100644 --- a/code/allapp/allapp-go-v3/go.mod +++ b/code/allapp/allapp-go-v3/go.mod @@ -7,6 +7,7 @@ require ( github.com/gofiber/fiber/v3 v3.1.0 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/jackc/pgx/v5 v5.9.1 + github.com/pkg/errors v0.9.1 github.com/spf13/viper v1.21.0 go.uber.org/zap v1.27.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1 @@ -15,6 +16,7 @@ require ( require ( github.com/andybalholm/brotli v1.2.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect @@ -31,7 +33,6 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/philhofer/fwd v1.2.0 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect @@ -41,6 +42,7 @@ require ( github.com/tinylib/msgp v1.6.3 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.69.0 // indirect + github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.10.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.49.0 // indirect diff --git a/code/allapp/allapp-go-v3/internal/bootstrap/server.go b/code/allapp/allapp-go-v3/internal/bootstrap/server.go index a6bc7061..60011f72 100644 --- a/code/allapp/allapp-go-v3/internal/bootstrap/server.go +++ b/code/allapp/allapp-go-v3/internal/bootstrap/server.go @@ -24,7 +24,7 @@ func RunServer(cfg *config.Config) error { app.Use(middleware.Logger()) // ===== router ===== - router.SetupRouter(app) + router.SetupRouter(app, cfg) addr := fmt.Sprintf(":%d", cfg.App.Port) diff --git a/code/allapp/allapp-go-v3/internal/config/config.go b/code/allapp/allapp-go-v3/internal/config/config.go index 9f1adb99..b1125e1e 100644 --- a/code/allapp/allapp-go-v3/internal/config/config.go +++ b/code/allapp/allapp-go-v3/internal/config/config.go @@ -7,11 +7,13 @@ type Config struct { JWT JwtConfig `mapstructure:"jwt"` UniqueID UniqueIDConfig `mapstructure:"unique_id"` Postgres PostgresConfig `mapstructure:"postgres"` + Wechat WechatConfig `mapstructure:"wechat"` } type AppConfig struct { - Name string `mapstructure:"name"` - Port int `mapstructure:"port"` + Name string `mapstructure:"name"` + Port int `mapstructure:"port"` + BaseUrl string `mapstructure:"base_url"` } type PostgresConfig struct { @@ -36,3 +38,8 @@ type UniqueIDConfig struct { DataCenterID uint16 `mapstructure:"datacenter_id"` WorkerID uint16 `mapstructure:"worker_id"` } + +type WechatConfig struct { + AppId string `mapstructure:"app_id"` + AppSecret string `mapstructure:"app_secret"` +} diff --git a/code/allapp/allapp-go-v3/internal/config/loader.go b/code/allapp/allapp-go-v3/internal/config/loader.go index c1bb3d04..d29d79bd 100644 --- a/code/allapp/allapp-go-v3/internal/config/loader.go +++ b/code/allapp/allapp-go-v3/internal/config/loader.go @@ -52,6 +52,7 @@ func Load() (*Config, error) { func setDefaults(v *viper.Viper) { v.SetDefault("app.name", "allapp-go") v.SetDefault("app.port", 8080) + v.SetDefault("app.base_url", "app") // 可以继续扩展 v.SetDefault("unique_id.datacenter_id", 1) diff --git a/code/allapp/allapp-go-v3/internal/handle/auth.go b/code/allapp/allapp-go-v3/internal/handle/auth.go new file mode 100644 index 00000000..a89b78ca --- /dev/null +++ b/code/allapp/allapp-go-v3/internal/handle/auth.go @@ -0,0 +1,201 @@ +package handle + +import ( + "allapp-go/internal/errors" + "allapp-go/internal/httpx" + "allapp-go/internal/types" + "allapp-go/pkg/db" + "allapp-go/pkg/jwtx" + "allapp-go/pkg/uniqueid" + "allapp-go/pkg/wechat" + "fmt" + + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/log" +) + +// LoginQq ======================== QQ 登录 ======================== +func LoginQq(c fiber.Ctx) error { + var req types.LoginQqDTO + vo := new(types.LoginVO) + + if err := httpx.BindAndValidate(c, &req); err != nil { + return errors.WithStack(err) + } + + return handleThirdLogin(c, req.Openid, req.Nickname, req.Avatar, 1, vo) +} + +// LoginWechat ======================== 微信登录 ======================== +func LoginWechat(c fiber.Ctx) error { + var req types.LoginWechatDTO + vo := new(types.LoginVO) + + if err := httpx.BindAndValidate(c, &req); err != nil { + return errors.WithStack(err) + } + + openid, token, err := wechat.GetWechatAccess(req.Code) + if err != nil { + log.Errorw("wechat access failed", "code", req.Code, "error", err) + return httpx.Fail(c, "微信登录失败,请重试") + } + + nickname, avatar, err := wechat.GetWechatUserInfo(token, openid) + if err != nil { + log.Errorw("wechat userinfo failed", "openid", openid, "error", err) + return httpx.Fail(c, "微信登录失败,请重试") + } + + return handleThirdLogin(c, openid, nickname, avatar, 0, vo) +} + +// ======================== 第三方登录主流程 ======================== +func handleThirdLogin( + c fiber.Ctx, + openid string, + nickname string, + avatar string, + loginType int16, + vo *types.LoginVO, +) error { + + dbClient := db.New() + + user, err := getUserByOpenID(dbClient, c, openid, loginType) + if err != nil { + return errors.WithStack(err) + } + + // 不存在 -> 注册 + if user == nil { + return registerUser(dbClient, c, openid, nickname, avatar, loginType, vo) + } + + // ========= 校验状态 ========= + id := toInt64(user["id"]) + status := toInt64(user["status"]) + + if status == 0 { + return httpx.Fail(c, "账号已被禁用") + } + + // ========= 更新登录时间 ========= + if err := dbClient.Update( + c.Context(), + "user", + "id", + map[string]any{ + "id": id, + "last_login_time": "NOW()", + }, + ); err != nil { + return errors.WithStack(err) + } + + // ========= token ========= + token, err := jwtx.CreateToken(c.Context(), map[string]any{ + "id": id, + }) + if err != nil { + return errors.WithStack(err) + } + + // ========= 返回 ========= + vo.Token = token + vo.UserId = id + vo.Nickname, _ = user["nickname"].(string) + vo.Avatar, _ = user["avatar"].(string) + + return httpx.OK(c, vo) +} + +// ======================== 注册 ======================== +func registerUser( + dbClient *db.Client, + c fiber.Ctx, + openid, nickname, avatar string, + loginType int16, + vo *types.LoginVO, +) error { + userId := uniqueid.NextId() + + if nickname == "" { + nickname = fmt.Sprintf("用户_%d", userId) + } + if avatar == "" { + avatar = "https://default-avatar-url.com/default.png" + } + + err := dbClient.WithTx(c.Context(), func(tx *db.Client) error { + + if err := tx.Insert(c.Context(), "user", "id", map[string]any{ + "id": userId, + "nickname": nickname, + "avatar": avatar, + "status": 1, + "create_time": "NOW()", + "update_time": "NOW()", + "last_login_time": "NOW()", + }); err != nil { + return err + } + + if err := tx.Insert(c.Context(), "user_oauth", "id", map[string]any{ + "id": uniqueid.NextId(), + "user_id": userId, + "type": loginType, + "openid": openid, + "create_time": "NOW()", + "update_time": "NOW()", + }); err != nil { + return err + } + + return nil + }) + + if err != nil { + return errors.WithStack(err) + } + + token, err := jwtx.CreateToken(c.Context(), map[string]any{ + "id": userId, + }) + if err != nil { + return errors.WithStack(err) + } + + vo.Token = token + vo.UserId = userId + vo.Nickname = nickname + vo.Avatar = avatar + + return httpx.OK(c, vo) +} + +// ======================== DB 查询封装(去重复 SQL) ======================== +func getUserByOpenID(dbClient *db.Client, c fiber.Ctx, openid string, loginType int16) (map[string]any, error) { + + users, err := dbClient.LoadDataBySQL( + c.Context(), + "SELECT u.* FROM user_oauth o JOIN user u ON u.id = o.user_id WHERE o.openid = ? AND o.type = ?", + []any{openid, loginType}, + ) + + if err != nil { + return nil, err + } + + if len(users) == 0 { + return nil, nil + } + + return users[0], nil +} + +// ======================== 类型工具(极简版) ======================== +func toInt64(v any) int64 { + i, _ := v.(int64) + return i +} diff --git a/code/allapp/allapp-go-v3/internal/handle/data_save.go b/code/allapp/allapp-go-v3/internal/handle/data_save.go index f550b99c..9bcae627 100644 --- a/code/allapp/allapp-go-v3/internal/handle/data_save.go +++ b/code/allapp/allapp-go-v3/internal/handle/data_save.go @@ -1,6 +1,7 @@ package handle import ( + "allapp-go/internal/errors" "allapp-go/internal/httpx" "allapp-go/internal/types" "allapp-go/pkg/db" @@ -12,7 +13,7 @@ func SaveData(c fiber.Ctx) error { var req []types.SaveDataReq if err := httpx.BindAndValidate(c, &req); err != nil { - return err + return errors.WithStack(err) } if len(req) == 0 { @@ -49,7 +50,7 @@ func SaveData(c fiber.Ctx) error { }) if err != nil { - return err + return errors.WithStack(err) } return httpx.OK(c, nil) diff --git a/code/allapp/allapp-go-v3/internal/router/router.go b/code/allapp/allapp-go-v3/internal/router/router.go index 57f3fae7..11fb63cf 100644 --- a/code/allapp/allapp-go-v3/internal/router/router.go +++ b/code/allapp/allapp-go-v3/internal/router/router.go @@ -1,14 +1,20 @@ package router import ( + "allapp-go/internal/config" "allapp-go/internal/handle" "github.com/gofiber/fiber/v3" ) -func SetupRouter(app *fiber.App) { +func SetupRouter(app *fiber.App, cfg *config.Config) { + api := app.Group(cfg.App.BaseUrl) + // ==================== data ==================== - app.Post("/data/loadData", handle.LoadData) - app.Post("/data/loadDataBySql", handle.LoadDataBySql) - app.Post("/data/saveData", handle.SaveData) + api.Post("/data/loadData", handle.LoadData) + api.Post("/data/loadDataBySql", handle.LoadDataBySql) + api.Post("/data/saveData", handle.SaveData) + + api.Post("/auth/login/qq", handle.LoginQq) + api.Post("/auth/login/wechat", handle.LoginWechat) } diff --git a/code/allapp/allapp-go-v3/internal/types/auth.go b/code/allapp/allapp-go-v3/internal/types/auth.go new file mode 100644 index 00000000..34d23586 --- /dev/null +++ b/code/allapp/allapp-go-v3/internal/types/auth.go @@ -0,0 +1,18 @@ +package types + +type LoginQqDTO struct { + Openid string `json:"openid" validate:"required"` + Nickname string `json:"nickname"` + Avatar string `json:"avatar"` +} + +type LoginWechatDTO struct { + Code string `json:"code" validate:"required"` +} + +type LoginVO struct { + Token string `json:"token"` + UserId int64 `json:"userId"` + Nickname string `json:"nickname"` + Avatar string `json:"avatar"` +} diff --git a/code/allapp/allapp-go-v3/pkg/common/common.go b/code/allapp/allapp-go-v3/pkg/common/common.go new file mode 100644 index 00000000..c95b8470 --- /dev/null +++ b/code/allapp/allapp-go-v3/pkg/common/common.go @@ -0,0 +1,45 @@ +package common + +import ( + "crypto/rand" + "encoding/json" + "math/big" +) + +// JSONStringToStruct 将 JSON 字符串反序列化到指定结构体类型 +func JSONStringToStruct[T any](jsonStr string) (*T, error) { + var result T + if err := json.Unmarshal([]byte(jsonStr), &result); err != nil { + return nil, err + } + return &result, nil +} + +func JSONStringToMap(jsonStr string) (map[string]interface{}, error) { + var result map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &result); err != nil { + return nil, err + } + return result, nil +} + +// GenerateCode 生成指定长度的随机邀请码(大写字母+数字) +// 不会返回错误,如果随机失败,会自动重试 +func GenerateCode(length int) string { + const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + code := make([]byte, length) + + for i := 0; i < length; i++ { + for { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + // 随机失败,重试 + continue + } + code[i] = charset[num.Int64()] + break + } + } + + return string(code) +} diff --git a/code/allapp/allapp-go-v3/pkg/db/select.go b/code/allapp/allapp-go-v3/pkg/db/select.go index 41f29cb1..6ea8caaa 100644 --- a/code/allapp/allapp-go-v3/pkg/db/select.go +++ b/code/allapp/allapp-go-v3/pkg/db/select.go @@ -13,7 +13,7 @@ func (c *Client) LoadData( orderBy string, searchColumns []string, args []any, -) ([][]any, error) { +) ([]map[string]any, error) { var where string @@ -50,12 +50,14 @@ func (c *Client) LoadDataBySQL( ctx context.Context, sql string, args []any, -) ([][]any, error) { +) ([]map[string]any, error) { return c.query(ctx, sql, args...) } -// 统一查询方法(核心优化) -func (c *Client) query(ctx context.Context, sql string, args ...any) ([][]any, error) { +// ========================== +// 核心查询方法(已升级) +// ========================== +func (c *Client) query(ctx context.Context, sql string, args ...any) ([]map[string]any, error) { sql = convertPlaceholder(sql) rows, err := c.exec().Query(ctx, sql, args...) @@ -64,7 +66,10 @@ func (c *Client) query(ctx context.Context, sql string, args ...any) ([][]any, e } defer rows.Close() - result := make([][]any, 0, 16) + // 获取字段名 + fields := rows.FieldDescriptions() + + result := make([]map[string]any, 0, 16) for rows.Next() { values, err := rows.Values() @@ -72,8 +77,13 @@ func (c *Client) query(ctx context.Context, sql string, args ...any) ([][]any, e return nil, err } - row := make([]any, len(values)) - copy(row, values) + row := make(map[string]any, len(values)) + + for i, f := range fields { + if i < len(values) { + row[string(f.Name)] = values[i] + } + } result = append(result, row) } diff --git a/code/allapp/allapp-go-v3/pkg/requestx/requestx.go b/code/allapp/allapp-go-v3/pkg/requestx/requestx.go new file mode 100644 index 00000000..fd3f1cc2 --- /dev/null +++ b/code/allapp/allapp-go-v3/pkg/requestx/requestx.go @@ -0,0 +1,99 @@ +package requestx + +import ( + "context" + "time" + + "github.com/gofiber/fiber/v3/client" +) + +// 全局 Fiber Client +var cc = client.New() + +func init() { + cc.SetTimeout(10 * time.Second) +} + +// mergeConfig 合并默认配置和用户配置 +func mergeConfig(defaultCfg, userCfg client.Config) client.Config { + if userCfg.Ctx != nil { + defaultCfg.Ctx = userCfg.Ctx + } + if userCfg.Body != nil { + defaultCfg.Body = userCfg.Body + } + if userCfg.Header != nil { + if defaultCfg.Header == nil { + defaultCfg.Header = map[string]string{} + } + for k, v := range userCfg.Header { + defaultCfg.Header[k] = v + } + } + if userCfg.Param != nil { + defaultCfg.Param = userCfg.Param + } + if userCfg.PathParam != nil { + defaultCfg.PathParam = userCfg.PathParam + } + if userCfg.FormData != nil { + defaultCfg.FormData = userCfg.FormData + } + if userCfg.Timeout != 0 { + defaultCfg.Timeout = userCfg.Timeout + } + if userCfg.UserAgent != "" { + defaultCfg.UserAgent = userCfg.UserAgent + } + if userCfg.Referer != "" { + defaultCfg.Referer = userCfg.Referer + } + defaultCfg.DisablePathNormalizing = userCfg.DisablePathNormalizing + defaultCfg.MaxRedirects = userCfg.MaxRedirects + + return defaultCfg +} + +func Get(url string, cfg client.Config) (string, int, error) { + defaultCfg := client.Config{ + Ctx: context.Background(), + } + finalCfg := mergeConfig(defaultCfg, cfg) + + resp, err := cc.Get(url, finalCfg) + if err != nil { + return "", 0, err + } + + return string(resp.Body()), resp.StatusCode(), nil +} + +func PostJSON(url string, data any, cfg client.Config) (string, int, error) { + defaultCfg := client.Config{ + Ctx: context.Background(), + Body: data, + Header: map[string]string{"Content-Type": "application/json"}, + } + finalCfg := mergeConfig(defaultCfg, cfg) + + resp, err := cc.Post(url, finalCfg) + if err != nil { + return "", 0, err + } + return string(resp.Body()), resp.StatusCode(), nil +} + +func PostForm(url string, formData map[string]string, cfg client.Config) (string, int, error) { + defaultCfg := client.Config{ + Ctx: context.Background(), + FormData: formData, + Header: map[string]string{"Content-Type": "application/x-www-form-urlencoded"}, + } + finalCfg := mergeConfig(defaultCfg, cfg) + + resp, err := cc.Post(url, finalCfg) + if err != nil { + return "", 0, err + } + return string(resp.Body()), resp.StatusCode(), nil +} diff --git a/code/allapp/allapp-go-v3/pkg/wechat/wechat.go b/code/allapp/allapp-go-v3/pkg/wechat/wechat.go new file mode 100644 index 00000000..6555c4c5 --- /dev/null +++ b/code/allapp/allapp-go-v3/pkg/wechat/wechat.go @@ -0,0 +1,102 @@ +package wechat + +import ( + "allapp-go/pkg/common" + "allapp-go/pkg/requestx" + "fmt" + "sync" + + "github.com/gofiber/fiber/v3/client" +) + +type jwtManager struct { + appid string + secret string +} + +var ( + manager *jwtManager + once sync.Once +) + +func Init(appid string, secret string) { + once.Do(func() { + manager = &jwtManager{ + appid: appid, + secret: secret, + } + }) +} + +func getManager() *jwtManager { + if manager == nil { + panic("wechat未初始化,请先调用Init") + } + return manager +} + +func GetWechatAccess(code string) (openid string, accessToken string, err error) { + bodyData, bodyCode, err := requestx.Get( + "https://api.weixin.qq.com/sns/oauth2/access_token", + client.Config{ + Param: map[string]string{ + "appid": getManager().appid, + "secret": getManager().secret, + "code": code, + "grant_type": "authorization_code", + }, + }, + ) + + if err != nil { + return "", "", err + } + + if bodyCode != 200 { + return "", "", fmt.Errorf("微信Http请求失败: %d", bodyCode) + } + + accessMap, err := common.JSONStringToMap(bodyData) + if err != nil { + return "", "", err + } + + accessToken, _ = accessMap["access_token"].(string) + openid, _ = accessMap["openid"].(string) + + if accessToken == "" || openid == "" { + return "", "", fmt.Errorf("获取微信登录凭证失败: %s", bodyData) + } + + return +} + +func GetWechatUserInfo(accessToken, openid string) (nickname, avatar string, err error) { + bodyData, bodyCode, err := requestx.Get( + "https://api.weixin.qq.com/sns/userinfo", + client.Config{ + Param: map[string]string{ + "access_token": accessToken, + "openid": openid, + }, + }, + ) + + if err != nil { + return "", "", err + } + + if bodyCode != 200 { + return "", "", fmt.Errorf("微信Http请求失败") + } + + userMap, err := common.JSONStringToMap(bodyData) + if err != nil { + return "", "", err + } + + nickname, _ = userMap["nickname"].(string) + avatar, _ = userMap["headimgurl"].(string) + + return +} diff --git a/code/app/app-rn4/app.json b/code/app/app-rn4/app.json index b007ead9..9da75177 100644 --- a/code/app/app-rn4/app.json +++ b/code/app/app-rn4/app.json @@ -5,8 +5,12 @@ "version": "1.0.0", "orientation": "portrait", "icon": "./assets/images/icon.png", - "scheme": "apprn", "userInterfaceStyle": "automatic", + "scheme": [ + "oneao", + "tencent102826474", + "wxdab3e21a1f7e392f" + ], "ios": { "icon": "./assets/expo.icon" }, @@ -18,7 +22,7 @@ "monochromeImage": "./assets/images/android-icon-monochrome.png" }, "predictiveBackGestureEnabled": false, - "package": "com.anonymous.apprn" + "package": "cn.oneao.app" }, "web": { "output": "static", @@ -26,6 +30,7 @@ "bundler": "metro" }, "plugins": [ + "./plugins/withAndroidSignature", "expo-router", [ "expo-splash-screen", @@ -37,7 +42,23 @@ } } ], - "expo-web-browser" + "expo-web-browser", + [ + "expo-qq", + { + "appId": "102826474" + } + ], + [ + "expo-build-properties", + { + "android": { + "usesCleartextTraffic": true, + "extraProguardRules": "-keep class com.tencent.mm.opensdk.** { *; }\n-keep class com.tencent.wxop.** { *; }\n-keep class com.tencent.mm.sdk.** { *; }" + } + } + ], + "expo-wechat" ], "experiments": { "typedRoutes": true, diff --git a/code/app/app-rn4/credentials/credentials.json b/code/app/app-rn4/credentials/credentials.json new file mode 100644 index 00000000..4f43db65 --- /dev/null +++ b/code/app/app-rn4/credentials/credentials.json @@ -0,0 +1,17 @@ +{ + "android": { + "keystore": { + "keystorePath": "credentials/oneao-android.keystore", + "keystorePassword": "zhang520..", + "keyAlias": "oneao-android", + "keyPassword": "zhang520.." + } + }, + "ios": { + "provisioningProfilePath": "ios/certs/profile.mobileprovision", + "distributionCertificate": { + "path": "ios/certs/dist-cert.p12", + "password": "password" + } + } +} diff --git a/code/app/app-rn4/credentials/oneao-android.keystore b/code/app/app-rn4/credentials/oneao-android.keystore new file mode 100644 index 0000000000000000000000000000000000000000..05b3583cf176c68ca217e89834dcc5f132920e4f GIT binary patch literal 2253 zcmciDc{J1u8wc?D%_=oimPE=rBr#-H*|LPeSc+^>ldNNHWH)sgC0n*3Mo6+|D>6#T zHoRRT&DbJIgUOb$kJq{9z4zR6`v3jo`RDW3bH3+$o`=KYus|RXcyEBe28s8>x#3T{ z`CSjhdtTquT&A_NEC>Vxqlr)sng`B(3JyR4Iq)F>fPtVy=&aQ$p}BaA=_4I9oINQI zmm$YqM7tN&Nb$Y#CKqKvwkJwm7~N8+H&5@MB8d_L7wWLMUTEVJ;%)u_`Sa2p2}J{W z*xioZtmIE7c+06HQo{)Qd{ioHN~mjpdDFD-&k5bC$}PEBR#Rd>H$wAEl(N_5MgwGk zzb&Oo!-{X?QH=n`tM}E_!BmI>?dX_82p>Cij>igB66Ro9Arx4;G%DM(Du+0=*`%vE za0q6wv~-RUSr|h;^FblfX`HE0Kq zyn9NOv`)HlVln!;rdyszbzQlM)dAO|YlQYC*_==<2|t|mCw+$ykq7dQGegvGP#I6( z3Dr`uOltV}n9q(yiCVx|Vu&%;79*lQ%7M8ok7Tnf)S4H&tMfuotNMHcl4YRmLyeu4 z^V6Q9+TH0rf|hd0`}gz z|J<8;cB-bT!9hC+!B^isGS^Mfw(Tr9`e%qIckcm&~2jURd@2M=$i$UmTL-Z?yM zXDkz(cWdF^9p}MoX7t;vrkT6cDr@X3Y%Sk%+SfH}?naFOu#9WZ~f?x3QSi0L&8!fK5jAJ2IkfKJW@ykB+{UNF)Ln1}#%4VOh2Qiud^ zXkwYkC%RD!M+;`Y<(S&o@8D?cI8}hmaEGWn4YM=XXM5@gRhqvouLM+Yz<(I7xVDQW z4uTFO$)`UJ3y;ASRD_~hD(Z7h!^b1kg8N7ZzdGYqMy;Ko2HYy?;Z7GRZ9_w|;Dg=P zk^f0h9g5*UjFUGI&lvt$%6cZ)E3fAHt2nx*@m;D)kHUSIxX|N}hK_m_7w5Iq~Dr#%=j1d(>eQmcoL3I?<_#i!|@{-q_N($yp z<{zwY-QgUxi<+qG)&5+LebiPP)%KTJ6^9ZZEs(?RLl$?|6ewDCx?BwuTIA;mO zrn!4tq&xPL(iU?PS9FXltN(CmHJ-Df6oGwsP1nEDG)~eP-H1GtcFOK?!sE*Yb3s|N zeK8y^MFYZ|*Ygk$=FsDNvx^(uuB{u>X$OCiXgGH{R>{v|WarxZ!}_)4OI%@Ylg{z) zCAiMV$Tn5-G#yd0_z8`YC_gzI=M|f-&`GhcG>S%HmOO;Fy;UFXGP6DE&*ckp%!bg# zln(+nMw1&0dILX>iF5^YTphY8!^r%$E8mbmZ%)6Ss9-Xq&rs*m%E5hxU}<%KvshE)7<KU|}68eAef8K`( zNc=mHy?+5A0=ytJ5#RvQVVve zwxljS25_0uyHeTA07C!}P$Z5%ffn8Ch$s(K5X!F&v!sadgvP&?p96L1CBX- z>iL)hyCo~&)&49Y!GpNTotkzjgcFj}NFJX?K*e5a_Zcyy$uj zV>0F$Yao9|!pUG { + if (config.modResults.language !== "groovy") { + throw new Error("only groovy build.gradle is supported"); + } + + config.modResults.contents = setAndroidSignature( + config.modResults.contents + ); + + return config; + }); +}; + +// ======================== 主入口 ======================== +function setAndroidSignature(appBuildGradle) { + if (!fs.existsSync(CREDENTIAL_PATH)) { + console.warn("⚠️ credentials.json 不存在,跳过签名配置"); + return appBuildGradle; + } + + const info = JSON.parse( + fs.readFileSync(CREDENTIAL_PATH, "utf8") + ); + + let output = appBuildGradle; + + // ⭐ 1. 重写 signingConfigs(关键) + output = replaceSigningConfigs(output, info); + + // ⭐ 2. 确保所有 debug/release 使用统一 keystore + output = unifySigningUsage(output); + + return output; +} + +// ======================== ⭐核心:重写 signingConfigs ======================== +function replaceSigningConfigs(appBuildGradle, info) { + const block = ` + signingConfigs { + release { + storeFile file(${JSON.stringify(KEYSTORE_PATH)}) + storePassword ${JSON.stringify(info.android.keystore.keystorePassword)} + keyAlias ${JSON.stringify(info.android.keystore.keyAlias)} + keyPassword ${JSON.stringify(info.android.keystore.keyPassword)} + } + + debug { + storeFile file(${JSON.stringify(KEYSTORE_PATH)}) + storePassword ${JSON.stringify(info.android.keystore.keystorePassword)} + keyAlias ${JSON.stringify(info.android.keystore.keyAlias)} + keyPassword ${JSON.stringify(info.android.keystore.keyPassword)} + } + } + `; + + const regex = /signingConfigs\s*\{[\s\S]*?\n\s*\}/m; + + if (regex.test(appBuildGradle)) { + return appBuildGradle.replace(regex, block.trim()); + } + + // 没有就插入 + return appBuildGradle.replace( + /(android\s*\{)/, + `$1\n${block.trim()}` + ); +} + +// ======================== ⭐统一使用 signingConfig ======================== +function unifySigningUsage(appBuildGradle) { + let output = appBuildGradle; + + // debug -> release(你如果想 debug 也用 release,可以改这里) + output = output.replace( + /signingConfig\s+signingConfigs\.debug/g, + "signingConfig signingConfigs.release" + ); + + return output; +} \ No newline at end of file diff --git a/code/app/app-rn4/pnpm-lock.yaml b/code/app/app-rn4/pnpm-lock.yaml index e5ef3d18..208c7039 100644 --- a/code/app/app-rn4/pnpm-lock.yaml +++ b/code/app/app-rn4/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: expo-blur: specifier: ~55.0.14 version: 55.0.14(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-build-properties: + specifier: ~55.0.13 + version: 55.0.13(expo@55.0.15) expo-constants: specifier: ~55.0.14 version: 55.0.14(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(typescript@5.9.3) @@ -62,6 +65,9 @@ importers: expo-linking: specifier: ~55.0.13 version: 55.0.13(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + expo-qq: + specifier: ^0.1.8 + version: 0.1.8(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) expo-router: specifier: ~55.0.12 version: 55.0.12(@expo/log-box@55.0.10)(@expo/metro-runtime@55.0.9)(@types/react@19.2.14)(expo-constants@55.0.14)(expo-font@55.0.6)(expo-linking@55.0.13)(expo@55.0.15)(react-dom@19.2.0(react@19.2.0))(react-native-gesture-handler@2.30.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-screens@4.23.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-web@0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) @@ -80,6 +86,9 @@ importers: expo-web-browser: specifier: ~55.0.14 version: 55.0.14(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)) + expo-wechat: + specifier: ^0.2.6 + version: 0.2.6(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) heroui-native: specifier: ^1.0.1 version: 1.0.2(f8f492cc9ab1e25a4f7a8dde00438ce1) @@ -2205,6 +2214,11 @@ packages: react: '*' react-native: '*' + expo-build-properties@55.0.13: + resolution: {integrity: sha512-UYZhUKyh7YQhbJdkBvo68WUQ7fOtZeSV7F8kfYkjEiN/ADRHG0WfEIiddvGfi9cH/5iwpptv/+Lu5cx6uvfegA==} + peerDependencies: + expo: '*' + expo-constants@55.0.14: resolution: {integrity: sha512-l23QVQCYBPKT5zbxxZdJeuhiunadvWdjcQ9+GC8h+02jCoLmWRk20064nCINnQTP3Hf+uLPteUiwYrJd0e446w==} peerDependencies: @@ -2280,6 +2294,13 @@ packages: react-native-worklets: optional: true + expo-qq@0.1.8: + resolution: {integrity: sha512-Khuu4LWOjpYGGBamtUFBxaA5q8oIcFiVgXkyRVqfNdSEP6Z0Fy3eBj3HuXP8mxonT/hJjG3GCX7ypJD7M9nU/g==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + expo-router@55.0.12: resolution: {integrity: sha512-Bm6IhI0Kl5/tDlCHPms8jDqy1O6HLHIOrMsEmmAQ5Lgg5UBtDfRThEyHPVOLNTOs8e7/bG/Ftz6a4UgQVA+NhQ==} peerDependencies: @@ -2354,6 +2375,13 @@ packages: expo: '*' react-native: '*' + expo-wechat@0.2.6: + resolution: {integrity: sha512-hLWFPDqSKwEgG8Jm0cTthyQftD/ip9xjJLQUEd+fJJVq8YEiEt0gJCCgCEMbr1SrEjoK4TUlkG/HETZobNReuQ==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + expo@55.0.15: resolution: {integrity: sha512-sHIvqG477UU1jZHhaexXbUgsU7y+xnYZqDW1HrUkEBYiuEb5lobvWLmwea76EBVkityQx46UDtepFtarpUJQqQ==} hasBin: true @@ -6385,6 +6413,13 @@ snapshots: react: 19.2.0 react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0) + expo-build-properties@55.0.13(expo@55.0.15): + dependencies: + '@expo/schema-utils': 55.0.3 + expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + resolve-from: 5.0.0 + semver: 7.7.4 + expo-constants@55.0.14(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(typescript@5.9.3): dependencies: '@expo/config': 55.0.15(typescript@5.9.3) @@ -6467,6 +6502,12 @@ snapshots: optionalDependencies: react-native-worklets: 0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-qq@0.1.8(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): + dependencies: + expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + react: 19.2.0 + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0) + expo-router@55.0.12(@expo/log-box@55.0.10)(@expo/metro-runtime@55.0.9)(@types/react@19.2.14)(expo-constants@55.0.14)(expo-font@55.0.6)(expo-linking@55.0.13)(expo@55.0.15)(react-dom@19.2.0(react@19.2.0))(react-native-gesture-handler@2.30.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-screens@4.23.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-web@0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): dependencies: '@expo/log-box': 55.0.10(@expo/dom-webview@55.0.5)(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) @@ -6556,6 +6597,12 @@ snapshots: expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3) react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0) + expo-wechat@0.2.6(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): + dependencies: + expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + react: 19.2.0 + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0) + expo@55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3): dependencies: '@babel/runtime': 7.29.2 diff --git a/code/app/app-rn4/src/api/index.ts b/code/app/app-rn4/src/api/index.ts deleted file mode 100644 index e6dc2675..00000000 --- a/code/app/app-rn4/src/api/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Method } from "alova"; -import type { AxiosError } from "axios"; -import { axiosRequestAdapter } from "@alova/adapter-axios"; -import { createAlova } from "alova"; -import { toast } from "sonner-native"; - -const alovaInstance = createAlova({ - baseURL: "", - requestAdapter: axiosRequestAdapter(), - timeout: 5000, - cacheFor: null, -}); diff --git a/code/app/app-rn4/src/app/_layout.tsx b/code/app/app-rn4/src/app/_layout.tsx index c8995f8f..59720142 100644 --- a/code/app/app-rn4/src/app/_layout.tsx +++ b/code/app/app-rn4/src/app/_layout.tsx @@ -7,6 +7,7 @@ import * as SplashScreen from "expo-splash-screen"; import "@/global.css"; import { useEffect, useState } from "react"; import { Stack } from "expo-router"; +import { Toaster } from "sonner-native"; SplashScreen.preventAutoHideAsync(); @@ -52,6 +53,8 @@ export default function RootLayout() { {!isDev && } + + ); } diff --git a/code/app/app-rn4/src/app/money/(tabs)/home/index.tsx b/code/app/app-rn4/src/app/money/(tabs)/home/index.tsx index 4ee6e265..dfcbb7b5 100644 --- a/code/app/app-rn4/src/app/money/(tabs)/home/index.tsx +++ b/code/app/app-rn4/src/app/money/(tabs)/home/index.tsx @@ -1,11 +1,24 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import LayoutView from "@/components/layout/view"; import { Text } from "react-native"; import EmptyState from "@/components/EmptyStatus"; +import { loadDataApi } from "@/request/api"; export default function Home() { const [dataList, setDataList] = useState([]); + useEffect(() => { + loadDataApi({ + view_name: "v_finance_record", + search_condition: "", + search_columns: [], + order_by: "", + args: [], + }).then((res) => { + console.log(res); + }); + }, []); + return ( diff --git a/code/app/app-rn4/src/request/api.ts b/code/app/app-rn4/src/request/api.ts new file mode 100644 index 00000000..b0dba2cc --- /dev/null +++ b/code/app/app-rn4/src/request/api.ts @@ -0,0 +1,32 @@ +import type { LoginQqParams, LoginResult, LoginWxParams } from "@/types/auth"; +import alovaInstance from "./index"; + +export function loginQqApi(params: LoginQqParams) { + return alovaInstance.Post>(`/auth/login/qq`, params); +} + +export function loginWxApi(params: LoginWxParams) { + return alovaInstance.Post>(`/auth/login/wechat`, params); +} + +interface LoadDataBySqlParams { + sql: string; + args: any[]; +} + +export function loadDataBySqlApi(params: LoadDataBySqlParams) { + return alovaInstance.Post>(`/data/loadDataBySql`, params); +} + +interface LoadDataParams { + view_name: string; + search_condition: string; + order_by: string; + search_columns: string[]; + args: string[]; +} + +export function loadDataApi(params: LoadDataParams) { + return alovaInstance.Post>(`/data/loadData`, params); +} + diff --git a/code/app/app-rn4/src/request/index.ts b/code/app/app-rn4/src/request/index.ts new file mode 100644 index 00000000..a4d78f9d --- /dev/null +++ b/code/app/app-rn4/src/request/index.ts @@ -0,0 +1,81 @@ +import type { Method } from "alova"; +import type { AxiosError } from "axios"; +import { axiosRequestAdapter } from "@alova/adapter-axios"; +import { createAlova } from "alova"; +import { toast } from "sonner-native"; +import { useUserStore } from "@/store/user"; + +enum HttpCode { + Success = 1000, + Unauthorized = 1001, + Fail = 500, + BusinessError = 2000, + SystemError = 3000, +} + +// 仅保留「无需登录」接口 +const WhiteList = ["/auth/login/**"]; + +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: process.env.EXPO_PUBLIC_API_URL, + requestAdapter: axiosRequestAdapter(), + timeout: 5000, + cacheFor: null, + + responded: { + onSuccess: async (response, _method: Method) => { + const resData = response.data as ResponseData; + + const result = { + ...resData, + isSuccess: resData.code === HttpCode.Success, + }; + + if (resData.code === HttpCode.Unauthorized) { + toast.error("登录状态已过期,请重新登录"); + useUserStore.getState().setAuthExpired(true); + return result; + } else if (resData.code === HttpCode.BusinessError) { + toast.error(resData.message || "业务异常,请稍后再试"); + return result; + } else if (resData.code === HttpCode.SystemError) { + toast.error(resData.message || "系统异常,请稍后再试"); + return result; + } + + return result; + }, + + onError: (error: AxiosError) => { + return Promise.reject(error); + }, + }, + + beforeRequest(method) { + // 白名单接口直接放行 + if (isWhiteListed(method.url)) return; + + const userStore = useUserStore.getState(); + + // 未登录直接拦截 + if (!userStore.isLogged()) { + userStore.setAuthExpired(true); + return Promise.reject(new Error("未登录")); + } + + // 注入 JWT + method.config.headers.Token = userStore.userInfo!.token; + }, +}); + +export default alovaInstance; diff --git a/code/app/app-rn4/src/store/user.ts b/code/app/app-rn4/src/store/user.ts index 5fb7aeaf..264fa1da 100644 --- a/code/app/app-rn4/src/store/user.ts +++ b/code/app/app-rn4/src/store/user.ts @@ -4,9 +4,15 @@ import { createJSONStorage, persist } from "zustand/middleware"; interface UserState { userInfo: UserInfo; + isLogged: () => boolean; + setUserInfo: (userInfo: UserInfo) => void; + clear: () => void; + + authExpired: boolean; + setAuthExpired: (v: boolean) => void; } const defaultUserInfo: UserInfo = { @@ -21,6 +27,8 @@ export const useUserStore = create()( (set, get) => ({ userInfo: defaultUserInfo, + authExpired: false, + isLogged: () => !!get().userInfo?.token, setUserInfo: (userInfo) => @@ -33,10 +41,19 @@ export const useUserStore = create()( userInfo: defaultUserInfo, }); }, + + setAuthExpired: (v: boolean) => + set({ + authExpired: v, + }), }), { name: "user-storage", storage: createJSONStorage(() => AsyncStorage), + + partialize: (state) => ({ + userInfo: state.userInfo, + }), }, ), ); diff --git a/code/app/app-rn4/src/types/auth.ts b/code/app/app-rn4/src/types/auth.ts new file mode 100644 index 00000000..844731a9 --- /dev/null +++ b/code/app/app-rn4/src/types/auth.ts @@ -0,0 +1,16 @@ +export interface LoginQqParams { + openid: string + nickname: string + avatar: string +} + +export interface LoginWxParams { + code: string +} + +export interface LoginResult { + token: string + userId: number + nickname: string + avatar: string +} diff --git a/code/app/app-rn4/src/types/global.d.ts b/code/app/app-rn4/src/types/global.d.ts index c816085b..1508108e 100644 --- a/code/app/app-rn4/src/types/global.d.ts +++ b/code/app/app-rn4/src/types/global.d.ts @@ -10,6 +10,14 @@ declare global { SystemError = 3000, } + interface ResponseData { + track_id: string; + code: number; + message: string; + data: T; + isSuccess: boolean; + } + interface UserInfo { id: number; token: string;