更新wz-admin和wz-api项目,调整环境变量配置以支持MongoDB连接,移除MySQL相关代码,确保API端口与前端一致,优化路由处理逻辑,提升系统性能与可维护性,同时更新文档以反映最新的架构变更。
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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()],
|
||||
|
||||
@@ -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=
|
||||
|
||||
|
||||
15
new/wz-api/.env.docker.example
Normal file
15
new/wz-api/.env.docker.example
Normal file
@@ -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
|
||||
@@ -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=
|
||||
|
||||
|
||||
@@ -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**,路径不变。
|
||||
|
||||
227
new/wz-api/cmd/seedmongo/main.go
Normal file
227
new/wz-api/cmd/seedmongo/main.go
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": "绑定成功"})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
288
new/wz-api/internal/store/mongo/auth.go
Normal file
288
new/wz-api/internal/store/mongo/auth.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
319
new/wz-api/internal/wzstore/mongo_doc.go
Normal file
319
new/wz-api/internal/wzstore/mongo_doc.go
Normal file
@@ -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",
|
||||
})
|
||||
}
|
||||
34
new/wz-api/scripts/mongo-local-practice.js
Normal file
34
new/wz-api/scripts/mongo-local-practice.js
Normal file
@@ -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({})}`);
|
||||
41
new/wz-api/scripts/mysql-seed-admin-login.sql
Normal file
41
new/wz-api/scripts/mysql-seed-admin-login.sql
Normal file
@@ -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;
|
||||
17
new/wz-api/scripts/mysql-seed-wz-document-user.sql
Normal file
17
new/wz-api/scripts/mysql-seed-wz-document-user.sql
Normal file
@@ -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);
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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`。
|
||||
|
||||
|
||||
2
new/wz-app/dist/index.html
vendored
2
new/wz-app/dist/index.html
vendored
@@ -11,7 +11,7 @@
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>玩值电竞 - WanZhi Esports</title>
|
||||
<script type="module" crossorigin src="/assets/index-mBioFLhP.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CnJ4AXBQ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CeISOfIA.css">
|
||||
</head>
|
||||
<body class="min-h-screen antialiased">
|
||||
|
||||
@@ -119,11 +119,22 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
|
||||
try {
|
||||
const r = await appAuthLogin(phone, password)
|
||||
const d = r.data
|
||||
if (d?.token) setToken(d.token)
|
||||
if (!d?.token) {
|
||||
toast({ title: "登录失败", description: "未返回 token,请检查 wz-api 与网络" })
|
||||
return false
|
||||
}
|
||||
setToken(d.token)
|
||||
setAuthChecked(false)
|
||||
await syncFromDatabase()
|
||||
if (!getToken()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: "登录失败",
|
||||
description: e instanceof Error ? e.message : "请核对手机号与密码",
|
||||
})
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
/**
|
||||
* 与 [new/wz-app/src/utils/config.js] 一致:
|
||||
* - 优先 `VITE_API_BASE_URL`,其次 `VITE_WZ_API_BASE`(计划中的命名);
|
||||
* - 开发时若未配置:用空串走 **Vite 代理** `/api` → wz-api,避免 CORS;
|
||||
* - 非开发且未配置:回退 `http://127.0.0.1:48080`(与 new/wz-api `.env.development` 中 PORT 一致;`vite preview` 直连 API 时用)。
|
||||
* - 优先 `VITE_API_BASE_URL`,其次 `VITE_WZ_API_BASE`(无尾斜杠);
|
||||
* - **开发 (vite dev)**:若两变量均为空,返回 `''`,请求走**同源** `/api` → Vite 代理到本机 wz-api(避免误配线上基址导致登录打远端)。
|
||||
* - **构建预览 / 生产**:未配置时回退 `http://127.0.0.1:4040`(与 wz-api 默认监听一致)。
|
||||
*/
|
||||
const WZ_DEFAULT_BASE = "http://127.0.0.1:48080"
|
||||
const WZ_DEFAULT_BASE = "http://127.0.0.1:4040"
|
||||
|
||||
function trimmedEnv(name: "VITE_API_BASE_URL" | "VITE_WZ_API_BASE"): string {
|
||||
const raw = import.meta.env[name]
|
||||
if (raw === undefined || raw === null) return ""
|
||||
const s = String(raw).trim()
|
||||
return s
|
||||
}
|
||||
|
||||
export function getApiBase(): string {
|
||||
const a = import.meta.env.VITE_API_BASE_URL
|
||||
const b = import.meta.env.VITE_WZ_API_BASE
|
||||
const a = trimmedEnv("VITE_API_BASE_URL")
|
||||
const b = trimmedEnv("VITE_WZ_API_BASE")
|
||||
for (const v of [a, b]) {
|
||||
if (v !== undefined && String(v).trim() !== "") {
|
||||
return String(v).replace(/\/$/, "")
|
||||
}
|
||||
if (v !== "") return v.replace(/\/$/, "")
|
||||
}
|
||||
if (import.meta.env.DEV) return ""
|
||||
return WZ_DEFAULT_BASE
|
||||
|
||||
@@ -54,7 +54,7 @@ export async function request<T = unknown>(path: string, options: RequestOptions
|
||||
if (!res.ok) {
|
||||
if (res.status === 502 || res.status === 503 || res.status === 504) {
|
||||
throw new Error(
|
||||
"玩值接口无响应。请启动 new/wz-api(端口须与 WZ_API_PROXY_TARGET 一致,本仓库默认 48080),并核对 Vite 代理。",
|
||||
"玩值接口无响应。请启动 new/wz-api(端口须与 WZ_API_PROXY_TARGET 一致,本仓库默认 4040),并核对 Vite 代理。",
|
||||
)
|
||||
}
|
||||
throw new Error(`HTTP ${res.status}`)
|
||||
@@ -75,7 +75,7 @@ export async function request<T = unknown>(path: string, options: RequestOptions
|
||||
if (!res.ok) {
|
||||
if (res.status === 502 || res.status === 503 || res.status === 504) {
|
||||
throw new Error(
|
||||
"玩值接口无响应(网关错误)。请启动 `new/wz-api`,并保证 `new/wz-app/.env.development` 里 WZ_API_PROXY_TARGET 与 wz-api 的 PORT 一致(本仓库默认 48080)。",
|
||||
"玩值接口无响应(网关错误)。请启动 `new/wz-api`,并保证 `new/wz-app/.env.development` 里 WZ_API_PROXY_TARGET 与 wz-api 的 PORT 一致(本仓库默认 4040)。",
|
||||
)
|
||||
}
|
||||
const msg =
|
||||
|
||||
@@ -8,8 +8,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
// `process.env.XXX` 在 vite 配置中不会自动读 .env,需 loadEnv,否则改 .env 不生效、易出现代理 502
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), "")
|
||||
// 与 new/wz-api 开发默认 PORT=48080(.env.development)一致
|
||||
const apiTarget = (env.WZ_API_PROXY_TARGET || "http://127.0.0.1:48080").replace(/\/$/, "")
|
||||
// 与 new/wz-api 开发/生产默认 PORT=4040(.env.development / master.py)一致
|
||||
const apiTarget = (env.WZ_API_PROXY_TARGET || "http://127.0.0.1:4040").replace(/\/$/, "")
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
约定:Next 中文件 `app/api/<segments>/route.ts` 对应 URL 为 `/api/<segments>`(动态段为 `[param]`)。新路径列遵循 [迁移-API前缀决议.md](迁移-API前缀决议.md)。**玩值管理端**与 Soul 历史路由隔离,统一 **`/api/admin/wz/*`**。
|
||||
**玩值 C 端**新功能以 **`new/wz-app` + wz-api `/api/app/*`** 为真源;**`old/app`** 与下表「old URL」列用于 **溯源与漏项对照**,不在 `old` 扩新业务能力。
|
||||
|
||||
**Mongo-only(wz-api 当前默认二进制)**
|
||||
- **仍挂载**:`/api/app/*`、`/api/screen/*`、`/api/public/build-info`、`POST/GET /api/admin`(登录与鉴权检查)、`/api/admin/auth/*`、**`/api/admin/wz/*`**(及 `RegisterWanzhiAdminRoutes` 下与 old Next 对齐的 mock 路径)。
|
||||
- **已自该构建移除(未恢复则用不到)**:全体 **`/api/miniprogram/*`**、**`/api/db/*`**、书籍/章节/支付/提现/匹配等原 Soul 路由;**`GET /read/:id`**、**`GET /ws/miniprogram`**。若需上述能力须另开带 MySQL 的 legacy 构建或重新实现为 Mongo。
|
||||
- **数据**:玩值集合以 Mongo **`ext_id`** 为主键字段;C 端账号在 **`app_users`** / **`user_auths`**,勿与玩值列表集合 **`users`** 混淆。详见 `new/wz-api/MIGRATION.md`。
|
||||
|
||||
**状态**:`未实现` | `进行中` | `已实现` | `废弃`
|
||||
|
||||
| # | old URL(逻辑路径) | 主要调用方 | 新 wz-api 路径 | 方法 | 状态 |
|
||||
|
||||
Reference in New Issue
Block a user