diff --git a/new/wz-admin/.env.example b/new/wz-admin/.env.example index 2728249..e3d9cec 100644 --- a/new/wz-admin/.env.example +++ b/new/wz-admin/.env.example @@ -1,6 +1,6 @@ -# 开发推荐:删除本行或留空,让浏览器请求同源 /api/*,由 Vite 代理到 wz-api(见 vite.config.ts,默认 127.0.0.1:48080) -# 若填写了完整地址,则浏览器直连该 API(需 wz-api CORS 允许当前前端 Origin,已默认含 http://localhost:40002) -# VITE_API_BASE_URL=http://localhost:48080 +# 开发推荐:删除本行或留空,让浏览器请求同源 /api/*,由 Vite 代理到 wz-api(见 vite.config.ts,默认 127.0.0.1:4040) +# 若填写了完整地址,则浏览器直连该 API(需 wz-api CORS 允许当前前端 Origin,已默认含 http://localhost:4041) +# VITE_API_BASE_URL=http://localhost:4040 # 联调开关:默认(不写或任意非 false)即走 wz-api + MySQL/wz_documents。 # 仅当需要「纯前端 MOCK、不接后端」时设为 false: diff --git a/new/wz-admin/vite.config.ts b/new/wz-admin/vite.config.ts index 2e6a7f3..a3dc010 100644 --- a/new/wz-admin/vite.config.ts +++ b/new/wz-admin/vite.config.ts @@ -4,10 +4,10 @@ import react from '@vitejs/plugin-react' import { defineConfig, loadEnv } from 'vite' // https://vite.dev/config/ -// 与 new/wz-api .env.development 中 PORT=48080 一致;可建 .env.development 设 WZ_API_PROXY_TARGET 覆盖 +// 与 new/wz-api PORT=4040(.env.development / 生产)一致;可建 .env.development 设 WZ_API_PROXY_TARGET 覆盖 export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), '') - const apiTarget = (env.WZ_API_PROXY_TARGET || 'http://127.0.0.1:48080').replace(/\/$/, '') + const apiTarget = (env.WZ_API_PROXY_TARGET || 'http://127.0.0.1:4040').replace(/\/$/, '') return { plugins: [react(), tailwindcss()], diff --git a/new/wz-api/.env.development b/new/wz-api/.env.development index 36f2367..94792d2 100644 --- a/new/wz-api/.env.development +++ b/new/wz-api/.env.development @@ -1,22 +1,28 @@ # wz-api 开发环境(air 见 .air.toml:先加载 .env 再叠加本文件) # 键顺序与 .env.production 对齐。以下取值保留开发环境:APP_ENV、PORT、GIN_MODE、APP_VERSION、 -# DB_DSN、API_BASE_URL、CORS_ORIGINS(勿把生产 MySQL 指到开发机进程)。 +# MONGODB_URI、API_BASE_URL、CORS_ORIGINS(勿把生产凭据指到错误环境)。 # 须设置 APP_ENV=development,或在 shell 中 export 后再 go run APP_ENV=development -# 与 new/wz-app 中 Vite 代理默认 48080 对齐;改端口请同步前端 WZ_API_PROXY_TARGET -PORT=48080 +# 与生产 PORT=4040、master.py 默认 DEPLOY_API_PORT 一致;改端口请同步 wz-app / wz-admin 的 WZ_API_PROXY_TARGET +PORT=4040 GIN_MODE=debug APP_VERSION=dev # 开发不写 SKIP_PROD_SECRET_CHECK(仅 GIN_MODE=release 时 config 会校验生产密钥) -# 必填:MySQL(GORM),开发库 souldev;生产见 .env.production 的 soul_miniprogram -DB_DSN=souldev:RXW2FeRcRdH2GtXy@tcp(56b4c23f6853c.gz.cdb.myqcloud.com:14413)/souldev?charset=utf8mb4&parseTime=True +# 必填:MongoDB(唯一业务库) +# 本地 Docker 官方镜像多数默认「无认证」,请用下一行(库名 wz 不存在会在首次写入时自动出现): +MONGODB_URI=mongodb://127.0.0.1:27017/wz +# 若 compose 里配置了 root 密码,示例:MONGODB_URI=mongodb://root:你的密码@127.0.0.1:27017/wz?authSource=admin +# 若容器映射在非 27017 端口,改为对应端口即可。 +# 连接云上 wzdj 用户时请改回(勿与无认证的本地实例混用同一串): +# MONGODB_URI=mongodb://wzdj:xxx@43.139.27.93:27017/wzdj?authSource=wzdj +# DB_DSN 已废弃,可删除或留空 -# 可选:玩值 Mongo(与生产同一实例与连接串;业务仍以 MySQL 为准) -MONGODB_URI=mongodb://wzdj:NR6aLyyEaZKWZXky@43.139.27.93:27017/wzdj?authSource=wzdj +# 可选:遗留字段,Mongo-only 构建不使用 +# DB_DSN= # 本服务对外可访问根 URL(无尾斜杠;支付/转账回调、H5 元数据等,缺省时由代码拼接) API_BASE_URL=http://127.0.0.1:4040 @@ -24,9 +30,9 @@ API_BASE_URL=http://127.0.0.1:4040 # 仅在本地 Vite 端口不在代码默认可列表时追加(与 defaultCORSOrigins 合并) CORS_ORIGINS=http://localhost:5175,http://127.0.0.1:5175 -SKIP_AUTO_MIGRATE=1 - -# REDIS_URL= +# Mongo-only:已无 MySQL AutoMigrate;以下两项可忽略或删除 +# 本地 Docker:redis-local-nwal 映射 6379:6379;无密码用 db 0 +REDIS_URL=redis://127.0.0.1:6379/2 # SYNC_ORDERS_INTERVAL_MINUTES=5 CRON_SECRET= diff --git a/new/wz-api/.env.docker.example b/new/wz-api/.env.docker.example new file mode 100644 index 0000000..a73595a --- /dev/null +++ b/new/wz-api/.env.docker.example @@ -0,0 +1,15 @@ +# 本地 Docker Mongo(官方镜像、未启用 --auth 时) +# 与仓库曾用的容器名一致,便于沿用脚本/习惯: +# +# docker stop wanzhi-mongodb 2>nul & docker rm wanzhi-mongodb 2>nul +# docker run -d --name wanzhi-mongodb -p 27017:27017 mongo:7 +# +# 连接串(库名 wz,与 seedmongo / wz-api 默认一致): +MONGODB_URI=mongodb://127.0.0.1:27017/wz + +# 若使用 docker run -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=secret: +# MONGODB_URI=mongodb://root:secret@127.0.0.1:27017/wz?authSource=admin + +# 初始化索引与演示数据: +# set MONGODB_URI=mongodb://127.0.0.1:27017/wz +# go run ./cmd/seedmongo diff --git a/new/wz-api/.env.production b/new/wz-api/.env.production index 56b8fe4..30bb8e9 100644 --- a/new/wz-api/.env.production +++ b/new/wz-api/.env.production @@ -11,21 +11,23 @@ APP_VERSION=0.0.0 # 上线前必去掉:用于预发/演练绕过 config.go 对默认密钥的校验(勿用于真实生产) SKIP_PROD_SECRET_CHECK=staging -# 必填:生产 MySQL -DB_DSN=cdb_outerroot:Zhiqun1984@tcp(56b4c23f6853c.gz.cdb.myqcloud.com:14413)/soul_miniprogram?charset=utf8mb4&parseTime=True - -# 可选:玩值 Mongo(与开发同一实例时可复用;业务仍以 MySQL 为准) +# 必填:MongoDB(唯一业务库) MONGODB_URI=mongodb://wzdj:NR6aLyyEaZKWZXky@43.139.27.93:27017/wzdj?authSource=wzdj +# 已废弃(Mongo-only):原 MySQL,可留空或删除 +# DB_DSN= # 玩值 API 公网根地址(无尾斜杠;回调与 H5 依赖) API_BASE_URL=https://wzapi.quwanzhi.com # 可不写:代码内 defaultCORSOrigins 已含 https://wzadmin.quwanzhi.com 与 wzdj 等;若有额外源再追加 CORS_ORIGINS=https://wzadmin.quwanzhi.com,http://wzadmin.quwanzhi.com,https://wzdj.quwanzhi.com,http://wzdj.quwanzhi.com -SKIP_AUTO_MIGRATE=1 +# Mongo-only:无 MySQL AutoMigrate,可忽略 +# 本地 Docker:redis-local-nwal 映射 6379:6379;无密码用 db 0 +REDIS_ADDR=host.docker.internal:26738 +REDIS_PASSWORD=WZX7HbnpZErnS6Tm +REDIS_DB=2 -# REDIS_URL= # SYNC_ORDERS_INTERVAL_MINUTES=5 CRON_SECRET= diff --git a/new/wz-api/MIGRATION.md b/new/wz-api/MIGRATION.md index 22d4784..8dc53b7 100644 --- a/new/wz-api/MIGRATION.md +++ b/new/wz-api/MIGRATION.md @@ -1,7 +1,15 @@ # wz-api 迁移说明 - **Go module** 已更名为 `wz-api`;import 路径为 `wz-api/internal/...`。 -- **业务来源**:以仓库根 `old/app/api` 与《开发文档/玩值/迁移-API对照表.md》为真源;当前代码体仍含 Soul 派对时期路由与模型,属**过渡态**。 -- **新 C 端前缀**:`/api/app/*` 已在 `router.Setup` 注册(现含 `GET /api/app/health`)。历史小程序路径 `/api/miniprogram/*` 保留至 wz-app 全量切换后再删。 -- **发版可观测**:`GET /api/public/build-info` 返回 `buildTime`(`BUILD_TIME`)与可选 `gitCommit`(环境变量 **`GIT_COMMIT`**,服务端截为 ≤12 字符,未设则为空串),便于与前端/终端对照运行二进制。 -- **剥离步骤**:按域删除或停用 Soul 专有 handler → 按对照表实现玩值接口 → 更新 AutoMigrate 与 scripts。 +- **Mongo-only(当前默认构建)**:**不再连接 MySQL**。持久化仅此几类 Mongo 集合: + - **账号**:`admin_users`、`app_users`、`user_auths`、`user_balances`(C 端与玩值业务集合 `users` 分离)。 + - **玩值文档**:与 `internal/wzstore/collections.go` 同名集合;文档根字段含 **`ext_id`**,业务字段与历史 `body` 扁平一致,便于 `mongoexport` 风格导入。 +- **环境变量**:**`MONGODB_URI` 必填**(别名 `MONGO_URI`)。`DB_DSN` 已废弃,可省略。 +- **启动**:`go run ./cmd/server`;数据初始化:`MONGODB_URI=... go run ./cmd/seedmongo`(管理员 `admin`/`admin123`;测试手机号 `13800138000`/`123456`)。 +- **健康检查**:`GET /health` 中 `database`/`mongo` 为 Mongo Ping 结果;`mysqlGone: true` 表示本构建不加载 MySQL。 +- **路由范围(本构建)**:仅 **`/api/app/*`**、`/api/screen/*`、`/api/public/*`、管理端鉴权相关与 **`/api/admin/wz/*`**(另含 `RegisterWanzhiAdminRoutes` 的 mock 契约)。**已自默认路由移除**(未单独打包 legacy):`/api/miniprogram/*`、`/api/db/*`、Soul 书籍/支付/提现等原依赖 MySQL 的全部分组;`/read/:id` H5 与 `/ws/miniprogram` 一并移除。 +- **发版可观测**:`GET /api/public/build-info` 返回 `buildTime`(`BUILD_TIME`)与可选 `gitCommit`(`GIT_COMMIT`,≤12 字符)。 + +## 历史说明(对照) + +- 业务来源仍以仓库 `old/app/api` 与《开发文档/玩值/迁移-API对照表.md》为溯源;表中「已实现」接口在 Mongo-only 下**数据源已切换为 Mongo**,路径不变。 diff --git a/new/wz-api/cmd/seedmongo/main.go b/new/wz-api/cmd/seedmongo/main.go new file mode 100644 index 0000000..490af80 --- /dev/null +++ b/new/wz-api/cmd/seedmongo/main.go @@ -0,0 +1,227 @@ +// Seedmongo 创建索引并写入最小管理员、可选 C 端测试账号与玩值示例文档。 +// 用法:MONGODB_URI=mongodb://127.0.0.1:27017/wz go run ./cmd/seedmongo +package main + +import ( + "context" + "fmt" + "log" + "os" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "golang.org/x/crypto/bcrypt" +) + +func main() { + uri := strings.TrimSpace(os.Getenv("MONGODB_URI")) + if uri == "" { + uri = strings.TrimSpace(os.Getenv("MONGO_URI")) + } + if uri == "" { + log.Fatal("请设置 MONGODB_URI") + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri)) + if err != nil { + log.Fatal(err) + } + defer func() { _ = client.Disconnect(context.Background()) }() + if err := client.Ping(ctx, nil); err != nil { + log.Fatal("ping: ", err) + } + db := client.Database(dbName(uri)) + + ensureIndexes(db) + seedAdmin(db) + seedAppUser(db) + seedPlayhubSample(db) + seedPlayhubExtended(db) + log.Println("seedmongo: 完成") +} + +func dbName(uri string) string { + // mongodb://host/dbname + i := strings.LastIndex(uri, "/") + if i <= 0 || i == len(uri)-1 { + return "wz" + } + path := uri[i+1:] + if j := strings.IndexAny(path, "?"); j >= 0 { + path = path[:j] + } + if path == "" { + return "wz" + } + return path +} + +func ensureIndexes(db *mongo.Database) { + ctx := context.Background() + _, _ = db.Collection("admin_users").Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "username", Value: 1}}, + Options: options.Index().SetUnique(true), + }) + _, _ = db.Collection("user_auths").Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "auth_type", Value: 1}, {Key: "auth_id", Value: 1}}, + Options: options.Index().SetUnique(true), + }) + _, _ = db.Collection("app_users").Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "id", Value: 1}}, + Options: options.Index().SetUnique(true), + }) + _, _ = db.Collection("user_balances").Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "user_id", Value: 1}}, + Options: options.Index().SetUnique(true), + }) + colls := []string{ + "users", "streamers", "streamerBindings", "guilds", "stars", "esportsVenues", "venueBookings", + "starPartyRooms", "starPartyMembers", "liveRooms", "liveSessions", "liveComments", "liveGifts", + "liveShowcaseItems", "userJourneyEvents", "accountPawns", "transactions", "commissionLedger", + "productOrders", "companionOrders", "pointCardOrders", "rechargeOrders", "withdrawOrders", + "products", "contentItems", "gamePointCards", "gameAccounts", "levelingProducts", "levelingOrders", + "rfmRuleSets", "screenConfig", "games", + } + for _, c := range colls { + _, _ = db.Collection(c).Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "ext_id", Value: 1}}, + Options: options.Index().SetUnique(true).SetSparse(true), + }) + } +} + +func seedAdmin(db *mongo.Database) { + ctx := context.Background() + n, _ := db.Collection("admin_users").CountDocuments(ctx, bson.M{}) + if n > 0 { + return + } + hash, err := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost) + if err != nil { + log.Fatal(err) + } + now := time.Now().UTC() + _, err = db.Collection("admin_users").InsertOne(ctx, bson.M{ + "id": uint32(1), "username": "admin", "password_hash": string(hash), + "role": "super_admin", "name": "卡若", "status": "active", + "created_at": now, "updated_at": now, + }) + if err != nil { + log.Printf("admin_users seed: %v", err) + } +} + +func seedAppUser(db *mongo.Database) { + ctx := context.Background() + n, _ := db.Collection("app_users").CountDocuments(ctx, bson.M{}) + if n > 0 { + return + } + phone := "13800138000" + userID := "user_seed001" + hash, err := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost) + if err != nil { + log.Fatal(err) + } + now := time.Now().UTC() + _, _ = db.Collection("app_users").InsertOne(ctx, bson.M{ + "id": userID, "nickname": "测试用户", "phone": phone, + "avatar": "", "created_at": now, "updated_at": now, + }) + _, _ = db.Collection("user_auths").InsertOne(ctx, bson.M{ + "user_id": userID, "auth_type": "phone", "auth_id": phone, + "credential": string(hash), "created_at": now, "updated_at": now, + }) + _, _ = db.Collection("user_balances").InsertOne(ctx, bson.M{ + "user_id": userID, "balance": 0.0, "created_at": now, "updated_at": now, + }) + fmt.Println("seedmongo: 已插入测试账号 13800138000 / 123456") +} + +func seedPlayhubSample(db *mongo.Database) { + ctx := context.Background() + n, _ := db.Collection("streamers").CountDocuments(ctx, bson.M{}) + if n > 0 { + return + } + now := time.Now().UTC().Format(time.RFC3339) + ext := "0123456789abcdef01234567" + _, _ = db.Collection("streamers").InsertOne(ctx, bson.M{ + "ext_id": ext, "name": "示例导师", "title": "王者荣耀", "game": "王者荣耀", + "status": "approved", "userId": "user_seed001", "fans": 1000, + "isLive": false, "createdAt": now, "updatedAt": now, + }) + fmt.Println("seedmongo: 已插入示例 streamer", ext) +} + +// seedPlayhubExtended 大屏、游戏、玩值 users 列表、公会等示例(各集合为空时才插入) +func seedPlayhubExtended(db *mongo.Database) { + ctx := context.Background() + now := time.Now().UTC().Format(time.RFC3339) + + if n, _ := db.Collection("screenConfig").CountDocuments(ctx, bson.M{}); n == 0 { + _, _ = db.Collection("screenConfig").InsertOne(ctx, bson.M{ + "ext_id": "default", + "title": "玩值电竞私域银行", + "subtitle": "电竞明星私域流量数字资产管理平台", + "updateInterval": 8000, + "theme": "dark", + "showQRCode": true, + "qrCodeUrl": "", + "yearOverYear": 15.2, + "monthOverMonth": 6.7, + "totalFans": 460000, + "championCount": 14, + "totalStars": 201, + "superIndividualCount": 1407, + "activeUserRate": 8.5, + "userValue": 413, + "createdAt": now, + "updatedAt": now, + }) + fmt.Println("seedmongo: 已插入 screenConfig default") + } + + if n, _ := db.Collection("games").CountDocuments(ctx, bson.M{}); n == 0 { + _, _ = db.Collection("games").InsertOne(ctx, bson.M{ + "ext_id": "abcdef0123456789abcdef01", + "name": "王者荣耀", + "shortName": "王者", + "category": "moba", + "status": "active", + "sortOrder": 1.0, + "createdAt": now, + "updatedAt": now, + }) + fmt.Println("seedmongo: 已插入示例 games") + } + + if n, _ := db.Collection("users").CountDocuments(ctx, bson.M{}); n == 0 { + _, _ = db.Collection("users").InsertOne(ctx, bson.M{ + "ext_id": "abcdef0123456789abcdef02", + "name": "玩值列表示例用户", + "phone": "13900139000", + "status": "active", + "isVip": false, + "balance": 100.0, + "createdAt": now, + "updatedAt": now, + }) + fmt.Println("seedmongo: 已插入玩值 users 列表示例") + } + + if n, _ := db.Collection("guilds").CountDocuments(ctx, bson.M{}); n == 0 { + _, _ = db.Collection("guilds").InsertOne(ctx, bson.M{ + "ext_id": "abcdef0123456789abcdef03", + "name": "示例公会", + "status": "active", + "createdAt": now, + "updatedAt": now, + }) + fmt.Println("seedmongo: 已插入示例 guilds") + } +} diff --git a/new/wz-api/cmd/server/main.go b/new/wz-api/cmd/server/main.go index 3415e3b..219976a 100644 --- a/new/wz-api/cmd/server/main.go +++ b/new/wz-api/cmd/server/main.go @@ -11,10 +11,10 @@ import ( "wz-api/internal/config" "wz-api/internal/database" - "wz-api/internal/handler" - "wz-api/internal/redis" "wz-api/internal/router" "wz-api/internal/wechat" + + "wz-api/internal/redis" ) func main() { @@ -24,16 +24,12 @@ func main() { } config.SetCurrent(cfg) log.Printf("CORS 允许源(%d 个): %v", len(cfg.CORSOrigins), cfg.CORSOrigins) - if err := database.Init(cfg.DBDSN); err != nil { - log.Fatal("database: ", err) - } - if cfg.MongoURI != "" { - if err := database.InitMongo(cfg.MongoURI); err != nil { - log.Printf("mongodb: 连接失败(已忽略,MySQL 仍可用): %v", err) - } else { - defer database.CloseMongo() - } + if err := database.InitMongo(cfg.MongoURI); err != nil { + log.Fatal("mongodb: ", err) } + defer database.CloseMongo() + _ = database.Init(cfg.DBDSN) + if err := wechat.Init(cfg); err != nil { log.Fatal("wechat: ", err) } @@ -54,15 +50,6 @@ func main() { Handler: r, } - // 预热 Redis 缓存,避免首请求冷启动 502 - go func() { - time.Sleep(2 * time.Second) // 等 DB 完全就绪 - handler.WarmAllChaptersCache() - handler.WarmBookPartsCache() - handler.WarmConfigCache() - handler.WarmLatestChaptersCache() - }() - go func() { log.Printf("wz-api listen on :%s (mode=%s)", cfg.Port, cfg.Mode) log.Printf(" -> 访问地址: http://localhost:%s (健康检查: http://localhost:%s/health)", cfg.Port, cfg.Port) @@ -74,28 +61,6 @@ func main() { } }() - // 内置订单对账定时任务(SYNC_ORDERS_INTERVAL_MINUTES > 0 时启动) - if cfg.SyncOrdersIntervalMinutes > 0 { - interval := time.Duration(cfg.SyncOrdersIntervalMinutes) * time.Minute - go func() { - // 启动后延迟 1 分钟执行第一次,避免与启动流程抢资源 - time.Sleep(1 * time.Minute) - ticker := time.NewTicker(interval) - defer ticker.Stop() - handler.SyncOrdersLogf("内置定时任务已启动,间隔 %d 分钟", cfg.SyncOrdersIntervalMinutes) - for { - ctx := context.Background() - synced, total, err := handler.RunSyncOrders(ctx, 7) - if err != nil { - handler.SyncOrdersLogf("对账失败: %v", err) - } else if total > 0 { - handler.SyncOrdersLogf("本轮检查 %d 笔,补齐 %d 笔漏单", total, synced) - } - <-ticker.C - } - }() - } - quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit diff --git a/new/wz-api/go.mod b/new/wz-api/go.mod index d0768e8..0b1e739 100644 --- a/new/wz-api/go.mod +++ b/new/wz-api/go.mod @@ -17,7 +17,6 @@ require ( golang.org/x/crypto v0.47.0 golang.org/x/time v0.8.0 gorm.io/datatypes v1.2.5 - gorm.io/driver/mysql v1.5.7 gorm.io/gorm v1.25.12 ) @@ -38,6 +37,7 @@ require ( golang.org/x/mod v0.31.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/tools v0.40.0 // indirect + gorm.io/driver/mysql v1.5.7 // indirect ) require ( diff --git a/new/wz-api/internal/config/config.go b/new/wz-api/internal/config/config.go index e8c819b..0d39642 100644 --- a/new/wz-api/internal/config/config.go +++ b/new/wz-api/internal/config/config.go @@ -15,7 +15,7 @@ import ( type Config struct { Port string Mode string - DBDSN string + DBDSN string // 已废弃(Mongo-only);保留字段避免旧配置引用编译错误 TrustedProxies []string CORSOrigins []string Version string // APP_VERSION,打包/部署前写在 .env,/health 返回 @@ -68,8 +68,8 @@ type Config struct { // Redis 连接地址(如 redis://localhost:6379/0),空表示不使用 Redis RedisURL string - // MongoDB(可选):与玩值旧栈 / 脚本迁移数据源对齐;当前业务读写仍以 MySQL(GORM) 为主 - MongoURI string // MONGODB_URI,非空则启动时连接并 Ping + // MongoDB:唯一业务持久化;别名 MONGO_URI + MongoURI string // MONGODB_URI 必填 } // BaseURLJoin 将路径拼接到 BaseURL,path 应以 / 开头 @@ -212,9 +212,6 @@ func Load() (*Config, error) { mode = "debug" } dsn := os.Getenv("DB_DSN") - if dsn == "" { - log.Fatal("DB_DSN 未配置,请在 .env 中配置 MySQL 连接串,例如:DB_DSN=user:pass@tcp(xxx.gz.cdb.myqcloud.com:3306)/dbname?charset=utf8mb4&parseTime=True") - } version := os.Getenv("APP_VERSION") if version == "" { @@ -323,11 +320,14 @@ func Load() (*Config, error) { // Redis:REDIS_URL 配置后启用;不配置则跳过。本地开发可设 REDIS_URL=redis://localhost:6379/0 redisURL := strings.TrimSpace(os.Getenv("REDIS_URL")) - // MongoDB:可选;别名 MONGO_URI + // MongoDB:必填 mongoURI := strings.TrimSpace(os.Getenv("MONGODB_URI")) if mongoURI == "" { mongoURI = strings.TrimSpace(os.Getenv("MONGO_URI")) } + if mongoURI == "" { + log.Fatal("MONGODB_URI 未配置,Mongo-only 构建需要 Mongo 连接串,例如:MONGODB_URI=mongodb://127.0.0.1:27017/wz") + } // H5 /read/:id 由 wz-api 本机提供,公网须与 API 同域或反代;默认与 API_BASE_URL 一致(避免前端站无 /read/ 路由导致 404) h5ReadPublic := strings.TrimSpace(os.Getenv("H5_READ_PUBLIC_BASE")) diff --git a/new/wz-api/internal/database/database.go b/new/wz-api/internal/database/database.go index 7dd9403..f69d0d8 100644 --- a/new/wz-api/internal/database/database.go +++ b/new/wz-api/internal/database/database.go @@ -1,334 +1,15 @@ package database -import ( - "log" - "os" - "strconv" - "strings" - "time" +import "gorm.io/gorm" - "wz-api/internal/model" +var nilDB *gorm.DB - "gorm.io/driver/mysql" - "gorm.io/gorm" - "gorm.io/gorm/logger" -) - -var db *gorm.DB - -// Init 使用 DSN 连接 MySQL,供 handler 通过 DB() 使用 +// Init MySQL 已移除;Mongo-only 构建下始终返回 nil,保留符号供遗留代码编译。 func Init(dsn string) error { - // 慢查询阈值:默认 5 秒,避免 GORM 默认 200ms 导致控制台刷屏;可通过 SLOW_SQL_THRESHOLD_MS 覆盖 - slowMs := 5000 - if s := os.Getenv("SLOW_SQL_THRESHOLD_MS"); s != "" { - if n, e := strconv.Atoi(s); e == nil && n > 0 { - slowMs = n - } - } - gormLogger := logger.New( - log.New(os.Stdout, "\r\n", log.LstdFlags), - logger.Config{ - SlowThreshold: time.Duration(slowMs) * time.Millisecond, - IgnoreRecordNotFoundError: true, - Colorful: true, - }, - ) - - var err error - db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: gormLogger}) - if err != nil { - return err - } - - // 连接池配置:防止腾讯云 CDB 等云数据库在空闲后主动断开长连接导致 "unexpected EOF" - sqlDB, err := db.DB() - if err != nil { - return err - } - sqlDB.SetMaxOpenConns(25) - sqlDB.SetMaxIdleConns(5) - sqlDB.SetConnMaxLifetime(5 * time.Minute) // 短于 MySQL wait_timeout,避免用到已关闭的连接 - sqlDB.SetConnMaxIdleTime(2 * time.Minute) // 空闲连接超过此时间自动关闭并重建 - - skipMigrate := strings.ToLower(strings.TrimSpace(os.Getenv("SKIP_AUTO_MIGRATE"))) - if skipMigrate == "1" || skipMigrate == "true" || skipMigrate == "yes" { - log.Println("database: SKIP_AUTO_MIGRATE enabled, skipping schema migration") - // 即使跳过 AutoMigrate,也补齐关键运行时字段,避免新功能因历史库缺列直接报错。 - ensurePersonSchema(db) - ensureCkbLeadSchema(db) - ensureLinkTagSchema(db) - // 开放平台 API Key / 请求日志表:全量 AutoMigrate 被跳过时仍需建表,否则管理端报 1146 - if err := db.AutoMigrate(&model.OpenPlatformApiKey{}, &model.OpenPlatformApiLog{}); err != nil { - log.Printf("database: open_platform tables migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.UserAuth{}); err != nil { - log.Printf("database: user_auths migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.WzDocument{}); err != nil { - log.Printf("database: wz_documents migrate warning: %v", err) - } - ensureOpenPlatformTablesRaw(db) - ensureUserDiscPdpColumns(db) - log.Println("database: connected") - return nil - } - - if err := db.AutoMigrate(&model.WechatCallbackLog{}); err != nil { - log.Printf("database: wechat_callback_logs migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.Withdrawal{}); err != nil { - log.Printf("database: withdrawals migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.MatchRecord{}); err != nil { - log.Printf("database: match_records migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.UserAddress{}); err != nil { - log.Printf("database: user_addresses migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.VipRole{}); err != nil { - log.Printf("database: vip_roles migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.Order{}); err != nil { - log.Printf("database: orders migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.UserBalance{}); err != nil { - log.Printf("database: user_balances migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.UserAuth{}); err != nil { - log.Printf("database: user_auths migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.BalanceTransaction{}); err != nil { - log.Printf("database: balance_transactions migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.Mentor{}); err != nil { - log.Printf("database: mentors migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.MentorConsultation{}); err != nil { - log.Printf("database: mentor_consultations migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.AuthorConfig{}); err != nil { - log.Printf("database: author_config migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.AdminUser{}); err != nil { - log.Printf("database: admin_users migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.CkbLeadRecord{}); err != nil { - log.Printf("database: ckb_lead_records migrate warning: %v", err) - } - ensureCkbLeadSchema(db) - if err := db.AutoMigrate(&model.Person{}); err != nil { - log.Printf("database: persons migrate warning: %v", err) - } - // persons 历史库可能因旧索引冲突导致 AutoMigrate 中断,补一层列级自愈,避免 /api/db/persons 报 Unknown column。 - ensurePersonSchema(db) - if err := db.AutoMigrate(&model.LinkTag{}); err != nil { - log.Printf("database: link_tags migrate warning: %v", err) - } - ensureLinkTagSchema(db) - // 以下表业务大量使用,必须参与 AutoMigrate,否则旧库缺字段会导致订单/用户/VIP 等接口报错 - if err := db.AutoMigrate(&model.User{}); err != nil { - log.Printf("database: users migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.SystemConfig{}); err != nil { - log.Printf("database: system_config migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.Chapter{}); err != nil { - log.Printf("database: chapters migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.Book{}); err != nil { - log.Printf("database: books migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.GiftPayRequest{}); err != nil { - log.Printf("database: gift_pay_requests migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.UserRule{}); err != nil { - log.Printf("database: user_rules migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.UserTrack{}); err != nil { - log.Printf("database: user_tracks migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.UserRuleCompletion{}); err != nil { - log.Printf("database: user_rule_completions migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.OpenPlatformApiKey{}); err != nil { - log.Printf("database: open_platform_api_keys migrate warning: %v", err) - } - if err := db.AutoMigrate(&model.OpenPlatformApiLog{}); err != nil { - log.Printf("database: open_platform_api_logs migrate warning: %v", err) - } - ensureOpenPlatformTablesRaw(db) - ensureUserDiscPdpColumns(db) - if err := db.AutoMigrate(&model.WzDocument{}); err != nil { - log.Printf("database: wz_documents migrate warning: %v", err) - } - log.Println("database: connected") return nil } -// ensureUserDiscPdpColumns users 表补充 disc/pdp(SKIP_AUTO_MIGRATE 时 User 未跑 AutoMigrate 时需手工列) -func ensureUserDiscPdpColumns(db *gorm.DB) { - m := db.Migrator() - if !m.HasColumn(&model.User{}, "disc") { - if err := db.Exec("ALTER TABLE users ADD COLUMN disc VARCHAR(64) NULL COMMENT 'DISC 测评'").Error; err != nil { - log.Printf("database: users add disc column warning: %v", err) - } - } - if !m.HasColumn(&model.User{}, "pdp") { - if err := db.Exec("ALTER TABLE users ADD COLUMN pdp VARCHAR(64) NULL COMMENT 'PDP 测评'").Error; err != nil { - log.Printf("database: users add pdp column warning: %v", err) - } - } -} - -// DB 返回全局 *gorm.DB,仅在 Init 成功后调用 +// DB 返回 nil;请勿在未迁移的路由中调用(已由 router 精简移除)。 func DB() *gorm.DB { - return db -} - -// ensureOpenPlatformTablesRaw AutoMigrate 失败或旧库无表时,用 CREATE IF NOT EXISTS 兜底(与 model 字段一致) -func ensureOpenPlatformTablesRaw(db *gorm.DB) { - m := db.Migrator() - if m.HasTable(&model.OpenPlatformApiKey{}) && m.HasTable(&model.OpenPlatformApiLog{}) { - return - } - sqlKeys := ` -CREATE TABLE IF NOT EXISTS open_platform_api_keys ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - name VARCHAR(120) NOT NULL DEFAULT '', - key_prefix VARCHAR(48) NOT NULL, - key_hash VARCHAR(64) NOT NULL, - revoked_at DATETIME(3) NULL, - created_at DATETIME(3) NULL, - updated_at DATETIME(3) NULL, - PRIMARY KEY (id), - KEY idx_open_platform_api_keys_revoked_at (revoked_at) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci` - sqlLogs := ` -CREATE TABLE IF NOT EXISTS open_platform_api_logs ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - api_key_id BIGINT UNSIGNED NOT NULL, - key_prefix VARCHAR(48) NOT NULL DEFAULT '', - method VARCHAR(16) NOT NULL DEFAULT '', - path VARCHAR(512) NOT NULL DEFAULT '', - status_code INT NOT NULL DEFAULT 0, - client_ip VARCHAR(64) NOT NULL DEFAULT '', - duration_ms INT NOT NULL DEFAULT 0, - request_body TEXT NULL, - response_body TEXT NULL, - created_at DATETIME(3) NULL, - PRIMARY KEY (id), - KEY idx_open_platform_api_logs_api_key_id (api_key_id), - KEY idx_open_platform_api_logs_created_at (created_at) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci` - if !m.HasTable(&model.OpenPlatformApiKey{}) { - if err := db.Exec(sqlKeys).Error; err != nil { - log.Printf("database: ensureOpenPlatformTablesRaw open_platform_api_keys warning: %v", err) - } - } - if !m.HasTable(&model.OpenPlatformApiLog{}) { - if err := db.Exec(sqlLogs).Error; err != nil { - log.Printf("database: ensureOpenPlatformTablesRaw open_platform_api_logs warning: %v", err) - } - } -} - -func ensurePersonSchema(db *gorm.DB) { - m := db.Migrator() - if !m.HasColumn(&model.Person{}, "is_pinned") { - if err := db.Exec("ALTER TABLE persons ADD COLUMN is_pinned TINYINT(1) NOT NULL DEFAULT 0 COMMENT '置顶到小程序首页'").Error; err != nil { - log.Printf("database: persons schema ensure warning: %v; action=add is_pinned", err) - } - } - if !m.HasColumn(&model.Person{}, "person_source") { - if err := db.Exec("ALTER TABLE persons ADD COLUMN person_source VARCHAR(32) NOT NULL DEFAULT '' COMMENT '来源:空=后台手工;vip_sync=超级个体同步'").Error; err != nil { - log.Printf("database: persons schema ensure warning: %v; action=add person_source", err) - } - } - if !m.HasIndex(&model.Person{}, "idx_persons_is_pinned") { - if err := db.Exec("CREATE INDEX idx_persons_is_pinned ON persons(is_pinned)").Error; err != nil { - log.Printf("database: persons schema ensure warning: %v; action=create idx_persons_is_pinned", err) - } - } - if !m.HasColumn(&model.Person{}, "home_entry_config") { - if err := db.Exec("ALTER TABLE persons ADD COLUMN home_entry_config TEXT NULL COMMENT '首页入口配置(打赏/上麦,JSON)'").Error; err != nil { - log.Printf("database: persons schema ensure warning: %v; action=add home_entry_config", err) - } - } -} - -func ensureCkbLeadSchema(db *gorm.DB) { - m := db.Migrator() - if !m.HasColumn(&model.CkbLeadRecord{}, "push_status") { - if err := db.Exec("ALTER TABLE ckb_lead_records ADD COLUMN push_status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '推送状态: pending/success/failed'").Error; err != nil { - log.Printf("database: ckb_lead_records schema ensure warning: %v; action=add push_status", err) - } - } - if !m.HasColumn(&model.CkbLeadRecord{}, "action") { - if err := db.Exec("ALTER TABLE ckb_lead_records ADD COLUMN action VARCHAR(20) NOT NULL DEFAULT 'lead' COMMENT '记录类型: lead/join/match'").Error; err != nil { - log.Printf("database: ckb_lead_records schema ensure warning: %v; action=add action", err) - } - } - if !m.HasColumn(&model.CkbLeadRecord{}, "plan_api_key") { - if err := db.Exec("ALTER TABLE ckb_lead_records ADD COLUMN plan_api_key VARCHAR(100) NOT NULL DEFAULT '' COMMENT '本次命中的获客计划 apiKey 快照'").Error; err != nil { - log.Printf("database: ckb_lead_records schema ensure warning: %v; action=add plan_api_key", err) - } - } - if !m.HasIndex(&model.CkbLeadRecord{}, "idx_ckb_lead_action") { - if err := db.Exec("CREATE INDEX idx_ckb_lead_action ON ckb_lead_records(action)").Error; err != nil { - log.Printf("database: ckb_lead_records schema ensure warning: %v; action=create idx_ckb_lead_action", err) - } - } - if !m.HasColumn(&model.CkbLeadRecord{}, "ckb_code") { - if err := db.Exec("ALTER TABLE ckb_lead_records ADD COLUMN ckb_code INT NOT NULL DEFAULT 0 COMMENT '存客宝响应 code(快照)'").Error; err != nil { - log.Printf("database: ckb_lead_records schema ensure warning: %v; action=add ckb_code", err) - } - } - if !m.HasColumn(&model.CkbLeadRecord{}, "ckb_message") { - if err := db.Exec("ALTER TABLE ckb_lead_records ADD COLUMN ckb_message VARCHAR(500) NOT NULL DEFAULT '' COMMENT '存客宝响应 message(快照)'").Error; err != nil { - log.Printf("database: ckb_lead_records schema ensure warning: %v; action=add ckb_message", err) - } - } - if !m.HasColumn(&model.CkbLeadRecord{}, "ckb_data") { - if err := db.Exec("ALTER TABLE ckb_lead_records ADD COLUMN ckb_data TEXT NULL COMMENT '存客宝响应 data(快照 JSON)'").Error; err != nil { - log.Printf("database: ckb_lead_records schema ensure warning: %v; action=add ckb_data", err) - } - } - if !m.HasColumn(&model.CkbLeadRecord{}, "retry_count") { - if err := db.Exec("ALTER TABLE ckb_lead_records ADD COLUMN retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数'").Error; err != nil { - log.Printf("database: ckb_lead_records schema ensure warning: %v; action=add retry_count", err) - } - } - if !m.HasColumn(&model.CkbLeadRecord{}, "last_push_at") { - if err := db.Exec("ALTER TABLE ckb_lead_records ADD COLUMN last_push_at DATETIME NULL COMMENT '最后推送时间'").Error; err != nil { - log.Printf("database: ckb_lead_records schema ensure warning: %v; action=add last_push_at", err) - } - } - if !m.HasColumn(&model.CkbLeadRecord{}, "next_retry_at") { - if err := db.Exec("ALTER TABLE ckb_lead_records ADD COLUMN next_retry_at DATETIME NULL COMMENT '下次重试时间'").Error; err != nil { - log.Printf("database: ckb_lead_records schema ensure warning: %v; action=add next_retry_at", err) - } - } - if !m.HasIndex(&model.CkbLeadRecord{}, "idx_ckb_lead_push_status") { - if err := db.Exec("CREATE INDEX idx_ckb_lead_push_status ON ckb_lead_records(push_status)").Error; err != nil { - log.Printf("database: ckb_lead_records schema ensure warning: %v; action=create idx_ckb_lead_push_status", err) - } - } - // 放宽 push_status 长度以容纳存客宝细粒度状态(pending_verify、expired 等) - if err := db.Exec("ALTER TABLE ckb_lead_records MODIFY COLUMN push_status VARCHAR(64) NOT NULL DEFAULT 'pending' COMMENT '推送状态'").Error; err != nil { - log.Printf("database: ckb_lead_records schema ensure warning: %v; action=widen push_status", err) - } -} - -func ensureLinkTagSchema(db *gorm.DB) { - m := db.Migrator() - if !m.HasColumn(&model.LinkTag{}, "pass_phone") { - if err := db.Exec("ALTER TABLE link_tags ADD COLUMN pass_phone TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否透传手机号'").Error; err != nil { - log.Printf("database: link_tags schema ensure warning: %v; action=add pass_phone", err) - } - } - if !m.HasColumn(&model.LinkTag{}, "phone_param_name") { - if err := db.Exec("ALTER TABLE link_tags ADD COLUMN phone_param_name VARCHAR(64) NOT NULL DEFAULT 'phone' COMMENT '手机号透传参数名'").Error; err != nil { - log.Printf("database: link_tags schema ensure warning: %v; action=add phone_param_name", err) - } - } + return nilDB } diff --git a/new/wz-api/internal/database/mongo.go b/new/wz-api/internal/database/mongo.go index 50e0262..e96d1c8 100644 --- a/new/wz-api/internal/database/mongo.go +++ b/new/wz-api/internal/database/mongo.go @@ -2,7 +2,10 @@ package database import ( "context" + "fmt" "log" + "net/url" + "strings" "time" "go.mongodb.org/mongo-driver/mongo" @@ -10,11 +13,20 @@ import ( ) var mongoClient *mongo.Client +var mongoDB *mongo.Database -// InitMongo 在非空 URI 时连接 MongoDB 并 Ping;失败返回 error。业务主库仍为 MySQL。 +func dbNameFromURI(uri string) string { + u, err := url.Parse(uri) + if err != nil || u.Path == "" || u.Path == "/" { + return "wz" + } + return strings.TrimPrefix(u.Path, "/") +} + +// InitMongo 连接 MongoDB 为唯一业务库;URI 为空返回 error。 func InitMongo(uri string) error { - if uri == "" { - return nil + if strings.TrimSpace(uri) == "" { + return fmt.Errorf("MONGODB_URI 未配置") } ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) defer cancel() @@ -30,15 +42,21 @@ func InitMongo(uri string) error { return err } mongoClient = c - log.Println("mongodb: 已连接并 Ping 成功") + mongoDB = c.Database(dbNameFromURI(uri)) + log.Println("mongodb: 已连接并 Ping 成功,数据库:", dbNameFromURI(uri)) return nil } -// MongoClient 返回已初始化的客户端;未配置或非空 URI 连接失败时为 nil。 +// MongoClient 返回已初始化的客户端。 func MongoClient() *mongo.Client { return mongoClient } +// MongoDatabase 返回当前业务库;须在 InitMongo 成功之后调用。 +func MongoDatabase() *mongo.Database { + return mongoDB +} + // CloseMongo 优雅断开 Mongo。 func CloseMongo() { if mongoClient == nil { @@ -50,4 +68,5 @@ func CloseMongo() { log.Printf("mongodb: disconnect: %v", err) } mongoClient = nil + mongoDB = nil } diff --git a/new/wz-api/internal/handler/admin.go b/new/wz-api/internal/handler/admin.go index bf796a1..5936135 100644 --- a/new/wz-api/internal/handler/admin.go +++ b/new/wz-api/internal/handler/admin.go @@ -1,16 +1,17 @@ package handler import ( + "errors" "net/http" "wz-api/internal/auth" "wz-api/internal/config" - "wz-api/internal/database" "wz-api/internal/model" + mongostore "wz-api/internal/store/mongo" "github.com/gin-gonic/gin" + mongodriver "go.mongodb.org/mongo-driver/mongo" "golang.org/x/crypto/bcrypt" - "gorm.io/gorm" ) // AdminCheck GET /api/admin 鉴权检查(JWT:Authorization Bearer 或 Cookie),已登录返回 success 或概览占位 @@ -43,7 +44,7 @@ func AdminCheck(c *gin.Context) { }) } -// AdminLogin POST /api/admin 登录(优先校验 admin_users 表,表空时回退 ADMIN_USERNAME/PASSWORD 并自动初始化) +// AdminLogin POST /api/admin 登录(admin_users;空库且 env 账号匹配时可创建首条 super_admin) func AdminLogin(c *gin.Context) { cfg := config.Get() if cfg == nil { @@ -60,12 +61,9 @@ func AdminLogin(c *gin.Context) { } username := trimSpace(body.Username) password := body.Password - db := database.DB() - // 1. 尝试从 admin_users 表校验 - var u model.AdminUser - err := db.Where("username = ?", username).First(&u).Error - if err == nil { + u, err := mongostore.FindAdminByUsername(username) + if err == nil && u != nil { if u.Status != "active" { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "账号已禁用"}) return @@ -87,8 +85,7 @@ func AdminLogin(c *gin.Context) { return } - // 2. 表内无匹配:若表为空且 env 账号正确,则创建初始 super_admin 并登录 - if err != gorm.ErrRecordNotFound { + if !errors.Is(err, mongodriver.ErrNoDocuments) && err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "系统错误"}) return } @@ -100,9 +97,8 @@ func AdminLogin(c *gin.Context) { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "用户名或密码错误"}) return } - // 表为空时初始化超级管理员 - var cnt int64 - if db.Model(&model.AdminUser{}).Count(&cnt).Error != nil || cnt > 0 { + cnt, err := mongostore.CountAdminUsers() + if err != nil || cnt > 0 { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "用户名或密码错误"}) return } @@ -111,14 +107,14 @@ func AdminLogin(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "初始化失败"}) return } - initial := model.AdminUser{ + initial := &model.AdminUser{ Username: cfg.AdminUsername, PasswordHash: string(hash), Role: "super_admin", Name: "卡若", Status: "active", } - if err := db.Create(&initial).Error; err != nil { + if err := mongostore.InsertAdmin(initial); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "初始化失败"}) return } diff --git a/new/wz-api/internal/handler/playhub_aggregate.go b/new/wz-api/internal/handler/playhub_aggregate.go index dea6197..6c1bc34 100644 --- a/new/wz-api/internal/handler/playhub_aggregate.go +++ b/new/wz-api/internal/handler/playhub_aggregate.go @@ -11,7 +11,6 @@ import ( "wz-api/internal/wzstore" "github.com/gin-gonic/gin" - "gorm.io/gorm" ) // playhubUnifiedOrder 与 old AdminOrder 对齐 @@ -27,11 +26,11 @@ type playhubUnifiedOrder struct { SourceTable string `json:"sourceTable"` } -func playhubCollectUnifiedOrders(db *gorm.DB) []playhubUnifiedOrder { +func playhubCollectUnifiedOrders() []playhubUnifiedOrder { now := time.Now().UTC().Format(time.RFC3339) var out []playhubUnifiedOrder addFrom := func(coll, typ, itemName string, mapFn func(map[string]interface{}, string) playhubUnifiedOrder) { - rows, err := wzstore.List(db, coll, 500) + rows, err := wzstore.List(coll, 500) if err != nil { return } @@ -197,12 +196,12 @@ func playhubTime(v interface{}, def string) string { return def } -func playhubDashboardPayload(db *gorm.DB) gin.H { - users, _ := wzstore.List(db, wzstore.CollUsers, 5000) - streamers, _ := wzstore.List(db, wzstore.CollStreamers, 2000) - pawns, _ := wzstore.List(db, wzstore.CollAccountPawns, 2000) - txs, _ := wzstore.List(db, wzstore.CollTransactions, 500) - orders := playhubCollectUnifiedOrders(db) +func playhubDashboardPayload() gin.H { + users, _ := wzstore.List(wzstore.CollUsers, 5000) + streamers, _ := wzstore.List(wzstore.CollStreamers, 2000) + pawns, _ := wzstore.List(wzstore.CollAccountPawns, 2000) + txs, _ := wzstore.List(wzstore.CollTransactions, 500) + orders := playhubCollectUnifiedOrders() var totalRevenue float64 for _, r := range txs { @@ -305,12 +304,12 @@ func playhubRecentOrdersSlice(orders []playhubUnifiedOrder) []gin.H { return out } -func playhubScreenDashboardStats(db *gorm.DB) gin.H { - u, _ := wzstore.List(db, wzstore.CollUsers, 5000) - s, _ := wzstore.List(db, wzstore.CollStreamers, 2000) - p, _ := wzstore.List(db, wzstore.CollAccountPawns, 2000) - tx, _ := wzstore.List(db, wzstore.CollTransactions, 500) - orders := playhubCollectUnifiedOrders(db) +func playhubScreenDashboardStats() gin.H { + u, _ := wzstore.List(wzstore.CollUsers, 5000) + s, _ := wzstore.List(wzstore.CollStreamers, 2000) + p, _ := wzstore.List(wzstore.CollAccountPawns, 2000) + tx, _ := wzstore.List(wzstore.CollTransactions, 500) + orders := playhubCollectUnifiedOrders() var totalRevenue float64 for _, r := range tx { diff --git a/new/wz-api/internal/handler/playhub_wz_admin.go b/new/wz-api/internal/handler/playhub_wz_admin.go index dd62dcd..c505355 100644 --- a/new/wz-api/internal/handler/playhub_wz_admin.go +++ b/new/wz-api/internal/handler/playhub_wz_admin.go @@ -7,7 +7,6 @@ import ( "strings" "time" - "wz-api/internal/database" "wz-api/internal/model" "wz-api/internal/wzstore" @@ -15,8 +14,7 @@ import ( ) func wzAdminJSONList(c *gin.Context, coll string, limit int) { - db := database.DB() - rows, err := wzstore.List(db, coll, limit) + rows, err := wzstore.List(coll, limit) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return @@ -33,8 +31,7 @@ func wzAdminJSONGet(c *gin.Context, coll, id string) { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 id"}) return } - db := database.DB() - d, err := wzstore.Get(db, coll, id) + d, err := wzstore.Get(coll, id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "未找到", "message": "未找到"}) return @@ -50,8 +47,7 @@ func WzAdminVenueByID(c *gin.Context) { wzAdminJSONGet(c, wzstore.CollEsportsVen // WzAdminUsers 玩值 C 端用户(wz_documents users) func WzAdminUsers(c *gin.Context) { - db := database.DB() - rows, err := wzstore.List(db, wzstore.CollUsers, 500) + rows, err := wzstore.List(wzstore.CollUsers, 500) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "data": []interface{}{}}) return @@ -84,7 +80,7 @@ func WzAdminUserPatch(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "参数错误"}) return } - if err := wzstore.UpsertByExtID(database.DB(), wzstore.CollUsers, id, body); err != nil { + if err := wzstore.UpsertByExtID(wzstore.CollUsers, id, body); err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "用户不存在"}) return } @@ -93,7 +89,7 @@ func WzAdminUserPatch(c *gin.Context) { // WzAdminUserDelete DELETE /api/admin/wz/users/:id func WzAdminUserDelete(c *gin.Context) { - if err := wzstore.Delete(database.DB(), wzstore.CollUsers, c.Param("id")); err != nil { + if err := wzstore.Delete(wzstore.CollUsers, c.Param("id")); err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "用户不存在"}) return } @@ -142,7 +138,7 @@ func WzAdminStreamers(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "参数错误"}) return } - id, err := wzstore.Insert(database.DB(), wzstore.CollStreamers, body) + id, err := wzstore.Insert(wzstore.CollStreamers, body) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "新增失败"}) return @@ -150,8 +146,7 @@ func WzAdminStreamers(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"id": id}}) return } - db := database.DB() - rows, err := wzstore.List(db, wzstore.CollStreamers, 500) + rows, err := wzstore.List(wzstore.CollStreamers, 500) if err != nil || len(rows) == 0 { c.JSON(http.StatusOK, gin.H{"success": true, "data": playhubLytiaoSeed()}) return @@ -162,17 +157,14 @@ func WzAdminStreamers(c *gin.Context) { guildName := interface{}(nil) gid := playhubStr(m["guildId"]) if gid != "" { - if g, e := wzstore.Get(db, wzstore.CollGuilds, gid); e == nil { + if g, e := wzstore.Get(wzstore.CollGuilds, gid); e == nil { gm := playhubDocMap(g) guildName = playhubStr(gm["name"]) } } m["id"] = r.ExtID m["guildName"] = guildName - var bindCnt int64 - db.Model(&model.WzDocument{}).Where("coll = ?", wzstore.CollStreamerBindings). - Where("JSON_UNQUOTE(JSON_EXTRACT(body,'$.streamerId')) = ?", r.ExtID). - Count(&bindCnt) + bindCnt, _ := wzstore.CountBindingsForStreamer(r.ExtID) m["bindingCount"] = bindCnt out = append(out, m) } @@ -193,8 +185,7 @@ func playhubLytiaoSeed() []gin.H { // WzAdminStreamerGet GET func WzAdminStreamerGet(c *gin.Context) { id := c.Param("id") - db := database.DB() - d, err := wzstore.Get(db, wzstore.CollStreamers, id) + d, err := wzstore.Get(wzstore.CollStreamers, id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "主播不存在"}) return @@ -203,16 +194,13 @@ func WzAdminStreamerGet(c *gin.Context) { guildName := interface{}(nil) gid := playhubStr(m["guildId"]) if gid != "" { - if g, e := wzstore.Get(db, wzstore.CollGuilds, gid); e == nil { + if g, e := wzstore.Get(wzstore.CollGuilds, gid); e == nil { guildName = playhubStr(playhubDocMap(g)["name"]) } } m["id"] = d.ExtID m["guildName"] = guildName - var cnt int64 - db.Model(&model.WzDocument{}).Where("coll = ?", wzstore.CollStreamerBindings). - Where("JSON_UNQUOTE(JSON_EXTRACT(body,'$.streamerId')) = ?", id). - Count(&cnt) + cnt, _ := wzstore.CountBindingsForStreamer(id) m["bindingCount"] = cnt m["bindingActiveCount"] = cnt m["bindingExpiredCount"] = 0 @@ -227,7 +215,7 @@ func WzAdminStreamerPatch(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "参数错误"}) return } - if err := wzstore.UpsertByExtID(database.DB(), wzstore.CollStreamers, id, body); err != nil { + if err := wzstore.UpsertByExtID(wzstore.CollStreamers, id, body); err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "主播不存在"}) return } @@ -236,7 +224,7 @@ func WzAdminStreamerPatch(c *gin.Context) { // WzAdminStreamerDelete DELETE func WzAdminStreamerDelete(c *gin.Context) { - if err := wzstore.Delete(database.DB(), wzstore.CollStreamers, c.Param("id")); err != nil { + if err := wzstore.Delete(wzstore.CollStreamers, c.Param("id")); err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "主播不存在"}) return } @@ -246,11 +234,11 @@ func WzAdminStreamerDelete(c *gin.Context) { // WzAdminStreamerBindings GET /api/admin/wz/streamers/:id/bindings func WzAdminStreamerBindings(c *gin.Context) { sid := c.Param("id") - db := database.DB() - var rows []model.WzDocument - _ = db.Where("coll = ?", wzstore.CollStreamerBindings). - Where("JSON_UNQUOTE(JSON_EXTRACT(body,'$.streamerId')) = ?", sid). - Find(&rows).Error + rows, err := wzstore.ListBindingsForStreamer(sid) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}}) + return + } out := make([]interface{}, 0, len(rows)) for i := range rows { out = append(out, wzstore.ToResponse(&rows[i])) @@ -265,12 +253,11 @@ func WzAdminSeedScreen(c *gin.Context) { // WzAdminDashboard GET func WzAdminDashboard(c *gin.Context) { - db := database.DB() - if _, err := wzstore.List(db, wzstore.CollUsers, 1); err != nil { + if _, err := wzstore.List(wzstore.CollUsers, 1); err != nil { c.JSON(http.StatusOK, gin.H{"success": true, "data": playhubDashboardMock()}) return } - c.JSON(http.StatusOK, gin.H{"success": true, "data": playhubDashboardPayload(db)}) + c.JSON(http.StatusOK, gin.H{"success": true, "data": playhubDashboardPayload()}) } func playhubDashboardMock() gin.H { @@ -286,10 +273,9 @@ func playhubDashboardMock() gin.H { // WzAdminFinance GET func WzAdminFinance(c *gin.Context) { - db := database.DB() - tx, _ := wzstore.List(db, wzstore.CollTransactions, 300) - rc, _ := wzstore.List(db, wzstore.CollRechargeOrders, 200) - wd, _ := wzstore.List(db, wzstore.CollWithdrawOrders, 200) + tx, _ := wzstore.List(wzstore.CollTransactions, 300) + rc, _ := wzstore.List(wzstore.CollRechargeOrders, 200) + wd, _ := wzstore.List(wzstore.CollWithdrawOrders, 200) mapRows := func(rows []model.WzDocument) []interface{} { o := make([]interface{}, 0, len(rows)) for i := range rows { @@ -309,7 +295,7 @@ func WzAdminFinance(c *gin.Context) { // WzAdminOrders GET unified func WzAdminOrders(c *gin.Context) { - orders := playhubCollectUnifiedOrders(database.DB()) + orders := playhubCollectUnifiedOrders() c.JSON(http.StatusOK, gin.H{"success": true, "data": orders}) } @@ -321,7 +307,7 @@ func WzAdminOrderDetail(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 id 或 sourceTable"}) return } - d, err := wzstore.Get(database.DB(), tab, id) + d, err := wzstore.Get(tab, id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "订单不存在"}) return @@ -342,8 +328,7 @@ func WzAdminOrderStatus(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "缺少 id / sourceTable / status"}) return } - db := database.DB() - if err := wzstore.UpdateJSONPartial(db, body.SourceTable, body.ID, map[string]interface{}{"status": body.Status}); err != nil { + if err := wzstore.UpdateJSONPartial(body.SourceTable, body.ID, map[string]interface{}{"status": body.Status}); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": "更新失败"}) return } @@ -352,8 +337,6 @@ func WzAdminOrderStatus(c *gin.Context) { // WzAdminUserValueSummary GET func WzAdminUserValueSummary(c *gin.Context) { - db := database.DB() - _ = db c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"message": "占位汇总:接入业务指标后可替换", "stats": gin.H{}}}) } @@ -382,7 +365,7 @@ func WzAdminMallImport(c *gin.Context) { "category": "peripheral", "price": 0, "status": "draft", "source": src, "createdAt": now, "updatedAt": now, } - id, _ := wzstore.Insert(database.DB(), wzstore.CollProducts, doc) + id, _ := wzstore.Insert(wzstore.CollProducts, doc) c.JSON(http.StatusOK, gin.H{"success": true, "message": "已创建占位商品","inserted": 1, "id": id}) } @@ -393,7 +376,7 @@ func WzAdminSyncFulu(c *gin.Context) { c.JSON(http.StatusServiceUnavailable, gin.H{"success": false, "message": "请配置 FULU_APP_KEY 与 FULU_APP_SECRET", "code": "missing_config"}) return } - n, _ := wzstore.Count(database.DB(), wzstore.CollGamePointCards) + n, _ := wzstore.Count(wzstore.CollGamePointCards) c.JSON(http.StatusOK, gin.H{"success": true, "message": "福禄同步接口已就绪", "currentCardsCount": n}) } diff --git a/new/wz-api/internal/handler/playhub_wz_analytics.go b/new/wz-api/internal/handler/playhub_wz_analytics.go index f136216..6675d81 100644 --- a/new/wz-api/internal/handler/playhub_wz_analytics.go +++ b/new/wz-api/internal/handler/playhub_wz_analytics.go @@ -6,8 +6,6 @@ import ( "wz-api/internal/auth" "wz-api/internal/config" - "wz-api/internal/database" - "wz-api/internal/model" "wz-api/internal/wzstore" "github.com/gin-gonic/gin" @@ -42,7 +40,6 @@ func WzAppAnalyticsJourney(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "未登录时需传 anonymousId(H5 匿名会话)"}) return } - db := database.DB() now := time.Now().UTC() inserted := 0 deduped := 0 @@ -60,11 +57,7 @@ func WzAppAnalyticsJourney(c *gin.Context) { invalid++ continue } - var cnt int64 - db.Model(&model.WzDocument{}).Where("coll = ?", wzstore.CollUserJourneyEvents). - Where("JSON_UNQUOTE(JSON_EXTRACT(body,'$.sessionId')) = ?", sessionID). - Where("JSON_UNQUOTE(JSON_EXTRACT(body,'$.clientEventId')) = ?", clientID). - Count(&cnt) + cnt, _ := wzstore.CountJourneyDedup(sessionID, clientID) if cnt > 0 { deduped++ continue @@ -92,7 +85,7 @@ func WzAppAnalyticsJourney(c *gin.Context) { if anon != "" { doc["anonymousId"] = anon } - if _, err := wzstore.Insert(db, wzstore.CollUserJourneyEvents, doc); err != nil { + if _, err := wzstore.Insert(wzstore.CollUserJourneyEvents, doc); err != nil { deduped++ continue } diff --git a/new/wz-api/internal/handler/playhub_wz_app.go b/new/wz-api/internal/handler/playhub_wz_app.go index 21e04da..7e58e86 100644 --- a/new/wz-api/internal/handler/playhub_wz_app.go +++ b/new/wz-api/internal/handler/playhub_wz_app.go @@ -4,7 +4,6 @@ import ( "net/http" "sort" - "wz-api/internal/database" "wz-api/internal/wzstore" "github.com/gin-gonic/gin" @@ -12,8 +11,7 @@ import ( // WzAppStreamersList GET /api/app/streamers func WzAppStreamersList(c *gin.Context) { - db := database.DB() - rows, err := wzstore.List(db, wzstore.CollStreamers, 200) + rows, err := wzstore.List(wzstore.CollStreamers, 200) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": "服务暂不可用"}) return @@ -60,8 +58,7 @@ func WzAppStreamerByID(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效 id"}) return } - db := database.DB() - d, err := wzstore.Get(db, wzstore.CollStreamers, id) + d, err := wzstore.Get(wzstore.CollStreamers, id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "未找到"}) return @@ -71,8 +68,7 @@ func WzAppStreamerByID(c *gin.Context) { // WzAppGamesList GET /api/app/games func WzAppGamesList(c *gin.Context) { - db := database.DB() - rows, err := wzstore.List(db, wzstore.CollGames, 50) + rows, err := wzstore.List(wzstore.CollGames, 50) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": "数据库未连接或未初始化"}) return diff --git a/new/wz-api/internal/handler/playhub_wz_app_mall.go b/new/wz-api/internal/handler/playhub_wz_app_mall.go index 2d69e77..127f4c7 100644 --- a/new/wz-api/internal/handler/playhub_wz_app_mall.go +++ b/new/wz-api/internal/handler/playhub_wz_app_mall.go @@ -6,7 +6,6 @@ import ( "strings" "time" - "wz-api/internal/database" "wz-api/internal/wzstore" "github.com/gin-gonic/gin" @@ -14,8 +13,7 @@ import ( // WzAppMallPointCardsList GET /api/app/mall/point-cards — C 端可售点卡目录(不暴露内部全量 body) func WzAppMallPointCardsList(c *gin.Context) { - db := database.DB() - rows, err := wzstore.List(db, wzstore.CollGamePointCards, 500) + rows, err := wzstore.List(wzstore.CollGamePointCards, 500) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": "服务暂不可用"}) return @@ -70,8 +68,7 @@ func WzAppMallPointCardByID(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效商品 id"}) return } - db := database.DB() - d, err := wzstore.Get(db, wzstore.CollGamePointCards, cardID) + d, err := wzstore.Get(wzstore.CollGamePointCards, cardID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "商品不存在"}) return @@ -115,8 +112,7 @@ func WzAppOrderPointCardPOST(c *gin.Context) { if qty > 99 { qty = 99 } - db := database.DB() - d, err := wzstore.Get(db, wzstore.CollGamePointCards, cardID) + d, err := wzstore.Get(wzstore.CollGamePointCards, cardID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "商品不存在"}) return @@ -155,7 +151,7 @@ func WzAppOrderPointCardPOST(c *gin.Context) { "skuName": playhubStr(m["name"]), "createdAt": time.Now().UTC().Format(time.RFC3339), } - extID, err := wzstore.Insert(db, wzstore.CollPointCardOrders, doc) + extID, err := wzstore.Insert(wzstore.CollPointCardOrders, doc) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": "创建订单失败"}) return diff --git a/new/wz-api/internal/handler/playhub_wz_live.go b/new/wz-api/internal/handler/playhub_wz_live.go index 7b888c7..3a3cb5a 100644 --- a/new/wz-api/internal/handler/playhub_wz_live.go +++ b/new/wz-api/internal/handler/playhub_wz_live.go @@ -7,8 +7,8 @@ import ( "strings" "time" - "wz-api/internal/database" "wz-api/internal/model" + mongostore "wz-api/internal/store/mongo" "wz-api/internal/wzstore" "github.com/gin-gonic/gin" @@ -29,14 +29,13 @@ func WzAppLiveContext(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效主播 id"}) return } - db := database.DB() - st, err := wzstore.Get(db, wzstore.CollStreamers, sid) + st, err := wzstore.Get(wzstore.CollStreamers, sid) if err != nil || playhubStr(playhubDocMap(st)["status"]) != "approved" { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "主播不存在或未审核"}) return } sm := playhubDocMap(st) - room, _ := wzstore.FindLiveRoomByStreamerID(db, sid) + room, _ := wzstore.FindLiveRoomByStreamerID(sid) var roomData interface{} = nil if room != nil { rm := playhubDocMap(room) @@ -51,12 +50,12 @@ func WzAppLiveContext(c *gin.Context) { guildID := playhubStr(sm["guildId"]) var guildData interface{} = nil if guildID != "" && playhubIsHexID(guildID) { - if g, e := wzstore.Get(db, wzstore.CollGuilds, guildID); e == nil { + if g, e := wzstore.Get(wzstore.CollGuilds, guildID); e == nil { gm := playhubDocMap(g) guildData = gin.H{"id": g.ExtID, "name": playhubStr(gm["name"])} } } - showRows, _ := wzstore.List(db, wzstore.CollLiveShowcaseItems, 50) + showRows, _ := wzstore.List(wzstore.CollLiveShowcaseItems, 50) var showcase []interface{} for _, r := range showRows { m := playhubDocMap(&r) @@ -82,13 +81,12 @@ func WzAppLiveCommentsGET(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效主播 id"}) return } - db := database.DB() - st, err := wzstore.Get(db, wzstore.CollStreamers, sid) + st, err := wzstore.Get(wzstore.CollStreamers, sid) if err != nil || playhubStr(playhubDocMap(st)["status"]) != "approved" { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "主播不存在或未审核"}) return } - rows, _ := wzstore.List(db, wzstore.CollLiveComments, 400) + rows, _ := wzstore.List(wzstore.CollLiveComments, 400) var forSid []model.WzDocument for i := range rows { m := playhubDocMap(&rows[i]) @@ -139,8 +137,7 @@ func WzAppLiveCommentsPOST(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效主播 id"}) return } - db := database.DB() - st, err := wzstore.Get(db, wzstore.CollStreamers, sid) + st, err := wzstore.Get(wzstore.CollStreamers, sid) if err != nil || playhubStr(playhubDocMap(st)["status"]) != "approved" { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "主播不存在或未审核"}) return @@ -169,7 +166,7 @@ func WzAppLiveCommentsPOST(c *gin.Context) { return } userName := playhubResolveUserName(uid) - room, _ := wzstore.FindLiveRoomByStreamerID(db, sid) + room, _ := wzstore.FindLiveRoomByStreamerID(sid) liveRoomID := "" if room != nil { liveRoomID = room.ExtID @@ -183,7 +180,7 @@ func WzAppLiveCommentsPOST(c *gin.Context) { "type": typ, "createdAt": time.Now().UTC().Format(time.RFC3339), } - eid, err := wzstore.Insert(db, wzstore.CollLiveComments, doc) + eid, err := wzstore.Insert(wzstore.CollLiveComments, doc) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": "发送失败"}) return @@ -192,16 +189,7 @@ func WzAppLiveCommentsPOST(c *gin.Context) { } func playhubResolveUserName(uid string) string { - var u model.User - if err := database.DB().Where("id = ?", uid).First(&u).Error; err == nil { - if u.Nickname != nil && strings.TrimSpace(*u.Nickname) != "" { - return strings.TrimSpace(*u.Nickname) - } - if u.Phone != nil && strings.TrimSpace(*u.Phone) != "" { - return strings.TrimSpace(*u.Phone) - } - } - return "用户" + return mongostore.AppUserDisplayName(uid) } // WzAppLiveGift POST /api/app/live/gift — 简化:记录礼物并更新主播礼物计数(完整分账后续可扩展) @@ -226,7 +214,6 @@ func WzAppLiveGift(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效主播"}) return } - db := database.DB() orderNo := fmt.Sprintf("GIFT%d%s", time.Now().UnixNano(), sid[:6]) price := body.Amount if price <= 0 { @@ -240,16 +227,16 @@ func WzAppLiveGift(c *gin.Context) { "orderNo": orderNo, "createdAt": time.Now().UTC().Format(time.RFC3339), } - if _, err := wzstore.Insert(db, wzstore.CollLiveGifts, gdoc); err != nil { + if _, err := wzstore.Insert(wzstore.CollLiveGifts, gdoc); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": "送礼失败"}) return } - st, err := wzstore.Get(db, wzstore.CollStreamers, sid) + st, err := wzstore.Get(wzstore.CollStreamers, sid) if err == nil { m := playhubDocMap(st) tg := playhubFloat(m["totalGifts"]) + price ti := playhubFloat(m["totalIncome"]) + price*0.5 - _ = wzstore.UpdateJSONPartial(db, wzstore.CollStreamers, sid, map[string]interface{}{"totalGifts": tg, "totalIncome": ti}) + _ = wzstore.UpdateJSONPartial(wzstore.CollStreamers, sid, map[string]interface{}{"totalGifts": tg, "totalIncome": ti}) } c.JSON(http.StatusOK, gin.H{ "success": true, @@ -280,8 +267,7 @@ func WzAppStreamerLivePOST(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "action 须为 start 或 stop"}) return } - db := database.DB() - st, err := wzstore.FindApprovedStreamerForUser(db, uid) + st, err := wzstore.FindApprovedStreamerForUser(uid) if err != nil { c.JSON(http.StatusForbidden, gin.H{"success": false, "message": "未找到已审核的主播档案"}) return @@ -302,7 +288,7 @@ func WzAppStreamerLivePOST(c *gin.Context) { pull = "" } } - _ = wzstore.UpdateJSONPartial(db, wzstore.CollStreamers, st.ExtID, map[string]interface{}{ + _ = wzstore.UpdateJSONPartial(wzstore.CollStreamers, st.ExtID, map[string]interface{}{ "isLive": true, "title": title, "updatedAt": now, }) roomBody := map[string]interface{}{ @@ -317,17 +303,17 @@ func WzAppStreamerLivePOST(c *gin.Context) { "createdAt": now, } // upsert room: try find by streamer - if r, e := wzstore.FindLiveRoomByStreamerID(db, st.ExtID); e == nil { - _ = wzstore.UpsertByExtID(db, wzstore.CollLiveRooms, r.ExtID, roomBody) + if r, e := wzstore.FindLiveRoomByStreamerID(st.ExtID); e == nil { + _ = wzstore.UpsertByExtID(wzstore.CollLiveRooms, r.ExtID, roomBody) } else { - _, _ = wzstore.Insert(db, wzstore.CollLiveRooms, roomBody) + _, _ = wzstore.Insert(wzstore.CollLiveRooms, roomBody) } c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"action": "start"}}) return } - _ = wzstore.UpdateJSONPartial(db, wzstore.CollStreamers, st.ExtID, map[string]interface{}{"isLive": false, "updatedAt": now}) - if r, e := wzstore.FindLiveRoomByStreamerID(db, st.ExtID); e == nil { - _ = wzstore.UpdateJSONPartial(db, wzstore.CollLiveRooms, r.ExtID, map[string]interface{}{"status": "offline", "updatedAt": now}) + _ = wzstore.UpdateJSONPartial(wzstore.CollStreamers, st.ExtID, map[string]interface{}{"isLive": false, "updatedAt": now}) + if r, e := wzstore.FindLiveRoomByStreamerID(st.ExtID); e == nil { + _ = wzstore.UpdateJSONPartial(wzstore.CollLiveRooms, r.ExtID, map[string]interface{}{"status": "offline", "updatedAt": now}) } c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"action": "stop"}}) } @@ -344,8 +330,7 @@ func WzAppStreamerLiveCloudGET(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "vendor 须为 tencent|aliyun|qiniu|volcengine|generic"}) return } - db := database.DB() - st, err := wzstore.FindApprovedStreamerForUser(db, uid) + st, err := wzstore.FindApprovedStreamerForUser(uid) if err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "未找到已审核的主播档案"}) return @@ -370,8 +355,7 @@ func WzAppAudienceInsights(c *gin.Context) { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"}) return } - db := database.DB() - me, err := wzstore.FindApprovedStreamerForUser(db, uid) + me, err := wzstore.FindApprovedStreamerForUser(uid) if err != nil { c.JSON(http.StatusForbidden, gin.H{"success": false, "message": "非主播或未审核"}) return @@ -383,7 +367,7 @@ func WzAppAudienceInsights(c *gin.Context) { return } streamerID := mySid - evs, _ := wzstore.List(db, wzstore.CollUserJourneyEvents, 4000) + evs, _ := wzstore.List(wzstore.CollUserJourneyEvents, 4000) since := time.Now().Add(-7 * 24 * time.Hour) var rows []gin.H for _, r := range evs { diff --git a/new/wz-api/internal/handler/playhub_wz_screen.go b/new/wz-api/internal/handler/playhub_wz_screen.go index 6756d7c..0bca5fa 100644 --- a/new/wz-api/internal/handler/playhub_wz_screen.go +++ b/new/wz-api/internal/handler/playhub_wz_screen.go @@ -3,7 +3,6 @@ package handler import ( "net/http" - "wz-api/internal/database" "wz-api/internal/wzstore" "github.com/gin-gonic/gin" @@ -13,8 +12,7 @@ const screenConfigExtID = "default" // WzScreenConfigGET GET /api/screen/config — 与 old 一致:根级返回(无 success 包裹) func WzScreenConfigGET(c *gin.Context) { - db := database.DB() - d, err := wzstore.Get(db, wzstore.CollScreenConfig, screenConfigExtID) + d, err := wzstore.Get(wzstore.CollScreenConfig, screenConfigExtID) defaultCfg := defaultScreenConfigMap() if err != nil { c.JSON(http.StatusOK, defaultCfg) @@ -43,8 +41,7 @@ func WzScreenConfigPOST(c *gin.Context) { } delete(body, "_id") body["id"] = screenConfigExtID - db := database.DB() - _ = wzstore.UpsertByExtID(db, wzstore.CollScreenConfig, screenConfigExtID, body) + _ = wzstore.UpsertByExtID(wzstore.CollScreenConfig, screenConfigExtID, body) c.JSON(http.StatusOK, gin.H{"ok": true}) } @@ -70,14 +67,12 @@ func defaultScreenConfigMap() map[string]interface{} { // WzScreenDataDashboard GET /api/screen/data/dashboard func WzScreenDataDashboard(c *gin.Context) { - db := database.DB() - c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"stats": playhubScreenDashboardStats(db)}}) + c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"stats": playhubScreenDashboardStats()}}) } // WzScreenStreamersGET GET /api/screen/streamers func WzScreenStreamersGET(c *gin.Context) { - db := database.DB() - rows, err := wzstore.List(db, wzstore.CollStreamers, 500) + rows, err := wzstore.List(wzstore.CollStreamers, 500) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "data": []interface{}{}}) return diff --git a/new/wz-api/internal/handler/playhub_wz_streamer.go b/new/wz-api/internal/handler/playhub_wz_streamer.go index a954ae7..1fbcdbe 100644 --- a/new/wz-api/internal/handler/playhub_wz_streamer.go +++ b/new/wz-api/internal/handler/playhub_wz_streamer.go @@ -7,12 +7,10 @@ import ( "strings" "time" - "wz-api/internal/database" "wz-api/internal/model" "wz-api/internal/wzstore" "github.com/gin-gonic/gin" - "gorm.io/gorm" ) // WzAppStreamerMe GET /api/app/streamer/me @@ -22,11 +20,10 @@ func WzAppStreamerMe(c *gin.Context) { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"}) return } - db := database.DB() - st, err := wzstore.FindApprovedStreamerForUser(db, uid) + st, err := wzstore.FindApprovedStreamerForUser(uid) if err != nil && errors.Is(err, wzstore.ErrNotFound) && (os.Getenv("PLAYHUB_STREAMER_AUTOPROVISION") == "1" || os.Getenv("STREAMER_DEV_AUTOPROVISION") == "1") { - st = playhubTryProvisionStreamer(db, uid) + st = playhubTryProvisionStreamer(uid) err = nil } if st == nil { @@ -34,7 +31,7 @@ func WzAppStreamerMe(c *gin.Context) { return } sm, _ := wzstore.BodyMap(st) - room, _ := wzstore.FindLiveRoomByStreamerID(db, st.ExtID) + room, _ := wzstore.FindLiveRoomByStreamerID(st.ExtID) var roomOut interface{} if room != nil { rm, _ := wzstore.BodyMap(room) @@ -65,32 +62,32 @@ func playhubBool(v interface{}) bool { return b } -func playhubTryProvisionStreamer(db *gorm.DB, uid string) *model.WzDocument { +func playhubTryProvisionStreamer(uid string) *model.WzDocument { now := time.Now().UTC().Format(time.RFC3339) body := map[string]interface{}{ - "userId": uid, - "name": "主播", - "title": "开发环境 · 自动过审主播", - "game": "英雄联盟", - "gameId": 2, - "avatar": "", - "coverImage": "/lol-stream.jpg", - "tags": []string{"dev", "auto-approved"}, - "level": 1, - "fans": 100, - "hotValue": 1, - "isLive": false, - "totalGifts": 0, - "totalIncome": 0, - "commissionRate": 0.7, - "status": "approved", - "verifiedAt": now, - "createdAt": now, - "updatedAt": now, + "userId": uid, + "name": "主播", + "title": "开发环境 · 自动过审主播", + "game": "英雄联盟", + "gameId": 2, + "avatar": "", + "coverImage": "/lol-stream.jpg", + "tags": []string{"dev", "auto-approved"}, + "level": 1, + "fans": 100, + "hotValue": 1, + "isLive": false, + "totalGifts": 0, + "totalIncome": 0, + "commissionRate": 0.7, + "status": "approved", + "verifiedAt": now, + "createdAt": now, + "updatedAt": now, "devAutoProvision": true, } - _, _ = wzstore.Insert(db, wzstore.CollStreamers, body) - st, err := wzstore.FindApprovedStreamerForUser(db, uid) + _, _ = wzstore.Insert(wzstore.CollStreamers, body) + st, err := wzstore.FindApprovedStreamerForUser(uid) if err != nil { return nil } @@ -123,18 +120,15 @@ func WzAppStreamerBind(c *gin.Context) { if sid == "" && len(ref) == 24 && playhubIsHexID(ref) { sid = ref } - db := database.DB() if sid == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的主播标识 ref"}) return } - _, err := wzstore.Get(db, wzstore.CollStreamers, sid) - if err != nil { + if _, err := wzstore.Get(wzstore.CollStreamers, sid); err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "主播不存在"}) return } days := 30 - // STREAMER_BINDING_DAYS optional now := time.Now().UTC() expires := now.Add(time.Duration(days) * 24 * time.Hour).Format(time.RFC3339) doc := map[string]interface{}{ @@ -146,15 +140,11 @@ func WzAppStreamerBind(c *gin.Context) { "createdAt": now.Format(time.RFC3339), "updatedAt": now.Format(time.RFC3339), } - var cnt int64 - db.Model(&model.WzDocument{}).Where("coll = ?", wzstore.CollStreamerBindings). - Where("JSON_UNQUOTE(JSON_EXTRACT(body,'$.userId')) = ?", userID). - Where("JSON_UNQUOTE(JSON_EXTRACT(body,'$.status')) = ?", "active"). - Count(&cnt) + cnt, _ := wzstore.CountActiveBindingsForUser(userID) if cnt > 0 { c.JSON(http.StatusOK, gin.H{"success": true, "message": "已有有效绑定,未变更"}) return } - _, _ = wzstore.Insert(db, wzstore.CollStreamerBindings, doc) + _, _ = wzstore.Insert(wzstore.CollStreamerBindings, doc) c.JSON(http.StatusOK, gin.H{"success": true, "message": "绑定成功"}) } diff --git a/new/wz-api/internal/handler/wanzhi_app.go b/new/wz-api/internal/handler/wanzhi_app.go index 5962f54..66fa116 100644 --- a/new/wz-api/internal/handler/wanzhi_app.go +++ b/new/wz-api/internal/handler/wanzhi_app.go @@ -1,17 +1,18 @@ package handler import ( + "errors" "net/http" "strings" "wz-api/internal/auth" "wz-api/internal/config" - "wz-api/internal/database" "wz-api/internal/model" + mongostore "wz-api/internal/store/mongo" "github.com/gin-gonic/gin" + mongodriver "go.mongodb.org/mongo-driver/mongo" "golang.org/x/crypto/bcrypt" - "gorm.io/gorm" ) func userDisplayName(u *model.User) string { @@ -63,10 +64,9 @@ func WanzhiAppLogin(c *gin.Context) { return } - db := database.DB() - var ua model.UserAuth - if err := db.Where("auth_type = ? AND auth_id = ?", "phone", phone).First(&ua).Error; err != nil { - if err == gorm.ErrRecordNotFound { + ua, err := mongostore.FindUserAuthByPhone(phone) + if err != nil { + if errors.Is(err, mongodriver.ErrNoDocuments) { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "手机号未注册,请先注册"}) return } @@ -78,8 +78,8 @@ func WanzhiAppLogin(c *gin.Context) { return } - var user model.User - if err := db.Where("id = ?", ua.UserID).First(&user).Error; err != nil { + user, err := mongostore.FindAppUserByID(ua.UserID) + if err != nil || user == nil { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "用户不存在"}) return } @@ -94,9 +94,9 @@ func WanzhiAppLogin(c *gin.Context) { "success": true, "data": gin.H{ "id": user.ID, - "name": userDisplayName(&user), - "phone": userPhoneStr(&user), - "avatar": userAvatarStr(c, &user), + "name": userDisplayName(user), + "phone": userPhoneStr(user), + "avatar": userAvatarStr(c, user), "token": token, }, }) @@ -133,9 +133,8 @@ func WanzhiAppRegister(c *gin.Context) { return } - db := database.DB() - var n int64 - if err := db.Model(&model.UserAuth{}).Where("auth_type = ? AND auth_id = ?", "phone", phone).Count(&n).Error; err != nil { + n, err := mongostore.CountPhoneAuths(phone) + if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "注册失败"}) return } @@ -151,42 +150,7 @@ func WanzhiAppRegister(c *gin.Context) { } userID := "user_" + randomSuffix() - nickname := name - phoneCopy := phone - avatar := "" - hasFullBook := false - earnings := 0.0 - pendingEarnings := 0.0 - referralCount := 0 - purchasedSections := "[]" - - err = db.Transaction(func(tx *gorm.DB) error { - u := model.User{ - ID: userID, - Nickname: &nickname, - Phone: &phoneCopy, - Avatar: &avatar, - HasFullBook: &hasFullBook, - PurchasedSections: &purchasedSections, - Earnings: &earnings, - PendingEarnings: &pendingEarnings, - ReferralCount: &referralCount, - } - if err := tx.Create(&u).Error; err != nil { - return err - } - ua := model.UserAuth{ - UserID: userID, - AuthType: "phone", - AuthID: phone, - Credential: string(hash), - } - if err := tx.Create(&ua).Error; err != nil { - return err - } - return nil - }) - if err != nil { + if err := mongostore.CreateAppRegister(userID, phone, name, string(hash)); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "注册失败"}) return } @@ -228,18 +192,13 @@ func WanzhiAppUserMe(c *gin.Context) { return } - db := database.DB() - var user model.User - if err := db.Where("id = ?", claims.UserID).First(&user).Error; err != nil { + user, err := mongostore.FindAppUserByID(claims.UserID) + if err != nil || user == nil { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"}) return } - var balance float64 - var ub model.UserBalance - if err := db.Where("user_id = ?", user.ID).First(&ub).Error; err == nil { - balance = ub.Balance - } + balance := mongostore.GetAppUserBalance(user.ID) isVip := false if user.IsVip != nil { @@ -250,9 +209,9 @@ func WanzhiAppUserMe(c *gin.Context) { "success": true, "data": gin.H{ "id": user.ID, - "name": userDisplayName(&user), - "phone": userPhoneStr(&user), - "avatar": userAvatarStr(c, &user), + "name": userDisplayName(user), + "phone": userPhoneStr(user), + "avatar": userAvatarStr(c, user), "level": 1, "balance": balance, "isVip": isVip, @@ -274,8 +233,8 @@ func PublicBuildInfo(c *gin.Context) { msg = "当前部署版本:" + bt } c.JSON(http.StatusOK, gin.H{ - "buildTime": bt, - "gitCommit": gc, - "message": msg, + "buildTime": bt, + "gitCommit": gc, + "message": msg, }) } diff --git a/new/wz-api/internal/router/router.go b/new/wz-api/internal/router/router.go index 4095b94..16b6198 100644 --- a/new/wz-api/internal/router/router.go +++ b/new/wz-api/internal/router/router.go @@ -2,6 +2,7 @@ package router import ( "context" + "strings" "time" "wz-api/internal/config" @@ -15,7 +16,7 @@ import ( "github.com/gin-gonic/gin" ) -// Setup 创建并配置 Gin 引擎。Soul 历史路由仍大量存在,玩值(old)契约按《开发文档/玩值/迁移-API对照表》逐步迁入;新 C 端统一走 /api/app/*。 +// Setup Mongo-only:仅注册玩值 C 端 /api/app、大屏 /api/screen、公共 /api/public、管理端登录与 /api/admin/wz/*(及 wanzhi_admin mock 契约)。 func Setup(cfg *config.Config) *gin.Engine { gin.SetMode(cfg.Mode) r := gin.New() @@ -23,14 +24,50 @@ func Setup(cfg *config.Config) *gin.Engine { r.Use(gin.Logger()) _ = r.SetTrustedProxies(cfg.TrustedProxies) - // CORS 须尽早注册,使 OPTIONS 预检在 Secure/gzip 之前得到 Access-Control-* 响应(避免上游或安全头导致无 CORS 头) - r.Use(cors.New(cors.Config{ + corsCfg := cors.Config{ AllowOrigins: cfg.CORSOrigins, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, AllowHeaders: []string{"Origin", "Content-Type", "Authorization", "Accept", "Accept-Language"}, AllowCredentials: true, MaxAge: 86400, - })) + } + if strings.ToLower(cfg.Mode) == "debug" { + allowedOrigin := make(map[string]struct{}, len(cfg.CORSOrigins)+8) + for _, o := range cfg.CORSOrigins { + if o = strings.TrimSpace(o); o != "" { + allowedOrigin[o] = struct{}{} + } + } + corsCfg.AllowOriginFunc = func(origin string) bool { + if origin == "" { + return false + } + if _, ok := allowedOrigin[origin]; ok { + return true + } + if strings.HasPrefix(origin, "http://localhost:") || strings.HasPrefix(origin, "http://127.0.0.1:") { + return true + } + if strings.HasPrefix(origin, "http://[::1]:") { + return true + } + if strings.HasPrefix(origin, "http://192.168.") || strings.HasPrefix(origin, "http://10.") { + return true + } + if strings.HasPrefix(origin, "http://172.16.") || strings.HasPrefix(origin, "http://172.17.") || + strings.HasPrefix(origin, "http://172.18.") || strings.HasPrefix(origin, "http://172.19.") || + strings.HasPrefix(origin, "http://172.20.") || strings.HasPrefix(origin, "http://172.21.") || + strings.HasPrefix(origin, "http://172.22.") || strings.HasPrefix(origin, "http://172.23.") || + strings.HasPrefix(origin, "http://172.24.") || strings.HasPrefix(origin, "http://172.25.") || + strings.HasPrefix(origin, "http://172.26.") || strings.HasPrefix(origin, "http://172.27.") || + strings.HasPrefix(origin, "http://172.28.") || strings.HasPrefix(origin, "http://172.29.") || + strings.HasPrefix(origin, "http://172.30.") || strings.HasPrefix(origin, "http://172.31.") { + return true + } + return false + } + } + r.Use(cors.New(corsCfg)) r.Use(middleware.Secure()) r.Use(gzip.Gzip(gzip.DefaultCompression)) rateLimiter := middleware.NewRateLimiter(100, 200) @@ -45,15 +82,8 @@ func Setup(cfg *config.Config) *gin.Engine { r.Static("/uploads", uploadDir) r.Static("/static", "./static") - // H5 落地页:朋友圈分享链接,文章预览 + 打开小程序按钮 - r.GET("/read/:id", handler.H5ReadPage) - - // WebSocket:小程序在线心跳(占位,后续可扩展管理端订阅、消息推送) - r.GET("/ws/miniprogram", handler.WsMiniprogram) - api := r.Group("/api") { - // ----- 玩值 C 端统一前缀(/api/app);C 端真源为 old/ Next+React,历史 uni 见 new/wz-app/README app := api.Group("/app") { app.GET("/health", func(c *gin.Context) { @@ -73,432 +103,34 @@ func Setup(cfg *config.Config) *gin.Engine { public.GET("/build-info", handler.PublicBuildInfo) } - // ----- 管理端 ----- api.GET("/admin", handler.AdminCheck) api.POST("/admin", handler.AdminLogin) api.POST("/admin/logout", handler.AdminLogout) - // 玩值管理端契约(旧 Next /api/admin/auth/login) api.POST("/admin/auth/login", handler.AdminLogin) api.POST("/admin/auth/logout", handler.AdminLogout) admin := api.Group("/admin") admin.Use(middleware.AdminAuth()) { - // 玩值 old 管理端契约(与 Soul 冲突的 GET /users、GET /orders 已移除,由本组接管) handler.RegisterWanzhiAdminRoutes(admin) - // 玩值真实数据:/api/admin/wz/*(wz_documents / MySQL) handler.RegisterPlayhubWzAdminRoutes(admin) - - admin.GET("/chapters", handler.AdminChaptersList) - admin.POST("/chapters", handler.AdminChaptersAction) - admin.PUT("/chapters", handler.AdminChaptersAction) - admin.DELETE("/chapters", handler.AdminChaptersAction) - admin.GET("/content", handler.AdminContent) - admin.POST("/content", handler.AdminContent) - admin.PUT("/content", handler.AdminContent) - admin.DELETE("/content", handler.AdminContent) - admin.GET("/dashboard/stats", handler.AdminDashboardStats) - admin.GET("/dashboard/recent-orders", handler.AdminDashboardRecentOrders) - admin.GET("/dashboard/new-users", handler.AdminDashboardNewUsers) - admin.GET("/dashboard/overview", handler.AdminDashboardOverview) - admin.GET("/dashboard/merchant-balance", handler.AdminDashboardMerchantBalance) - admin.GET("/distribution/overview", handler.AdminDistributionOverview) - admin.GET("/distribution/ranking", handler.AdminDistributionRanking) - admin.GET("/payment", handler.AdminPayment) - admin.POST("/payment", handler.AdminPayment) - admin.PUT("/payment", handler.AdminPayment) - admin.DELETE("/payment", handler.AdminPayment) - admin.GET("/referral", handler.AdminReferral) - admin.POST("/referral", handler.AdminReferral) - admin.PUT("/referral", handler.AdminReferral) - admin.DELETE("/referral", handler.AdminReferral) - admin.GET("/withdrawals", handler.AdminWithdrawalsList) - admin.PUT("/withdrawals", handler.AdminWithdrawalsAction) - admin.POST("/withdrawals/cancel", handler.AdminWithdrawalsCancel) - admin.POST("/withdrawals/sync", handler.AdminWithdrawalsSync) - admin.GET("/withdrawals/auto-approve", handler.AdminWithdrawalsAutoApproveGet) - admin.PUT("/withdrawals/auto-approve", handler.AdminWithdrawalsAutoApprovePut) - admin.GET("/withdraw-test", handler.AdminWithdrawTest) - admin.POST("/withdraw-test", handler.AdminWithdrawTest) - admin.GET("/settings", handler.AdminSettingsGet) - admin.POST("/settings", handler.AdminSettingsPost) - admin.GET("/linked-miniprograms", handler.AdminLinkedMpList) - admin.POST("/linked-miniprograms", handler.AdminLinkedMpCreate) - admin.PUT("/linked-miniprograms", handler.AdminLinkedMpUpdate) - admin.DELETE("/linked-miniprograms/:id", handler.AdminLinkedMpDelete) - admin.GET("/referral-settings", handler.AdminReferralSettingsGet) - admin.POST("/referral-settings", handler.AdminReferralSettingsPost) - // 存客宝开放 API 辅助接口:设备列表(供链接人与事选择设备) - admin.GET("/ckb/devices", handler.AdminCKBDevices) - // 存客宝开放 API 辅助接口:获客计划列表(供链接人与事一键选择计划覆盖参数) - admin.GET("/ckb/plans", handler.AdminCKBPlans) - admin.GET("/author-settings", handler.AdminAuthorSettingsGet) - admin.POST("/author-settings", handler.AdminAuthorSettingsPost) - admin.GET("/shensheshou/query", handler.AdminShensheShouQuery) - admin.POST("/shensheshou/enrich", handler.AdminShensheShouEnrich) - admin.POST("/shensheshou/ingest", handler.AdminShensheShouIngest) - admin.POST("/shensheshou/batch-enrich", handler.AdminShensheShouBatchEnrich) - admin.PUT("/orders/refund", handler.AdminOrderRefund) - admin.GET("/users/:id/balance", handler.AdminUserBalanceGet) - admin.POST("/users/:id/balance/adjust", handler.AdminUserBalanceAdjust) - admin.GET("/balance/summary", handler.AdminBalanceSummary) - admin.GET("/users/online-stats", handler.AdminUsersOnlineStats) - admin.POST("/users", handler.AdminUsersAction) - admin.PUT("/users", handler.AdminUsersAction) - admin.DELETE("/users", handler.AdminUsersAction) - admin.GET("/gift-pay-requests", handler.AdminGiftPayRequestsList) - admin.GET("/super-individual/stats", handler.AdminSuperIndividualStats) - admin.GET("/user/track", handler.UserTrackGet) - admin.GET("/track/stats", handler.AdminTrackStats) - admin.GET("/ckb/plan-check", handler.AdminCKBPlanCheck) - admin.GET("/mbti-avatars", handler.AdminMbtiAvatarsGet) - admin.POST("/mbti-avatars", handler.AdminMbtiAvatarsPost) - admin.POST("/mbti-avatars/batch-assign", handler.AdminMbtiAvatarsBatchAssign) - admin.GET("/open-platform/keys", handler.AdminOpenPlatformKeysList) - admin.POST("/open-platform/keys", handler.AdminOpenPlatformKeysCreate) - admin.POST("/open-platform/keys/:id/revoke", handler.AdminOpenPlatformKeyRevoke) - admin.GET("/open-platform/logs", handler.AdminOpenPlatformLogsList) - admin.GET("/dashboard/leads", handler.AdminDashboardLeads) - admin.GET("/chapters/:id/click-stats", handler.AdminChapterClickStats) - admin.GET("/dashboard/content-ranking", handler.AdminDashboardContentRanking) - admin.GET("/rfm-weights", handler.AdminRFMWeightsGet) - admin.PUT("/rfm-weights", handler.AdminRFMWeightsPut) - admin.GET("/leaderboard", handler.AdminLeaderboard) - admin.POST("/ckb/push-content", handler.AdminCKBPushContent) } - - // ----- 开放平台(API Key:Authorization Bearer op_sk_... 或 X-API-Key)----- - open := api.Group("/open") - open.Use(handler.OpenPlatformAuthMiddleware()) - { - open.POST("/user/profile", handler.OpenPlatformPatchUserProfile) - } - - // ----- 鉴权 ----- - api.POST("/auth/login", handler.AuthLogin) - api.POST("/auth/reset-password", handler.AuthResetPassword) - - // ----- 书籍/章节(只读,写操作由 /api/db/book 管理端路由承担) ----- - api.GET("/book/books", handler.BooksList) - // Deprecated: 小程序已迁移至 book/parts + chapters-by-part,保留以兼容 next-project/管理端 - api.GET("/book/all-chapters", handler.BookAllChapters) - api.GET("/book/parts", handler.BookParts) - api.GET("/book/chapters-by-part", handler.BookChaptersByPart) - api.GET("/book/chapter/:id", handler.BookChapterByID) - api.GET("/book/chapter/by-mid/:mid", handler.BookChapterByMID) - api.GET("/book/chapters", handler.BookChapters) - // POST/PUT/DELETE /api/book/chapters 已移除:写操作须由管理端 /api/db/book(AdminAuth)完成 - api.GET("/book/hot", handler.BookHot) - api.GET("/book/recommended", handler.BookRecommended) - api.GET("/book/latest-chapters", handler.BookLatestChapters) - api.GET("/book/search", handler.BookSearch) - api.GET("/book/stats", handler.BookStats) - api.GET("/book/sync", handler.BookSync) - api.POST("/book/sync", handler.BookSync) - - // ----- CKB ----- - api.POST("/ckb/join", handler.CKBJoin) - api.POST("/ckb/match", handler.CKBMatch) - api.GET("/ckb/sync", handler.CKBSync) - api.POST("/ckb/sync", handler.CKBSync) - api.GET("/ckb/content", handler.CKBContentList) - - // ----- 配置 ----- - api.GET("/config", handler.GetConfig) - // Deprecated: 小程序已迁移至 /miniprogram/config/core + audit-mode + read-extras,保留以兼容 next-project - api.GET("/db/config", handler.GetPublicDBConfig) - - // ----- 内容 ----- - api.GET("/content", handler.ContentGet) - - // ----- 定时任务(须携带 X-Cron-Secret 请求头,与 .env CRON_SECRET 一致) ----- - cron := api.Group("/cron") - cron.Use(middleware.CronAuth()) - { - cron.GET("/sync-orders", handler.CronSyncOrders) - cron.POST("/sync-orders", handler.CronSyncOrders) - cron.GET("/retry-order-webhooks", handler.CronRetryOrderWebhooks) - cron.POST("/retry-order-webhooks", handler.CronRetryOrderWebhooks) - cron.GET("/retry-ckb-leads", handler.CronRetryCkbLeads) - cron.POST("/retry-ckb-leads", handler.CronRetryCkbLeads) - cron.GET("/sync-vip-ckb-plans", handler.CronSyncVipCkbPlans) - cron.POST("/sync-vip-ckb-plans", handler.CronSyncVipCkbPlans) - cron.GET("/unbind-expired", handler.CronUnbindExpired) - cron.POST("/unbind-expired", handler.CronUnbindExpired) - } - - // ----- 数据库(管理端) ----- - db := api.Group("/db") - db.Use(middleware.AdminAuth()) - { - db.GET("/books", handler.DBBooksAction) - db.POST("/books", handler.DBBooksAction) - db.PUT("/books", handler.DBBooksAction) - db.DELETE("/books", handler.DBBooksAction) - db.GET("/book", handler.DBBookAction) - db.POST("/book", handler.DBBookAction) - db.PUT("/book", handler.DBBookAction) - db.DELETE("/book", handler.DBBookDelete) - db.GET("/chapters", handler.DBChapters) - db.POST("/chapters", handler.DBChapters) - db.GET("/config/full", handler.DBConfigGet) // 管理端拉全量配置;GET /api/db/config 已用于公开接口 GetPublicDBConfig - db.POST("/config", handler.DBConfigPost) - db.DELETE("/config", handler.DBConfigDelete) - db.GET("/distribution", handler.DBDistribution) - db.GET("/init", handler.DBInitGet) - db.POST("/init", handler.DBInit) - db.GET("/migrate", handler.DBMigrateGet) - db.POST("/migrate", handler.DBMigratePost) - db.GET("/users", handler.DBUsersList) - db.POST("/users", handler.DBUsersAction) - db.PUT("/users", handler.DBUsersAction) - db.DELETE("/users", handler.DBUsersDelete) - db.GET("/users/referrals", handler.DBUsersReferrals) - db.GET("/users/rfm", handler.DBUsersRFM) - db.GET("/users/rfm-single", handler.DBUserRFMSingle) - db.GET("/users/journey-stats", handler.DBUsersJourneyStats) - db.GET("/users/journey-users", handler.DBUsersJourneyUsers) - db.GET("/users/tracks", handler.DBUserTracksList) - db.GET("/vip-roles", handler.DBVipRolesList) - db.POST("/vip-roles", handler.DBVipRolesAction) - db.PUT("/vip-roles", handler.DBVipRolesAction) - db.DELETE("/vip-roles", handler.DBVipRolesAction) - db.GET("/vip-members", handler.DBVipMembersList) - db.PUT("/vip-members/webhook", handler.DBVipMemberWebhookSet) - db.PUT("/persons/home-entry-config", handler.DBPersonHomeEntryConfigSave) - db.GET("/match-records", handler.DBMatchRecordsList) - db.GET("/match-pool-counts", handler.DBMatchPoolCounts) - db.GET("/mentors", handler.DBMentorsList) - db.POST("/mentors", handler.DBMentorsAction) - db.PUT("/mentors", handler.DBMentorsAction) - db.DELETE("/mentors", handler.DBMentorsAction) - db.GET("/mentor-consultations", handler.DBMentorConsultationsList) - db.GET("/persons", handler.DBPersonList) - db.GET("/persons/check-unique", handler.DBPersonCheckUnique) - db.GET("/person", handler.DBPersonDetail) - db.GET("/persons/pinned-token", handler.DBPersonPinnedToken) - db.PUT("/persons/pin", handler.DBPersonPin) - db.POST("/persons", handler.DBPersonSave) - db.DELETE("/persons", handler.DBPersonDelete) - db.GET("/link-tags", handler.DBLinkTagList) - db.POST("/link-tags", handler.DBLinkTagSave) - db.DELETE("/link-tags", handler.DBLinkTagDelete) - db.GET("/persons/pinned", handler.DBPersonPinnedList) - db.GET("/ckb-leads", handler.DBCKBLeadList) - db.POST("/ckb-leads/retry", handler.DBCKBLeadRetry) - db.POST("/ckb-leads/delete", handler.DBCKBLeadDelete) - db.POST("/ckb-leads/delete-batch", handler.DBCKBLeadDeleteBatch) - db.GET("/ckb-person-leads", handler.DBCKBPersonLeads) - db.GET("/ckb-plan-stats", handler.CKBPlanStats) - db.GET("/user-rules", handler.DBUserRulesList) - db.POST("/user-rules", handler.DBUserRulesAction) - db.PUT("/user-rules", handler.DBUserRulesAction) - db.DELETE("/user-rules", handler.DBUserRulesAction) - } - - // ----- 分销 ----- - api.GET("/distribution", handler.DistributionGet) - api.POST("/distribution", handler.DistributionGet) - api.PUT("/distribution", handler.DistributionGet) - api.GET("/distribution/auto-withdraw-config", handler.DistributionAutoWithdrawConfig) - api.POST("/distribution/auto-withdraw-config", handler.DistributionAutoWithdrawConfig) - api.DELETE("/distribution/auto-withdraw-config", handler.DistributionAutoWithdrawConfig) - api.GET("/distribution/messages", handler.DistributionMessages) - api.POST("/distribution/messages", handler.DistributionMessages) - - // ----- 文档生成 ----- - api.POST("/documentation/generate", handler.DocGenerate) - - // ----- 找伙伴 ----- - api.GET("/match/config", handler.MatchConfigGet) - api.POST("/match/config", handler.MatchConfigPost) - api.POST("/match/users", matchLimiter.Middleware(), handler.MatchUsers) - - // ----- 菜单 ----- - api.GET("/menu", handler.MenuGet) - - // /api/orders 已移入 admin 组(需鉴权),见下方 - - // ----- 支付 ----- - api.POST("/payment/alipay/notify", handler.PaymentAlipayNotify) - api.POST("/payment/callback", handler.PaymentCallback) - api.POST("/payment/create-order", handler.PaymentCreateOrder) - api.GET("/payment/methods", handler.PaymentMethods) - api.GET("/payment/query", handler.PaymentQuery) - api.GET("/payment/status/:orderSn", handler.PaymentStatusOrderSn) - api.POST("/payment/verify", handler.PaymentVerify) - api.POST("/payment/wechat/notify", handler.PaymentWechatNotify) - api.GET("/payment/wechat/transfer/notify", handler.PaymentWechatTransferNotify) - api.POST("/payment/wechat/transfer/notify", handler.PaymentWechatTransferNotify) - - // ----- 推荐 ----- - api.POST("/referral/bind", handler.ReferralBind) - api.GET("/referral/data", handler.ReferralData) - api.POST("/referral/visit", handler.ReferralVisit) - - // ----- 搜索 ----- - api.GET("/search", handler.SearchGet) - - // ----- 同步 ----- - api.GET("/sync", handler.SyncGet) - api.POST("/sync", handler.SyncPost) - api.PUT("/sync", handler.SyncPut) - - // ----- 上传 ----- - api.POST("/upload", handler.UploadPost) - api.DELETE("/upload", handler.UploadDelete) - - // ----- 用户 ----- - api.GET("/user/addresses", handler.UserAddressesGet) - api.POST("/user/addresses", handler.UserAddressesPost) - api.GET("/user/addresses/:id", handler.UserAddressesByID) - api.PUT("/user/addresses/:id", handler.UserAddressesByID) - api.DELETE("/user/addresses/:id", handler.UserAddressesByID) - api.GET("/user/check-purchased", handler.UserCheckPurchased) - api.GET("/user/profile", handler.UserProfileGet) - api.POST("/user/profile", handler.UserProfilePost) - api.GET("/user/purchase-status", handler.UserPurchaseStatus) - api.GET("/user/reading-progress", handler.UserReadingProgressGet) - api.POST("/user/reading-progress", handler.UserReadingProgressPost) - api.GET("/user/track", handler.UserTrackGet) - api.POST("/user/track", handler.UserTrackPost) - api.POST("/user/update", handler.UserUpdate) - // ----- 微信登录 ----- - api.POST("/wechat/login", handler.WechatLogin) - api.POST("/wechat/phone-login", handler.WechatPhoneLogin) - - // ----- 小程序组(所有小程序端接口统一在 /api/miniprogram 下) ----- - miniprogram := api.Group("/miniprogram") - { - // config 拆分接口(优先匹配,路径更具体) - miniprogram.GET("/config/audit-mode", handler.GetAuditMode) - miniprogram.GET("/config/core", handler.GetCoreConfig) - miniprogram.GET("/config/read-extras", handler.GetReadExtras) - miniprogram.GET("/config/mbti-avatars", handler.MiniprogramMbtiAvatarsGet) - // Deprecated: 保留以兼容线上,计划迁移至上述拆分接口 - miniprogram.GET("/config", handler.GetPublicDBConfig) - miniprogram.POST("/login", handler.MiniprogramLogin) - miniprogram.POST("/phone-login", handler.WechatPhoneLogin) - miniprogram.POST("/dev/login-as", handler.MiniprogramDevLoginAs) // 开发专用:按 userId 切换账号 - miniprogram.POST("/dev/login-by-phone", handler.MiniprogramDevLoginByPhone) // 开发专用:按手机号登录(密码可空) - miniprogram.POST("/phone", handler.MiniprogramPhone) - miniprogram.GET("/pay", handler.MiniprogramPay) - miniprogram.POST("/pay", handler.MiniprogramPay) - miniprogram.POST("/pay/notify", handler.MiniprogramPayNotify) // 微信支付回调,URL 需在商户平台配置 - miniprogram.POST("/qrcode", handler.MiniprogramQrcode) - miniprogram.GET("/qrcode/image", handler.MiniprogramQrcodeImage) - // 书籍列表(多书架构) - miniprogram.GET("/book/books", handler.BooksList) - // Deprecated: 小程序已迁移至 book/parts + chapters-by-part - miniprogram.GET("/book/all-chapters", handler.BookAllChapters) - miniprogram.GET("/book/parts", handler.BookParts) - miniprogram.GET("/book/chapters-by-part", handler.BookChaptersByPart) - miniprogram.GET("/book/chapter/by-mid/:mid", handler.BookChapterByMID) - miniprogram.GET("/book/chapter/by-id/:id", handler.BookChapterByID) - miniprogram.GET("/book/hot", handler.BookHot) - miniprogram.GET("/book/recommended", handler.BookRecommended) - miniprogram.GET("/book/ranking", handler.BookRanking) - miniprogram.GET("/book/latest-chapters", handler.BookLatestChapters) - miniprogram.GET("/book/search", handler.BookSearch) - miniprogram.GET("/book/stats", handler.BookStats) - miniprogram.POST("/referral/visit", handler.ReferralVisit) - miniprogram.POST("/referral/bind", handler.ReferralBind) - miniprogram.GET("/referral/data", handler.ReferralData) - miniprogram.GET("/earnings", handler.MyEarnings) - miniprogram.GET("/earnings/leaderboard", handler.EarningsLeaderboard) - miniprogram.GET("/match/config", handler.MatchConfigGet) - miniprogram.GET("/match/history", handler.MiniprogramMatchHistory) - miniprogram.POST("/match/users", matchLimiter.Middleware(), handler.MatchUsers) - miniprogram.POST("/ckb/join", handler.CKBJoin) - miniprogram.POST("/ckb/match", handler.CKBMatch) - miniprogram.GET("/ckb/pinned-person", handler.CKBPinnedPerson) - miniprogram.POST("/ckb/lead", handler.CKBLead) - miniprogram.POST("/ckb/index-lead", handler.CKBIndexLead) - miniprogram.POST("/upload", handler.UploadPost) - miniprogram.DELETE("/upload", handler.UploadDelete) - miniprogram.GET("/user/addresses", handler.UserAddressesGet) - miniprogram.POST("/user/addresses", handler.UserAddressesPost) - miniprogram.GET("/user/addresses/:id", handler.UserAddressesByID) - miniprogram.PUT("/user/addresses/:id", handler.UserAddressesByID) - miniprogram.DELETE("/user/addresses/:id", handler.UserAddressesByID) - miniprogram.GET("/user/check-purchased", handler.UserCheckPurchased) - miniprogram.GET("/user/dashboard-stats", handler.UserDashboardStats) - miniprogram.GET("/user/profile", handler.UserProfileGet) - miniprogram.POST("/user/profile", handler.UserProfilePost) - miniprogram.GET("/user/purchase-status", handler.UserPurchaseStatus) - miniprogram.GET("/user/reading-progress", handler.UserReadingProgressGet) - miniprogram.POST("/user/reading-progress", handler.UserReadingProgressPost) - miniprogram.POST("/user/update", handler.UserUpdate) - miniprogram.POST("/withdraw", handler.WithdrawPost) - miniprogram.GET("/withdraw/records", handler.WithdrawRecords) - miniprogram.GET("/withdraw/pending-confirm", handler.WithdrawPendingConfirm) - miniprogram.POST("/withdraw/confirm-received", handler.WithdrawConfirmReceived) - miniprogram.GET("/withdraw/confirm-info", handler.WithdrawConfirmInfo) - // VIP 接口(小程序专用,按使用方区分路径) - miniprogram.GET("/vip/status", handler.VipStatus) - miniprogram.GET("/vip/profile", handler.VipProfileGet) - miniprogram.POST("/vip/profile", handler.VipProfilePost) - miniprogram.GET("/vip/members", handler.VipMembers) - // 用户列表/单个(首页超级个体、会员详情回退) - miniprogram.GET("/users", handler.MiniprogramUsers) - miniprogram.GET("/orders", handler.MiniprogramOrders) - // 导师(stitch_soul) - miniprogram.GET("/mentors", handler.MiniprogramMentorsList) - miniprogram.GET("/mentors/:id", handler.MiniprogramMentorsDetail) - miniprogram.POST("/mentors/:id/book", handler.MiniprogramMentorsBook) - miniprogram.GET("/about/author", handler.MiniprogramAboutAuthor) - miniprogram.GET("/persons/pinned", handler.DBPersonPinnedList) - // 埋点 - miniprogram.POST("/track", handler.MiniprogramTrackPost) - // 规则引擎(用户旅程触达) - miniprogram.GET("/user-rules", handler.MiniprogramUserRulesGet) - miniprogram.POST("/user-rules/complete", handler.MiniprogramUserRuleComplete) - // 余额 - miniprogram.GET("/balance", handler.BalanceGet) - miniprogram.GET("/balance/transactions", handler.BalanceTransactionsGet) - miniprogram.POST("/balance/recharge", handler.BalanceRechargePost) - miniprogram.POST("/balance/recharge/confirm", handler.BalanceRechargeConfirmPost) - miniprogram.POST("/balance/refund", handler.BalanceRefundPost) - miniprogram.POST("/balance/consume", handler.BalanceConsumePost) - miniprogram.GET("/gift/link", handler.GiftLinkGet) - // 代付(改造后:发起人支付,好友领取) - miniprogram.POST("/gift-pay/create", handler.GiftPayCreate) - miniprogram.POST("/gift-pay/initiator-pay", handler.GiftPayInitiatorPay) - miniprogram.GET("/gift-pay/detail", handler.GiftPayDetail) - miniprogram.POST("/gift-pay/redeem", handler.GiftPayRedeem) - miniprogram.POST("/gift-pay/cancel", handler.GiftPayCancel) - miniprogram.GET("/gift-pay/my-requests", handler.GiftPayMyRequests) - miniprogram.GET("/my/super-stats", handler.MiniprogramMySuperStats) - miniprogram.GET("/my/super-leads", handler.MiniprogramMySuperLeads) - miniprogram.GET("/my/super-lead-tracks", handler.MiniprogramMySuperLeadTracks) - } - - // ----- 提现 ----- - api.POST("/withdraw", handler.WithdrawPost) - api.GET("/withdraw/records", handler.WithdrawRecords) - api.GET("/withdraw/pending-confirm", handler.WithdrawPendingConfirm) - // 提现测试(固定用户 1 元,无需 admin 鉴权,仅用于脚本/本地调试) - api.GET("/withdraw-test", handler.AdminWithdrawTest) - api.POST("/withdraw-test", handler.AdminWithdrawTest) - - // ----- 提现 V3(独立实现,依文档 提现功能完整技术文档.md) ----- - api.POST("/v3/withdraw/initiate", handler.WithdrawV3Initiate) - api.POST("/v3/withdraw/notify", handler.WithdrawV3Notify) - api.POST("/v3/withdraw/query", handler.WithdrawV3Query) } - // 根路径不返回任何页面(仅 204) r.GET("/", func(c *gin.Context) { c.Status(204) }) - // 健康检查:返回状态、版本号、数据库与 Redis 连接状态 r.GET("/health", func(c *gin.Context) { - dbStatus := "ok" - if sqlDB, err := database.DB().DB(); err != nil { - dbStatus = "error" - } else if err := sqlDB.Ping(); err != nil { - dbStatus = "disconnected" + mongoStatus := "ok" + if mc := database.MongoClient(); mc != nil { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if err := mc.Ping(ctx, nil); err != nil { + mongoStatus = "error" + } + } else { + mongoStatus = "disabled" } redisStatus := "disabled" @@ -511,10 +143,13 @@ func Setup(cfg *config.Config) *gin.Engine { } c.JSON(200, gin.H{ - "status": "ok", - "version": cfg.Version, - "database": dbStatus, - "redis": redisStatus, + "status": "ok", + "version": cfg.Version, + "database": mongoStatus, + "mongo": mongoStatus, + "redis": redisStatus, + "storage": "mongodb", + "mysqlGone": true, }) }) diff --git a/new/wz-api/internal/store/mongo/auth.go b/new/wz-api/internal/store/mongo/auth.go new file mode 100644 index 0000000..a2daa61 --- /dev/null +++ b/new/wz-api/internal/store/mongo/auth.go @@ -0,0 +1,288 @@ +// Package mongostore 封装 C 端账号、管理端登录等 Mongo 集合访问。 +// C 端主档:app_users;认证:user_auths;余额:user_balances;后台:admin_users(与玩值 users 业务集合分离)。 +package mongostore + +import ( + "context" + "time" + + "wz-api/internal/database" + "wz-api/internal/model" + + "go.mongodb.org/mongo-driver/bson" + mongodriver "go.mongodb.org/mongo-driver/mongo" +) + +const ( + CollAdminUsers = "admin_users" + CollAppUsers = "app_users" + CollUserAuths = "user_auths" + CollUserBalances = "user_balances" +) + +func coll(name string) *mongodriver.Collection { + return database.MongoDatabase().Collection(name) +} + +func opCtx() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), 25*time.Second) +} + +// FindAdminByUsername 按用户名查管理员;无文档时 ErrNoDocuments。 +func FindAdminByUsername(username string) (*model.AdminUser, error) { + ctx, cancel := opCtx() + defer cancel() + var raw bson.M + err := coll(CollAdminUsers).FindOne(ctx, bson.M{"username": username}).Decode(&raw) + if err != nil { + return nil, err + } + return decodeAdmin(raw) +} + +func decodeAdmin(raw bson.M) (*model.AdminUser, error) { + id := uintFromValue(raw["id"]) + u := &model.AdminUser{ + ID: id, + Username: stringFromValue(raw["username"]), + PasswordHash: stringFromValue(raw["password_hash"]), + Role: stringFromValue(raw["role"]), + Name: stringFromValue(raw["name"]), + Status: stringFromValue(raw["status"]), + } + if u.Status == "" { + u.Status = "active" + } + if ts, ok := raw["created_at"].(time.Time); ok { + u.CreatedAt = ts + } else if s := stringFromValue(raw["created_at"]); s != "" { + if t, e := time.Parse(time.RFC3339, s); e == nil { + u.CreatedAt = t + } + } + if ts, ok := raw["updated_at"].(time.Time); ok { + u.UpdatedAt = ts + } else if s := stringFromValue(raw["updated_at"]); s != "" { + if t, e := time.Parse(time.RFC3339, s); e == nil { + u.UpdatedAt = t + } + } + return u, nil +} + +// CountAdminUsers admin_users 文档数。 +func CountAdminUsers() (int64, error) { + ctx, cancel := opCtx() + defer cancel() + return coll(CollAdminUsers).CountDocuments(ctx, bson.M{}) +} + +// InsertAdmin 创建管理员(Mongo 侧自增 id:取当前最大 id+1)。 +func InsertAdmin(a *model.AdminUser) error { + ctx, cancel := opCtx() + defer cancel() + if a.ID == 0 { + n, err := CountAdminUsers() + if err != nil { + return err + } + a.ID = uint(n) + 1 + } + now := time.Now().UTC() + if a.CreatedAt.IsZero() { + a.CreatedAt = now + } + a.UpdatedAt = now + doc := bson.M{ + "id": a.ID, + "username": a.Username, + "password_hash": a.PasswordHash, + "role": a.Role, + "name": a.Name, + "status": a.Status, + "created_at": a.CreatedAt, + "updated_at": a.UpdatedAt, + } + _, err := coll(CollAdminUsers).InsertOne(ctx, doc) + return err +} + +// FindUserAuthByPhone auth_type=phone 且 auth_id=手机号。 +func FindUserAuthByPhone(phone string) (*model.UserAuth, error) { + ctx, cancel := opCtx() + defer cancel() + var raw bson.M + err := coll(CollUserAuths).FindOne(ctx, bson.M{"auth_type": "phone", "auth_id": phone}).Decode(&raw) + if err != nil { + return nil, err + } + return decodeUserAuth(raw) +} + +func decodeUserAuth(raw bson.M) (*model.UserAuth, error) { + ua := &model.UserAuth{ + UserID: stringFromValue(raw["user_id"]), + AuthType: stringFromValue(raw["auth_type"]), + AuthID: stringFromValue(raw["auth_id"]), + Credential: stringFromValue(raw["credential"]), + } + return ua, nil +} + +// CountPhoneAuths 手机号是否已注册。 +func CountPhoneAuths(phone string) (int64, error) { + ctx, cancel := opCtx() + defer cancel() + return coll(CollUserAuths).CountDocuments(ctx, bson.M{"auth_type": "phone", "auth_id": phone}) +} + +// CreateAppRegister 写入 app_users + user_auths(含 bcrypt);userID 须已生成。 +func CreateAppRegister(userID, phone, nickname, hash string) error { + ctx, cancel := opCtx() + defer cancel() + now := time.Now().UTC() + nick := nickname + phoneCopy := phone + avatar := "" + hasFullBook := false + purchased := "[]" + earnings := 0.0 + pending := 0.0 + ref := 0 + + uDoc := bson.M{ + "id": userID, + "nickname": nick, + "phone": phoneCopy, + "avatar": avatar, + "has_full_book": hasFullBook, + "purchased_sections": purchased, + "earnings": earnings, + "pending_earnings": pending, + "referral_count": ref, + "created_at": now, + "updated_at": now, + } + if _, err := coll(CollAppUsers).InsertOne(ctx, uDoc); err != nil { + return err + } + authDoc := bson.M{ + "user_id": userID, + "auth_type": "phone", + "auth_id": phone, + "credential": hash, + "created_at": now, + "updated_at": now, + } + if _, err := coll(CollUserAuths).InsertOne(ctx, authDoc); err != nil { + return err + } + balDoc := bson.M{ + "user_id": userID, + "balance": 0.0, + "created_at": now, + "updated_at": now, + } + _, _ = coll(CollUserBalances).InsertOne(ctx, balDoc) + return nil +} + +// FindAppUserByID C 端 users 主档(集合 app_users)。 +func FindAppUserByID(id string) (*model.User, error) { + ctx, cancel := opCtx() + defer cancel() + var raw bson.M + err := coll(CollAppUsers).FindOne(ctx, bson.M{"id": id}).Decode(&raw) + if err != nil { + return nil, err + } + return decodeAppUser(raw) +} + +func decodeAppUser(raw bson.M) (*model.User, error) { + u := &model.User{ + ID: stringFromValue(raw["id"]), + } + if v, ok := raw["nickname"].(string); ok && v != "" { + u.Nickname = &v + } else if s := stringFromValue(raw["nickname"]); s != "" { + u.Nickname = &s + } + if v, ok := raw["phone"].(string); ok && v != "" { + u.Phone = &v + } else if s := stringFromValue(raw["phone"]); s != "" { + u.Phone = &s + } + if v, ok := raw["avatar"].(string); ok { + u.Avatar = &v + } + if b, ok := raw["is_vip"].(bool); ok { + u.IsVip = &b + } + // 时间 + if ts, ok := raw["created_at"].(time.Time); ok { + u.CreatedAt = ts + } + if ts, ok := raw["updated_at"].(time.Time); ok { + u.UpdatedAt = ts + } + return u, nil +} + +// GetAppUserBalance 读取 user_balances.balance。 +func GetAppUserBalance(userID string) float64 { + ctx, cancel := opCtx() + defer cancel() + var raw bson.M + err := coll(CollUserBalances).FindOne(ctx, bson.M{"user_id": userID}).Decode(&raw) + if err != nil { + return 0 + } + switch x := raw["balance"].(type) { + case float64: + return x + case int32: + return float64(x) + case int64: + return float64(x) + default: + return 0 + } +} + +// AppUserDisplayName 供直播间等展示:nickname 或手机号。 +func AppUserDisplayName(userID string) string { + u, err := FindAppUserByID(userID) + if err != nil || u == nil { + return "用户" + } + if u.Nickname != nil && *u.Nickname != "" { + return *u.Nickname + } + if u.Phone != nil && *u.Phone != "" { + return *u.Phone + } + return "用户" +} + +func stringFromValue(v interface{}) string { + if v == nil { + return "" + } + s, _ := v.(string) + return s +} + +func uintFromValue(v interface{}) uint { + switch x := v.(type) { + case int32: + return uint(x) + case int64: + return uint(x) + case float64: + return uint(x) + default: + return 0 + } +} + diff --git a/new/wz-api/internal/wzstore/document.go b/new/wz-api/internal/wzstore/document.go index e21733c..d6c4050 100644 --- a/new/wz-api/internal/wzstore/document.go +++ b/new/wz-api/internal/wzstore/document.go @@ -5,12 +5,10 @@ import ( "encoding/hex" "encoding/json" "errors" - "time" "wz-api/internal/model" "gorm.io/datatypes" - "gorm.io/gorm" ) // ErrNotFound 文档不存在 @@ -22,99 +20,34 @@ func newExtID() string { return hex.EncodeToString(b) } -// List 按集合列出(按 body 内 updatedAt/createdAt 降序由应用层排序;此处按 id 降序近似) -func List(db *gorm.DB, coll string, limit int) ([]model.WzDocument, error) { - if limit <= 0 || limit > 2000 { - limit = 500 - } - var rows []model.WzDocument - err := db.Where("coll = ?", coll).Order("id DESC").Limit(limit).Find(&rows).Error - return rows, err +// List 按 Mongo 集合列出(_id 降序)。 +func List(coll string, limit int) ([]model.WzDocument, error) { + return mongoListDocuments(coll, limit) } // Get 按集合 + ext_id -func Get(db *gorm.DB, coll, extID string) (*model.WzDocument, error) { - var d model.WzDocument - err := db.Where("coll = ? AND ext_id = ?", coll, extID).First(&d).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ErrNotFound - } - return &d, err +func Get(coll, extID string) (*model.WzDocument, error) { + return mongoGet(coll, extID) } -// FindByJSONField 等值查询,path 为 JSON path,如 $.userId -func FindByJSONField(db *gorm.DB, coll, path, value string) (*model.WzDocument, error) { - var d model.WzDocument - err := db.Where("coll = ?", coll). - Where("JSON_UNQUOTE(JSON_EXTRACT(body, ?)) = ?", path, value). - First(&d).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ErrNotFound - } - return &d, err +// FindByJSONField 等值查询;path 可为 $.userId 或 userId(与 Mongo 顶层层级字段一致)。 +func FindByJSONField(coll, path, value string) (*model.WzDocument, error) { + return mongoFindByField(coll, jsonPathToKey(path), value) } -// FindOneByField $.key = val -func FindOneByField(db *gorm.DB, coll, key, val string) (*model.WzDocument, error) { - return FindByJSONField(db, coll, "$."+key, val) +// FindOneByField key 与 value 等值 +func FindOneByField(coll, key, val string) (*model.WzDocument, error) { + return mongoFindByField(coll, key, val) } -// Insert 新建文档(body 为 map,自动生成 ext_id) -func Insert(db *gorm.DB, coll string, body map[string]interface{}) (extID string, err error) { - extID = newExtID() - now := time.Now().UTC().Format(time.RFC3339) - if body == nil { - body = map[string]interface{}{} - } - if _, ok := body["createdAt"]; !ok { - body["createdAt"] = now - } - body["updatedAt"] = now - raw, err := json.Marshal(body) - if err != nil { - return "", err - } - d := model.WzDocument{Coll: coll, ExtID: extID, Body: datatypes.JSON(raw)} - err = db.Create(&d).Error - return extID, err +// Insert 新建文档(自动生成 ext_id) +func Insert(coll string, body map[string]interface{}) (extID string, err error) { + return mongoInsert(coll, body) } -// UpsertByExtID 按 ext_id 写入或更新(patch 合并进已有 body) -func UpsertByExtID(db *gorm.DB, coll, extID string, body map[string]interface{}) error { - if extID == "" { - return errors.New("ext_id required") - } - now := time.Now().UTC().Format(time.RFC3339) - var d model.WzDocument - err := db.Where("coll = ? AND ext_id = ?", coll, extID).First(&d).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - if body == nil { - body = map[string]interface{}{} - } - if body["createdAt"] == nil { - body["createdAt"] = now - } - body["updatedAt"] = now - raw, e := json.Marshal(body) - if e != nil { - return e - } - return db.Create(&model.WzDocument{Coll: coll, ExtID: extID, Body: datatypes.JSON(raw)}).Error - } - if err != nil { - return err - } - merged, e := mergeJSON(d.Body, body) - if e != nil { - return e - } - merged["updatedAt"] = now - raw, e := json.Marshal(merged) - if e != nil { - return e - } - d.Body = datatypes.JSON(raw) - return db.Save(&d).Error +// UpsertByExtID 按 ext_id 写入或更新 +func UpsertByExtID(coll, extID string, body map[string]interface{}) error { + return mongoUpsertByExtID(coll, extID, body) } func mergeJSON(base datatypes.JSON, patch map[string]interface{}) (map[string]interface{}, error) { @@ -134,34 +67,13 @@ func mergeJSON(base datatypes.JSON, patch map[string]interface{}) (map[string]in } // UpdateJSONPartial 合并更新 -func UpdateJSONPartial(db *gorm.DB, coll, extID string, patch map[string]interface{}) error { - d, err := Get(db, coll, extID) - if err != nil { - return err - } - merged, err := mergeJSON(d.Body, patch) - if err != nil { - return err - } - merged["updatedAt"] = time.Now().UTC().Format(time.RFC3339) - raw, err := json.Marshal(merged) - if err != nil { - return err - } - d.Body = datatypes.JSON(raw) - return db.Save(d).Error +func UpdateJSONPartial(coll, extID string, patch map[string]interface{}) error { + return mongoUpdateJSONPartial(coll, extID, patch) } // Delete 删除文档 -func Delete(db *gorm.DB, coll, extID string) error { - res := db.Where("coll = ? AND ext_id = ?", coll, extID).Delete(&model.WzDocument{}) - if res.Error != nil { - return res.Error - } - if res.RowsAffected == 0 { - return ErrNotFound - } - return nil +func Delete(coll, extID string) error { + return mongoDelete(coll, extID) } // BodyMap 解析 body @@ -187,31 +99,13 @@ func ToResponse(d *model.WzDocument) map[string]interface{} { } // Count 集合文档数 -func Count(db *gorm.DB, coll string) (int64, error) { - var n int64 - err := db.Model(&model.WzDocument{}).Where("coll = ?", coll).Count(&n).Error - return n, err +func Count(coll string) (int64, error) { + return mongoCount(coll) } // FindApprovedStreamerForUser body.userId 且 status=approved -func FindApprovedStreamerForUser(db *gorm.DB, userID string) (*model.WzDocument, error) { - var rows []model.WzDocument - err := db.Where("coll = ?", CollStreamers). - Where("JSON_UNQUOTE(JSON_EXTRACT(body,'$.userId')) = ?", userID). - Find(&rows).Error - if err != nil { - return nil, err - } - for i := range rows { - m, e := BodyMap(&rows[i]) - if e != nil { - continue - } - if strFromMap(m, "status") == "approved" { - return &rows[i], nil - } - } - return nil, ErrNotFound +func FindApprovedStreamerForUser(userID string) (*model.WzDocument, error) { + return mongoFindApprovedStreamerForUser(userID) } func strFromMap(m map[string]interface{}, k string) string { @@ -231,18 +125,27 @@ func strFromMap(m map[string]interface{}, k string) string { return raw } -// FindLiveRoomByStreamerExtID streamerId 存 body 为 hex 字符串 -func FindLiveRoomByStreamerID(db *gorm.DB, streamerExtID string) (*model.WzDocument, error) { - var rows []model.WzDocument - err := db.Where("coll = ?", CollLiveRooms). - Where("JSON_UNQUOTE(JSON_EXTRACT(body,'$.streamerId')) = ?", streamerExtID). - Find(&rows).Error - if err != nil { - return nil, err - } - if len(rows) == 0 { - // legacy: body.streamerId 可能是嵌套对象 id — 再试 Mongo ObjectId 字符串匹配整 body - return nil, ErrNotFound - } - return &rows[0], nil +// FindLiveRoomByStreamerExtID streamerId 匹配 ext_id +func FindLiveRoomByStreamerID(streamerExtID string) (*model.WzDocument, error) { + return mongoFindLiveRoomByStreamerID(streamerExtID) +} + +// CountBindingsForStreamer streamerBindings.streamerId +func CountBindingsForStreamer(streamerExtID string) (int64, error) { + return mongoCountBindingsForStreamer(streamerExtID) +} + +// ListBindingsForStreamer 绑定列表 +func ListBindingsForStreamer(streamerExtID string) ([]model.WzDocument, error) { + return mongoListBindingsForStreamer(streamerExtID) +} + +// CountJourneyDedup 旅程事件去重(sessionId + clientEventId) +func CountJourneyDedup(sessionID, clientID string) (int64, error) { + return mongoCountJourneyDedup(sessionID, clientID) +} + +// CountActiveBindingsForUser userId+status=active +func CountActiveBindingsForUser(userID string) (int64, error) { + return mongoCountActiveBindingsForUser(userID) } diff --git a/new/wz-api/internal/wzstore/mongo_doc.go b/new/wz-api/internal/wzstore/mongo_doc.go new file mode 100644 index 0000000..8fef217 --- /dev/null +++ b/new/wz-api/internal/wzstore/mongo_doc.go @@ -0,0 +1,319 @@ +package wzstore + +import ( + "context" + "encoding/json" + "errors" + "strings" + "time" + + "wz-api/internal/database" + "wz-api/internal/model" + + "go.mongodb.org/mongo-driver/bson" + mongodriver "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "gorm.io/datatypes" +) + +const mongoTimeout = 25 * time.Second + +func mongoCtx() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), mongoTimeout) +} + +func mcoll(name string) *mongodriver.Collection { + return database.MongoDatabase().Collection(name) +} + +func extIDFilter(extID string) bson.M { + return bson.M{"$or": []bson.M{ + {"ext_id": extID}, + {"extId": extID}, + }} +} + +func rawDocToWzDocument(collName string, raw bson.M) (model.WzDocument, error) { + extID := pickExtID(raw) + if extID == "" { + return model.WzDocument{}, errors.New("wzstore mongo: missing ext_id") + } + bodyMap := make(map[string]interface{}) + for k, v := range raw { + if k == "_id" || k == "ext_id" || k == "extId" { + continue + } + bodyMap[k] = v + } + rawJSON, err := json.Marshal(bodyMap) + if err != nil { + return model.WzDocument{}, err + } + return model.WzDocument{Coll: collName, ExtID: extID, Body: datatypes.JSON(rawJSON)}, nil +} + +func pickExtID(raw bson.M) string { + if v := raw["ext_id"]; v != nil { + if s, ok := v.(string); ok { + return s + } + } + if v := raw["extId"]; v != nil { + if s, ok := v.(string); ok { + return s + } + } + return "" +} + +// mongoListDocuments 按集合读取文档。 +func mongoListDocuments(collName string, limit int) ([]model.WzDocument, error) { + ctx, cancel := mongoCtx() + defer cancel() + if limit <= 0 || limit > 2000 { + limit = 500 + } + cur, err := mcoll(collName).Find(ctx, bson.M{}, options.Find(). + SetSort(bson.D{{Key: "_id", Value: -1}}). + SetLimit(int64(limit))) + if err != nil { + return nil, err + } + defer cur.Close(ctx) + var out []model.WzDocument + for cur.Next(ctx) { + var raw bson.M + if err := cur.Decode(&raw); err != nil { + continue + } + d, err := rawDocToWzDocument(collName, raw) + if err != nil { + continue + } + out = append(out, d) + } + return out, cur.Err() +} + +func mongoGet(collName, extID string) (*model.WzDocument, error) { + ctx, cancel := mongoCtx() + defer cancel() + var raw bson.M + err := mcoll(collName).FindOne(ctx, extIDFilter(extID)).Decode(&raw) + if err != nil { + if errors.Is(err, mongodriver.ErrNoDocuments) { + return nil, ErrNotFound + } + return nil, err + } + d, err := rawDocToWzDocument(collName, raw) + if err != nil { + return nil, err + } + return &d, nil +} + +func mongoFindByField(collName, key, val string) (*model.WzDocument, error) { + ctx, cancel := mongoCtx() + defer cancel() + var raw bson.M + err := mcoll(collName).FindOne(ctx, bson.M{key: val}).Decode(&raw) + if err != nil { + if errors.Is(err, mongodriver.ErrNoDocuments) { + return nil, ErrNotFound + } + return nil, err + } + d, err := rawDocToWzDocument(collName, raw) + if err != nil { + return nil, err + } + return &d, nil +} + +func jsonPathToKey(path string) string { + if strings.HasPrefix(path, "$.") { + return path[2:] + } + return path +} + +func mongoInsert(collName string, body map[string]interface{}) (string, error) { + ctx, cancel := mongoCtx() + defer cancel() + extID := newExtID() + now := time.Now().UTC().Format(time.RFC3339) + if body == nil { + body = map[string]interface{}{} + } + doc := bson.M{"ext_id": extID} + for k, v := range body { + if k == "ext_id" || k == "extId" || k == "_id" { + continue + } + doc[k] = v + } + if _, ok := doc["createdAt"]; !ok { + doc["createdAt"] = now + } + doc["updatedAt"] = now + if _, err := mcoll(collName).InsertOne(ctx, doc); err != nil { + return "", err + } + return extID, nil +} + +func mongoUpsertByExtID(collName, extID string, body map[string]interface{}) error { + if extID == "" { + return errors.New("ext_id required") + } + _, err := mongoGet(collName, extID) + if errors.Is(err, ErrNotFound) { + now := time.Now().UTC().Format(time.RFC3339) + if body == nil { + body = map[string]interface{}{} + } + doc := bson.M{"ext_id": extID} + for k, v := range body { + if k == "ext_id" || k == "extId" || k == "_id" { + continue + } + doc[k] = v + } + if doc["createdAt"] == nil { + doc["createdAt"] = now + } + doc["updatedAt"] = now + ctx, cancel := mongoCtx() + defer cancel() + _, err := mcoll(collName).InsertOne(ctx, doc) + return err + } + if err != nil { + return err + } + return mongoUpdateJSONPartial(collName, extID, body) +} + +func mongoUpdateJSONPartial(collName, extID string, patch map[string]interface{}) error { + d, err := mongoGet(collName, extID) + if err != nil { + return err + } + merged, err := mergeJSON(d.Body, patch) + if err != nil { + return err + } + merged["updatedAt"] = time.Now().UTC().Format(time.RFC3339) + + ctx, cancel := mongoCtx() + defer cancel() + var raw bson.M + if err := mcoll(collName).FindOne(ctx, extIDFilter(extID)).Decode(&raw); err != nil { + return err + } + id := raw["_id"] + for k, v := range merged { + raw[k] = v + } + raw["_id"] = id + raw["ext_id"] = extID + delete(raw, "extId") + _, err = mcoll(collName).ReplaceOne(ctx, bson.M{"_id": id}, raw) + return err +} + +func mongoDelete(collName, extID string) error { + ctx, cancel := mongoCtx() + defer cancel() + res, err := mcoll(collName).DeleteOne(ctx, extIDFilter(extID)) + if err != nil { + return err + } + if res.DeletedCount == 0 { + return ErrNotFound + } + return nil +} + +func mongoCount(collName string) (int64, error) { + ctx, cancel := mongoCtx() + defer cancel() + return mcoll(collName).CountDocuments(ctx, bson.M{}) +} + +func mongoFindApprovedStreamerForUser(userID string) (*model.WzDocument, error) { + ctx, cancel := mongoCtx() + defer cancel() + cur, err := mcoll(CollStreamers).Find(ctx, bson.M{"userId": userID}) + if err != nil { + return nil, err + } + defer cur.Close(ctx) + for cur.Next(ctx) { + var raw bson.M + if err := cur.Decode(&raw); err != nil { + continue + } + d, err := rawDocToWzDocument(CollStreamers, raw) + if err != nil { + continue + } + m, _ := BodyMap(&d) + if strFromMap(m, "status") == "approved" { + return &d, nil + } + } + return nil, ErrNotFound +} + +func mongoFindLiveRoomByStreamerID(streamerExtID string) (*model.WzDocument, error) { + return mongoFindByField(CollLiveRooms, "streamerId", streamerExtID) +} + +func mongoCountBindingsForStreamer(streamerExtID string) (int64, error) { + ctx, cancel := mongoCtx() + defer cancel() + return mcoll(CollStreamerBindings).CountDocuments(ctx, bson.M{"streamerId": streamerExtID}) +} + +func mongoListBindingsForStreamer(streamerExtID string) ([]model.WzDocument, error) { + ctx, cancel := mongoCtx() + defer cancel() + cur, err := mcoll(CollStreamerBindings).Find(ctx, bson.M{"streamerId": streamerExtID}) + if err != nil { + return nil, err + } + defer cur.Close(ctx) + var out []model.WzDocument + for cur.Next(ctx) { + var raw bson.M + if err := cur.Decode(&raw); err != nil { + continue + } + d, err := rawDocToWzDocument(CollStreamerBindings, raw) + if err != nil { + continue + } + out = append(out, d) + } + return out, cur.Err() +} + +func mongoCountJourneyDedup(sessionID, clientID string) (int64, error) { + ctx, cancel := mongoCtx() + defer cancel() + return mcoll(CollUserJourneyEvents).CountDocuments(ctx, bson.M{ + "sessionId": sessionID, + "clientEventId": clientID, + }) +} + +func mongoCountActiveBindingsForUser(userID string) (int64, error) { + ctx, cancel := mongoCtx() + defer cancel() + return mcoll(CollStreamerBindings).CountDocuments(ctx, bson.M{ + "userId": userID, + "status": "active", + }) +} diff --git a/new/wz-api/scripts/mongo-local-practice.js b/new/wz-api/scripts/mongo-local-practice.js new file mode 100644 index 0000000..bcfaaa2 --- /dev/null +++ b/new/wz-api/scripts/mongo-local-practice.js @@ -0,0 +1,34 @@ +/** + * 本地 Docker Mongo(wanzhi-mongodb 等)练手:创建库 + users 集合示例文档。 + * + * 连接(与容器环境变量 MONGO_INITDB_ROOT_* 一致时): + * mongosh "mongodb://admin:admin123@127.0.0.1:27017/wanzhi_local?authSource=admin" + * + * 在 mongosh 内执行: + * load("e:/.../new/wz-api/scripts/mongo-local-practice.js") + * + * 说明:wz-api 管理端「玩值用户列表」读的是 MySQL 表 wz_documents(coll='users'), + * 不会读本脚本写入的 Mongo 集合;本脚本仅用于熟悉 Mongo / Compass / 与旧栈集合名对齐。 + */ + +const dbName = "wanzhi_local"; +const collName = "users"; + +db = db.getSiblingDB(dbName); + +db[collName].insertMany([ + { + name: "练习用户", + phone: "13800138000", + // 以下为文档字段示意;玩值 HTTP 登录不走 Mongo 此密码 + loginUsername: "admin", + loginPasswordPlain: "admin123", + status: "active", + isVip: false, + balance: 0, + createdAt: new Date(), + updatedAt: new Date(), + }, +]); + +print(`OK: ${dbName}.${collName} count = ${db[collName].countDocuments({})}`); diff --git a/new/wz-api/scripts/mysql-seed-admin-login.sql b/new/wz-api/scripts/mysql-seed-admin-login.sql new file mode 100644 index 0000000..47210fa --- /dev/null +++ b/new/wz-api/scripts/mysql-seed-admin-login.sql @@ -0,0 +1,41 @@ +-- 管理端 wz-admin:POST /api/admin 登录账号(username + password) +-- 代码优先查 MySQL `admin_users`,使用 bcrypt 校验 `password_hash`。 +-- 默认口令 admin123 的哈希由 golang.org/x/crypto/bcrypt DefaultCost 生成(与 AdminLogin 一致)。 +-- +-- 在 .env.development 指向的库执行(示例为 souldev,请按实际 DB 名修改 USE)。 +-- 若 SKIP_AUTO_MIGRATE=1 且从未全量迁库,可能尚无 admin_users 表,下面含 CREATE IF NOT EXISTS。 + +USE souldev; + +CREATE TABLE IF NOT EXISTS admin_users ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + username VARCHAR(64) NOT NULL, + password_hash VARCHAR(128) NOT NULL, + role VARCHAR(32) NOT NULL DEFAULT 'admin', + name VARCHAR(64) DEFAULT NULL, + status VARCHAR(16) NOT NULL DEFAULT 'active', + created_at DATETIME(3) NULL, + updated_at DATETIME(3) NULL, + deleted_at DATETIME(3) NULL, + PRIMARY KEY (id), + UNIQUE KEY idx_admin_users_username (username), + KEY idx_admin_users_deleted_at (deleted_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT INTO admin_users (username, password_hash, role, name, status, created_at, updated_at) +VALUES ( + 'admin', + '$2a$10$f1SdpLVBRhy7hbq6XqmE5OSauzFg1etbCDBdsyIiKxc.Pm7ETBkBq', + 'super_admin', + '本地练习', + 'active', + UTC_TIMESTAMP(3), + UTC_TIMESTAMP(3) +) +ON DUPLICATE KEY UPDATE + password_hash = VALUES(password_hash), + role = VALUES(role), + status = VALUES(status), + name = VALUES(name), + updated_at = UTC_TIMESTAMP(3), + deleted_at = NULL; diff --git a/new/wz-api/scripts/mysql-seed-wz-document-user.sql b/new/wz-api/scripts/mysql-seed-wz-document-user.sql new file mode 100644 index 0000000..0875add --- /dev/null +++ b/new/wz-api/scripts/mysql-seed-wz-document-user.sql @@ -0,0 +1,17 @@ +-- 玩值管理端 GET /api/admin/wz/users:数据来自 MySQL `wz_documents`,其中 coll = 'users', +-- body 为 JSON(与旧 Mongo 集合 users 迁移策略一致)。 +-- 在云库 souldev(或你的 DB)执行;表须已存在(SKIP_AUTO_MIGRATE=1 时启动会尝试 AutoMigrate WzDocument)。 + +USE souldev; + +INSERT INTO wz_documents (`coll`, `ext_id`, `body`, `created_at`, `updated_at`) +VALUES ( + 'users', + 'localdevuser000000000001', + CAST('{"name":"列表练习用户","phone":"13900139000","status":"active","isVip":false,"balance":88,"createdAt":"2026-04-29T00:00:00Z","updatedAt":"2026-04-29T00:00:00Z"}' AS JSON), + UTC_TIMESTAMP(3), + UTC_TIMESTAMP(3) +) +ON DUPLICATE KEY UPDATE + `body` = VALUES(`body`), + `updated_at` = UTC_TIMESTAMP(3); diff --git a/new/wz-api/soul-api b/new/wz-api/soul-api deleted file mode 100644 index ebc4045..0000000 Binary files a/new/wz-api/soul-api and /dev/null differ diff --git a/new/wz-app/.env.development b/new/wz-app/.env.development index f19c7d2..b77b3d1 100644 --- a/new/wz-app/.env.development +++ b/new/wz-app/.env.development @@ -1,4 +1,6 @@ -# 须与 new/wz-api 的 PORT 一致(本仓库 .env.development 默认 48080) -WZ_API_PROXY_TARGET=http://127.0.0.1:48080 -# 可填完整根地址以绕过代理直连(通常不必) -VITE_API_BASE_URL=https://wzapi.quwanzhi.com +# 须与 new/wz-api 的 PORT 一致(与生产一致,默认 4040) +WZ_API_PROXY_TARGET=http://127.0.0.1:4040 + +# 本地 / 手机连电脑 IP 调试:不要填 VITE_API_BASE_URL,保持走当前站点同源 /api → Vite 代理到本机 wz-api。 +# 仅当刻意直连某套后端时取消注释并改为完整根地址(无尾斜杠),例如: +# VITE_API_BASE_URL=https://wzapi.quwanzhi.com diff --git a/new/wz-app/.env.example b/new/wz-app/.env.example index 9421a02..5fb3414 100644 --- a/new/wz-app/.env.example +++ b/new/wz-app/.env.example @@ -1,8 +1,9 @@ # 生产/预览:填写 wz-api 根地址,无尾斜杠 # -# 开发:WZ_API_PROXY_TARGET 须与 new/wz-api 的 PORT 一致(本仓库默认 48080) -WZ_API_PROXY_TARGET=http://127.0.0.1:48080 -# 留空则走 /api 代理;直连远端或本机 API 时填写完整根地址,如 https://wzapi.quwanzhi.com -VITE_API_BASE_URL=https://wzapi.quwanzhi.com +# 开发:WZ_API_PROXY_TARGET 须与 new/wz-api 的 PORT 一致(本仓库默认 4040,与生产 listen 一致) +WZ_API_PROXY_TARGET=http://127.0.0.1:4040 +# 开发默认留空/不设置:浏览器请求同源 /api,由 Vite 代理到 WZ_API_PROXY_TARGET。 +# 预览或直连线上时才填,例如: +# VITE_API_BASE_URL=https://wzapi.quwanzhi.com # 可选别名,与 VITE_API_BASE_URL 二选一 # VITE_WZ_API_BASE= diff --git a/new/wz-app/README.md b/new/wz-app/README.md index 411b0f5..b769d69 100644 --- a/new/wz-app/README.md +++ b/new/wz-app/README.md @@ -9,14 +9,15 @@ ## 本地开发(必读,避免登录 /api 502) -1. **先启动后端** `new/wz-api`(需 `DB_DSN` 等;**`PORT` 见 `new/wz-api/.env.development`,本仓库默认 `48080`**)。可访问 `http://127.0.0.1:48080/health` 确认存活。 +1. **先启动后端** `new/wz-api`(需 `DB_DSN` 等;**`PORT` 见 `new/wz-api/.env.development`,与生产一致默认 `4040`**)。可访问 `http://127.0.0.1:4040/health` 确认存活。 2. 本目录执行 `npm install` → **`npm run dev`**(Vite 默认 5173)。 -3. 前端请求 `/api/...` 由 Vite 代理到 **`WZ_API_PROXY_TARGET`**(**必须与 wz-api 的 `PORT` 一致**;与仓库默认对齐为 **48080**)。 -4. **手机/局域网**访问 dev:使用 `npm run dev`(已配置 `--host` 时见 `package.json`),用电脑 IP + `:5173`(或终端里给出的端口如 `5174`)打开;此时 `/api` 仍由运行 Vite 的电脑本机代理到 `127.0.0.1:PORT`(若手机需直连 wz-api,可设 `VITE_API_BASE_URL=http://电脑IP:PORT` 并保证 wz-api 监听 `0.0.0.0`、防火墙放行 **同一 PORT**)。 +3. `.env.development` 中 **`VITE_API_BASE_URL` / `VITE_WZ_API_BASE` 请留空**(或不写),让浏览器走**同源** `/api` → Vite 代理;若填成线上 `https://wzapi...`,登录/注册会打到**远端**而非本机 wz-api。 +4. 前端请求 `/api/...` 由 Vite 代理到 **`WZ_API_PROXY_TARGET`**(**必须与 wz-api 的 `PORT` 一致**;与仓库默认对齐为 **4040**)。 +5. **手机/局域网**访问 dev:使用 `npm run dev`(已配置 `--host` 时见 `package.json`),用电脑 IP + `:5173`(或终端里给出的端口如 `5174`)打开;此时 `/api` 仍由运行 Vite 的电脑本机代理到 `127.0.0.1:PORT`(若手机需直连 wz-api,可设 `VITE_API_BASE_URL=http://电脑IP:PORT` 并保证 wz-api 监听 `0.0.0.0`、防火墙放行 **同一 PORT**)。 -5. **`GET /api/app/mall/point-cards` 在浏览器里 404**:先**绕过 Vite**访问后端根地址(端口与 wz-api 的 `PORT` 一致,默认 `48080`):`http://127.0.0.1:48080/api/app/mall/point-cards`。 +6. **`GET /api/app/mall/point-cards` 在浏览器里 404**:先**绕过 Vite**访问后端根地址(端口与 wz-api 的 `PORT` 一致,默认 `4040`):`http://127.0.0.1:4040/api/app/mall/point-cards`。 - 若该直连仍是 **404**,而 **`GET /api/app/streamers` 为 200**,说明当前正在跑的 wz-api **可执行文件过旧、未编进点卡路由**,与仓库 `new/wz-api` 不一致;请在 `new/wz-api` 执行 `go build -o 你的路径/wz-api.exe ./cmd/server` 或 `go run ./cmd/server`,**停掉旧进程后**用新构建重启。 - - 若直连为 **502/连不上** 而前端 404,多为未启动 wz-api 或 `WZ_API_PROXY_TARGET` 与 wz-api `PORT` 不一致(见上第 1、3 步)。 + - 若直连为 **502/连不上** 而前端 404,多为未启动 wz-api 或 `WZ_API_PROXY_TARGET` 与 wz-api `PORT` 不一致(见上第 1、4 步)。 与请求封装、错误提示:`src/lib/request.ts`、`src/lib/config.ts`。 diff --git a/new/wz-app/dist/index.html b/new/wz-app/dist/index.html index 85c73da..6011320 100644 --- a/new/wz-app/dist/index.html +++ b/new/wz-app/dist/index.html @@ -11,7 +11,7 @@ rel="stylesheet" />