feat(db,docs): 优化数据库连接池与生产日志,新增 Web 访客端接入指南
This commit is contained in:
@@ -288,13 +288,12 @@ cd web && npx tsc --noEmit
|
||||
|
||||
---
|
||||
|
||||
## 设计稿与需求
|
||||
## 相关文档
|
||||
|
||||
`docs/` 下保留各页面 HTML 原型与需求说明:
|
||||
|
||||
- `docs/spec/` 开发需求
|
||||
- `docs/agent/` 租户端页面
|
||||
- `docs/admin/` 平台端页面
|
||||
- **[访客端对接与嵌入指南](docs/INTEGRATION_GUIDE.md)**(外部网站/SPA/App 快速集成 Widget)
|
||||
- `docs/spec/` 开发需求与原型
|
||||
- `docs/agent/` 租户坐席端页面
|
||||
- `docs/admin/` 平台管理端页面
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ DB_USER=kefu
|
||||
DB_PASSWORD=请改成强密码
|
||||
DB_NAME=kefu_cloud
|
||||
DB_SSLMODE=disable
|
||||
# 数据库连接池(可选,默认 50/10/60m/15m)
|
||||
# DB_MAX_OPEN_CONNS=50
|
||||
# DB_MAX_IDLE_CONNS=10
|
||||
# DB_CONN_MAX_LIFETIME_MINUTES=60
|
||||
# DB_CONN_MAX_IDLE_MINUTES=15
|
||||
|
||||
# ========== JWT(deploy.sh 可自动生成)==========
|
||||
JWT_SECRET=请改成随机长字符串
|
||||
|
||||
@@ -98,6 +98,10 @@ services:
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
DB_NAME: ${DB_NAME:-kefu_cloud}
|
||||
DB_SSLMODE: ${DB_SSLMODE:-disable}
|
||||
DB_MAX_OPEN_CONNS: ${DB_MAX_OPEN_CONNS:-50}
|
||||
DB_MAX_IDLE_CONNS: ${DB_MAX_IDLE_CONNS:-10}
|
||||
DB_CONN_MAX_LIFETIME_MINUTES: ${DB_CONN_MAX_LIFETIME_MINUTES:-60}
|
||||
DB_CONN_MAX_IDLE_MINUTES: ${DB_CONN_MAX_IDLE_MINUTES:-15}
|
||||
JWT_SECRET: ${JWT_SECRET:?请设置 JWT_SECRET}
|
||||
CORS_ALLOW_ORIGINS: ${CORS_ALLOW_ORIGINS:-*}
|
||||
STORAGE_ENDPOINT: minio:9000
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
# 在线客服 Web 端接入指南
|
||||
|
||||
本文档面向接入在线客服的网站开发与运维人员,帮助您在 1 分钟内将客服系统集成至您的企业官网、电商商城、Web 业务系统或单页应用(SPA)中。
|
||||
|
||||
---
|
||||
|
||||
## 1. 接入前准备
|
||||
|
||||
您只需要向客服系统管理员获取一个专属的 **渠道标识(Channel Key)**:
|
||||
|
||||
| 参数 | 说明 | 示例 |
|
||||
| :--- | :--- | :--- |
|
||||
| **服务地址** | 客服系统统一入口 | `https://cloud.400cc.cn/` |
|
||||
| **渠道标识(data-id)** | 您企业专属的渠道密钥 | `WK_8a3f2e` |
|
||||
|
||||
> 💡 **获取方式**:登录客服工作台 -> 左侧菜单「系统设置」 -> 「渠道管理」 -> 复制您渠道的专属代码(其中的 `data-id` 即为您的 Channel Key)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 快速接入(标准网页 / HTML)
|
||||
|
||||
只需在您网站 HTML 页面的 `</body>` 标签前添加一行 JavaScript 代码即可完成接入:
|
||||
|
||||
```html
|
||||
<!-- 在线客服组件 -->
|
||||
<script
|
||||
src="https://cloud.400cc.cn/widget.js"
|
||||
data-id="WK_8a3f2e"
|
||||
async>
|
||||
</script>
|
||||
```
|
||||
|
||||
### 接入效果
|
||||
引入脚本后,页面右下角将自动挂载悬浮客服图标:
|
||||
* **点击展开**:弹出响应式聊天窗口(默认 400×600px,移动端浏览器自动适配全屏)。
|
||||
* **自动提醒**:客服回复时,访客端自动显示未读消息红点徽标与声音提示。
|
||||
* **最小化与恢复**:访客可随时收起或重新唤起聊天窗,聊天记录在同一浏览器内自动保留。
|
||||
|
||||
---
|
||||
|
||||
## 3. 自定义触发按钮(用您网页中的按钮打开客服)
|
||||
|
||||
如果您希望点击网页中现有的导航栏「在线咨询」、商品详情页的「联系客服」按钮来打开对话框,可以使用 SDK 提供的标准 JavaScript API。
|
||||
|
||||
### 3.1 代码示例
|
||||
```html
|
||||
<!-- 您的业务按钮 -->
|
||||
<button type="button" onclick="openKefu()">联系在线客服</button>
|
||||
|
||||
<script>
|
||||
function openKefu() {
|
||||
if (window.KefuWidget) {
|
||||
window.KefuWidget.open();
|
||||
} else {
|
||||
alert('客服组件正在加载中,请稍候...');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 3.2 JavaScript API 速查
|
||||
|
||||
| 方法 / 属性 | 说明 | 示例 |
|
||||
| :--- | :--- | :--- |
|
||||
| `window.KefuWidget.open()` | 打开客服聊天窗口 | `window.KefuWidget.open()` |
|
||||
| `window.KefuWidget.close()` | 收起 / 关闭客服聊天窗口 | `window.KefuWidget.close()` |
|
||||
| `window.KefuWidget.channelKey` | 获取当前生效的渠道 Key | `console.log(window.KefuWidget.channelKey)` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 现代单页应用接入(Vue / React / Next.js)
|
||||
|
||||
客服组件**原生适配单页应用(SPA)**。组件内部会自动侦听浏览器的路由变化(`history.pushState`、`replaceState`、`popstate` 以及页面 `<title>` 变更),无需您手写任何额外打点,客服端坐席就能实时看到访客当前正浏览哪个页面或商品。
|
||||
|
||||
### 4.1 Vue 3 / Vite 接入
|
||||
直接在项目根目录的 `index.html` 引入(推荐):
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>企业官网</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
|
||||
<!-- 在线客服挂载 -->
|
||||
<script src="https://cloud.400cc.cn/widget.js" data-id="WK_8a3f2e" async></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
若需在特定组件或登录成功后动态加载:
|
||||
```vue
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
onMounted(() => {
|
||||
if (window.__KEFU_WIDGET_LOADED__) return
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://cloud.400cc.cn/widget.js'
|
||||
script.setAttribute('data-id', 'WK_8a3f2e')
|
||||
script.async = true
|
||||
document.body.appendChild(script)
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### 4.2 React / Next.js 接入
|
||||
在根组件或页面布局中引入:
|
||||
```tsx
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export function KefuWidget() {
|
||||
useEffect(() => {
|
||||
if ((window as any).__KEFU_WIDGET_LOADED__) return
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://cloud.400cc.cn/widget.js'
|
||||
script.setAttribute('data-id', 'WK_8a3f2e')
|
||||
script.async = true
|
||||
document.body.appendChild(script)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
Next.js 推荐写法:
|
||||
```tsx
|
||||
import Script from 'next/script'
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body>
|
||||
{children}
|
||||
<Script
|
||||
src="https://cloud.400cc.cn/widget.js"
|
||||
data-id="WK_8a3f2e"
|
||||
strategy="lazyOnload"
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 局部内嵌(Iframe 嵌入指定区域)
|
||||
|
||||
如果您不希望使用右下角悬浮球,而是希望将客服对话框固定嵌入在网页的某个指定区域、独立页面或自定义抽屉内,可以使用无边框内嵌链接:
|
||||
|
||||
### 内嵌 URL 格式
|
||||
```text
|
||||
https://cloud.400cc.cn/widget/embed?channel_key=WK_8a3f2e&embedded=1
|
||||
```
|
||||
|
||||
### HTML 内嵌代码示例
|
||||
```html
|
||||
<div style="width: 100%; height: 600px; border-radius: 8px; overflow: hidden; border: 1px solid #e5e7eb;">
|
||||
<iframe
|
||||
src="https://cloud.400cc.cn/widget/embed?channel_key=WK_8a3f2e&embedded=1"
|
||||
style="width: 100%; height: 100%; border: none; display: block;"
|
||||
allow="clipboard-write">
|
||||
</iframe>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 客服系统开箱即用的高级体验
|
||||
|
||||
接入后,访客与客服将自动享有以下原生能力(**无需接入方额外编码**):
|
||||
|
||||
1. **浏览足迹感知**:访客在网站上浏览不同页面或商品时,客服端工作台实时展示访客当前的访问 URL 及浏览历史轨迹。
|
||||
2. **输入中即时感知与线索提取**:
|
||||
* 访客在输入框输入内容时,坐席端实时显示“正在输入...”。
|
||||
* 若访客输入中包含了**手机号**或**微信号**,系统会自动识别并提取,即便访客因故提前关闭网页,客服也能及时沉淀意向线索。
|
||||
3. **断网重连无损同步**:由于网络切换、移动端熄屏等导致的连接断开,重新联网后会自动按消息序号拉取补齐期间的所有客服回复,不会遗漏消息。
|
||||
4. **满意度评价与离线留言**:客服结束会话后自动弹出满意度打分弹窗;非工作时间或无坐席在线时自动引导访客提交离线留言。
|
||||
|
||||
---
|
||||
|
||||
## 7. 常见问题与排查指南(FAQ)
|
||||
|
||||
#### Q1:页面右下角没有显示客服图标?
|
||||
1. 打开浏览器控制台(F12 -> Console),检查是否有网络加载拦截报错。
|
||||
2. 确认 `<script>` 标签中的 `data-id` 是否完整准确。
|
||||
3. 检查您网站是否有更高层级的遮罩元素(客服悬浮球层级为 `z-index: 2147483000`)。
|
||||
|
||||
#### Q2:点击后提示“渠道不存在或已关闭”?
|
||||
* 请联系客服系统管理员,在客服后台「系统设置」 -> 「渠道管理」中确认该渠道状态是否处于「已启用」状态。
|
||||
|
||||
#### Q3:网站配置了严格的内容安全策略(CSP),如何放行?
|
||||
如果您的网站服务器配置了 `Content-Security-Policy` 响应头,请放行 `cloud.400cc.cn`:
|
||||
```http
|
||||
Content-Security-Policy:
|
||||
script-src 'self' https://cloud.400cc.cn;
|
||||
frame-src 'self' https://cloud.400cc.cn;
|
||||
connect-src 'self' https://cloud.400cc.cn wss://cloud.400cc.cn;
|
||||
img-src 'self' data: blob: https://cloud.400cc.cn;
|
||||
```
|
||||
|
||||
#### Q4:手机浏览器上打开,输入框是否会被虚拟键盘遮挡?
|
||||
* 客服组件已针对 iOS / Android 软键盘弹出做了视口高度(`100dvh` / `window.visualViewport`)适配。
|
||||
* 请确保您网站的 `<head>` 中包含标准的移动端视口声明:
|
||||
```html
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
```
|
||||
@@ -22,12 +22,16 @@ type ServerConfig struct {
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Password string
|
||||
Name string
|
||||
SSLMode string
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Password string
|
||||
Name string
|
||||
SSLMode string
|
||||
MaxOpenConns int
|
||||
MaxIdleConns int
|
||||
ConnMaxLifetime time.Duration
|
||||
ConnMaxIdleTime time.Duration
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
@@ -64,12 +68,16 @@ func Load() *Config {
|
||||
Mode: getEnv("GIN_MODE", "debug"),
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Host: getEnv("DB_HOST", "localhost"),
|
||||
Port: getEnv("DB_PORT", "5433"),
|
||||
User: getEnv("DB_USER", "postgres"),
|
||||
Password: getEnv("DB_PASSWORD", "postgres"),
|
||||
Name: getEnv("DB_NAME", "kefu_cloud"),
|
||||
SSLMode: getEnv("DB_SSLMODE", "disable"),
|
||||
Host: getEnv("DB_HOST", "localhost"),
|
||||
Port: getEnv("DB_PORT", "5433"),
|
||||
User: getEnv("DB_USER", "postgres"),
|
||||
Password: getEnv("DB_PASSWORD", "postgres"),
|
||||
Name: getEnv("DB_NAME", "kefu_cloud"),
|
||||
SSLMode: getEnv("DB_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvInt("DB_MAX_OPEN_CONNS", 50),
|
||||
MaxIdleConns: getEnvInt("DB_MAX_IDLE_CONNS", 10),
|
||||
ConnMaxLifetime: time.Duration(getEnvInt("DB_CONN_MAX_LIFETIME_MINUTES", 60)) * time.Minute,
|
||||
ConnMaxIdleTime: time.Duration(getEnvInt("DB_CONN_MAX_IDLE_MINUTES", 15)) * time.Minute,
|
||||
},
|
||||
JWT: func() JWTConfig {
|
||||
hours := getEnvInt("JWT_EXPIRE_HOURS", 24)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
@@ -10,21 +14,100 @@ import (
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
// DBPoolConfig 数据库连接池配置
|
||||
type DBPoolConfig struct {
|
||||
MaxOpenConns int
|
||||
MaxIdleConns int
|
||||
ConnMaxLifetime time.Duration
|
||||
ConnMaxIdleTime time.Duration
|
||||
}
|
||||
|
||||
// DefaultDBPoolConfig 返回连接池配置,优先读取环境变量,缺省提供生产合理的默认值
|
||||
func DefaultDBPoolConfig() DBPoolConfig {
|
||||
maxOpen := getEnvInt("DB_MAX_OPEN_CONNS", 50)
|
||||
maxIdle := getEnvInt("DB_MAX_IDLE_CONNS", 10)
|
||||
lifetimeMin := getEnvInt("DB_CONN_MAX_LIFETIME_MINUTES", 60)
|
||||
idleTimeMin := getEnvInt("DB_CONN_MAX_IDLE_MINUTES", 15)
|
||||
|
||||
if maxOpen <= 0 {
|
||||
maxOpen = 50
|
||||
}
|
||||
if maxIdle <= 0 {
|
||||
maxIdle = 10
|
||||
}
|
||||
if maxIdle > maxOpen {
|
||||
maxIdle = maxOpen
|
||||
}
|
||||
|
||||
return DBPoolConfig{
|
||||
MaxOpenConns: maxOpen,
|
||||
MaxIdleConns: maxIdle,
|
||||
ConnMaxLifetime: time.Duration(lifetimeMin) * time.Minute,
|
||||
ConnMaxIdleTime: time.Duration(idleTimeMin) * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyPoolConfig 将连接池参数应用至底层 *sql.DB
|
||||
func ApplyPoolConfig(sqlDB *sql.DB, cfg DBPoolConfig) {
|
||||
if sqlDB == nil {
|
||||
return
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
|
||||
sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime)
|
||||
sqlDB.SetConnMaxIdleTime(cfg.ConnMaxIdleTime)
|
||||
}
|
||||
|
||||
// BuildGormLogger 根据 GIN_MODE 区分日志策略:生产模式仅记录慢查询与错误,避免全量 SQL 撑满磁盘
|
||||
func BuildGormLogger() logger.Interface {
|
||||
mode := os.Getenv("GIN_MODE")
|
||||
if mode == "release" {
|
||||
return logger.New(
|
||||
log.New(os.Stdout, "\r\n", log.LstdFlags),
|
||||
logger.Config{
|
||||
SlowThreshold: 200 * time.Millisecond,
|
||||
LogLevel: logger.Warn,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
Colorful: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
return logger.Default.LogMode(logger.Info)
|
||||
}
|
||||
|
||||
func getEnvInt(key string, fallback int) int {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func InitDB(dsn string) {
|
||||
var err error
|
||||
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
Logger: BuildGormLogger(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("数据库连接失败: %v", err)
|
||||
}
|
||||
|
||||
sqlDB, err := DB.DB()
|
||||
if err != nil {
|
||||
log.Fatalf("获取底层数据库连接失败: %v", err)
|
||||
}
|
||||
|
||||
poolCfg := DefaultDBPoolConfig()
|
||||
ApplyPoolConfig(sqlDB, poolCfg)
|
||||
|
||||
err = Migrate(DB)
|
||||
if err != nil {
|
||||
log.Fatalf("数据库迁移失败: %v", err)
|
||||
}
|
||||
|
||||
log.Println("数据库迁移完成")
|
||||
log.Printf("数据库连接成功并完成迁移 [pool: max_open=%d, max_idle=%d, lifetime=%v, idletime=%v]",
|
||||
poolCfg.MaxOpenConns, poolCfg.MaxIdleConns, poolCfg.ConnMaxLifetime, poolCfg.ConnMaxIdleTime)
|
||||
}
|
||||
|
||||
func Migrate(db *gorm.DB) error {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func TestDefaultDBPoolConfig(t *testing.T) {
|
||||
// 默认兜底测试
|
||||
cfg := DefaultDBPoolConfig()
|
||||
if cfg.MaxOpenConns <= 0 || cfg.MaxIdleConns <= 0 {
|
||||
t.Fatalf("连接池默认值非法: %+v", cfg)
|
||||
}
|
||||
if cfg.MaxIdleConns > cfg.MaxOpenConns {
|
||||
t.Fatalf("MaxIdleConns 不能大于 MaxOpenConns: %+v", cfg)
|
||||
}
|
||||
|
||||
// 环境变量覆盖测试
|
||||
os.Setenv("DB_MAX_OPEN_CONNS", "120")
|
||||
os.Setenv("DB_MAX_IDLE_CONNS", "30")
|
||||
os.Setenv("DB_CONN_MAX_LIFETIME_MINUTES", "120")
|
||||
os.Setenv("DB_CONN_MAX_IDLE_MINUTES", "30")
|
||||
defer func() {
|
||||
os.Unsetenv("DB_MAX_OPEN_CONNS")
|
||||
os.Unsetenv("DB_MAX_IDLE_CONNS")
|
||||
os.Unsetenv("DB_CONN_MAX_LIFETIME_MINUTES")
|
||||
os.Unsetenv("DB_CONN_MAX_IDLE_MINUTES")
|
||||
}()
|
||||
|
||||
customCfg := DefaultDBPoolConfig()
|
||||
if customCfg.MaxOpenConns != 120 {
|
||||
t.Errorf("期望 MaxOpenConns=120, 实际=%d", customCfg.MaxOpenConns)
|
||||
}
|
||||
if customCfg.MaxIdleConns != 30 {
|
||||
t.Errorf("期望 MaxIdleConns=30, 实际=%d", customCfg.MaxIdleConns)
|
||||
}
|
||||
if customCfg.ConnMaxLifetime != 120*time.Minute {
|
||||
t.Errorf("期望 ConnMaxLifetime=120m, 实际=%v", customCfg.ConnMaxLifetime)
|
||||
}
|
||||
if customCfg.ConnMaxIdleTime != 30*time.Minute {
|
||||
t.Errorf("期望 ConnMaxIdleTime=30m, 实际=%v", customCfg.ConnMaxIdleTime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyPoolConfig(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:pool_test_%d?mode=memory&cache=shared", time.Now().UnixNano())), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("打开 sqlite 失败: %v", err)
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
t.Fatalf("获取 sql.DB 失败: %v", err)
|
||||
}
|
||||
|
||||
cfg := DBPoolConfig{
|
||||
MaxOpenConns: 25,
|
||||
MaxIdleConns: 5,
|
||||
ConnMaxLifetime: 30 * time.Minute,
|
||||
ConnMaxIdleTime: 10 * time.Minute,
|
||||
}
|
||||
ApplyPoolConfig(sqlDB, cfg)
|
||||
ApplyPoolConfig(nil, cfg) // 防空指针保护
|
||||
|
||||
stats := sqlDB.Stats()
|
||||
if stats.MaxOpenConnections != 25 {
|
||||
t.Errorf("期望 MaxOpenConnections=25, 实际=%d", stats.MaxOpenConnections)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildGormLogger(t *testing.T) {
|
||||
os.Setenv("GIN_MODE", "release")
|
||||
relLog := BuildGormLogger()
|
||||
if relLog == nil {
|
||||
t.Fatal("release logger 为空")
|
||||
}
|
||||
|
||||
os.Setenv("GIN_MODE", "debug")
|
||||
debLog := BuildGormLogger()
|
||||
if debLog == nil {
|
||||
t.Fatal("debug logger 为空")
|
||||
}
|
||||
os.Unsetenv("GIN_MODE")
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 避免测试时与真实 sql.DB 产生类型告警
|
||||
var _ *sql.DB
|
||||
var _ = logger.Warn
|
||||
}
|
||||
Reference in New Issue
Block a user