5706 lines
174 KiB
Go
5706 lines
174 KiB
Go
package main
|
||
|
||
import (
|
||
"bytes"
|
||
"centerBook/controller"
|
||
"centerBook/service"
|
||
"context"
|
||
"crypto/md5"
|
||
"database/sql"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"net/http"
|
||
"net/url"
|
||
"os"
|
||
"os/signal"
|
||
"path/filepath"
|
||
"reflect"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"syscall"
|
||
"time"
|
||
|
||
"github.com/gin-contrib/cors"
|
||
"github.com/go-redis/redis/v8"
|
||
|
||
"centerBook/es"
|
||
"centerBook/monitor"
|
||
"centerBook/util/redisClient"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
_ "github.com/go-sql-driver/mysql"
|
||
"github.com/gorilla/websocket"
|
||
"github.com/patrickmn/go-cache"
|
||
"github.com/xuri/excelize/v2"
|
||
)
|
||
|
||
// BookCenterController 图书中心控制器
|
||
type BookCenterController struct {
|
||
//db *sql.DB
|
||
db *sql.DB // 原有数据库连接
|
||
cache *cache.Cache
|
||
redis *redis.Client // Redis客户端(使用go-redis库)
|
||
zhishuDB *sql.DB
|
||
}
|
||
|
||
// WebSocket连接升级器配置
|
||
var upgrader = websocket.Upgrader{
|
||
ReadBufferSize: 1024,
|
||
WriteBufferSize: 1024,
|
||
CheckOrigin: func(r *http.Request) bool {
|
||
return true // 允许所有来源,生产环境应限制特定域名
|
||
},
|
||
}
|
||
|
||
// WebSocketManager 管理所有WebSocket连接
|
||
type WebSocketManager struct {
|
||
clients map[*websocket.Conn]bool // 活跃客户端连接
|
||
broadcast chan []byte // 广播消息通道
|
||
mu sync.Mutex // 保护clients的互斥锁
|
||
}
|
||
|
||
const (
|
||
detailPagesUrl = "http://175.27.224.66:9999"
|
||
//detailPagesUrl = "http://10.206.0.10:8091" // 详情页面URL
|
||
bookBaseRedisurl = "36.212.1.63:6379" // 图书基础信息Redis
|
||
//bookBaseRedisurl = "10.206.0.17:6666"
|
||
bookUploadUrl = "http://175.27.224.66:8070"
|
||
fixedToken = "9ef1895d4b004c98b63159e21aac51da893c23c9" // 图片上传 token
|
||
GCM_IV_LENGTH = 12
|
||
GCM_TAG_LENGTH = 16
|
||
)
|
||
|
||
// NewWebSocketManager 创建新的WebSocket管理器
|
||
func NewWebSocketManager() *WebSocketManager {
|
||
return &WebSocketManager{
|
||
clients: make(map[*websocket.Conn]bool),
|
||
broadcast: make(chan []byte, 256), // 带缓冲的通道
|
||
}
|
||
}
|
||
|
||
// handleConnections 处理WebSocket连接
|
||
func (manager *WebSocketManager) handleConnections(w http.ResponseWriter, r *http.Request) {
|
||
// 升级HTTP连接到WebSocket
|
||
ws, err := upgrader.Upgrade(w, r, nil)
|
||
if err != nil {
|
||
log.Printf("WebSocket升级失败: %v", err)
|
||
return
|
||
}
|
||
defer ws.Close()
|
||
|
||
// 注册新客户端
|
||
manager.mu.Lock()
|
||
manager.clients[ws] = true
|
||
manager.mu.Unlock()
|
||
|
||
log.Println("新的WebSocket客户端连接")
|
||
|
||
// 保持连接并处理消息
|
||
for {
|
||
_, message, err := ws.ReadMessage()
|
||
if err != nil {
|
||
manager.removeClient(ws)
|
||
break
|
||
}
|
||
|
||
// 处理客户端消息(示例:记录日志)
|
||
log.Printf("收到WebSocket消息: %s", message)
|
||
}
|
||
}
|
||
|
||
// removeClient 移除断开连接的客户端
|
||
func (manager *WebSocketManager) removeClient(ws *websocket.Conn) {
|
||
manager.mu.Lock()
|
||
defer manager.mu.Unlock()
|
||
|
||
if _, ok := manager.clients[ws]; ok {
|
||
ws.Close()
|
||
delete(manager.clients, ws)
|
||
log.Println("WebSocket客户端断开连接")
|
||
}
|
||
}
|
||
|
||
// handleMessages 处理广播消息
|
||
func (manager *WebSocketManager) handleMessages() {
|
||
for {
|
||
msg := <-manager.broadcast
|
||
|
||
manager.mu.Lock()
|
||
for client := range manager.clients {
|
||
err := client.WriteMessage(websocket.TextMessage, msg)
|
||
if err != nil {
|
||
log.Printf("发送WebSocket消息错误: %v", err)
|
||
manager.removeClient(client)
|
||
}
|
||
}
|
||
manager.mu.Unlock()
|
||
}
|
||
}
|
||
|
||
// Broadcast 发送广播消息
|
||
func (manager *WebSocketManager) Broadcast(message interface{}) {
|
||
msg, err := json.Marshal(message)
|
||
if err != nil {
|
||
log.Printf("广播消息编码错误: %v", err)
|
||
return
|
||
}
|
||
manager.broadcast <- msg
|
||
}
|
||
|
||
func main() {
|
||
// 1. 初始化SQL监控器
|
||
InitSQLMonitor(1000) // 最多保存1000条SQL执行记录
|
||
|
||
// 2. 初始化数据库控制器
|
||
//bookCenter, err := NewBookCenterController()
|
||
//if err != nil {
|
||
// log.Fatalf("初始化图书中心控制器失败: %v", err)
|
||
//}
|
||
//defer bookCenter.db.Close()
|
||
// ======================= 新增:初始化 ES ============================
|
||
/////** 旧ES,ES0 **/
|
||
//esClient, err := es.NewESClient(
|
||
// []string{"http://103.236.91.138:9200"},
|
||
// "elastic",
|
||
// "5mRDIUg52VC0fp14nw-F",
|
||
//)
|
||
|
||
///** ES,ES1 **/
|
||
//esClient, err := es.NewESClient(
|
||
// []string{"http://103.236.74.207:9200"},
|
||
// "elastic",
|
||
// "y3h2fdYyrXewkSrY6uhh",
|
||
//)
|
||
|
||
/** 新ES,任务2 **/
|
||
//esClient, err := es.NewESClient(
|
||
// []string{"http://localhost:9200"},
|
||
// "elastic",
|
||
// "zDzSXel3PFwx9=6Ybmqv",
|
||
//)
|
||
/** ES2 本地测试 **/
|
||
//esClient, err := es.NewESClient(
|
||
// []string{"https://103.236.91.138:9200"},
|
||
// "elastic",
|
||
// "7AYdoW=mn3yHZ*3A=Z1x",
|
||
//)
|
||
esClient, err := es.NewESClient(
|
||
[]string{"http://36.212.1.63:9200"},
|
||
"elastic",
|
||
"zDzSXel3PFwx9=6Ybmqva",
|
||
)
|
||
|
||
if err != nil {
|
||
log.Fatalf("初始化 ES 客户端失败: %s", err)
|
||
}
|
||
|
||
esService := es.NewESSearchService(esClient)
|
||
|
||
redisClient.AddClient("db4", "36.212.12.247:6379", "long6166@@", 2)
|
||
redisClient.AddClient("db1", "36.212.12.247:6379", "long6166@@", 7)
|
||
//redisClient.AddClient("test", "127.0.0.1:6379", "", 0)
|
||
// ===================================================================
|
||
|
||
// 3. 初始化IP日志路径
|
||
EnsureIPLogPathInitialized()
|
||
// 4. 创建Gin路由
|
||
r := gin.Default()
|
||
// 配置CORS中间件
|
||
r.Use(cors.New(cors.Config{
|
||
AllowOrigins: []string{"http://localhost:82", "http://103.236.91.138:9009", "https://test.centerbook.buzhiyushu.cn", "https://centerbook.buzhiyushu.cn"},
|
||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||
AllowHeaders: []string{
|
||
"Origin",
|
||
"Content-Type",
|
||
"Authorization",
|
||
"clientid",
|
||
"Content-Language",
|
||
"X-Requested-With",
|
||
},
|
||
ExposeHeaders: []string{"Content-Length", "Content-Type"}, // ✅ 增加 Content-Type
|
||
AllowCredentials: true,
|
||
MaxAge: 1 * time.Second,
|
||
}))
|
||
// 统一请求审计日志(记录 endpoint、URI、状态码、IP、UA、耗时)
|
||
r.Use(RequestAuditLogger())
|
||
// 未匹配路由统一处理(详细记录 404 来源)
|
||
r.NoRoute(NotFoundHandler)
|
||
// 暴露 ip.txt(用于通过反向代理映射到 https://book.center.buzhiyushu.cn/ip.txt)
|
||
r.GET("/ip.txt", IPLogHandler())
|
||
// 5. 初始化WebSocket管理器
|
||
wsManager := NewWebSocketManager()
|
||
go wsManager.handleMessages()
|
||
r = gin.Default()
|
||
|
||
// 创建限流器
|
||
//rl := middleware.NewRateLimiter()
|
||
|
||
//// 6. 注册API路由
|
||
////根据条件查询图书信息
|
||
//// 单ip限流一分钟两次
|
||
//r.GET("/api/bookBase/getBookBaseInfo", rl.LimitMiddleware(5, time.Minute), bookCenter.GetBookBaseInfo)
|
||
////r.GET("/api/bookBase/getBookBaseInfo", bookCenter.GetBookBaseInfo)
|
||
////随机获取图书信息(核价专用)
|
||
//r.GET("/api/bookBase/getRandomBookBaseInfo", bookCenter.GetRandomBookBaseInfo)
|
||
////优化查询图书信息
|
||
//r.GET("/api/bookBase/GetBookBaseInfoOptimized", bookCenter.GetBookBaseInfoOptimized)
|
||
////根据ISBN查询图书信息
|
||
//r.GET("/api/bookBase/getBookByISBN", bookCenter.GetBookByISBN)
|
||
////根据ID查询图书信息
|
||
//r.GET("/api/bookBase/getBookById", bookCenter.GetBookByID)
|
||
////批量修改违规数据
|
||
//r.POST("/api/bookBase/putBookBaseInfoToIll", bookCenter.SetBookBaseInfoToIll)
|
||
////根据ISBN批量修改违规数据
|
||
//r.POST("/api/bookBase/putBookBaseInfoToIllByISBN", bookCenter.SetBookBaseInfoToIllByISBN)
|
||
////修改图书书名
|
||
//r.POST("/api/bookBase/putBookBaseName", bookCenter.SetBookBaseInfoBookName)
|
||
////新增图书数据
|
||
//r.POST("/api/bookBase/addBookBaseInfo", bookCenter.InsertBaseInfo)
|
||
////修改销量数据
|
||
//r.POST("/api/bookBase/updateSales", bookCenter.UpdateSales)
|
||
////根据ISBN获取书籍小图
|
||
//r.GET("/api/bookBase/getBookPicByISBN", bookCenter.GetBookPicByISBN)
|
||
////上传图书图片并更新book_pic_new字段
|
||
//r.POST("/api/bookBase/uploadBookPic", bookCenter.UploadBookPic)
|
||
////根据isbn查询图书信息并存储(xcx使用)
|
||
//r.GET("/api/bookBase/getBookByIsbnXcx", bookCenter.getBookByIsbnXcx)
|
||
//r.POST("/api/bookBase/getBookByIsbnXcx", bookCenter.getBookByIsbnXcx)
|
||
////选品中心核价码解密
|
||
//r.GET("/api/bookBase/deleterIsbn", bookCenter.exportISBNs)
|
||
//r.GET("/api/bookBase/updateBooks", bookCenter.updateBooks)
|
||
//// ---------------- erp相关 ------------------
|
||
//erp.RegisterERPRoutes(r, bookCenter.zhishuDB) // ERP 路由注册集中管理
|
||
//
|
||
//// =================== ⭐ 挂载 加密 pricingLink 接口 =====================
|
||
//r.GET("/pricingLink", func(c *gin.Context) {
|
||
// PricingLinkHandler(c.Writer, c.Request)
|
||
//})
|
||
//// =================== ⭐ 挂载 解密 pricingLink 接口 =====================
|
||
//r.GET("/pricingLinkDec", func(c *gin.Context) {
|
||
// PricingLinkDecHandler(c.Writer, c.Request)
|
||
//})
|
||
//// =================================================================
|
||
//// 6. 健康检查端点
|
||
//r.GET("/health", bookCenter.HealthCheck)
|
||
//r.GET("/ready", bookCenter.ReadyCheck)
|
||
|
||
// 7. SQL健康监控端点
|
||
//sqlHealthController := NewSQLHealthController()
|
||
//r.GET("/api/sql-health/stats", sqlHealthController.GetSQLStats)
|
||
//r.GET("/api/sql-health/recent", sqlHealthController.GetRecentSQLRecords)
|
||
//r.GET("/api/sql-health/slow-queries", sqlHealthController.GetSlowQueries)
|
||
//r.GET("/api/sql-health/failed-queries", sqlHealthController.GetFailedQueries)
|
||
//r.POST("/api/sql-health/clear", sqlHealthController.ClearSQLRecords)
|
||
//r.GET("/api/sql-health/dashboard", sqlHealthController.GetSQLHealthDashboard)
|
||
// 8. API 监控端点(ES 和 Redis 调用监控)
|
||
apiMonitorController := monitor.NewAPIMonitorController()
|
||
r.GET("/api/api-monitor/stats", apiMonitorController.GetAPIStats)
|
||
r.GET("/api/api-monitor/all-stats", apiMonitorController.GetAllAPIStats)
|
||
r.GET("/api/api-monitor/es-calls", apiMonitorController.GetAPIESCalls)
|
||
r.GET("/api/api-monitor/redis-calls", apiMonitorController.GetAPIRedisCalls)
|
||
r.GET("/api/api-monitor/dashboard", apiMonitorController.GetAPIMonitorDashboard)
|
||
r.GET("/api/api-monitor/call-detail", apiMonitorController.GetAPICallDetail)
|
||
// =================== ⭐ ES =====================
|
||
|
||
// ISBN 模糊搜索
|
||
r.GET("/api/es/searchByISBNLike", esService.SearchBooksHandler) //监控
|
||
// ISBN 精确搜索
|
||
r.GET("/api/es/searchByISBN", esService.SearchBookByISBNHandler) //监控
|
||
// ISBN 精确搜索 为psi提供
|
||
r.GET("/api/es/searchByISBNtoPsi", esService.SearchBookByISBNHandlerToPsi) //监控
|
||
// 书名搜索
|
||
r.GET("/api/es/searchByBookName", esService.SearchBookByBookNameHandler)
|
||
// 全字段搜索
|
||
r.GET("/api/es/searchAll", esService.SearchBooksAllFieldsHandler)
|
||
// 根据条件查询 ES 图书信息
|
||
//r.GET("/api/es/getBookBaseInfoES", esService.SearchBookBaseInfoESHandler) //1
|
||
|
||
//------------------------------------------------------------------------
|
||
// 初始化控制器 新
|
||
bookSearchService := service.NewBookService(esClient)
|
||
esService.SyncRedisByISBN = bookSearchService.SyncRedisByISBN
|
||
bookController := controller.NewBookController(bookSearchService)
|
||
// 根据条件查询 ES 图书信息
|
||
r.GET("/api/es/getBookBaseInfoES", bookController.SearchBookBaseInfoHandler)
|
||
// 新增:根据ISBN查询ES中是否存在,不存在则新增数据,存在则根据参数更新
|
||
r.POST("/api/es/addBookToES", bookController.AddBookToESHandler)
|
||
// 更新:根据ISBN通用更新图书字段
|
||
r.POST("/api/es/updateBookFieldsByISBN", bookController.UpdateBookFieldsByISBNHandler)
|
||
// 更新:根据ISBN通用更新图书字段
|
||
r.POST("/api/es/updateBookCatIdByISBN", bookController.UpdateBookCatIdByISBNHandler)
|
||
// 删除:根据ISBN删除ES数据
|
||
r.GET("/api/es/DeleteBookByISBN", bookController.DeleteBookHandler)
|
||
// 新增:根据ID删除ES数据
|
||
r.GET("/api/es/DeleteBookByID", bookController.DeleteBookByIDHandler)
|
||
//------------------------------------------------------------------------
|
||
|
||
// 新:核价软件用批量获取
|
||
r.GET("/api/es/batchGetBookBaseInfoES", esService.BatchGetBookBaseInfoESHandler) //监控
|
||
// 多条件高级搜索
|
||
r.GET("/api/es/searchAdvanced", esService.SearchBooksByConditionsHandler)
|
||
// ID 范围计数
|
||
r.GET("/api/es/countByIDRange", esService.CountByIDRangeHandler)
|
||
// 根据 ISBN 获取在售数量并更新 ES 的 sell_counts
|
||
r.POST("/api/es/updateSellCountsByISBN", esService.UpdateSellCountsByISBNHandler)
|
||
// 新增:按入参直接更新在售数量
|
||
r.POST("/api/es/updateSellCountsDirect", esService.UpdateSellCountsDirectHandler)
|
||
// 新增:根据ISBN更新图书的is_suit字段
|
||
r.POST("/api/es/updateBookSuitByISBN", esService.UpdateBookSuitByISBNHandler)
|
||
// 新增:根据ISBN通用更新图书字段
|
||
//r.POST("/api/es/updateBookFieldsByISBN", esService.UpdateBookFieldsByISBNHandler) //1
|
||
// 新增:根据ISBN查询ES中是否存在,不存在则新增数据,存在则根据参数更新
|
||
//r.POST("/api/es/addBookToES", esService.AddBookToESHandler) //1
|
||
// 新增:完整插入接口,支持所有字段,
|
||
r.POST("/api/es/addBookFullToES", esService.AddBookFullToESHandler)
|
||
// 新增:批量插入接口,支持同时插入多本图书
|
||
r.POST("/api/es/batchAddBookToES", esService.BatchAddBookToESHandler)
|
||
// 新增:根据ISBN检查书籍是否存在
|
||
r.GET("/api/es/checkBookExists", esService.CheckBookExistsByISBNHandler)
|
||
r.POST("/api/es/checkBookExists", esService.CheckBookExistsByISBNHandler)
|
||
// 删除:根据ISBN删除ES数据
|
||
//r.GET("/api/es/DeleteBookByISBN", esService.DeleteBookHandler) //1
|
||
// 新增:根据ID删除ES数据
|
||
//r.GET("/api/es/DeleteBookByID", esService.DeleteBookByIDHandler) //1
|
||
// 新增:检查书名是否包含套装关键字
|
||
r.GET("/api/es/checkBookSuit", esService.CheckBookSuitHandler)
|
||
|
||
// 8. WebSocket端点
|
||
r.GET("/ws", func(c *gin.Context) {
|
||
wsManager.handleConnections(c.Writer, c.Request)
|
||
})
|
||
// 4. 启动HTTP服务器
|
||
port := ":9009"
|
||
server := &http.Server{
|
||
Addr: port,
|
||
Handler: r,
|
||
}
|
||
|
||
go func() {
|
||
log.Printf("服务器正在监听 %s 端口...", port)
|
||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||
log.Fatalf("服务器启动失败: %v", err)
|
||
}
|
||
}()
|
||
|
||
// 5. 优雅关闭处理
|
||
quit := make(chan os.Signal, 1)
|
||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||
<-quit
|
||
log.Println("正在关闭服务器...")
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
|
||
if err := server.Shutdown(ctx); err != nil {
|
||
log.Fatal("服务器关闭出错:", err)
|
||
}
|
||
log.Println("服务器已成功关闭")
|
||
}
|
||
|
||
// NewBookCenterController 创建图书中心控制器实例
|
||
func NewBookCenterController() (*BookCenterController, error) {
|
||
// 数据库连接配置
|
||
// 资源服务器
|
||
//targetDSN := "book_center:Ma6XCWE82psHYt3x@tcp(175.27.224.66:3306)/book_center?charset=utf8mb4&parseTime=True&loc=Local"
|
||
// 资源服务器 - 内网连接
|
||
//targetDSN := "book_center:Ma6XCWE82psHYt3x@tcp(10.206.0.10:3306)/book_center?charset=utf8mb4&parseTime=True&loc=Local"
|
||
// 新数据库
|
||
targetDSN := "root:Long6166@@@tcp(nj-cynosdbmysql-grp-1v6vxn5f.sql.tencentcdb.com:26247)/book_center?charset=utf8mb4&parseTime=True&loc=Local"
|
||
// 内网
|
||
//targetDSN := "root:Long6166@@@tcp(10.206.16.4:3306)/book_center?charset=utf8mb4&parseTime=True&loc=Local"
|
||
// erp数据库
|
||
|
||
//查询新后台用户ViP信息
|
||
//打开中心数据连接
|
||
db, err := sql.Open("mysql", targetDSN)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("数据库连接失败: %v", err)
|
||
}
|
||
|
||
// 测试中心书库连接
|
||
if err = db.Ping(); err != nil {
|
||
return nil, fmt.Errorf("数据库连接测试失败: %v", err)
|
||
}
|
||
|
||
// 修改后 - 根据服务器配置调整
|
||
db.SetMaxOpenConns(150)
|
||
db.SetMaxIdleConns(50)
|
||
db.SetConnMaxLifetime(30 * time.Minute)
|
||
// 新增连接最大空闲时间
|
||
db.SetConnMaxIdleTime(10 * time.Minute)
|
||
|
||
// 初始化 Redis 客户端
|
||
rdb := redis.NewClient(&redis.Options{
|
||
Addr: bookBaseRedisurl,
|
||
Password: "long6166", // 无密码
|
||
})
|
||
|
||
// 测试 Redis 连接
|
||
ctx := context.Background()
|
||
_, err = rdb.Ping(ctx).Result()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("Redis 连接失败: %v", err)
|
||
}
|
||
|
||
// -----------------------
|
||
// 3️⃣ 新增 Zhishu 数据库连接
|
||
// -----------------------
|
||
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local&allowAllFiles=true",
|
||
"zhishu", "XsRR4K3ATizyc5BK", "146.56.227.42:3306", "zhishu")
|
||
|
||
zhishuDB, err := sql.Open("mysql", dsn)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("Zhishu 数据库连接失败: %v", err)
|
||
}
|
||
|
||
zhishuDB.SetMaxOpenConns(150)
|
||
zhishuDB.SetMaxIdleConns(50)
|
||
zhishuDB.SetConnMaxLifetime(30 * time.Minute)
|
||
zhishuDB.SetConnMaxIdleTime(10 * time.Minute)
|
||
|
||
if err = zhishuDB.Ping(); err != nil {
|
||
return nil, fmt.Errorf("Zhishu 数据库连接测试失败: %v", err)
|
||
}
|
||
log.Println("Zhishu 数据库连接成功")
|
||
|
||
// -----------------------
|
||
// 4️⃣ 返回控制器实例
|
||
// -----------------------
|
||
return &BookCenterController{
|
||
db: db,
|
||
cache: cache.New(5*time.Minute, 10*time.Minute),
|
||
redis: rdb,
|
||
zhishuDB: zhishuDB, // ✅ 新增字段
|
||
}, nil
|
||
}
|
||
|
||
// HealthCheck 综合健康检查
|
||
func (bc *BookCenterController) HealthCheck(c *gin.Context) {
|
||
// 检查数据库连接
|
||
if err := bc.db.Ping(); err != nil {
|
||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||
"status": "down",
|
||
"message": "数据库连接失败",
|
||
"error": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
// 检查缓存状态
|
||
bc.cache.Set("healthcheck", "ok", 1*time.Minute)
|
||
if _, found := bc.cache.Get("healthcheck"); !found {
|
||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||
"status": "down",
|
||
"message": "缓存服务异常",
|
||
})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"status": "up",
|
||
"message": "服务运行正常",
|
||
"details": gin.H{
|
||
"database": "connected",
|
||
"cache": "working",
|
||
"time": time.Now().Format(time.RFC3339),
|
||
},
|
||
})
|
||
}
|
||
|
||
// ReadyCheck 就绪检查
|
||
func (bc *BookCenterController) ReadyCheck(c *gin.Context) {
|
||
// 检查数据库连接
|
||
if err := bc.db.Ping(); err != nil {
|
||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||
"status": "not_ready",
|
||
"message": "数据库连接失败",
|
||
"error": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"status": "ready",
|
||
"message": "服务准备就绪",
|
||
})
|
||
}
|
||
|
||
// GetBookBaseInfo 条件查询+(缓存15分钟)
|
||
// 优化点:
|
||
// 1) 计数查询不再携带 ORDER BY,避免不必要的排序开销;
|
||
// 2) 在子查询中加入 FORCE INDEX(idx_search_limit) 引导优化器使用覆盖索引;
|
||
// 3) 采用子查询筛选 id + 主表 JOIN 的方式返回字段,兼顾分页与排序性能。
|
||
func (bc *BookCenterController) GetBookBaseInfo(c *gin.Context) {
|
||
//在控制台打印访问信息(合并IP与查询参数)
|
||
log.Printf("GetBookBaseInfo 请求 | IP: %s | 参数: %v", c.ClientIP(), c.Request.URL.Query())
|
||
|
||
// 启动计时器
|
||
startTime := time.Now()
|
||
|
||
// 生成缓存键
|
||
cacheKey := generateCacheKey(c.Request.URL.Query())
|
||
|
||
// 尝试从缓存获取
|
||
if cachedData, found := bc.cache.Get(cacheKey); found {
|
||
c.JSON(http.StatusOK, cachedData)
|
||
return
|
||
}
|
||
|
||
saleSelect := c.DefaultQuery("saleSelect", "2")
|
||
|
||
// 根据查询参数动态选择索引提示(避免错误强制使用不匹配索引)
|
||
// 当筛选包含具体的销量字段时,优先选择以该字段为前缀的复合索引
|
||
var chosenIndex string
|
||
switch saleSelect {
|
||
case "7":
|
||
chosenIndex = "idx_core_search" // day_sale_7 为前缀
|
||
case "15":
|
||
chosenIndex = "idx_day_sale_15_search"
|
||
case "30":
|
||
chosenIndex = "idx_day_sale_30_search"
|
||
case "60":
|
||
// 如需,可建立对应 day_sale_60 索引;暂不强制
|
||
chosenIndex = ""
|
||
case "90":
|
||
chosenIndex = ""
|
||
case "180":
|
||
// idx_core_search 内含 day_sale_180,但前缀是 day_sale_7,这里不强制
|
||
chosenIndex = ""
|
||
case "365":
|
||
chosenIndex = ""
|
||
default:
|
||
// 未选择具体销量窗口,但若存在数量筛选,优先数量复合索引
|
||
if c.Query("sell_counts") != "" || c.Query("buy_counts") != "" {
|
||
chosenIndex = "idx_query_opt"
|
||
}
|
||
}
|
||
// 记录索引提示选择,便于线上观测
|
||
if chosenIndex != "" {
|
||
log.Printf("索引提示选择: %s", chosenIndex)
|
||
} else {
|
||
log.Printf("索引提示选择: 无(交由优化器决定)")
|
||
}
|
||
// 构建子查询 - 用于快速筛选ID
|
||
// 按参数动态添加索引提示
|
||
subQuery := "SELECT id FROM book_center"
|
||
if chosenIndex != "" {
|
||
subQuery += " FORCE INDEX (" + chosenIndex + ")"
|
||
}
|
||
subQuery += " WHERE 1=1"
|
||
var subArgs []interface{}
|
||
|
||
// 设置默认过滤条件(确保参数顺序稳定)
|
||
// 注意:Go 的 map 迭代顺序不稳定,这里用有序切片保证追加顺序
|
||
defaultOrder := []struct {
|
||
field string
|
||
value interface{}
|
||
}{
|
||
{field: "vio_book", value: 0},
|
||
{field: "book_set", value: 0},
|
||
{field: "onenum_mbooks", value: 0},
|
||
{field: "ill_publisher", value: 0},
|
||
{field: "ill_author", value: 0},
|
||
}
|
||
// 传入isbn时只查询isbn,其他参数不作为参数查询数据库
|
||
hasISBN := c.Query("isbn") != ""
|
||
var conditionOrder []string
|
||
if !hasISBN {
|
||
for _, item := range defaultOrder {
|
||
userValue := c.Query(item.field)
|
||
if userValue == "" {
|
||
subQuery += fmt.Sprintf(" AND %s = ?", item.field)
|
||
subArgs = append(subArgs, item.value)
|
||
conditionOrder = append(conditionOrder, item.field)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 定义各种查询字段类型
|
||
exactFields := []string{
|
||
"id", "isbn", "publisher",
|
||
}
|
||
|
||
// 根据 saleSelect 决定使用哪个字段作为销售数量字段
|
||
var saleCountField string
|
||
switch saleSelect {
|
||
case "7":
|
||
saleCountField = "day_sale_7"
|
||
case "15":
|
||
saleCountField = "day_sale_15"
|
||
case "30":
|
||
saleCountField = "day_sale_30"
|
||
case "60":
|
||
saleCountField = "day_sale_60"
|
||
case "90":
|
||
saleCountField = "day_sale_90"
|
||
case "180":
|
||
saleCountField = "day_sale_180"
|
||
case "365":
|
||
saleCountField = "day_sale_365"
|
||
case "0":
|
||
saleCountField = "this_year_sale"
|
||
case "1":
|
||
saleCountField = "last_year_sale"
|
||
default:
|
||
saleCountField = "buy_counts"
|
||
}
|
||
|
||
numericRangeFields := []string{"buy_counts", "sell_counts", "day_sale_7", "day_sale_15", "day_sale_30", "day_sale_60", "day_sale_90", "day_sale_180", "day_sale_365", "this_year_sale", "last_year_sale"}
|
||
boolFields := []string{"book_pic", "vio_book", "book_set", "onenum_mbooks", "ill_publisher", "ill_author"}
|
||
dateFields := []string{"print_time", "publiction_times"}
|
||
camelToSnakeMap := map[string]string{
|
||
"bookName": "book_name",
|
||
"vioBook": "vio_book",
|
||
"bookSet": "book_set",
|
||
"onenumMbooks": "onenum_mbooks",
|
||
"illPublisher": "ill_publisher",
|
||
"illAuthor": "ill_author",
|
||
"bookPic": "book_pic",
|
||
}
|
||
|
||
// 定义要排除的参数名
|
||
excludedParams := []string{"page", "per_page", "is_pricing", "saleSelect", "last_id", "pageNum", "pageSize"}
|
||
|
||
// 首先应用默认过滤条件(按照固定顺序)
|
||
// 同时记录 WHERE 条件追加顺序,便于排查与索引前缀不一致的问题
|
||
//var conditionOrder []string
|
||
//for _, item := range defaultOrder {
|
||
// userValue := c.Query(item.field)
|
||
// if userValue == "" {
|
||
// subQuery += fmt.Sprintf(" AND %s = ?", item.field)
|
||
// subArgs = append(subArgs, item.value)
|
||
// conditionOrder = append(conditionOrder, item.field)
|
||
// }
|
||
//}
|
||
|
||
// 处理查询参数 - 构建子查询条件(保证稳定顺序)
|
||
// 1) 优先处理与索引前缀相关的标志位与销量/数量字段
|
||
// 2) 然后处理分类/图片等残余过滤
|
||
// 3) 最后处理其它精确与日期/模糊字段
|
||
priorityOrder := []string{
|
||
// flags
|
||
"vio_book", "book_set", "onenum_mbooks", "ill_publisher", "ill_author",
|
||
// sales/counts
|
||
"day_sale_7", "day_sale_15", "day_sale_30", "day_sale_60", "day_sale_90", "day_sale_180", "day_sale_365", "this_year_sale", "last_year_sale",
|
||
"sell_counts", "buy_counts",
|
||
// residual filters often used
|
||
"category", "book_pic",
|
||
// exacts
|
||
"id", "isbn", "publisher",
|
||
// text
|
||
"author", "book_name",
|
||
// dates
|
||
"print_time", "publiction_times",
|
||
}
|
||
// 收集所有请求参数键
|
||
rawParams := c.Request.URL.Query()
|
||
used := make(map[string]bool)
|
||
// 按优先级构造有序键列表
|
||
var orderedKeys []string
|
||
for _, k := range priorityOrder {
|
||
if v, ok := rawParams[k]; ok && len(v) > 0 && v[0] != "" && !contains(excludedParams, k) {
|
||
orderedKeys = append(orderedKeys, k)
|
||
used[k] = true
|
||
}
|
||
}
|
||
// 将剩余键按字典序加入(保证稳定)
|
||
for k, v := range rawParams {
|
||
if used[k] || contains(excludedParams, k) || len(v) == 0 || v[0] == "" {
|
||
continue
|
||
}
|
||
orderedKeys = append(orderedKeys, k)
|
||
}
|
||
sort.Strings(orderedKeys[len(orderedKeys)-len(orderedKeys):]) // no-op for clarity; keys已按优先级+余下加入
|
||
|
||
if hasISBN {
|
||
// 仅允许 ISBN,不允许任何过滤字段介入
|
||
orderedKeys = []string{"isbn"}
|
||
}
|
||
|
||
// 按有序键处理
|
||
for _, key := range orderedKeys {
|
||
values := rawParams[key]
|
||
value := values[0]
|
||
// 处理驼峰参数转换为下划线
|
||
if dbField, ok := camelToSnakeMap[key]; ok {
|
||
key = dbField
|
||
}
|
||
|
||
// 特殊处理book_pic参数
|
||
if key == "book_pic" {
|
||
if value == "0" {
|
||
subQuery += " AND (book_pic IS NULL OR book_pic = '')"
|
||
} else if value == "1" {
|
||
subQuery += " AND (book_pic IS NOT NULL AND book_pic != '')"
|
||
}
|
||
conditionOrder = append(conditionOrder, key)
|
||
continue
|
||
}
|
||
|
||
// 特殊处理category参数
|
||
if key == "category" {
|
||
// 对URL编码的值进行解码
|
||
decodedValue, err := url.QueryUnescape(value)
|
||
if err != nil {
|
||
decodedValue = value
|
||
}
|
||
|
||
// 特殊处理:排除大学教材
|
||
if decodedValue == "排除大学教材" {
|
||
subQuery += " AND category != ?"
|
||
subArgs = append(subArgs, "图书/教材教辅考试/大学教材")
|
||
} else {
|
||
// 使用LIKE进行模糊匹配
|
||
subQuery += " AND category LIKE ?"
|
||
subArgs = append(subArgs, "%"+decodedValue+"%")
|
||
}
|
||
conditionOrder = append(conditionOrder, key)
|
||
continue
|
||
}
|
||
|
||
// 特殊处理buy_count参数 - 根据saleSelect使用不同的字段
|
||
if key == "buy_counts" {
|
||
key = saleCountField // 替换为动态的销售字段
|
||
}
|
||
|
||
if contains(exactFields, key) {
|
||
subQuery += fmt.Sprintf(" AND %s = ?", key)
|
||
subArgs = append(subArgs, value)
|
||
conditionOrder = append(conditionOrder, key)
|
||
continue
|
||
}
|
||
|
||
if contains(numericRangeFields, key) {
|
||
// 特殊处理:如果buy_counts或sell_counts为0到999999的范围,则跳过不加入查询条件
|
||
if (key == "buy_counts" || key == "sell_counts") && strings.Contains(value, ",") {
|
||
parts := strings.Split(value, ",")
|
||
if len(parts) == 2 && parts[0] == "0" && parts[1] == "999999" {
|
||
// 跳过0到999999的范围查询,不加入WHERE条件
|
||
conditionOrder = append(conditionOrder, key)
|
||
continue
|
||
}
|
||
}
|
||
|
||
// 所有数值字段都使用>=条件,不使用BETWEEN,以便更好地利用索引
|
||
if strings.Contains(value, ",") {
|
||
parts := strings.Split(value, ",")
|
||
if len(parts) == 2 {
|
||
// 如果是类似"0,999999"的格式,只使用第一个值作为大于等于条件
|
||
subQuery += fmt.Sprintf(" AND %s >= ?", key)
|
||
if num, err := strconv.Atoi(parts[0]); err == nil {
|
||
subArgs = append(subArgs, num)
|
||
} else {
|
||
subArgs = append(subArgs, parts[0])
|
||
}
|
||
}
|
||
} else {
|
||
subQuery += fmt.Sprintf(" AND %s >= ?", key)
|
||
subArgs = append(subArgs, value)
|
||
conditionOrder = append(conditionOrder, key)
|
||
}
|
||
continue
|
||
}
|
||
|
||
if contains(boolFields, key) {
|
||
if value == "0" {
|
||
subQuery += fmt.Sprintf(" AND %s = 0", key)
|
||
} else if value == "1" {
|
||
subQuery += fmt.Sprintf(" AND %s = 1", key)
|
||
}
|
||
continue
|
||
}
|
||
|
||
if contains(dateFields, key) {
|
||
if strings.Contains(value, ",") {
|
||
parts := strings.Split(value, ",")
|
||
if len(parts) == 2 {
|
||
subQuery += fmt.Sprintf(" AND %s BETWEEN ? AND ?", key)
|
||
subArgs = append(subArgs, parts[0], parts[1])
|
||
}
|
||
} else {
|
||
subQuery += fmt.Sprintf(" AND %s >= ? AND %s < DATE_ADD(?, INTERVAL 1 DAY)", key, key)
|
||
subArgs = append(subArgs, value, value)
|
||
}
|
||
continue
|
||
}
|
||
// 特殊处理书名和作者的全模糊查询
|
||
if key == "book_name" || key == "author" {
|
||
subQuery += fmt.Sprintf(" AND %s LIKE ?", key)
|
||
subArgs = append(subArgs, "%"+value+"%") // 全模糊匹配
|
||
continue
|
||
}
|
||
|
||
// 对于已知前缀的查询使用索引
|
||
if strings.HasPrefix(value, "%") {
|
||
subQuery += fmt.Sprintf(" AND %s LIKE ?", key)
|
||
} else {
|
||
subQuery += fmt.Sprintf(" AND %s LIKE ?", key)
|
||
value = value + "%"
|
||
}
|
||
subArgs = append(subArgs, value)
|
||
}
|
||
|
||
// 处理分页
|
||
perPage, err := getIntParam(c, "per_page", "pageSize", 10)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "每页数量参数无效"})
|
||
return
|
||
}
|
||
origPerPage := perPage
|
||
perPage = sanitizePerPage(perPage)
|
||
if perPage != origPerPage {
|
||
log.Printf("per_page 超出限制: %d -> %d (已裁剪)", origPerPage, perPage)
|
||
LogLargePerPage(c, origPerPage, perPage)
|
||
}
|
||
|
||
page, err := getInt64Param(c, "page", "pageNum", 1)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "页码参数无效"})
|
||
return
|
||
}
|
||
|
||
// 如果请求的 per_page 超过上限,则直接返回空数组,避免重查询
|
||
if origPerPage != perPage {
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"data": []map[string]interface{}{},
|
||
"total": 0,
|
||
"per_page": origPerPage,
|
||
"current_page": page,
|
||
})
|
||
return
|
||
}
|
||
|
||
// 为 COUNT 查询单独保留一个不含排序与分页的过滤语句
|
||
filterQuery := subQuery
|
||
|
||
// 子查询添加排序和分页(仅用于实际数据查询)
|
||
subQuery = filterQuery + " ORDER BY id DESC"
|
||
offset := (page - 1) * int64(perPage)
|
||
subQuery += " LIMIT ? OFFSET ?"
|
||
subArgs = append(subArgs, perPage, offset)
|
||
|
||
// 构建最终的优化查询 - 使用子查询+JOIN
|
||
finalQuery := `SELECT
|
||
bc.id,
|
||
bc.book_name,
|
||
bc.book_pic,
|
||
bc.isbn,
|
||
bc.author,
|
||
bc.category,
|
||
bc.publisher,
|
||
bc.publication_time,
|
||
bc.binding_layout,
|
||
bc.fix_price,
|
||
bc.buy_counts,
|
||
bc.sell_counts,
|
||
bc.vio_book,
|
||
bc.book_set,
|
||
bc.onenum_mbooks,
|
||
bc.ill_publisher,
|
||
bc.ill_author,
|
||
bc.day_sale_7,
|
||
bc.day_sale_15,
|
||
bc.day_sale_30,
|
||
bc.day_sale_60,
|
||
bc.day_sale_90,
|
||
bc.day_sale_180,
|
||
bc.day_sale_365,
|
||
bc.this_year_sale,
|
||
bc.last_year_sale,
|
||
bc.total_sale,
|
||
bc.update_time,
|
||
bc.book_pic_new
|
||
FROM (` + subQuery + `) AS ids
|
||
JOIN book_center bc ON ids.id = bc.id
|
||
ORDER BY bc.id DESC`
|
||
|
||
// 打印执行的SQL语句和参数
|
||
log.Printf("执行优化SQL查询: %s", finalQuery)
|
||
log.Printf("SQL参数: %v 访问ip:%s", subArgs, c.ClientIP())
|
||
|
||
// 执行查询
|
||
stmt, err := bc.db.Prepare(finalQuery)
|
||
if err != nil {
|
||
log.Printf("Prepare statement error: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "准备查询语句失败"})
|
||
return
|
||
}
|
||
defer stmt.Close()
|
||
|
||
var rows *sql.Rows
|
||
rows, err = stmt.Query(subArgs...)
|
||
if err != nil {
|
||
maxRetries := 3
|
||
for i := 0; i < maxRetries; i++ {
|
||
rows, err = stmt.Query(subArgs...)
|
||
if err == nil {
|
||
break
|
||
}
|
||
time.Sleep(time.Second * time.Duration(i+1))
|
||
}
|
||
if err != nil {
|
||
log.Printf("Database query error: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"error": "数据库查询失败",
|
||
"details": err.Error(),
|
||
"query": finalQuery,
|
||
})
|
||
return
|
||
}
|
||
}
|
||
defer rows.Close()
|
||
|
||
// 处理查询结果
|
||
columns, err := rows.Columns()
|
||
if err != nil {
|
||
log.Printf("Get columns error: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取列信息失败"})
|
||
return
|
||
}
|
||
|
||
var results []map[string]interface{}
|
||
for rows.Next() {
|
||
values := make([]interface{}, len(columns))
|
||
pointers := make([]interface{}, len(columns))
|
||
for i := range values {
|
||
pointers[i] = &values[i]
|
||
}
|
||
|
||
if err := rows.Scan(pointers...); err != nil {
|
||
log.Printf("Row scan error: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取行数据失败"})
|
||
return
|
||
}
|
||
|
||
row := make(map[string]interface{})
|
||
for i, col := range columns {
|
||
if col == "publication_time" {
|
||
if values[i] != nil {
|
||
switch v := values[i].(type) {
|
||
case int64:
|
||
t := time.Unix(v, 0)
|
||
row[col] = t.Format("2006-01")
|
||
case []byte:
|
||
timeStr := string(v)
|
||
if t, err := time.Parse("2006-01-02", timeStr); err == nil {
|
||
row[col] = t.Format("2006-01")
|
||
} else if t, err := time.Parse(time.RFC3339, timeStr); err == nil {
|
||
row[col] = t.Format("2006-01")
|
||
} else {
|
||
row[col] = timeStr
|
||
}
|
||
default:
|
||
row[col] = values[i]
|
||
}
|
||
} else {
|
||
row[col] = nil
|
||
}
|
||
} else if col == "update_time" {
|
||
if values[i] != nil {
|
||
switch v := values[i].(type) {
|
||
case int64:
|
||
// 处理时间戳,转换为标准时间格式
|
||
t := time.Unix(v, 0)
|
||
row[col] = t.Format("2006-01-02 15:04:05")
|
||
case []byte:
|
||
// 处理字符串形式的时间戳
|
||
timeStr := string(v)
|
||
if timestamp, err := strconv.ParseInt(timeStr, 10, 64); err == nil {
|
||
t := time.Unix(timestamp, 0)
|
||
row[col] = t.Format("2006-01-02 15:04:05")
|
||
} else {
|
||
row[col] = timeStr
|
||
}
|
||
case string:
|
||
// 处理字符串形式的时间戳
|
||
if timestamp, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||
t := time.Unix(timestamp, 0)
|
||
row[col] = t.Format("2006-01-02 15:04:05")
|
||
} else {
|
||
row[col] = v
|
||
}
|
||
default:
|
||
row[col] = values[i]
|
||
}
|
||
} else {
|
||
row[col] = nil
|
||
}
|
||
} else {
|
||
if b, ok := values[i].([]byte); ok {
|
||
row[col] = string(b)
|
||
} else {
|
||
row[col] = values[i]
|
||
}
|
||
}
|
||
}
|
||
results = append(results, row)
|
||
}
|
||
|
||
if err := rows.Err(); err != nil {
|
||
log.Printf("Rows error: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "处理结果集时出错"})
|
||
return
|
||
}
|
||
|
||
// 获取总数 - 使用不带 ORDER BY/LIMIT 的过滤查询,避免排序开销
|
||
log.Printf("执行计数过滤SQL: %s", filterQuery)
|
||
countQuery := "SELECT COUNT(*) FROM (" + filterQuery + ") AS count_table"
|
||
var total int
|
||
countArgs := subArgs[:len(subArgs)-2]
|
||
|
||
// 打印总数查询的SQL语句和参数
|
||
log.Printf("执行总数查询SQL: %s", countQuery)
|
||
log.Printf("总数查询SQL参数: %v", countArgs)
|
||
|
||
if err := bc.db.QueryRow(countQuery, countArgs...).Scan(&total); err != nil {
|
||
log.Printf("Count query error: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取总数失败"})
|
||
return
|
||
}
|
||
if results == nil {
|
||
results = []map[string]interface{}{}
|
||
}
|
||
// 构建响应数据
|
||
responseData := gin.H{
|
||
"data": results,
|
||
"total": total,
|
||
"per_page": perPage,
|
||
"current_page": page,
|
||
}
|
||
|
||
// 将结果存入缓存,有效期15分钟
|
||
bc.cache.Set(cacheKey, responseData, 15*time.Minute)
|
||
|
||
// 记录查询时间
|
||
elapsed := time.Since(startTime)
|
||
// 输出WHERE条件追加顺序,辅助分析索引前缀匹配情况
|
||
log.Printf("WHERE 条件顺序: %v", conditionOrder)
|
||
log.Printf("Optimized query executed in %v", elapsed)
|
||
|
||
c.JSON(http.StatusOK, responseData)
|
||
}
|
||
|
||
// 将下划线命名转换为驼峰命名
|
||
//func toCamelCase(s string) string {
|
||
// parts := strings.Split(s, "_")
|
||
// for i := 1; i < len(parts); i++ {
|
||
// parts[i] = strings.Title(parts[i])
|
||
// }
|
||
// return strings.Join(parts, "")
|
||
//}
|
||
|
||
// generateCacheKey 生成唯一的缓存键}
|
||
|
||
// GetBookBaseInfoOptimized 优化的图书基础信息查询方法
|
||
// 根据是否传递销量相关参数区分普通查询和高级搜索
|
||
func (bc *BookCenterController) GetBookBaseInfoOptimized(c *gin.Context) {
|
||
startTime := time.Now()
|
||
|
||
// 生成缓存键
|
||
cacheKey := generateCacheKey(c.Request.URL.Query())
|
||
|
||
// 尝试从缓存获取
|
||
if cachedData, found := bc.cache.Get(cacheKey); found {
|
||
c.JSON(http.StatusOK, cachedData)
|
||
return
|
||
}
|
||
|
||
queryParams := c.Request.URL.Query()
|
||
|
||
// 获取分页参数
|
||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||
pageSize, _ := strconv.Atoi(c.DefaultQuery("per_page", "20"))
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if pageSize < 1 || pageSize > 100 {
|
||
pageSize = 20
|
||
}
|
||
offset := (page - 1) * pageSize
|
||
|
||
log.Printf("查询参数: %v, 分页: page=%d, per_page=%d", queryParams, page, pageSize)
|
||
|
||
//----------------------------------高级搜索------------------------------
|
||
subQuery := "WHERE 1=1"
|
||
subArgs := []interface{}{}
|
||
|
||
// saleSelect 决定销量字段
|
||
saleSelect := c.DefaultQuery("saleSelect", "2")
|
||
var saleCountField string
|
||
switch saleSelect {
|
||
case "7":
|
||
saleCountField = "day_sale_7"
|
||
case "15":
|
||
saleCountField = "day_sale_15"
|
||
case "30":
|
||
saleCountField = "day_sale_30"
|
||
case "60":
|
||
saleCountField = "day_sale_60"
|
||
case "90":
|
||
saleCountField = "day_sale_90"
|
||
case "180":
|
||
saleCountField = "day_sale_180"
|
||
case "365":
|
||
saleCountField = "day_sale_365"
|
||
default:
|
||
saleCountField = ""
|
||
}
|
||
|
||
// 1️⃣ 销量字段筛选
|
||
// ✅ 仅当 saleCountField 不为空时才拼接条件
|
||
if saleCountField != "" {
|
||
subQuery += fmt.Sprintf(" AND %s >= ?", saleCountField)
|
||
subArgs = append(subArgs, 1)
|
||
}
|
||
// 2️⃣ isbn
|
||
if v := queryParams.Get("isbn"); v != "" {
|
||
subQuery += " AND isbn = ?"
|
||
subArgs = append(subArgs, v)
|
||
}
|
||
// book_name
|
||
if v := queryParams.Get("book_name"); v != "" {
|
||
subQuery += " AND book_name = ?"
|
||
subArgs = append(subArgs, v)
|
||
}
|
||
// author
|
||
if v := queryParams.Get("author"); v != "" {
|
||
subQuery += " AND author = ?"
|
||
subArgs = append(subArgs, v)
|
||
}
|
||
// publisher
|
||
if v := queryParams.Get("publisher"); v != "" {
|
||
subQuery += " AND publisher = ?"
|
||
subArgs = append(subArgs, v)
|
||
}
|
||
// 3️⃣ bookPic
|
||
if v := queryParams.Get("book_pic"); v != "" {
|
||
switch v {
|
||
case "1":
|
||
subQuery += " AND book_pic > ''" // 能用索引
|
||
case "0":
|
||
subQuery += " AND book_pic = ''"
|
||
}
|
||
}
|
||
|
||
// 2️⃣ vio_book
|
||
if v := queryParams.Get("vio_book"); v != "" {
|
||
subQuery += " AND vio_book = ?"
|
||
subArgs = append(subArgs, v)
|
||
}
|
||
|
||
// 3️⃣ book_set
|
||
if v := queryParams.Get("book_set"); v != "" {
|
||
subQuery += " AND book_set = ?"
|
||
subArgs = append(subArgs, v)
|
||
}
|
||
|
||
// 4️⃣ onenum_mbooks
|
||
if v := queryParams.Get("onenum_mbooks"); v != "" {
|
||
subQuery += " AND onenum_mbooks = ?"
|
||
subArgs = append(subArgs, v)
|
||
}
|
||
|
||
// 5️⃣ ill_publisher
|
||
if v := queryParams.Get("ill_publisher"); v != "" {
|
||
subQuery += " AND ill_publisher = ?"
|
||
subArgs = append(subArgs, v)
|
||
}
|
||
|
||
// 6️⃣ ill_author
|
||
if v := queryParams.Get("ill_author"); v != "" {
|
||
subQuery += " AND ill_author = ?"
|
||
subArgs = append(subArgs, v)
|
||
}
|
||
|
||
// 7️⃣ category
|
||
if v := queryParams.Get("category"); v != "" {
|
||
if v == "排除大学教材" {
|
||
subQuery += " AND category != ?"
|
||
subArgs = append(subArgs, "图书/教材教辅考试/大学教材")
|
||
} else {
|
||
subQuery += " AND category = ?"
|
||
subArgs = append(subArgs, v)
|
||
}
|
||
}
|
||
|
||
// 8️⃣ buy_counts
|
||
if vals := queryParams["buy_counts"]; len(vals) > 0 {
|
||
parts := strings.Split(vals[0], ",")
|
||
if len(parts) > 0 {
|
||
subQuery += " AND buy_counts >= ?"
|
||
subArgs = append(subArgs, parts[0])
|
||
}
|
||
}
|
||
|
||
// 9️⃣ sell_counts
|
||
if vals := queryParams["sell_counts"]; len(vals) > 0 {
|
||
parts := strings.Split(vals[0], ",")
|
||
if len(parts) > 0 {
|
||
subQuery += " AND sell_counts >= ?"
|
||
subArgs = append(subArgs, parts[0])
|
||
}
|
||
}
|
||
|
||
log.Println("最终 SQL 条件:", subQuery)
|
||
|
||
// ------------------ 统计总数 ------------------
|
||
countQuery := "SELECT COUNT(id) FROM book_center " + subQuery
|
||
log.Printf("查询总数 SQL: %s, 参数: %v", countQuery, subArgs)
|
||
|
||
var total int
|
||
if err := bc.db.QueryRow(countQuery, subArgs...).Scan(&total); err != nil {
|
||
log.Printf("Count query error: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取总数失败"})
|
||
return
|
||
}
|
||
|
||
// ------------------ 查询数据列表 ------------------
|
||
fields := `id, book_name, book_pic, isbn, author, category, publisher,
|
||
publication_time, binding_layout, fix_price, buy_counts, sell_counts,
|
||
vio_book, book_set, onenum_mbooks, ill_publisher, ill_author,
|
||
day_sale_7, day_sale_15, day_sale_30, day_sale_60, day_sale_90,
|
||
day_sale_180, day_sale_365, this_year_sale, last_year_sale,
|
||
total_sale, update_time`
|
||
|
||
dataQuery := fmt.Sprintf("SELECT %s FROM book_center %s ORDER BY id DESC LIMIT ? OFFSET ?", fields, subQuery)
|
||
subArgsWithLimit := append(subArgs, pageSize, offset)
|
||
log.Printf("查询数据 SQL: %s, 参数: %v", dataQuery, subArgsWithLimit)
|
||
|
||
rows, err := bc.db.Query(dataQuery, subArgsWithLimit...)
|
||
if err != nil {
|
||
log.Printf("Data query error: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询数据失败"})
|
||
return
|
||
}
|
||
defer rows.Close()
|
||
|
||
var results []map[string]interface{}
|
||
for rows.Next() {
|
||
var book struct {
|
||
ID int64 `db:"id"`
|
||
BookName string `db:"book_name"`
|
||
BookPic string `db:"book_pic"`
|
||
ISBN string `db:"isbn"`
|
||
Author string `db:"author"`
|
||
Category string `db:"category"`
|
||
Publisher string `db:"publisher"`
|
||
PublicationTime *int64 `db:"publication_time"`
|
||
BindingLayout string `db:"binding_layout"`
|
||
FixPrice *int `db:"fix_price"`
|
||
BuyCounts *int64 `db:"buy_counts"`
|
||
SellCounts *int64 `db:"sell_counts"`
|
||
VioBook int `db:"vio_book"`
|
||
BookSet int `db:"book_set"`
|
||
OnenumMbooks *int `db:"onenum_mbooks"`
|
||
IllPublisher int `db:"ill_publisher"`
|
||
IllAuthor int `db:"ill_author"`
|
||
DaySale7 int `db:"day_sale_7"`
|
||
DaySale15 int `db:"day_sale_15"`
|
||
DaySale30 int `db:"day_sale_30"`
|
||
DaySale60 int `db:"day_sale_60"`
|
||
DaySale90 int `db:"day_sale_90"`
|
||
DaySale180 int `db:"day_sale_180"`
|
||
DaySale365 int `db:"day_sale_365"`
|
||
ThisYearSale int `db:"this_year_sale"`
|
||
LastYearSale int `db:"last_year_sale"`
|
||
TotalSale int `db:"total_sale"`
|
||
UpdateTime int64 `db:"update_time"`
|
||
}
|
||
|
||
if err := rows.Scan(
|
||
&book.ID, &book.BookName, &book.BookPic, &book.ISBN, &book.Author,
|
||
&book.Category, &book.Publisher, &book.PublicationTime, &book.BindingLayout,
|
||
&book.FixPrice, &book.BuyCounts, &book.SellCounts, &book.VioBook,
|
||
&book.BookSet, &book.OnenumMbooks, &book.IllPublisher, &book.IllAuthor,
|
||
&book.DaySale7, &book.DaySale15, &book.DaySale30, &book.DaySale60,
|
||
&book.DaySale90, &book.DaySale180, &book.DaySale365, &book.ThisYearSale,
|
||
&book.LastYearSale, &book.TotalSale, &book.UpdateTime,
|
||
); err != nil {
|
||
log.Printf("Row scan error: %v", err)
|
||
continue
|
||
}
|
||
|
||
// 格式化 publication_time
|
||
var publicationTimeStr string
|
||
if book.PublicationTime != nil {
|
||
t := time.Unix(*book.PublicationTime, 0)
|
||
publicationTimeStr = t.Format("2006-01")
|
||
}
|
||
|
||
// 格式化 update_time
|
||
updateTimeStr := ""
|
||
if book.UpdateTime != 0 {
|
||
t := time.Unix(book.UpdateTime, 0)
|
||
updateTimeStr = t.Format("2006-01-02 15:04:05")
|
||
}
|
||
|
||
// 转换为与示例完全一致的格式
|
||
bookMap := map[string]interface{}{
|
||
"id": book.ID,
|
||
"book_name": book.BookName,
|
||
"book_pic": book.BookPic,
|
||
"isbn": book.ISBN,
|
||
"author": book.Author,
|
||
"category": book.Category,
|
||
"publisher": book.Publisher,
|
||
"publication_time": publicationTimeStr,
|
||
"binding_layout": book.BindingLayout,
|
||
"fix_price": book.FixPrice,
|
||
"buy_counts": book.BuyCounts,
|
||
"sell_counts": book.SellCounts,
|
||
"vio_book": book.VioBook,
|
||
"book_set": book.BookSet,
|
||
"onenum_mbooks": book.OnenumMbooks,
|
||
"ill_publisher": book.IllPublisher,
|
||
"ill_author": book.IllAuthor,
|
||
"day_sale_7": book.DaySale7,
|
||
"day_sale_15": book.DaySale15,
|
||
"day_sale_30": book.DaySale30,
|
||
"day_sale_60": book.DaySale60,
|
||
"day_sale_90": book.DaySale90,
|
||
"day_sale_180": book.DaySale180,
|
||
"day_sale_365": book.DaySale365,
|
||
"this_year_sale": book.ThisYearSale,
|
||
"last_year_sale": book.LastYearSale,
|
||
"total_sale": book.TotalSale,
|
||
"update_time": updateTimeStr,
|
||
}
|
||
|
||
results = append(results, bookMap)
|
||
}
|
||
|
||
if err = rows.Err(); err != nil {
|
||
log.Printf("Rows iteration error: %v", err)
|
||
}
|
||
|
||
elapsed := time.Since(startTime)
|
||
log.Printf("执行耗时: %v, 总数: %d, 返回数据: %d 条", elapsed, total, len(results))
|
||
|
||
// 返回结果
|
||
responseData := gin.H{
|
||
"current_page": page,
|
||
"data": results,
|
||
"per_page": pageSize,
|
||
"total": total,
|
||
}
|
||
|
||
// 缓存 15 分钟
|
||
bc.cache.Set(cacheKey, responseData, 15*time.Minute)
|
||
|
||
c.JSON(http.StatusOK, responseData)
|
||
}
|
||
|
||
func generateCacheKey(values url.Values) string {
|
||
// 复制并排序查询参数以确保相同的查询生成相同的键
|
||
params := make(url.Values)
|
||
for k, v := range values {
|
||
params[k] = v
|
||
}
|
||
|
||
// 对参数进行排序
|
||
var keys []string
|
||
for k := range params {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Strings(keys)
|
||
|
||
// 构建缓存键
|
||
var buf bytes.Buffer
|
||
for _, k := range keys {
|
||
vs := params[k]
|
||
buf.WriteString(k)
|
||
buf.WriteString("=")
|
||
buf.WriteString(strings.Join(vs, ","))
|
||
buf.WriteString("&")
|
||
}
|
||
|
||
// 使用MD5哈希生成固定长度的键
|
||
hash := md5.Sum(buf.Bytes())
|
||
return fmt.Sprintf("book_query:%x", hash)
|
||
}
|
||
|
||
// getIntParam 获取整数参数
|
||
func getIntParam(c *gin.Context, primaryParam string, secondaryParam string, defaultValue int) (int, error) {
|
||
paramStr := c.Query(primaryParam)
|
||
if paramStr == "" {
|
||
paramStr = c.Query(secondaryParam)
|
||
}
|
||
if paramStr == "" {
|
||
return defaultValue, nil
|
||
}
|
||
return strconv.Atoi(paramStr)
|
||
}
|
||
|
||
// getInt64Param 获取64位整数参数
|
||
func getInt64Param(c *gin.Context, primaryParam string, secondaryParam string, defaultValue int64) (int64, error) {
|
||
paramStr := c.Query(primaryParam)
|
||
if paramStr == "" {
|
||
paramStr = c.Query(secondaryParam)
|
||
}
|
||
if paramStr == "" {
|
||
return defaultValue, nil
|
||
}
|
||
return strconv.ParseInt(paramStr, 10, 64)
|
||
}
|
||
|
||
// sanitizePerPage 校验并限制 per_page 的范围,防止过大导致数据库压力。
|
||
// 返回值:合法的每页数量,若超出上限则裁剪到最大值。
|
||
func sanitizePerPage(n int) int {
|
||
if n <= 0 {
|
||
return 10
|
||
}
|
||
const maxPerPage = 1000
|
||
if n > maxPerPage {
|
||
return maxPerPage
|
||
}
|
||
return n
|
||
}
|
||
|
||
// contains 检查字符串是否在切片中
|
||
func contains(slice []string, item string) bool {
|
||
for _, s := range slice {
|
||
if s == item {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// GetRandomBookBaseInfo 条件查询(随机,不需要加入缓存)
|
||
func (bc *BookCenterController) GetRandomBookBaseInfo(c *gin.Context) {
|
||
bc.GetBookBaseInfo(c)
|
||
// 启动计时器
|
||
//startTime := time.Now()
|
||
//
|
||
//saleSelect := c.DefaultQuery("saleSelect", "2")
|
||
//
|
||
//// 初始化查询语句和参数
|
||
//baseQuery := "SELECT id,book_name,isbn,book_set,publisher,author FROM book_center WHERE 1=1"
|
||
//
|
||
//var args []interface{}
|
||
//
|
||
//// 定义各种查询字段类型
|
||
//exactFields := []string{
|
||
// "id", "isbn", "publisher",
|
||
//}
|
||
//
|
||
//// 根据 saleSelect 决定使用哪个字段作为销售数量字段
|
||
//var saleCountField string
|
||
//switch saleSelect {
|
||
//case "7":
|
||
// saleCountField = "day_sale_7"
|
||
//case "15":
|
||
// saleCountField = "day_sale_15"
|
||
//case "30":
|
||
// saleCountField = "day_sale_30"
|
||
//case "60":
|
||
// saleCountField = "day_sale_60"
|
||
//case "90":
|
||
// saleCountField = "day_sale_90"
|
||
//case "180":
|
||
// saleCountField = "day_sale_180"
|
||
//case "365":
|
||
// saleCountField = "day_sale_365"
|
||
//case "0":
|
||
// saleCountField = "this_year_sale"
|
||
//case "1":
|
||
// saleCountField = "last_year_sale"
|
||
//default:
|
||
// saleCountField = "buy_counts"
|
||
//}
|
||
//
|
||
//numericRangeFields := []string{"buy_counts", "sell_counts", "day_sale_7", "day_sale_15", "day_sale_30", "day_sale_60", "day_sale_90", "day_sale_180", "day_sale_365", "this_year_sale", "last_year_sale"}
|
||
//boolFields := []string{"book_pic", "vio_book", "book_set", "onenum_mbooks", "ill_publisher", "ill_author"}
|
||
//dateFields := []string{"print_time", "publiction_times"}
|
||
//camelToSnakeMap := map[string]string{
|
||
// "bookName": "book_name",
|
||
// "vioBook": "vio_book",
|
||
// "bookSet": "book_set",
|
||
// "onenumMbooks": "onenum_mbooks",
|
||
// "illPublisher": "ill_publisher",
|
||
// "illAuthor": "ill_author",
|
||
// "bookPic": "book_pic",
|
||
//}
|
||
//
|
||
//// 定义要排除的参数名
|
||
//excludedParams := []string{"page", "per_page", "is_pricing", "saleSelect"}
|
||
//
|
||
//// 处理查询参数
|
||
//query := baseQuery
|
||
//for key, values := range c.Request.URL.Query() {
|
||
// if contains(excludedParams, key) {
|
||
// continue
|
||
// }
|
||
// if len(values) == 0 || values[0] == "" {
|
||
// continue
|
||
// }
|
||
//
|
||
// value := values[0]
|
||
// // 处理驼峰参数转换为下划线
|
||
// if dbField, ok := camelToSnakeMap[key]; ok {
|
||
// key = dbField
|
||
// }
|
||
//
|
||
// // 特殊处理book_pic参数
|
||
// if key == "book_pic" {
|
||
// if value == "0" {
|
||
// query += " AND (book_pic IS NULL OR book_pic = '')"
|
||
// } else if value == "1" {
|
||
// query += " AND (book_pic IS NOT NULL AND book_pic != '')"
|
||
// }
|
||
// continue
|
||
// }
|
||
//
|
||
// // 特殊处理category参数
|
||
// if key == "category" {
|
||
// // 对URL编码的值进行解码
|
||
// decodedValue, err := url.QueryUnescape(value)
|
||
// if err != nil {
|
||
// decodedValue = value
|
||
// }
|
||
// // 使用LIKE进行模糊匹配
|
||
// query += " AND category LIKE ?"
|
||
// args = append(args, "%"+decodedValue+"%")
|
||
// continue
|
||
// }
|
||
//
|
||
// // 特殊处理buy_count参数 - 根据saleSelect使用不同的字段
|
||
// if key == "buy_counts" {
|
||
// key = saleCountField
|
||
// }
|
||
//
|
||
// if contains(exactFields, key) {
|
||
// query += fmt.Sprintf(" AND %s = ?", key)
|
||
// args = append(args, value)
|
||
// continue
|
||
// }
|
||
//
|
||
// if contains(numericRangeFields, key) {
|
||
// if strings.Contains(value, ",") {
|
||
// parts := strings.Split(value, ",")
|
||
// if len(parts) == 2 {
|
||
// query += fmt.Sprintf(" AND %s BETWEEN ? AND ?", key)
|
||
// args = append(args, parts[0], parts[1])
|
||
// }
|
||
// } else {
|
||
// query += fmt.Sprintf(" AND %s > ?", key)
|
||
// args = append(args, value)
|
||
// }
|
||
// continue
|
||
// }
|
||
//
|
||
// if contains(boolFields, key) {
|
||
// if value == "0" {
|
||
// query += fmt.Sprintf(" AND %s = 0", key)
|
||
// } else if value == "1" {
|
||
// query += fmt.Sprintf(" AND %s = 1", key)
|
||
// }
|
||
// continue
|
||
// }
|
||
//
|
||
// if contains(dateFields, key) {
|
||
// if strings.Contains(value, ",") {
|
||
// parts := strings.Split(value, ",")
|
||
// if len(parts) == 2 {
|
||
// query += fmt.Sprintf(" AND %s BETWEEN ? AND ?", key)
|
||
// args = append(args, parts[0], parts[1])
|
||
// }
|
||
// } else {
|
||
// query += fmt.Sprintf(" AND %s >= ? AND %s < DATE_ADD(?, INTERVAL 1 DAY)", key, key)
|
||
// args = append(args, value, value)
|
||
// }
|
||
// continue
|
||
// }
|
||
// //// 特殊处理书名和作者的全模糊查询
|
||
// //if key == "book_name" || key == "author" {
|
||
// // query += fmt.Sprintf(" AND %s LIKE ?", key)
|
||
// // args = append(args, "%"+value+"%")
|
||
// // continue
|
||
// //}
|
||
// if key == "book_name" || key == "author" {
|
||
// query += fmt.Sprintf(" AND MATCH(%s) AGAINST(? IN BOOLEAN MODE)", key)
|
||
// args = append(args, value)
|
||
// continue
|
||
// }
|
||
// // 对于已知前缀的查询使用索引
|
||
// if strings.HasPrefix(value, "%") {
|
||
// query += fmt.Sprintf(" AND %s LIKE ?", key)
|
||
// } else {
|
||
// query += fmt.Sprintf(" AND %s LIKE ?", key)
|
||
// value = value + "%"
|
||
// }
|
||
// args = append(args, value)
|
||
//}
|
||
//
|
||
//// 获取总数
|
||
//countQuery := "SELECT COUNT(*) FROM (" + query + ") AS count_table"
|
||
//var total int
|
||
//if err := bc.db.QueryRow(countQuery, args...).Scan(&total); err != nil {
|
||
// log.Printf("Count query error: %v", err)
|
||
// c.JSON(http.StatusInternalServerError, gin.H{"error": "获取总数失败"})
|
||
// return
|
||
//}
|
||
//
|
||
//// 处理分页参数
|
||
//perPage, err := getIntParam(c, "per_page", "pageSize", 10)
|
||
//if err != nil {
|
||
// c.JSON(http.StatusBadRequest, gin.H{"error": "每页数量参数无效"})
|
||
// return
|
||
//}
|
||
//
|
||
//page, err := getInt64Param(c, "page", "pageNum", 1)
|
||
//if err != nil {
|
||
// c.JSON(http.StatusBadRequest, gin.H{"error": "页码参数无效"})
|
||
// return
|
||
//}
|
||
//
|
||
//var results []map[string]interface{}
|
||
//
|
||
//// 优化后的随机查询逻辑
|
||
//randomQuery := query + " ORDER BY RAND() LIMIT ?"
|
||
//randomArgs := append(args, perPage)
|
||
//
|
||
//// 打印SQL查询语句和参数用于调试
|
||
////log.Printf("GetRandomBookBaseInfo SQL查询: %s", randomQuery)
|
||
////log.Printf("GetRandomBookBaseInfo 查询参数: %+v", randomArgs)
|
||
//
|
||
//stmt, err := bc.db.Prepare(randomQuery)
|
||
//if err != nil {
|
||
// log.Printf("Prepare statement error: %v", err)
|
||
// c.JSON(http.StatusInternalServerError, gin.H{"error": "准备查询语句失败"})
|
||
// return
|
||
//}
|
||
//defer stmt.Close()
|
||
//
|
||
//rows, err := stmt.Query(randomArgs...)
|
||
//if err != nil {
|
||
// log.Printf("Database query error: %v", err)
|
||
// c.JSON(http.StatusInternalServerError, gin.H{
|
||
// "error": "数据库查询失败",
|
||
// "details": err.Error(),
|
||
// "query": randomQuery,
|
||
// })
|
||
// return
|
||
//}
|
||
//defer rows.Close()
|
||
//
|
||
//// 处理查询结果
|
||
//columns, err := rows.Columns()
|
||
//if err != nil {
|
||
// log.Printf("Get columns error: %v", err)
|
||
// c.JSON(http.StatusInternalServerError, gin.H{"error": "获取列信息失败"})
|
||
// return
|
||
//}
|
||
//
|
||
//for rows.Next() {
|
||
// values := make([]interface{}, len(columns))
|
||
// pointers := make([]interface{}, len(columns))
|
||
// for i := range values {
|
||
// pointers[i] = &values[i]
|
||
// }
|
||
//
|
||
// if err := rows.Scan(pointers...); err != nil {
|
||
// log.Printf("Row scan error: %v", err)
|
||
// c.JSON(http.StatusInternalServerError, gin.H{"error": "读取行数据失败"})
|
||
// return
|
||
// }
|
||
//
|
||
// row := make(map[string]interface{})
|
||
//
|
||
// for i, col := range columns {
|
||
// if b, ok := values[i].([]byte); ok {
|
||
// row[col] = string(b)
|
||
// } else {
|
||
// row[col] = values[i]
|
||
// }
|
||
// }
|
||
//
|
||
// results = append(results, row)
|
||
//}
|
||
//
|
||
//if err := rows.Err(); err != nil {
|
||
// log.Printf("Rows error: %v", err)
|
||
// c.JSON(http.StatusInternalServerError, gin.H{"error": "处理结果集时出错"})
|
||
// return
|
||
//}
|
||
//
|
||
//// 构建响应数据
|
||
//responseData := gin.H{
|
||
// "data": results,
|
||
// "total": total,
|
||
// "per_page": perPage,
|
||
// "current_page": page,
|
||
//}
|
||
//
|
||
//// 记录查询时间
|
||
//elapsed := time.Since(startTime)
|
||
//log.Printf("Query executed in %v", elapsed)
|
||
//
|
||
//c.JSON(http.StatusOK, responseData)
|
||
}
|
||
|
||
// GetBookByISBN 根据isbn查询(判断是否合法,在查询Redis,没有在调用博查询详情数据的接口)
|
||
func (bc *BookCenterController) GetBookByISBN(c *gin.Context) {
|
||
// 获取ISBN参数
|
||
isbn := c.Query("isbn")
|
||
if isbn == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "ISBN不能为空"})
|
||
return
|
||
}
|
||
|
||
// 验证ISBN格式
|
||
if !isValidISBN(isbn) {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "ISBN格式不正确"})
|
||
return
|
||
}
|
||
|
||
// 1. 先尝试从Redis获取
|
||
redisKey := isbn
|
||
bookData, err := bc.redis.Get(context.Background(), redisKey).Result()
|
||
if err == nil {
|
||
var result map[string]interface{}
|
||
if err := json.Unmarshal([]byte(bookData), &result); err == nil {
|
||
bookPicSMissing := false
|
||
bookPicNewMissing := false
|
||
publisherMissing := false
|
||
publicationTimeMissing := false
|
||
|
||
// 检查图片字段
|
||
if bookPicS, exists := result["book_pic_s"]; !exists || bookPicS == nil || bookPicS == "" {
|
||
bookPicSMissing = true
|
||
}
|
||
if bookPicNew, exists := result["book_pic_new"]; !exists || bookPicNew == nil || bookPicNew == "" {
|
||
bookPicNewMissing = true
|
||
}
|
||
|
||
// 检查 publisher 是否为空
|
||
if publisher, exists := result["publisher"]; !exists || publisher == nil || publisher == "" {
|
||
publisherMissing = true
|
||
}
|
||
|
||
// 检查 publication_time 是否为空或为 0
|
||
if pubTime, exists := result["publication_time"]; !exists || pubTime == nil {
|
||
publicationTimeMissing = true
|
||
} else {
|
||
switch v := pubTime.(type) {
|
||
case float64:
|
||
if v == 0 {
|
||
publicationTimeMissing = true
|
||
}
|
||
case int, int64:
|
||
if reflect.ValueOf(v).Int() == 0 {
|
||
publicationTimeMissing = true
|
||
}
|
||
case string:
|
||
if v == "" || v == "0" {
|
||
publicationTimeMissing = true
|
||
}
|
||
}
|
||
}
|
||
|
||
// ===== 如果 publisher 或 publication_time 缺失,从外部API补充 =====
|
||
if publisherMissing || publicationTimeMissing {
|
||
log.Printf("Redis中ISBN %s的publisher或publication_time字段缺失,调用外部API补充", isbn)
|
||
|
||
apiUrl := detailPagesUrl + "/api/image-url?isbn=" + isbn
|
||
log.Printf("调用外部API: %s", apiUrl)
|
||
|
||
resp, err := http.Get(apiUrl)
|
||
if err == nil && resp.StatusCode == http.StatusOK {
|
||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
|
||
var apiResponse struct {
|
||
Success bool `json:"success"`
|
||
Data struct {
|
||
Detail struct {
|
||
Publisher string `json:"publisher"`
|
||
PublicationTime FlexInt64 `json:"publication_time"`
|
||
} `json:"detail"`
|
||
} `json:"data"`
|
||
}
|
||
|
||
if err := json.Unmarshal(bodyBytes, &apiResponse); err == nil && apiResponse.Success {
|
||
if publisherMissing && apiResponse.Data.Detail.Publisher != "" {
|
||
result["publisher"] = apiResponse.Data.Detail.Publisher
|
||
log.Printf("外部API补充publisher字段: %s", apiResponse.Data.Detail.Publisher)
|
||
}
|
||
if publicationTimeMissing && apiResponse.Data.Detail.PublicationTime > 0 {
|
||
result["publication_time"] = apiResponse.Data.Detail.PublicationTime
|
||
log.Printf("外部API补充publication_time字段: %v", apiResponse.Data.Detail.PublicationTime)
|
||
}
|
||
|
||
// 更新 Redis
|
||
if resultJSON, err := json.Marshal(result); err == nil {
|
||
if err := bc.redis.Set(context.Background(), redisKey, resultJSON, 0).Err(); err != nil {
|
||
log.Printf("更新Redis失败: %v", err)
|
||
} else {
|
||
log.Printf("成功更新Redis中ISBN %s的publisher/publication_time字段", isbn)
|
||
}
|
||
}
|
||
|
||
// 更新数据库
|
||
query := `UPDATE book_center SET publisher = ?, publication_time = ? WHERE isbn = ?`
|
||
_, err = bc.db.Exec(query,
|
||
result["publisher"],
|
||
result["publication_time"],
|
||
isbn,
|
||
)
|
||
if err != nil {
|
||
log.Printf("更新数据库publisher/publication_time失败: %v", err)
|
||
} else {
|
||
log.Printf("成功更新数据库中ISBN %s的publisher/publication_time字段", isbn)
|
||
}
|
||
} else {
|
||
log.Printf("外部API调用或解析失败: %v", err)
|
||
}
|
||
} else {
|
||
log.Printf("外部API调用失败或返回状态码错误: %v, 状态码: %d", err, resp.StatusCode)
|
||
}
|
||
}
|
||
|
||
// ===== 图片字段缺失处理(保持你原有逻辑) =====
|
||
if bookPicSMissing || bookPicNewMissing {
|
||
log.Printf("Redis中ISBN %s的book_pic_s或book_pic_new字段缺失,从数据库查询补充", isbn)
|
||
query := `SELECT book_pic_s, book_pic_new FROM book_center WHERE isbn = ?`
|
||
var dbBookPicS, dbBookPicNew sql.NullString
|
||
|
||
err := bc.db.QueryRow(query, isbn).Scan(&dbBookPicS, &dbBookPicNew)
|
||
if err == nil {
|
||
if bookPicSMissing && dbBookPicS.Valid && dbBookPicS.String != "" {
|
||
result["book_pic_s"] = dbBookPicS.String
|
||
} else if bookPicSMissing {
|
||
result["book_pic_s"] = ""
|
||
}
|
||
|
||
if bookPicNewMissing && dbBookPicNew.Valid && dbBookPicNew.String != "" {
|
||
result["book_pic_new"] = dbBookPicNew.String
|
||
} else if bookPicNewMissing {
|
||
result["book_pic_new"] = ""
|
||
}
|
||
|
||
// 更新Redis缓存
|
||
if resultJSON, err := json.Marshal(result); err == nil {
|
||
if err := bc.redis.Set(context.Background(), redisKey, resultJSON, 0).Err(); err != nil {
|
||
log.Printf("更新Redis缓存失败: %v", err)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 调用 UploadBookImage 方法
|
||
bc.processBookImageUpload(result, isbn)
|
||
|
||
c.JSON(http.StatusOK, gin.H{"data": result, "source": "redis"})
|
||
return
|
||
}
|
||
}
|
||
|
||
// 2. Redis中没有,尝试从数据库获取
|
||
log.Printf("Redis中未找到ISBN %s,尝试从数据库查询", isbn)
|
||
query := `SELECT
|
||
id, book_name, book_pic, book_pic_s, book_pic_new, isbn, author, category, publisher,
|
||
publication_time, binding_layout, fix_price, buy_counts, sell_counts,
|
||
vio_book, book_set, onenum_mbooks, ill_publisher, ill_author,
|
||
day_sale_7, day_sale_15, day_sale_30, day_sale_60, day_sale_90,
|
||
day_sale_180, day_sale_365, this_year_sale, last_year_sale, total_sale,
|
||
update_time
|
||
FROM book_center
|
||
WHERE isbn = ?`
|
||
|
||
var (
|
||
id, bookName, bookPic, dbIsbn, author, category, publisher string
|
||
bindingLayout sql.NullString
|
||
bookPicS, bookPicNew sql.NullString
|
||
buyCount, sellCount sql.NullString
|
||
publicationTime interface{}
|
||
fixPrice int64
|
||
vioBook, bookSet, onenumMbooks, illPublisher, illAuthor int
|
||
daySale7, daySale15, daySale30, daySale60, daySale90 int
|
||
daySale180, daySale365, thisYearSale, lastYearSale, totalSale int
|
||
updateTime interface{}
|
||
)
|
||
|
||
err = bc.db.QueryRow(query, isbn).Scan(
|
||
&id, &bookName, &bookPic, &bookPicS, &bookPicNew, &dbIsbn, &author, &category, &publisher,
|
||
&publicationTime, &bindingLayout, &fixPrice, &buyCount, &sellCount,
|
||
&vioBook, &bookSet, &onenumMbooks, &illPublisher, &illAuthor,
|
||
&daySale7, &daySale15, &daySale30, &daySale60, &daySale90,
|
||
&daySale180, &daySale365, &thisYearSale, &lastYearSale, &totalSale,
|
||
&updateTime)
|
||
|
||
if err == nil {
|
||
// 数据库中找到数据,构建返回结果
|
||
log.Printf("数据库中找到ISBN %s的数据", isbn)
|
||
|
||
// 处理book_pic_s字段
|
||
bookPicSValue := ""
|
||
if bookPicS.Valid && bookPicS.String != "" {
|
||
bookPicSValue = bookPicS.String
|
||
}
|
||
|
||
// 处理book_pic_new字段
|
||
bookPicNewValue := ""
|
||
if bookPicNew.Valid && bookPicNew.String != "" {
|
||
bookPicNewValue = bookPicNew.String
|
||
}
|
||
|
||
// 处理binding_layout字段
|
||
bindingLayoutValue := ""
|
||
if bindingLayout.Valid && bindingLayout.String != "" {
|
||
bindingLayoutValue = bindingLayout.String
|
||
}
|
||
|
||
// 处理buy_counts字段
|
||
buyCountValue := ""
|
||
if buyCount.Valid && buyCount.String != "" {
|
||
buyCountValue = buyCount.String
|
||
}
|
||
|
||
// 处理sell_counts字段
|
||
sellCountValue := ""
|
||
if sellCount.Valid && sellCount.String != "" {
|
||
sellCountValue = sellCount.String
|
||
}
|
||
|
||
result := map[string]interface{}{
|
||
"id": id,
|
||
"book_name": bookName,
|
||
"book_pic": bookPic,
|
||
"book_pic_s": bookPicSValue,
|
||
"book_pic_new": bookPicNewValue,
|
||
"isbn": dbIsbn,
|
||
"author": author,
|
||
"category": category,
|
||
"publisher": publisher,
|
||
"publication_time": publicationTime,
|
||
"binding_layout": bindingLayoutValue,
|
||
"fix_price": fixPrice,
|
||
"buy_counts": buyCountValue,
|
||
"sell_counts": sellCountValue,
|
||
"vio_book": vioBook,
|
||
"book_set": bookSet,
|
||
"onenum_mbooks": onenumMbooks,
|
||
"ill_publisher": illPublisher,
|
||
"ill_author": illAuthor,
|
||
"day_sale_7": daySale7,
|
||
"day_sale_15": daySale15,
|
||
"day_sale_30": daySale30,
|
||
"day_sale_60": daySale60,
|
||
"day_sale_90": daySale90,
|
||
"day_sale_180": daySale180,
|
||
"day_sale_365": daySale365,
|
||
"this_year_sale": thisYearSale,
|
||
"last_year_sale": lastYearSale,
|
||
"total_sale": totalSale,
|
||
"update_time": updateTime,
|
||
}
|
||
|
||
// 调用 UploadBookImage 方法并更新字段
|
||
bc.processBookImageUpload(result, isbn)
|
||
|
||
// 将数据库查询结果同步到Redis缓存
|
||
if resultJSON, err := json.Marshal(result); err == nil {
|
||
if err := bc.redis.Set(context.Background(), redisKey, resultJSON, 0).Err(); err != nil {
|
||
log.Printf("同步数据到Redis失败: %v", err)
|
||
} else {
|
||
log.Printf("成功将ISBN %s的数据同步到Redis", isbn)
|
||
}
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{"data": result, "source": "database"})
|
||
return
|
||
} else if err != sql.ErrNoRows {
|
||
// 数据库查询出错(非记录不存在)
|
||
log.Printf("数据库查询失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库查询失败"})
|
||
return
|
||
}
|
||
|
||
// 3. Redis和数据库中都没有,调用外部API
|
||
log.Printf("Redis和数据库中都未找到ISBN %s,调用外部API", isbn)
|
||
//apiUrl := "http://175.27.224.66:8091/api/image-url?isbn=" + isbn
|
||
apiUrl := detailPagesUrl + "/api/image-url?isbn=" + isbn
|
||
|
||
// 打印请求URL
|
||
log.Printf("调用外部API: %s", apiUrl)
|
||
|
||
resp, err := http.Get(apiUrl)
|
||
if err != nil {
|
||
log.Printf("调用外部API失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "调用外部API失败"})
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 打印响应状态码
|
||
log.Printf("外部API响应状态码: %d", resp.StatusCode)
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
log.Printf("外部API返回错误状态码: %d", resp.StatusCode)
|
||
c.JSON(http.StatusNotFound, gin.H{"error": "未找到该ISBN对应的图书"})
|
||
return
|
||
}
|
||
|
||
// 读取响应体
|
||
bodyBytes, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
log.Printf("读取API响应体失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取API响应失败"})
|
||
return
|
||
}
|
||
|
||
// 打印原始响应数据
|
||
log.Printf("外部API原始响应数据: %s", string(bodyBytes))
|
||
|
||
// 解析API响应
|
||
var apiResponse struct {
|
||
Success bool `json:"success"`
|
||
Data struct {
|
||
Detail struct {
|
||
Editor string `json:"editor"`
|
||
Languages string `json:"languages"`
|
||
PrintTime string `json:"print_time"`
|
||
ISBN string `json:"isbn"`
|
||
BookPic string `json:"book_pic"`
|
||
BookName string `json:"book_name"`
|
||
Author string `json:"author"`
|
||
Format string `json:"format"`
|
||
Publisher string `json:"publisher"`
|
||
Paper string `json:"paper"`
|
||
PublicationTime FlexInt64 `json:"publication_time"`
|
||
Pages string `json:"pages"`
|
||
Edition string `json:"edition"`
|
||
Wordage string `json:"wordage"`
|
||
Category string `json:"category"`
|
||
FixPrice string `json:"fix_price"`
|
||
BindingLayout string `json:"binding_layout"`
|
||
BuyCount string `json:"buyCount"`
|
||
SellCount string `json:"sellCount"`
|
||
Content string `json:"content"`
|
||
} `json:"detail"`
|
||
ImageURL string `json:"image_url"`
|
||
} `json:"data"`
|
||
}
|
||
|
||
// 使用已读取的bodyBytes进行解析
|
||
if err := json.Unmarshal(bodyBytes, &apiResponse); err != nil {
|
||
log.Printf("解析API响应失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "解析API响应失败"})
|
||
return
|
||
}
|
||
|
||
// 打印解析后的API响应结构
|
||
log.Printf("解析后的API响应: %+v", apiResponse)
|
||
log.Printf("外部API返回的BindingLayout值: '%s'", apiResponse.Data.Detail.BindingLayout)
|
||
|
||
// 转换数据格式
|
||
transformedData := map[string]interface{}{
|
||
"author": apiResponse.Data.Detail.Author,
|
||
"binding_layout": apiResponse.Data.Detail.BindingLayout,
|
||
"book_name": apiResponse.Data.Detail.BookName,
|
||
"book_pic": apiResponse.Data.ImageURL,
|
||
"book_pic_s": nil,
|
||
"book_set": 0,
|
||
"buy_count": apiResponse.Data.Detail.BuyCount,
|
||
"buy_counts": parseCount(apiResponse.Data.Detail.BuyCount),
|
||
"cat_id": 0,
|
||
"category": apiResponse.Data.Detail.Category,
|
||
"content": apiResponse.Data.Detail.Content,
|
||
"create_by": 1,
|
||
"create_time": time.Now().Unix(),
|
||
"day_sale_15": 0,
|
||
"day_sale_180": 0,
|
||
"day_sale_30": 0,
|
||
"day_sale_365": 0,
|
||
"day_sale_60": 0,
|
||
"day_sale_7": 0,
|
||
"day_sale_90": 0,
|
||
"edition": apiResponse.Data.Detail.Edition,
|
||
"editor": apiResponse.Data.Detail.Editor,
|
||
"fix_price": parsePrice(apiResponse.Data.Detail.FixPrice),
|
||
"format": apiResponse.Data.Detail.Format,
|
||
"id": 0,
|
||
"ill_author": 0,
|
||
"ill_publisher": 0,
|
||
"isbn": getISBNValue(apiResponse.Data.Detail.ISBN, isbn),
|
||
"languages": apiResponse.Data.Detail.Languages,
|
||
"last_year_sale": 0,
|
||
"onenum_mbooks": 0,
|
||
"pages": parseCount(apiResponse.Data.Detail.Pages),
|
||
"paper": apiResponse.Data.Detail.Paper,
|
||
"print_time": parseTime(apiResponse.Data.Detail.PrintTime),
|
||
"publication_time": apiResponse.Data.Detail.PublicationTime,
|
||
"publiction_times": apiResponse.Data.Detail.PublicationTime,
|
||
"publisher": apiResponse.Data.Detail.Publisher,
|
||
"remark": "",
|
||
"sell_count": apiResponse.Data.Detail.SellCount,
|
||
"sell_counts": parseCount(apiResponse.Data.Detail.SellCount),
|
||
"shipment_cycle": 0,
|
||
"sold_out_times": "[]",
|
||
"this_year_sale": 0,
|
||
"total_sale": 0,
|
||
"update_by": 0,
|
||
"update_time": time.Now().Unix(),
|
||
"vio_book": 0,
|
||
"wordage": apiResponse.Data.Detail.Wordage,
|
||
}
|
||
// 添加详细的调试日志,检查transformedData中的binding_layout字段
|
||
log.Printf("=== GetBookByISBN Debug Info ===")
|
||
log.Printf("ISBN: %s", isbn)
|
||
log.Printf("外部API返回的BindingLayout原始值: '%s'", apiResponse.Data.Detail.BindingLayout)
|
||
log.Printf("transformedData中的binding_layout值: '%v'", transformedData["binding_layout"])
|
||
log.Printf("transformedData中binding_layout字段类型: %T", transformedData["binding_layout"])
|
||
|
||
// 检查transformedData中是否包含binding_layout字段
|
||
if val, exists := transformedData["binding_layout"]; exists {
|
||
log.Printf("✅ binding_layout字段存在于transformedData中,值为: '%v'", val)
|
||
} else {
|
||
log.Printf("❌ binding_layout字段不存在于transformedData中")
|
||
}
|
||
|
||
// 打印transformedData的所有键
|
||
keys := make([]string, 0, len(transformedData))
|
||
for k := range transformedData {
|
||
keys = append(keys, k)
|
||
}
|
||
log.Printf("transformedData包含的所有字段: %v", keys)
|
||
log.Printf("=== End Debug Info ===")
|
||
|
||
// 3. 异步存储到Redis和MySQL
|
||
go func() {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
log.Printf("异步存储过程发生panic: %v, ISBN: %s", r, isbn)
|
||
}
|
||
}()
|
||
|
||
// 存储到Redis - 将数据转换为JSON字符串
|
||
jsonData, err := json.Marshal(transformedData)
|
||
if err != nil {
|
||
log.Printf("序列化数据失败: %v, ISBN: %s", err, isbn)
|
||
return
|
||
}
|
||
if err := bc.redis.Set(context.Background(), redisKey, jsonData, 0).Err(); err != nil {
|
||
log.Printf("存储到Redis失败: %v, ISBN: %s", err, isbn)
|
||
}
|
||
|
||
// 存储到中心书库(MySQL)
|
||
if err := bc.saveToBookCenter(transformedData); err != nil {
|
||
log.Printf("存储到中心书库失败: %v, ISBN: %s", err, isbn)
|
||
}
|
||
}()
|
||
|
||
// 最终响应前的调试日志
|
||
log.Printf("=== 准备返回响应 ===")
|
||
log.Printf("ISBN: %s", isbn)
|
||
|
||
// 检查最终响应数据中的binding_layout字段
|
||
if val, exists := transformedData["binding_layout"]; exists {
|
||
log.Printf("✅ 最终响应数据包含binding_layout字段,值为: '%v'", val)
|
||
} else {
|
||
log.Printf("❌ 最终响应数据不包含binding_layout字段")
|
||
}
|
||
|
||
// 序列化响应数据用于日志记录
|
||
responseJSON, err := json.Marshal(transformedData)
|
||
if err != nil {
|
||
log.Printf("序列化响应数据失败: %v", err)
|
||
} else {
|
||
log.Printf("完整响应数据JSON: %s", string(responseJSON))
|
||
}
|
||
log.Printf("=== 响应发送完成 ===")
|
||
|
||
// 调用 UploadBookImage 方法并更新字段
|
||
bc.processBookImageUpload(transformedData, isbn)
|
||
|
||
c.JSON(http.StatusOK, gin.H{"data": transformedData, "source": "external_api"})
|
||
|
||
}
|
||
|
||
// 存储到中心书库(MySQL)
|
||
func (bc *BookCenterController) saveToBookCenter(data map[string]interface{}) error {
|
||
// 检查数据库连接
|
||
if err := bc.db.Ping(); err != nil {
|
||
log.Printf("saveToBookCenter 数据库连接失败: %v", err)
|
||
return fmt.Errorf("数据库连接失败: %w", err)
|
||
}
|
||
|
||
// 先检查ISBN是否已存在
|
||
var existingID int
|
||
checkQuery := "SELECT id FROM book_center WHERE isbn = ? LIMIT 1"
|
||
err := bc.db.QueryRow(checkQuery, data["isbn"]).Scan(&existingID)
|
||
if err == nil {
|
||
return nil // ISBN已存在,不需要插入
|
||
} else if err != sql.ErrNoRows {
|
||
log.Printf("saveToBookCenter 检查ISBN是否存在时出错: %v", err)
|
||
return fmt.Errorf("检查ISBN是否存在时出错: %w", err)
|
||
}
|
||
|
||
// 生成ISBN的MD5哈希值
|
||
isbnHash := generateISBNHash(data["isbn"].(string))
|
||
|
||
// 处理JSON字段 - 将map转换为JSON字符串
|
||
var bookPicS, bookPicNew interface{}
|
||
|
||
// 处理book_pic_s字段
|
||
if val, exists := data["book_pic_s"]; exists && val != nil {
|
||
if mapVal, ok := val.(map[string]interface{}); ok {
|
||
if jsonBytes, err := json.Marshal(mapVal); err == nil {
|
||
bookPicS = string(jsonBytes)
|
||
log.Printf("saveToBookCenter 转换book_pic_s为JSON: %s", string(jsonBytes))
|
||
} else {
|
||
log.Printf("saveToBookCenter book_pic_s JSON序列化失败: %v", err)
|
||
bookPicS = nil
|
||
}
|
||
} else {
|
||
bookPicS = val
|
||
}
|
||
} else {
|
||
bookPicS = nil
|
||
}
|
||
|
||
// 处理book_pic_new字段
|
||
if val, exists := data["book_pic_new"]; exists && val != nil {
|
||
if mapVal, ok := val.(map[string]interface{}); ok {
|
||
if jsonBytes, err := json.Marshal(mapVal); err == nil {
|
||
bookPicNew = string(jsonBytes)
|
||
log.Printf("saveToBookCenter 转换book_pic_new为JSON: %s", string(jsonBytes))
|
||
} else {
|
||
log.Printf("saveToBookCenter book_pic_new JSON序列化失败: %v", err)
|
||
bookPicNew = nil
|
||
}
|
||
} else {
|
||
bookPicNew = val
|
||
}
|
||
} else {
|
||
bookPicNew = nil
|
||
}
|
||
|
||
query := `INSERT INTO book_center (
|
||
category, book_name, book_pic, book_pic_s, book_pic_new, isbn, isbn_hash, author,
|
||
editor, binding_layout, publisher, edition, format, languages,
|
||
publication_time, print_time, paper, pages, wordage, fix_price,
|
||
content, remark, vio_book, book_set, onenum_mbooks, ill_publisher,
|
||
ill_author, day_sale_7, day_sale_15, day_sale_30, day_sale_60,
|
||
day_sale_90, day_sale_180, day_sale_365, this_year_sale, last_year_sale,
|
||
total_sale, sold_out_times, shipment_cycle, cat_id, buy_counts, sell_counts, del_flag,
|
||
create_by, create_time, update_by, update_time
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||
|
||
result, err := bc.db.Exec(query,
|
||
data["category"],
|
||
data["book_name"],
|
||
data["book_pic"],
|
||
bookPicS,
|
||
bookPicNew,
|
||
data["isbn"],
|
||
isbnHash, // isbn_hash
|
||
data["author"],
|
||
data["editor"],
|
||
data["binding_layout"],
|
||
data["publisher"],
|
||
data["edition"],
|
||
data["format"],
|
||
data["languages"],
|
||
data["publication_time"], // 注意拼写
|
||
data["print_time"],
|
||
data["paper"],
|
||
data["pages"],
|
||
data["wordage"],
|
||
data["fix_price"],
|
||
data["content"],
|
||
data["remark"],
|
||
data["vio_book"],
|
||
data["book_set"],
|
||
data["onenum_mbooks"],
|
||
data["ill_publisher"],
|
||
data["ill_author"],
|
||
data["day_sale_7"],
|
||
data["day_sale_15"],
|
||
data["day_sale_30"],
|
||
data["day_sale_60"],
|
||
data["day_sale_90"],
|
||
data["day_sale_180"],
|
||
data["day_sale_365"],
|
||
data["this_year_sale"],
|
||
data["last_year_sale"],
|
||
data["total_sale"],
|
||
data["sold_out_times"],
|
||
data["shipment_cycle"],
|
||
data["cat_id"],
|
||
data["buy_counts"],
|
||
data["sell_counts"],
|
||
0, // del_flag
|
||
1, // create_by
|
||
time.Now().Unix(), // create_time
|
||
1, // update_by
|
||
time.Now().Unix(), // update_time
|
||
)
|
||
|
||
if err != nil {
|
||
log.Printf("saveToBookCenter 插入书籍失败: %v, ISBN: %v", err, data["isbn"])
|
||
log.Printf("saveToBookCenter 失败的完整数据: %+v", data)
|
||
return fmt.Errorf("插入书籍失败: %w", err)
|
||
}
|
||
|
||
// 获取插入的行ID
|
||
lastInsertID, err := result.LastInsertId()
|
||
if err != nil {
|
||
log.Printf("saveToBookCenter 获取插入ID失败: %v", err)
|
||
} else {
|
||
log.Printf("saveToBookCenter 成功插入书籍,ID: %d, ISBN: %v", lastInsertID, data["isbn"])
|
||
}
|
||
|
||
// 验证插入是否成功
|
||
var count int
|
||
countQuery := "SELECT COUNT(*) FROM book_center WHERE isbn = ?"
|
||
if err := bc.db.QueryRow(countQuery, data["isbn"]).Scan(&count); err != nil {
|
||
log.Printf("saveToBookCenter 验证插入失败: %v", err)
|
||
} else {
|
||
log.Printf("saveToBookCenter 验证结果: ISBN %v 在数据库中的记录数: %d", data["isbn"], count)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// 辅助函数:解析数量
|
||
func parseCount(countStr string) int {
|
||
if countStr == "" {
|
||
return 0
|
||
}
|
||
count, err := strconv.Atoi(countStr)
|
||
if err != nil {
|
||
return 0
|
||
}
|
||
return count
|
||
}
|
||
|
||
// 辅助函数:解析价格(单位:分)
|
||
func parsePrice(priceStr string) int {
|
||
if priceStr == "" {
|
||
return 0
|
||
}
|
||
price, err := strconv.ParseFloat(priceStr, 64)
|
||
if err != nil {
|
||
return 0
|
||
}
|
||
return int(price * 100)
|
||
}
|
||
|
||
// 辅助函数:解析时间
|
||
func parseTime(timeStr string) int64 {
|
||
if timeStr == "" {
|
||
return 0
|
||
}
|
||
// 这里可以根据实际时间格式进行解析
|
||
// 示例中简单返回0
|
||
return 0
|
||
}
|
||
|
||
// ISBN验证函数
|
||
func isValidISBN(isbn string) bool {
|
||
// 移除所有非数字和X字符
|
||
cleaned := strings.ReplaceAll(strings.ToUpper(isbn), "-", "")
|
||
cleaned = strings.ReplaceAll(cleaned, " ", "")
|
||
|
||
// ISBN-10或ISBN-13验证
|
||
if len(cleaned) == 10 {
|
||
return isValidISBN10(cleaned)
|
||
} else if len(cleaned) == 13 {
|
||
return isValidISBN13(cleaned)
|
||
}
|
||
return false
|
||
}
|
||
|
||
// 检验isbn(10位)
|
||
func isValidISBN10(s string) bool {
|
||
// 检查长度是否为10
|
||
if len(s) != 10 {
|
||
return false
|
||
}
|
||
|
||
// 检查前9位是否全部为数字
|
||
if _, err := strconv.Atoi(s[:9]); err != nil {
|
||
return false
|
||
}
|
||
|
||
// 检查最后一位是否为数字或'X'
|
||
lastChar := s[9]
|
||
if !(lastChar >= '0' && lastChar <= '9') && lastChar != 'X' {
|
||
return false
|
||
}
|
||
|
||
// 计算校验和
|
||
sum := 0
|
||
for i := 0; i < 9; i++ {
|
||
digit, _ := strconv.Atoi(string(s[i]))
|
||
sum += digit * (10 - i)
|
||
}
|
||
|
||
// 处理校验位
|
||
var checkDigit int
|
||
if lastChar == 'X' {
|
||
checkDigit = 10
|
||
} else {
|
||
checkDigit, _ = strconv.Atoi(string(lastChar))
|
||
}
|
||
|
||
return (sum+checkDigit)%11 == 0
|
||
}
|
||
|
||
// 校验isbn(13位)
|
||
func isValidISBN13(s string) bool {
|
||
// 检查长度是否为13
|
||
if len(s) != 13 {
|
||
return false
|
||
}
|
||
|
||
// 检查是否全部为数字
|
||
if _, err := strconv.Atoi(s); err != nil {
|
||
return false
|
||
}
|
||
|
||
// 计算校验和
|
||
sum := 0
|
||
for i := 0; i < 12; i++ {
|
||
digit, _ := strconv.Atoi(string(s[i]))
|
||
if i%2 == 0 { // 奇数位(从0开始计数)
|
||
sum += digit
|
||
} else { // 偶数位
|
||
sum += digit * 3
|
||
}
|
||
}
|
||
|
||
// 计算校验位
|
||
checkDigit, _ := strconv.Atoi(string(s[12]))
|
||
calculatedCheck := (10 - (sum % 10)) % 10
|
||
|
||
return checkDigit == calculatedCheck
|
||
}
|
||
|
||
// BookInfo 构建Redis中数据结构体
|
||
type BookInfo struct {
|
||
ID FlexInt `json:"id"`
|
||
Author string `json:"author"`
|
||
BindingLayout string `json:"binding_layout"`
|
||
BookName string `json:"book_name"`
|
||
BookPic string `json:"book_pic"`
|
||
BookPicS BookPicS `json:"book_pic_s"`
|
||
BookPicNew BookPicS `json:"book_pic_new"` // 如果可能也是混合类型 BookSet FlexInt `json:"book_set"`
|
||
BookSet FlexInt `json:"book_set"`
|
||
BuyCount string `json:"buy_count"`
|
||
DaySale7 FlexInt `json:"day_sale_7"`
|
||
DaySale15 FlexInt `json:"day_sale_15"`
|
||
DaySale30 FlexInt `json:"day_sale_30"`
|
||
DaySale60 FlexInt `json:"day_sale_60"`
|
||
DaySale90 FlexInt `json:"day_sale_90"`
|
||
DaySale180 FlexInt `json:"day_sale_180"`
|
||
DaySale365 FlexInt `json:"day_sale_365"`
|
||
FixPrice FlexInt64 `json:"fix_price"`
|
||
IllAuthor FlexInt `json:"ill_author"`
|
||
IllPublisher FlexInt `json:"ill_publisher"`
|
||
Isbn string `json:"isbn"`
|
||
LastYearSale FlexInt `json:"last_year_sale"`
|
||
OnenumMbooks FlexInt `json:"onenum_mbooks"`
|
||
PublicationTime FlexInt64 `json:"publication_time"`
|
||
Publisher string `json:"publisher"`
|
||
SellCount string `json:"sell_count"`
|
||
ThisYearSale FlexInt `json:"this_year_sale"`
|
||
TotalSale FlexInt `json:"total_sale"`
|
||
VioBook FlexInt `json:"vio_book"`
|
||
}
|
||
|
||
// SetBookBaseInfoToIllRequest 定义请求参数结构
|
||
type SetBookBaseInfoToIllRequest struct {
|
||
IDs interface{} `json:"ids"` // 可以是字符串或数组
|
||
VioBook *int `json:"vio_book"` // 使用指针表示可选字段
|
||
BookSet *int `json:"book_set"` // 使用指针表示可选字段
|
||
OnenumMbooks *int `json:"onenum_mbooks"` // 使用指针表示可选字段
|
||
IllPublisher *int `json:"ill_publisher"` // 使用指针表示可选字段
|
||
IllAuthor *int `json:"ill_author"` // 使用指针表示可选字段
|
||
}
|
||
|
||
// SetBookBaseInfoToIllByISBNRequest 根据ISBN修改违规信息的请求参数结构
|
||
type SetBookBaseInfoToIllByISBNRequest struct {
|
||
ISBNs interface{} `json:"isbns"` // 可以是字符串或数组
|
||
VioBook *int `json:"vio_book"` // 使用指针表示可选字段
|
||
BookSet *int `json:"book_set"` // 使用指针表示可选字段
|
||
OnenumMbooks *int `json:"onenum_mbooks"` // 使用指针表示可选字段
|
||
IllPublisher *int `json:"ill_publisher"` // 使用指针表示可选字段
|
||
IllAuthor *int `json:"ill_author"` // 使用指针表示可选字段
|
||
}
|
||
|
||
// FlexInt64 灵活的int64类型,可以接受字符串或数字
|
||
type FlexInt64 int64
|
||
|
||
// UnmarshalJSON 自定义JSON解析,支持字符串和数字
|
||
func (fi *FlexInt64) UnmarshalJSON(data []byte) error {
|
||
// 去除引号
|
||
str := strings.Trim(string(data), `"`)
|
||
|
||
// 如果是空字符串或null,设为0
|
||
if str == "" || str == "null" {
|
||
*fi = 0
|
||
return nil
|
||
}
|
||
|
||
// 处理日期格式
|
||
if strings.Contains(str, "-") {
|
||
// 尝试解析日期格式
|
||
formats := []string{"2006-01", "2006-01-02", "2006"}
|
||
for _, format := range formats {
|
||
if t, err := time.Parse(format, str); err == nil {
|
||
*fi = FlexInt64(t.Unix())
|
||
return nil
|
||
}
|
||
}
|
||
}
|
||
|
||
// 尝试解析为整数
|
||
val, err := strconv.ParseInt(str, 10, 64)
|
||
if err != nil {
|
||
*fi = 0 // 解析失败时设为0
|
||
return nil
|
||
}
|
||
|
||
*fi = FlexInt64(val)
|
||
return nil
|
||
}
|
||
|
||
// MarshalJSON 自定义JSON序列化
|
||
func (fi FlexInt64) MarshalJSON() ([]byte, error) {
|
||
return []byte(strconv.FormatInt(int64(fi), 10)), nil
|
||
}
|
||
|
||
// FlexInt 灵活的int类型,可以接受字符串或数字
|
||
type FlexInt int
|
||
|
||
// UnmarshalJSON 自定义JSON解析,支持字符串和数字
|
||
func (fi *FlexInt) UnmarshalJSON(data []byte) error {
|
||
// 去除引号
|
||
str := strings.Trim(string(data), `"`)
|
||
|
||
// 如果是空字符串或null,设为0
|
||
if str == "" || str == "null" {
|
||
*fi = 0
|
||
return nil
|
||
}
|
||
|
||
// 尝试解析为整数
|
||
val, err := strconv.Atoi(str)
|
||
if err != nil {
|
||
*fi = 0 // 解析失败时设为0
|
||
return nil
|
||
}
|
||
|
||
*fi = FlexInt(val)
|
||
return nil
|
||
}
|
||
|
||
// MarshalJSON 自定义JSON序列化
|
||
func (fi FlexInt) MarshalJSON() ([]byte, error) {
|
||
return []byte(strconv.Itoa(int(fi))), nil
|
||
}
|
||
|
||
// FlexNumber 灵活的数字类型,可以接受字符串或数字
|
||
type FlexNumber struct {
|
||
Value int
|
||
}
|
||
|
||
// UnmarshalJSON 自定义JSON解析,支持字符串和数字
|
||
func (fn *FlexNumber) UnmarshalJSON(data []byte) error {
|
||
// 去除引号
|
||
str := strings.Trim(string(data), `"`)
|
||
|
||
// 如果是空字符串,设为0
|
||
if str == "" || str == "null" {
|
||
fn.Value = 0
|
||
return nil
|
||
}
|
||
|
||
// 尝试解析为整数
|
||
val, err := strconv.Atoi(str)
|
||
if err != nil {
|
||
return fmt.Errorf("无法解析为数字: %s", str)
|
||
}
|
||
|
||
fn.Value = val
|
||
return nil
|
||
}
|
||
|
||
// UpdateSalesRequest 修改销量请求结构体
|
||
type UpdateSalesRequest struct {
|
||
ISBN string `json:"isbn" binding:"required"`
|
||
DaySale7 FlexNumber `json:"daySale7"`
|
||
DaySale15 FlexNumber `json:"daySale15"`
|
||
DaySale30 FlexNumber `json:"daySale30"`
|
||
DaySale60 FlexNumber `json:"daySale60"`
|
||
DaySale90 FlexNumber `json:"daySale90"`
|
||
DaySale180 FlexNumber `json:"daySale180"`
|
||
DaySale365 FlexNumber `json:"daySale365"`
|
||
}
|
||
|
||
// SetBookBaseInfoToIll 批量设置违规信息(mysql+Redis)
|
||
func (bc *BookCenterController) SetBookBaseInfoToIll(c *gin.Context) {
|
||
startTime := time.Now()
|
||
|
||
// 1. 解析请求参数
|
||
var request SetBookBaseInfoToIllRequest
|
||
if err := c.ShouldBindJSON(&request); err != nil {
|
||
bc.sendErrorResponse(c, http.StatusBadRequest, 3000, "参数解析错误", err.Error())
|
||
return
|
||
}
|
||
|
||
// 2. 处理IDs参数
|
||
ids, err := bc.parseIDs(request.IDs)
|
||
if err != nil {
|
||
bc.sendErrorResponse(c, http.StatusBadRequest, 3001, "ID参数错误", err.Error())
|
||
return
|
||
}
|
||
|
||
// 3. 准备更新数据
|
||
updateData := bc.prepareUpdateData(request)
|
||
|
||
if len(updateData) == 0 {
|
||
bc.sendSuccessResponse(c, http.StatusOK, "暂未修改", nil)
|
||
return
|
||
}
|
||
|
||
// 4. 开启事务
|
||
tx, err := bc.db.Begin()
|
||
if err != nil {
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5001, "系统错误", "开启事务失败")
|
||
return
|
||
}
|
||
|
||
// 5. 执行MySQL更新
|
||
log.Printf("SetBookBaseInfoToIll: 开始执行MySQL更新,IDs: %v, updateData: %+v", ids, updateData)
|
||
rowsAffected, err := bc.updateMySQL(tx, ids, updateData)
|
||
if err != nil {
|
||
tx.Rollback()
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5002, "系统错误", "数据库更新失败")
|
||
return
|
||
}
|
||
|
||
if rowsAffected == 0 {
|
||
tx.Rollback()
|
||
bc.sendSuccessResponse(c, http.StatusOK, "暂未修改", nil)
|
||
return
|
||
}
|
||
|
||
// 6. 获取这些ID对应的ISBN列表
|
||
isbns, err := bc.getISBNsByIDs(tx, ids)
|
||
if err != nil {
|
||
tx.Rollback()
|
||
log.Printf("获取ISBN列表失败: %v", err)
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5003, "系统错误", "获取ISBN列表失败")
|
||
return
|
||
}
|
||
|
||
// 7. 更新Redis
|
||
if err := bc.updateBookInfoInRedis(isbns, updateData); err != nil {
|
||
tx.Rollback()
|
||
log.Printf("Redis更新失败: %v", err)
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5004, "系统错误", "Redis更新失败")
|
||
return
|
||
}
|
||
|
||
// 8. 提交事务
|
||
if err := tx.Commit(); err != nil {
|
||
log.Printf("提交事务失败: %v", err)
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5005, "系统错误", "提交事务失败")
|
||
return
|
||
}
|
||
|
||
// 9. 清除相关的查询缓存
|
||
if err := bc.clearQueryCache(); err != nil {
|
||
log.Printf("清除查询缓存失败: %v", err)
|
||
// 不影响主流程,只记录日志
|
||
}
|
||
|
||
// 10. 记录处理耗时
|
||
elapsed := time.Since(startTime)
|
||
log.Printf("批量更新完成,影响行数: %d, 处理耗时: %v", rowsAffected, elapsed)
|
||
|
||
// 11. 返回成功响应
|
||
bc.sendSuccessResponse(c, http.StatusOK, "修改成功", gin.H{
|
||
"rows_affected": rowsAffected,
|
||
"books_updated": len(isbns),
|
||
"time_cost": elapsed.String(),
|
||
})
|
||
}
|
||
|
||
// SetBookBaseInfoToIllByISBN 根据ISBN批量设置违规信息(mysql+Redis)
|
||
func (bc *BookCenterController) SetBookBaseInfoToIllByISBN(c *gin.Context) {
|
||
startTime := time.Now()
|
||
|
||
// 1. 解析请求参数
|
||
var request SetBookBaseInfoToIllByISBNRequest
|
||
if err := c.ShouldBindJSON(&request); err != nil {
|
||
bc.sendErrorResponse(c, http.StatusBadRequest, 3000, "参数解析错误", err.Error())
|
||
return
|
||
}
|
||
|
||
// 2. 处理ISBNs参数
|
||
isbns, err := bc.parseISBNs(request.ISBNs)
|
||
if err != nil {
|
||
bc.sendErrorResponse(c, http.StatusBadRequest, 3001, "ISBN参数错误", err.Error())
|
||
return
|
||
}
|
||
|
||
// 3. 准备更新数据
|
||
updateData := bc.prepareUpdateDataByISBN(request)
|
||
|
||
if len(updateData) == 0 {
|
||
bc.sendSuccessResponse(c, http.StatusOK, "暂未修改", nil)
|
||
return
|
||
}
|
||
|
||
// 4. 开启事务
|
||
tx, err := bc.db.Begin()
|
||
if err != nil {
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5001, "系统错误", "开启事务失败")
|
||
return
|
||
}
|
||
|
||
// 5. 执行MySQL更新(根据ISBN)
|
||
log.Printf("SetBookBaseInfoToIllByISBN: 开始执行MySQL更新,ISBNs: %v, updateData: %+v", isbns, updateData)
|
||
rowsAffected, err := bc.updateMySQLByISBN(tx, isbns, updateData)
|
||
if err != nil {
|
||
tx.Rollback()
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5002, "系统错误", "数据库更新失败")
|
||
return
|
||
}
|
||
|
||
if rowsAffected == 0 {
|
||
tx.Rollback()
|
||
bc.sendSuccessResponse(c, http.StatusOK, "暂未修改", nil)
|
||
return
|
||
}
|
||
|
||
// 6. 更新Redis
|
||
if err := bc.updateBookInfoInRedis(isbns, updateData); err != nil {
|
||
tx.Rollback()
|
||
log.Printf("Redis更新失败: %v", err)
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5004, "系统错误", "Redis更新失败")
|
||
return
|
||
}
|
||
|
||
// 7. 提交事务
|
||
if err := tx.Commit(); err != nil {
|
||
log.Printf("提交事务失败: %v", err)
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5005, "系统错误", "提交事务失败")
|
||
return
|
||
}
|
||
|
||
// 8. 清除相关的查询缓存
|
||
if err := bc.clearQueryCache(); err != nil {
|
||
log.Printf("清除查询缓存失败: %v", err)
|
||
// 不影响主流程,只记录日志
|
||
}
|
||
|
||
// 9. 记录处理耗时
|
||
elapsed := time.Since(startTime)
|
||
log.Printf("根据ISBN批量更新完成,影响行数: %d, 处理耗时: %v", rowsAffected, elapsed)
|
||
|
||
// 10. 返回成功响应
|
||
bc.sendSuccessResponse(c, http.StatusOK, "修改成功", gin.H{
|
||
"rows_affected": rowsAffected,
|
||
"books_updated": len(isbns),
|
||
"time_cost": elapsed.String(),
|
||
})
|
||
}
|
||
|
||
// clearQueryCache 清除查询缓存
|
||
func (bc *BookCenterController) clearQueryCache() error {
|
||
// 获取所有以 "book_query:" 开头的缓存键
|
||
items := bc.cache.Items()
|
||
var keysToDelete []string
|
||
|
||
for key := range items {
|
||
if strings.HasPrefix(key, "book_query:") {
|
||
keysToDelete = append(keysToDelete, key)
|
||
}
|
||
}
|
||
|
||
// 删除所有查询缓存
|
||
for _, key := range keysToDelete {
|
||
bc.cache.Delete(key)
|
||
}
|
||
|
||
log.Printf("清除了 %d 个查询缓存", len(keysToDelete))
|
||
return nil
|
||
}
|
||
|
||
// parseIDs 解析IDs参数
|
||
func (bc *BookCenterController) parseIDs(ids interface{}) ([]int, error) {
|
||
var result []int
|
||
|
||
switch v := ids.(type) {
|
||
case string:
|
||
if v == "" {
|
||
return nil, fmt.Errorf("必备参数为空ids")
|
||
}
|
||
strIDs := strings.Split(v, ",")
|
||
for _, strID := range strIDs {
|
||
id, err := strconv.Atoi(strID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("ID格式错误: %s", strID)
|
||
}
|
||
result = append(result, id)
|
||
}
|
||
case []interface{}:
|
||
if len(v) == 0 {
|
||
return nil, fmt.Errorf("必备参数为空ids")
|
||
}
|
||
for _, id := range v {
|
||
switch idVal := id.(type) {
|
||
case float64:
|
||
result = append(result, int(idVal))
|
||
case string:
|
||
idInt, err := strconv.Atoi(idVal)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("ID格式错误: %s", idVal)
|
||
}
|
||
result = append(result, idInt)
|
||
default:
|
||
return nil, fmt.Errorf("ID类型错误,必须是字符串或数字")
|
||
}
|
||
}
|
||
default:
|
||
return nil, fmt.Errorf("ID类型错误,必须是字符串或数组")
|
||
}
|
||
|
||
if len(result) == 0 {
|
||
return nil, fmt.Errorf("未提供有效的ID")
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// prepareUpdateData 准备要更新的数据
|
||
func (bc *BookCenterController) prepareUpdateData(request SetBookBaseInfoToIllRequest) map[string]interface{} {
|
||
updateData := make(map[string]interface{})
|
||
|
||
if request.VioBook != nil {
|
||
updateData["vio_book"] = *request.VioBook
|
||
}
|
||
if request.BookSet != nil {
|
||
updateData["book_set"] = *request.BookSet
|
||
}
|
||
if request.OnenumMbooks != nil {
|
||
updateData["onenum_mbooks"] = *request.OnenumMbooks
|
||
}
|
||
if request.IllPublisher != nil {
|
||
updateData["ill_publisher"] = *request.IllPublisher
|
||
}
|
||
if request.IllAuthor != nil {
|
||
updateData["ill_author"] = *request.IllAuthor
|
||
}
|
||
|
||
return updateData
|
||
}
|
||
|
||
// updateMySQL 更新MySQL数据库
|
||
func (bc *BookCenterController) updateMySQL(tx *sql.Tx, ids []int, updateData map[string]interface{}) (int64, error) {
|
||
log.Printf("updateMySQL: 开始构建SQL查询,IDs: %v, updateData: %+v", ids, updateData)
|
||
|
||
// 构建SQL查询
|
||
query := "UPDATE book_center SET "
|
||
var setParts []string
|
||
var args []interface{}
|
||
|
||
for field, value := range updateData {
|
||
setParts = append(setParts, field+" = ?")
|
||
args = append(args, value)
|
||
log.Printf("updateMySQL: 添加更新字段: %s = %v", field, value)
|
||
}
|
||
query += strings.Join(setParts, ", ") + " WHERE id IN ("
|
||
|
||
// 添加ID占位符
|
||
placeholders := make([]string, len(ids))
|
||
for i := range ids {
|
||
placeholders[i] = "?"
|
||
args = append(args, ids[i])
|
||
}
|
||
query += strings.Join(placeholders, ",") + ")"
|
||
|
||
log.Printf("updateMySQL: 构建的SQL查询: %s", query)
|
||
log.Printf("updateMySQL: SQL参数: %v", args)
|
||
|
||
// 执行MySQL更新
|
||
log.Printf("updateMySQL: 开始执行SQL更新")
|
||
result, err := tx.Exec(query, args...)
|
||
if err != nil {
|
||
log.Printf("updateMySQL: ❌ SQL执行失败: %v", err)
|
||
return 0, fmt.Errorf("数据库更新错误: %v", err)
|
||
}
|
||
log.Printf("updateMySQL: ✅ SQL执行成功")
|
||
|
||
// 检查影响的行数
|
||
rowsAffected, err := result.RowsAffected()
|
||
if err != nil {
|
||
log.Printf("updateMySQL: ❌ 获取影响行数失败: %v", err)
|
||
return 0, fmt.Errorf("获取影响行数错误: %v", err)
|
||
}
|
||
|
||
log.Printf("updateMySQL: ✅ 更新完成,影响行数: %d", rowsAffected)
|
||
return rowsAffected, nil
|
||
}
|
||
|
||
// getISBNsByIDs 根据ID列表获取对应的ISBN列表
|
||
func (bc *BookCenterController) getISBNsByIDs(tx *sql.Tx, ids []int) ([]string, error) {
|
||
query := "SELECT isbn FROM book_center WHERE id IN ("
|
||
placeholders := make([]string, len(ids))
|
||
args := make([]interface{}, len(ids))
|
||
for i, id := range ids {
|
||
placeholders[i] = "?"
|
||
args[i] = id
|
||
}
|
||
query += strings.Join(placeholders, ",") + ")"
|
||
|
||
rows, err := tx.Query(query, args...)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("查询ISBN失败: %v", err)
|
||
}
|
||
defer rows.Close()
|
||
|
||
var isbns []string
|
||
for rows.Next() {
|
||
var isbn string
|
||
if err := rows.Scan(&isbn); err != nil {
|
||
return nil, fmt.Errorf("扫描ISBN失败: %v", err)
|
||
}
|
||
isbns = append(isbns, isbn)
|
||
}
|
||
|
||
if err := rows.Err(); err != nil {
|
||
return nil, fmt.Errorf("行遍历错误: %v", err)
|
||
}
|
||
|
||
return isbns, nil
|
||
}
|
||
|
||
// updateBookInfoInRedis 更新Redis中的书籍信息
|
||
func (bc *BookCenterController) updateBookInfoInRedis(isbns []string, updateData map[string]interface{}) error {
|
||
log.Printf("updateBookInfoInRedis: 开始更新Redis,ISBNs: %v, updateData: %+v", isbns, updateData)
|
||
|
||
ctx := context.Background()
|
||
pipe := bc.redis.Pipeline()
|
||
|
||
// 定义允许更新的字段列表
|
||
allowedFields := map[string]bool{
|
||
"vio_book": true,
|
||
"book_set": true,
|
||
"onenum_mbooks": true,
|
||
"ill_publisher": true,
|
||
"ill_author": true,
|
||
"book_name": true,
|
||
"book_pic": true,
|
||
// 销量相关字段
|
||
"day_sale_7": true,
|
||
"day_sale_15": true,
|
||
"day_sale_30": true,
|
||
"day_sale_60": true,
|
||
"day_sale_90": true,
|
||
"day_sale_180": true,
|
||
"day_sale_365": true,
|
||
"this_year_sale": true,
|
||
"last_year_sale": true,
|
||
"total_sale": true,
|
||
"buy_counts": true,
|
||
"sell_counts": true,
|
||
"update_time": true,
|
||
}
|
||
|
||
// 准备要更新的字段
|
||
fieldsToUpdate := make(map[string]interface{})
|
||
for field, value := range updateData {
|
||
if allowedFields[field] {
|
||
// 处理int和string两种类型
|
||
switch v := value.(type) {
|
||
case int:
|
||
fieldsToUpdate[field] = v
|
||
case string:
|
||
fieldsToUpdate[field] = v
|
||
default:
|
||
log.Printf("警告: 字段 %s 的值类型不支持: %T", field, value)
|
||
continue
|
||
}
|
||
}
|
||
}
|
||
|
||
if len(fieldsToUpdate) == 0 {
|
||
return fmt.Errorf("没有有效的字段需要更新")
|
||
}
|
||
|
||
// 批量更新Redis
|
||
for _, isbn := range isbns {
|
||
key := fmt.Sprintf(isbn)
|
||
log.Printf("updateBookInfoInRedis: 处理ISBN: %s", isbn)
|
||
|
||
// 先获取现有数据
|
||
existingData, err := bc.redis.Get(ctx, key).Result()
|
||
if err != nil && err != redis.Nil {
|
||
log.Printf("updateBookInfoInRedis: 获取Redis数据失败(key: %s): %v", key, err)
|
||
return fmt.Errorf("获取Redis数据失败(key: %s): %v", key, err)
|
||
}
|
||
|
||
var bookInfo BookInfo
|
||
if existingData != "" {
|
||
log.Printf("updateBookInfoInRedis: 找到现有数据,长度: %d", len(existingData))
|
||
if err := json.Unmarshal([]byte(existingData), &bookInfo); err != nil {
|
||
log.Printf("updateBookInfoInRedis: 解析Redis数据失败(key: %s): %v, 原始数据: %s", key, err, existingData)
|
||
return fmt.Errorf("解析Redis数据失败(key: %s): %v", key, err)
|
||
}
|
||
log.Printf("updateBookInfoInRedis: 成功解析现有数据,ID: %v", bookInfo.ID)
|
||
} else {
|
||
log.Printf("updateBookInfoInRedis: 未找到现有数据,将创建新记录")
|
||
}
|
||
|
||
// 更新字段
|
||
if val, ok := fieldsToUpdate["vio_book"]; ok {
|
||
bookInfo.VioBook = FlexInt(val.(int))
|
||
}
|
||
if val, ok := fieldsToUpdate["book_set"]; ok {
|
||
bookInfo.BookSet = FlexInt(val.(int))
|
||
}
|
||
if val, ok := fieldsToUpdate["onenum_mbooks"]; ok {
|
||
bookInfo.OnenumMbooks = FlexInt(val.(int))
|
||
}
|
||
if val, ok := fieldsToUpdate["ill_publisher"]; ok {
|
||
bookInfo.IllPublisher = FlexInt(val.(int))
|
||
}
|
||
if val, ok := fieldsToUpdate["ill_author"]; ok {
|
||
bookInfo.IllAuthor = FlexInt(val.(int))
|
||
}
|
||
if val, ok := fieldsToUpdate["book_name"]; ok {
|
||
bookInfo.BookName = val.(string)
|
||
}
|
||
if val, ok := fieldsToUpdate["book_pic"]; ok {
|
||
bookInfo.BookPic = val.(string)
|
||
}
|
||
// 销量字段更新
|
||
if val, ok := fieldsToUpdate["day_sale_7"]; ok {
|
||
bookInfo.DaySale7 = FlexInt(val.(int))
|
||
}
|
||
if val, ok := fieldsToUpdate["day_sale_15"]; ok {
|
||
bookInfo.DaySale15 = FlexInt(val.(int))
|
||
}
|
||
if val, ok := fieldsToUpdate["day_sale_30"]; ok {
|
||
bookInfo.DaySale30 = FlexInt(val.(int))
|
||
}
|
||
if val, ok := fieldsToUpdate["day_sale_60"]; ok {
|
||
bookInfo.DaySale60 = FlexInt(val.(int))
|
||
}
|
||
if val, ok := fieldsToUpdate["day_sale_90"]; ok {
|
||
bookInfo.DaySale90 = FlexInt(val.(int))
|
||
}
|
||
if val, ok := fieldsToUpdate["day_sale_180"]; ok {
|
||
bookInfo.DaySale180 = FlexInt(val.(int))
|
||
}
|
||
if val, ok := fieldsToUpdate["day_sale_365"]; ok {
|
||
bookInfo.DaySale365 = FlexInt(val.(int))
|
||
}
|
||
if val, ok := fieldsToUpdate["this_year_sale"]; ok {
|
||
bookInfo.ThisYearSale = FlexInt(val.(int))
|
||
}
|
||
if val, ok := fieldsToUpdate["last_year_sale"]; ok {
|
||
bookInfo.LastYearSale = FlexInt(val.(int))
|
||
}
|
||
if val, ok := fieldsToUpdate["total_sale"]; ok {
|
||
bookInfo.TotalSale = FlexInt(val.(int))
|
||
}
|
||
|
||
// 序列化并保存回Redis
|
||
updatedData, err := json.Marshal(bookInfo)
|
||
if err != nil {
|
||
return fmt.Errorf("序列化书籍数据失败: %v", err)
|
||
}
|
||
|
||
pipe.Set(ctx, key, updatedData, 0)
|
||
}
|
||
|
||
// 执行pipeline
|
||
if _, err := pipe.Exec(ctx); err != nil {
|
||
return fmt.Errorf("执行Redis pipeline失败: %v", err)
|
||
}
|
||
|
||
log.Printf("成功更新 %d 个Redis键", len(isbns))
|
||
return nil
|
||
}
|
||
|
||
// sendErrorResponse 发送错误响应
|
||
func (bc *BookCenterController) sendErrorResponse(c *gin.Context, statusCode, code int, msg, subMsg string) {
|
||
c.JSON(statusCode, gin.H{
|
||
"errorResponse": map[string]interface{}{
|
||
"code": code,
|
||
"msg": msg,
|
||
"subCode": code,
|
||
"subMsg": subMsg,
|
||
},
|
||
})
|
||
}
|
||
|
||
// sendSuccessResponse 发送成功响应
|
||
func (bc *BookCenterController) sendSuccessResponse(c *gin.Context, statusCode int, msg string, data interface{}) {
|
||
response := gin.H{
|
||
"code": 200,
|
||
"msg": msg,
|
||
}
|
||
if data != nil {
|
||
response["data"] = data
|
||
}
|
||
c.JSON(statusCode, response)
|
||
}
|
||
|
||
// SetBookBaseInfoBookName 更新图书名称(MySQL+Redis双写)
|
||
func (bc *BookCenterController) SetBookBaseInfoBookName(c *gin.Context) {
|
||
// 解析请求参数
|
||
var request struct {
|
||
ID string `json:"id"`
|
||
BookName string `json:"book_name"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&request); err != nil {
|
||
bc.sendErrorResponse(c, http.StatusBadRequest, 3000, "参数解析错误", err.Error())
|
||
return
|
||
}
|
||
|
||
// 检查必备参数
|
||
if request.ID == "" || request.BookName == "" {
|
||
bc.sendErrorResponse(c, http.StatusBadRequest, 3000, "必备参数为空", "id和book_name不能为空")
|
||
return
|
||
}
|
||
|
||
// 开始数据库事务
|
||
tx, err := bc.db.Begin()
|
||
if err != nil {
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 500, "数据库事务启动失败", err.Error())
|
||
return
|
||
}
|
||
defer func() {
|
||
if err != nil {
|
||
tx.Rollback()
|
||
}
|
||
}()
|
||
|
||
// 1. 执行MySQL更新
|
||
query := "UPDATE book_center SET book_name = ? WHERE id = ?"
|
||
result, err := tx.Exec(query, request.BookName, request.ID)
|
||
if err != nil {
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 500, "数据库更新失败", err.Error())
|
||
return
|
||
}
|
||
|
||
// 检查影响的行数
|
||
rowsAffected, err := result.RowsAffected()
|
||
if err != nil {
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 500, "无法确定更新结果", err.Error())
|
||
return
|
||
}
|
||
|
||
if rowsAffected == 0 {
|
||
tx.Rollback()
|
||
bc.sendSuccessResponse(c, http.StatusOK, "暂未修改(可能ID不存在)", nil)
|
||
return
|
||
}
|
||
|
||
// 2. 获取对应的ISBN
|
||
idInt, err := strconv.Atoi(request.ID)
|
||
if err != nil {
|
||
bc.sendErrorResponse(c, http.StatusBadRequest, 3000, "ID格式不正确", "ID必须为数字")
|
||
return
|
||
}
|
||
|
||
isbns, err := bc.getISBNsByIDs(tx, []int{idInt})
|
||
if err != nil {
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 500, "获取ISBN失败", err.Error())
|
||
return
|
||
}
|
||
|
||
if len(isbns) == 0 {
|
||
tx.Rollback()
|
||
bc.sendErrorResponse(c, http.StatusNotFound, 404, "未找到对应的ISBN", "")
|
||
return
|
||
}
|
||
|
||
// 3. 更新Redis缓存
|
||
updateData := map[string]interface{}{
|
||
"book_name": request.BookName,
|
||
}
|
||
err = bc.updateBookInfoInRedis(isbns, updateData)
|
||
if err != nil {
|
||
tx.Rollback()
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 500, "Redis更新失败", err.Error())
|
||
return
|
||
}
|
||
|
||
// 提交事务
|
||
if err = tx.Commit(); err != nil {
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 500, "事务提交失败", err.Error())
|
||
return
|
||
}
|
||
|
||
bc.sendSuccessResponse(c, http.StatusOK, "修改成功", nil)
|
||
}
|
||
|
||
// UpdateSales 修改销量数据(MySQL+Redis双写)
|
||
func (bc *BookCenterController) UpdateSales(c *gin.Context) {
|
||
startTime := time.Now()
|
||
|
||
log.Printf("=== UpdateSales 接口开始执行 ===")
|
||
log.Printf("UpdateSales: 请求开始时间: %s", startTime.Format("2006-01-02 15:04:05.000"))
|
||
|
||
// 1. 解析请求参数
|
||
var request UpdateSalesRequest
|
||
if err := c.ShouldBindJSON(&request); err != nil {
|
||
log.Printf("UpdateSales: 参数解析失败: %v", err)
|
||
bc.sendErrorResponse(c, http.StatusBadRequest, 3000, "参数解析错误", err.Error())
|
||
return
|
||
}
|
||
|
||
log.Printf("UpdateSales: 成功解析请求参数")
|
||
log.Printf("UpdateSales: 请求参数详情 - ISBN: %s", request.ISBN)
|
||
log.Printf("UpdateSales: 销量数据 - 7天: %d, 15天: %d, 30天: %d, 60天: %d, 90天: %d, 180天: %d, 365天: %d",
|
||
request.DaySale7.Value, request.DaySale15.Value, request.DaySale30.Value,
|
||
request.DaySale60.Value, request.DaySale90.Value, request.DaySale180.Value, request.DaySale365.Value)
|
||
|
||
// 2. 验证ISBN格式
|
||
if !isValidISBN(request.ISBN) {
|
||
log.Printf("UpdateSales: ISBN格式验证失败: %s", request.ISBN)
|
||
bc.sendErrorResponse(c, http.StatusBadRequest, 3001, "ISBN格式不正确", "")
|
||
return
|
||
}
|
||
|
||
log.Printf("UpdateSales: ISBN格式验证通过: %s", request.ISBN)
|
||
log.Printf("UpdateSales: 开始检查数据库中是否存在该ISBN")
|
||
|
||
// 3. 检查数据库中是否存在该ISBN
|
||
var bookID int
|
||
checkQuery := "SELECT id FROM book_center WHERE isbn = ? LIMIT 1"
|
||
log.Printf("UpdateSales: 执行数据库查询: %s, 参数: %s", checkQuery, request.ISBN)
|
||
|
||
err := bc.db.QueryRow(checkQuery, request.ISBN).Scan(&bookID)
|
||
|
||
if err == sql.ErrNoRows {
|
||
// 4. 数据库中不存在,调用外部API获取图书信息并插入
|
||
log.Printf("UpdateSales: ❌ ISBN %s 不存在于数据库,开始从外部API获取数据", request.ISBN)
|
||
log.Printf("UpdateSales: 准备调用fetchBookDataFromAPI方法")
|
||
|
||
bookData, err := bc.fetchBookDataFromAPI(request.ISBN)
|
||
if err != nil {
|
||
log.Printf("UpdateSales: ❌ 从外部API获取数据失败: %v", err)
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5001, "获取图书信息失败", err.Error())
|
||
return
|
||
}
|
||
|
||
log.Printf("UpdateSales: ✅ 成功从外部API获取图书数据")
|
||
log.Printf("UpdateSales: 图书基本信息 - 书名: %v, 作者: %v, 出版社: %v",
|
||
bookData["book_name"], bookData["author"], bookData["publisher"])
|
||
|
||
// 5. 将销量数据添加到bookData中
|
||
log.Printf("UpdateSales: 开始合并销量数据到图书信息中")
|
||
bookData["day_sale_7"] = request.DaySale7.Value
|
||
bookData["day_sale_15"] = request.DaySale15.Value
|
||
bookData["day_sale_30"] = request.DaySale30.Value
|
||
bookData["day_sale_60"] = request.DaySale60.Value
|
||
bookData["day_sale_90"] = request.DaySale90.Value
|
||
bookData["day_sale_180"] = request.DaySale180.Value
|
||
bookData["day_sale_365"] = request.DaySale365.Value
|
||
|
||
log.Printf("UpdateSales: ✅ 销量数据合并完成")
|
||
|
||
// 6. 插入到数据库
|
||
log.Printf("UpdateSales: 开始调用saveToBookCenter插入数据到MySQL")
|
||
if err := bc.saveToBookCenter(bookData); err != nil {
|
||
log.Printf("UpdateSales: ❌ 插入图书数据失败: %v", err)
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5002, "插入图书数据失败", err.Error())
|
||
return
|
||
}
|
||
log.Printf("UpdateSales: ✅ 成功插入数据到MySQL数据库")
|
||
|
||
// 7. 存储到Redis
|
||
log.Printf("UpdateSales: 开始存储数据到Redis缓存")
|
||
jsonData, err := json.Marshal(bookData)
|
||
if err != nil {
|
||
log.Printf("UpdateSales: ❌ 序列化数据失败: %v", err)
|
||
} else {
|
||
if err := bc.redis.Set(context.Background(), request.ISBN, jsonData, 0).Err(); err != nil {
|
||
log.Printf("UpdateSales: ❌ 存储到Redis失败: %v", err)
|
||
} else {
|
||
log.Printf("UpdateSales: ✅ 成功存储到Redis, ISBN: %s", request.ISBN)
|
||
}
|
||
}
|
||
|
||
elapsed := time.Since(startTime)
|
||
log.Printf("UpdateSales: 新增图书并设置销量完成,ISBN: %s, 处理耗时: %v", request.ISBN, elapsed)
|
||
|
||
// 8. 返回成功响应
|
||
bc.sendSuccessResponse(c, http.StatusOK, "图书新增并设置销量成功", gin.H{
|
||
"isbn": request.ISBN,
|
||
"action": "created_and_updated",
|
||
"time_cost": elapsed.String(),
|
||
"updated_fields": gin.H{
|
||
"day_sale_7": request.DaySale7.Value,
|
||
"day_sale_15": request.DaySale15.Value,
|
||
"day_sale_30": request.DaySale30.Value,
|
||
"day_sale_60": request.DaySale60.Value,
|
||
"day_sale_90": request.DaySale90.Value,
|
||
"day_sale_180": request.DaySale180.Value,
|
||
"day_sale_365": request.DaySale365.Value,
|
||
},
|
||
})
|
||
return
|
||
|
||
} else if err != nil {
|
||
// 数据库查询出错
|
||
log.Printf("UpdateSales: ❌ 检查ISBN存在性失败: %v", err)
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5003, "系统错误", "检查图书存在性失败")
|
||
return
|
||
}
|
||
|
||
// 9. 数据库中存在该ISBN,直接更新销量
|
||
log.Printf("UpdateSales: ✅ ISBN %s 存在于数据库 (ID: %d),开始更新销量", request.ISBN, bookID)
|
||
log.Printf("UpdateSales: 准备开启数据库事务进行更新操作")
|
||
|
||
// 开启事务
|
||
tx, err := bc.db.Begin()
|
||
if err != nil {
|
||
log.Printf("开启事务失败: %v", err)
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5004, "系统错误", "开启事务失败")
|
||
return
|
||
}
|
||
defer func() {
|
||
if err != nil {
|
||
tx.Rollback()
|
||
}
|
||
}()
|
||
|
||
// 10. 构建更新SQL
|
||
updateQuery := `UPDATE book_center SET
|
||
day_sale_7 = ?, day_sale_15 = ?, day_sale_30 = ?, day_sale_60 = ?,
|
||
day_sale_90 = ?, day_sale_180 = ?, day_sale_365 = ?,
|
||
update_time = ?
|
||
WHERE isbn = ?`
|
||
|
||
// 11. 执行MySQL更新
|
||
result, err := tx.Exec(updateQuery,
|
||
request.DaySale7.Value,
|
||
request.DaySale15.Value,
|
||
request.DaySale30.Value,
|
||
request.DaySale60.Value,
|
||
request.DaySale90.Value,
|
||
request.DaySale180.Value,
|
||
request.DaySale365.Value,
|
||
time.Now().Unix(),
|
||
request.ISBN,
|
||
)
|
||
if err != nil {
|
||
log.Printf("MySQL更新失败: %v", err)
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5005, "系统错误", "数据库更新失败")
|
||
return
|
||
}
|
||
|
||
// 12. 检查影响的行数
|
||
rowsAffected, err := result.RowsAffected()
|
||
if err != nil {
|
||
log.Printf("获取影响行数失败: %v", err)
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5006, "系统错误", "获取影响行数失败")
|
||
return
|
||
}
|
||
|
||
if rowsAffected == 0 {
|
||
bc.sendErrorResponse(c, http.StatusNotFound, 4004, "更新失败", "未找到对应的图书记录")
|
||
return
|
||
}
|
||
|
||
// 13. 更新Redis缓存
|
||
updateData := map[string]interface{}{
|
||
"day_sale_7": request.DaySale7.Value,
|
||
"day_sale_15": request.DaySale15.Value,
|
||
"day_sale_30": request.DaySale30.Value,
|
||
"day_sale_60": request.DaySale60.Value,
|
||
"day_sale_90": request.DaySale90.Value,
|
||
"day_sale_180": request.DaySale180.Value,
|
||
"day_sale_365": request.DaySale365.Value,
|
||
"update_time": time.Now().Unix(),
|
||
}
|
||
|
||
err = bc.updateBookInfoInRedis([]string{request.ISBN}, updateData)
|
||
if err != nil {
|
||
log.Printf("Redis更新失败: %v", err)
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5007, "系统错误", "Redis更新失败")
|
||
return
|
||
}
|
||
|
||
// 14. 提交事务
|
||
if err = tx.Commit(); err != nil {
|
||
log.Printf("事务提交失败: %v", err)
|
||
bc.sendErrorResponse(c, http.StatusInternalServerError, 5008, "系统错误", "事务提交失败")
|
||
return
|
||
}
|
||
|
||
// 15. 清除相关缓存
|
||
bc.cache.Flush()
|
||
|
||
elapsed := time.Since(startTime)
|
||
log.Printf("UpdateSales: 销量修改完成,ISBN: %s, 影响行数: %d, 处理耗时: %v",
|
||
request.ISBN, rowsAffected, elapsed)
|
||
|
||
// 16. 返回成功响应
|
||
bc.sendSuccessResponse(c, http.StatusOK, "销量修改成功", gin.H{
|
||
"isbn": request.ISBN,
|
||
"action": "updated",
|
||
"rows_affected": rowsAffected,
|
||
"time_cost": elapsed.String(),
|
||
"updated_fields": gin.H{
|
||
"day_sale_7": request.DaySale7.Value,
|
||
"day_sale_15": request.DaySale15.Value,
|
||
"day_sale_30": request.DaySale30.Value,
|
||
"day_sale_60": request.DaySale60.Value,
|
||
"day_sale_90": request.DaySale90.Value,
|
||
"day_sale_180": request.DaySale180.Value,
|
||
"day_sale_365": request.DaySale365.Value,
|
||
},
|
||
})
|
||
}
|
||
|
||
// GetBookByID 根据ID查询图书信息
|
||
func (bc *BookCenterController) GetBookByID(c *gin.Context) {
|
||
// 获取ID参数
|
||
id := c.Param("id")
|
||
if id == "" {
|
||
id = c.Query("id") // 兼容查询参数
|
||
if id == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "ID参数不能为空"})
|
||
return
|
||
}
|
||
}
|
||
|
||
// 构建查询语句
|
||
query := `
|
||
SELECT
|
||
id, book_name, book_pic, isbn, author, publisher,
|
||
publication_time, binding_layout, fix_price,
|
||
buy_count, sell_count, vio_book, book_set,
|
||
onenum_mbooks, ill_publisher, ill_author,
|
||
day_sale_7, day_sale_15, day_sale_30, day_sale_60,
|
||
day_sale_90, day_sale_180, day_sale_365,
|
||
this_year_sale, last_year_sale, total_sale
|
||
FROM book_center
|
||
WHERE id = ?`
|
||
|
||
// 定义接收变量
|
||
var (
|
||
idVal, isbn, bookName, bookPic, author, publisher, bindingLayout string
|
||
publicationTime interface{}
|
||
fixPrice float64
|
||
buyCount, sellCount string
|
||
vioBook, bookSet, onenumMbooks, illPublisher, illAuthor int
|
||
daySale7, daySale15, daySale30, daySale60, daySale90 int
|
||
daySale180, daySale365, thisYearSale, lastYearSale, totalSale int
|
||
)
|
||
|
||
// 执行查询
|
||
err := bc.db.QueryRow(query, id).Scan(
|
||
&idVal, &bookName, &bookPic, &isbn, &author, &publisher,
|
||
&publicationTime, &bindingLayout, &fixPrice,
|
||
&buyCount, &sellCount, &vioBook, &bookSet, &onenumMbooks,
|
||
&illPublisher, &illAuthor,
|
||
&daySale7, &daySale15, &daySale30, &daySale60, &daySale90,
|
||
&daySale180, &daySale365, &thisYearSale, &lastYearSale, &totalSale,
|
||
)
|
||
|
||
if err != nil {
|
||
if err == sql.ErrNoRows {
|
||
c.JSON(http.StatusNotFound, gin.H{"error": "未找到对应的图书信息"})
|
||
} else {
|
||
log.Printf("数据库查询错误: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"error": "读取数据失败",
|
||
"details": err.Error(),
|
||
})
|
||
}
|
||
return
|
||
}
|
||
// 构建返回结果(数组格式)
|
||
bookData := gin.H{
|
||
"id": idVal,
|
||
"book_name": bookName,
|
||
"book_pic": bookPic,
|
||
"isbn": isbn,
|
||
"author": author,
|
||
"publisher": publisher,
|
||
"publication_time": publicationTime,
|
||
"binding_layout": bindingLayout,
|
||
"fix_price": fixPrice,
|
||
"buy_count": buyCount,
|
||
"sell_count": sellCount,
|
||
"vio_book": vioBook,
|
||
"book_set": bookSet,
|
||
"onenum_mbooks": onenumMbooks,
|
||
"ill_publisher": illPublisher,
|
||
"ill_author": illAuthor,
|
||
"day_sale_7": daySale7,
|
||
"day_sale_15": daySale15,
|
||
"day_sale_30": daySale30,
|
||
"day_sale_60": daySale60,
|
||
"day_sale_90": daySale90,
|
||
"day_sale_180": daySale180,
|
||
"day_sale_365": daySale365,
|
||
"this_year_sale": thisYearSale,
|
||
"last_year_sale": lastYearSale,
|
||
"total_sale": totalSale,
|
||
}
|
||
// 将单个对象包装成数组
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"data": []gin.H{bookData}, // 注意这里用[]gin.H将对象转为数组
|
||
})
|
||
//// 构建返回结果
|
||
//c.JSON(http.StatusOK, gin.H{
|
||
// "data": gin.H{
|
||
// "id": idVal,
|
||
// "book_name": bookName,
|
||
// "book_pic": bookPic,
|
||
// "isbn": isbn,
|
||
// "author": author,
|
||
// "publisher": publisher,
|
||
// "publication_time": publicationTime, // 直接返回原始值
|
||
// "binding_layout": bindingLayout,
|
||
// "fix_price": fixPrice,
|
||
// "buy_count": buyCount,
|
||
// "sell_count": sellCount,
|
||
// "vio_book": vioBook,
|
||
// "book_set": bookSet,
|
||
// "onenum_mbooks": onenumMbooks,
|
||
// "ill_publisher": illPublisher,
|
||
// "ill_author": illAuthor,
|
||
// "day_sale_7": daySale7,
|
||
// "day_sale_15": daySale15,
|
||
// "day_sale_30": daySale30,
|
||
// "day_sale_60": daySale60,
|
||
// "day_sale_90": daySale90,
|
||
// "day_sale_180": daySale180,
|
||
// "day_sale_365": daySale365,
|
||
// "this_year_sale": thisYearSale,
|
||
// "last_year_sale": lastYearSale,
|
||
// "total_sale": totalSale,
|
||
// },
|
||
//})
|
||
}
|
||
|
||
// InsertBaseInfo 插入图书基本信息
|
||
type BookBaseInfo struct {
|
||
Id int `form:"id"`
|
||
Category string `form:"category"`
|
||
BookName string `form:"book_name"`
|
||
BookPic string `form:"book_pic"`
|
||
BookPicS string `form:"book_pic_s"` // 改为 string 类型
|
||
Isbn string `form:"isbn"`
|
||
Author string `form:"author"`
|
||
Editor string `form:"editor"`
|
||
BindingLayout string `form:"binding_layout"`
|
||
Publisher string `form:"publisher"`
|
||
Edition string `form:"edition"`
|
||
Format string `form:"format"`
|
||
Languages string `form:"languages"`
|
||
PublicationTime FlexInt64 `form:"publication_time"`
|
||
PrintTime int64 `form:"print_time"`
|
||
Paper string `form:"paper"`
|
||
Pages int `form:"pages"`
|
||
Wordage string `form:"wordage"`
|
||
FixPrice int64 `form:"fix_price"`
|
||
Content string `form:"content"`
|
||
Remark string `form:"remark"`
|
||
VioBook int `form:"vio_book"`
|
||
BookSet int `form:"book_set"`
|
||
OnenumMbooks int `form:"onenum_mbooks"`
|
||
IllPublisher int `form:"ill_publisher"`
|
||
IllAuthor int `form:"ill_author"`
|
||
DaySale7 int `form:"day_sale_7"`
|
||
DaySale15 int `form:"day_sale_15"`
|
||
DaySale30 int `form:"day_sale_30"`
|
||
DaySale60 int `form:"day_sale_60"`
|
||
DaySale90 int `form:"day_sale_90"`
|
||
DaySale180 int `form:"day_sale_180"`
|
||
DaySale365 int `form:"day_sale_365"`
|
||
ThisYearSale int `form:"this_year_sale"`
|
||
LastYearSale int `form:"last_year_sale"`
|
||
TotalSale int `form:"total_sale"`
|
||
SoldOutTimes string `form:"sold_out_times"`
|
||
ShipmentCycle int `form:"shipment_cycle"`
|
||
BuyCount string `form:"buy_count"`
|
||
SellCount string `form:"sell_count"`
|
||
PublictionTimes interface{} `form:"publiction_times"`
|
||
CatId int `form:"cat_id"`
|
||
BuyCounts int64 `form:"buy_counts"`
|
||
SellCounts int64 `form:"sell_counts"`
|
||
CreateBy int64 `form:"create_by"`
|
||
CreateTime int64 `form:"create_time"`
|
||
UpdateBy int64 `form:"update_by"`
|
||
UpdateTime int64 `form:"update_time"`
|
||
}
|
||
|
||
func getISBNValue(apiISBN, fallbackISBN string) string {
|
||
if apiISBN == "" {
|
||
return fallbackISBN
|
||
}
|
||
return apiISBN
|
||
}
|
||
|
||
// generateISBNHash 生成ISBN的数字哈希值(用于分区)
|
||
func generateISBNHash(isbn string) uint64 {
|
||
hash := md5.Sum([]byte(isbn))
|
||
// 取MD5哈希的前8个字节转换为uint64
|
||
var result uint64
|
||
for i := 0; i < 8; i++ {
|
||
result = (result << 8) | uint64(hash[i])
|
||
}
|
||
return result
|
||
}
|
||
|
||
// fetchBookDataFromAPI 从外部API获取图书数据
|
||
func (bc *BookCenterController) fetchBookDataFromAPI(isbn string) (map[string]interface{}, error) {
|
||
log.Printf("fetchBookDataFromAPI: 开始从外部API获取图书数据, ISBN: %s", isbn)
|
||
|
||
// 调用外部API
|
||
apiUrl := detailPagesUrl + "/api/image-url?isbn=" + isbn
|
||
log.Printf("fetchBookDataFromAPI: 调用外部API: %s", apiUrl)
|
||
|
||
resp, err := http.Get(apiUrl)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("调用外部API失败: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
log.Printf("fetchBookDataFromAPI: 外部API响应状态码: %d", resp.StatusCode)
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
return nil, fmt.Errorf("外部API返回错误状态码: %d", resp.StatusCode)
|
||
}
|
||
|
||
// 读取响应体
|
||
bodyBytes, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取API响应体失败: %w", err)
|
||
}
|
||
|
||
log.Printf("fetchBookDataFromAPI: 外部API原始响应数据: %s", string(bodyBytes))
|
||
|
||
// 解析API响应
|
||
var apiResponse struct {
|
||
Success bool `json:"success"`
|
||
Data struct {
|
||
Detail struct {
|
||
Editor string `json:"editor"`
|
||
Languages string `json:"languages"`
|
||
PrintTime string `json:"print_time"`
|
||
ISBN string `json:"isbn"`
|
||
BookPic string `json:"book_pic"`
|
||
BookName string `json:"book_name"`
|
||
Author string `json:"author"`
|
||
Format string `json:"format"`
|
||
Publisher string `json:"publisher"`
|
||
Paper string `json:"paper"`
|
||
PublicationTime FlexInt64 `json:"publication_time"`
|
||
Pages string `json:"pages"`
|
||
Edition string `json:"edition"`
|
||
Wordage string `json:"wordage"`
|
||
Category string `json:"category"`
|
||
FixPrice string `json:"fix_price"`
|
||
BindingLayout string `json:"binding_layout"`
|
||
BuyCount string `json:"buyCount"`
|
||
SellCount string `json:"sellCount"`
|
||
Content string `json:"content"`
|
||
} `json:"detail"`
|
||
ImageURL string `json:"image_url"`
|
||
} `json:"data"`
|
||
}
|
||
|
||
if err := json.Unmarshal(bodyBytes, &apiResponse); err != nil {
|
||
return nil, fmt.Errorf("解析API响应失败: %w", err)
|
||
}
|
||
|
||
log.Printf("fetchBookDataFromAPI: 解析后的API响应: %+v", apiResponse)
|
||
|
||
// 转换数据格式
|
||
transformedData := map[string]interface{}{
|
||
"author": apiResponse.Data.Detail.Author,
|
||
"binding_layout": apiResponse.Data.Detail.BindingLayout,
|
||
"book_name": apiResponse.Data.Detail.BookName,
|
||
"book_pic": apiResponse.Data.ImageURL,
|
||
"book_pic_s": nil,
|
||
"book_set": 0,
|
||
"buy_count": apiResponse.Data.Detail.BuyCount,
|
||
"buy_counts": parseCount(apiResponse.Data.Detail.BuyCount),
|
||
"cat_id": 0,
|
||
"category": apiResponse.Data.Detail.Category,
|
||
"content": apiResponse.Data.Detail.Content,
|
||
"create_by": 1,
|
||
"create_time": time.Now().Unix(),
|
||
"day_sale_15": 0,
|
||
"day_sale_180": 0,
|
||
"day_sale_30": 0,
|
||
"day_sale_365": 0,
|
||
"day_sale_60": 0,
|
||
"day_sale_7": 0,
|
||
"day_sale_90": 0,
|
||
"edition": apiResponse.Data.Detail.Edition,
|
||
"editor": apiResponse.Data.Detail.Editor,
|
||
"fix_price": parsePrice(apiResponse.Data.Detail.FixPrice),
|
||
"format": apiResponse.Data.Detail.Format,
|
||
"id": 0,
|
||
"ill_author": 0,
|
||
"ill_publisher": 0,
|
||
"isbn": getISBNValue(apiResponse.Data.Detail.ISBN, isbn),
|
||
"languages": apiResponse.Data.Detail.Languages,
|
||
"last_year_sale": 0,
|
||
"onenum_mbooks": 0,
|
||
"pages": parseCount(apiResponse.Data.Detail.Pages),
|
||
"paper": apiResponse.Data.Detail.Paper,
|
||
"print_time": parseTime(apiResponse.Data.Detail.PrintTime),
|
||
"publication_time": apiResponse.Data.Detail.PublicationTime,
|
||
"publiction_times": apiResponse.Data.Detail.PublicationTime,
|
||
"publisher": apiResponse.Data.Detail.Publisher,
|
||
"remark": "",
|
||
"sell_count": apiResponse.Data.Detail.SellCount,
|
||
"sell_counts": parseCount(apiResponse.Data.Detail.SellCount),
|
||
"shipment_cycle": 0,
|
||
"sold_out_times": "[]",
|
||
"this_year_sale": 0,
|
||
"total_sale": 0,
|
||
"update_by": 0,
|
||
"update_time": time.Now().Unix(),
|
||
"vio_book": 0,
|
||
"wordage": apiResponse.Data.Detail.Wordage,
|
||
}
|
||
|
||
log.Printf("fetchBookDataFromAPI: 数据转换完成, ISBN: %s", isbn)
|
||
return transformedData, nil
|
||
}
|
||
func (bc *BookCenterController) InsertBaseInfo(c *gin.Context) {
|
||
// 1. 解析请求数据
|
||
var book BookBaseInfo
|
||
//if err := c.ShouldBindJSON(&book); err != nil {
|
||
// c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数解析失败: " + err.Error()})
|
||
// return
|
||
//}
|
||
if err := c.ShouldBind(&book); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数解析失败: " + err.Error()})
|
||
return
|
||
}
|
||
// 将 book.BookPicS 转换为 sql.NullString
|
||
bookPicS := sql.NullString{
|
||
String: book.BookPicS,
|
||
Valid: book.BookPicS != "", // 如果非空,则 Valid=true
|
||
}
|
||
|
||
// 如果 BookPicS 为空,设置默认值 {}
|
||
if !bookPicS.Valid {
|
||
bookPicS = sql.NullString{
|
||
String: "{}",
|
||
Valid: true,
|
||
}
|
||
}
|
||
err1 := json.Unmarshal([]byte(book.BookPicS), &bookPicS)
|
||
if err1 != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数解析失败: " + err1.Error()})
|
||
return
|
||
}
|
||
|
||
// 调试日志 - 打印接收到的原始数据
|
||
log.Printf("Received book_pic_s raw value: %v", book.BookPicS)
|
||
// 2. 检查ISBN是否为空
|
||
if book.Isbn == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "ISBN不能为空"})
|
||
return
|
||
}
|
||
|
||
// 3. 检查图书是否已存在
|
||
//var existingID int
|
||
//err := bc.db.QueryRow("SELECT * FROM book_center WHERE isbn = ?", book.Isbn).Scan(&existingID)
|
||
//将查询出的结果存储到变量中
|
||
|
||
//// 4. 获取分布式锁(防止并发冲突)
|
||
//lockKey := fmt.Sprintf("lock:%s", book.Isbn)
|
||
//locked, err2 := bc.redis.SetNX(context.Background(), lockKey, "1", 10*time.Second).Result()
|
||
//if err2 != nil || !locked {
|
||
// c.JSON(http.StatusConflict, gin.H{"error": "系统繁忙,请稍后重试"})
|
||
// return
|
||
//}
|
||
//defer bc.redis.Del(context.Background(), lockKey)
|
||
|
||
var dbBook BookBaseInfo
|
||
|
||
//err := bc.db.QueryRow("SELECT id,category,book_name,book_pic,book_pic_s,isbn,author,editor,binding_layout,publisher,edition,format," +
|
||
// "languages,publication_time,print_time,paper,pages,wordage,fix_price,content,remark,vio_book,book_set,onenum_mbooks,ill_publisher," +
|
||
// "ill_author ,day_sale_7,day_sale_15,day_sale_30,day_sale_60,day_sale_90,day_sale_180,day_sale_365,this_year_sale,last_year_sale," +
|
||
// "total_sale,sold_out_times,shipment_cycle,buy_count,sell_count,publiction_times,cat_id,buy_counts,sell_counts,create_by,create_time," +
|
||
// "`update_by,update_time FROM book_center WHERE isbn = ?`", book.Isbn).Scan
|
||
//(
|
||
//err1 = bc.db.QueryRow("SELECT id,category,book_name,book_pic,COALESCE(book_pic_s, '{}') AS book_pic_s,isbn,author,editor,binding_layout,publisher,edition,format,"+
|
||
// "languages,publication_time,print_time,paper,pages,wordage,fix_price,content,remark,vio_book,book_set,onenum_mbooks,ill_publisher,"+
|
||
// "ill_author,day_sale_7,day_sale_15,day_sale_30,day_sale_60,day_sale_90,day_sale_180,day_sale_365,this_year_sale,last_year_sale,"+
|
||
// "total_sale,sold_out_times,shipment_cycle,buy_count,sell_count,publiction_times,cat_id,buy_counts,sell_counts,create_by,create_time,"+
|
||
// "update_by,update_time FROM book_center WHERE isbn = ?", book.Isbn).Scan(
|
||
// &dbBook.Id,
|
||
// &dbBook.Category,
|
||
// &dbBook.BookName,
|
||
// &dbBook.BookPic,
|
||
// &dbBook.BookPicS,
|
||
// &dbBook.Isbn,
|
||
// &dbBook.Author,
|
||
// &dbBook.Editor,
|
||
// &dbBook.BindingLayout,
|
||
// &dbBook.Publisher,
|
||
// &dbBook.Edition,
|
||
// &dbBook.Format,
|
||
// &dbBook.Languages,
|
||
// &dbBook.PublicationTime,
|
||
// &dbBook.PrintTime,
|
||
// &dbBook.Paper,
|
||
// &dbBook.Pages,
|
||
// &dbBook.Wordage,
|
||
// &dbBook.FixPrice,
|
||
// &dbBook.Content,
|
||
// &dbBook.Remark,
|
||
// &dbBook.VioBook,
|
||
// &dbBook.BookSet,
|
||
// &dbBook.OnenumMbooks,
|
||
// &dbBook.IllPublisher,
|
||
// &dbBook.IllAuthor,
|
||
// &dbBook.DaySale7,
|
||
// &dbBook.DaySale15,
|
||
// &dbBook.DaySale30,
|
||
// &dbBook.DaySale60,
|
||
// &dbBook.DaySale90,
|
||
// &dbBook.DaySale180,
|
||
// &dbBook.DaySale365,
|
||
// &dbBook.ThisYearSale,
|
||
// &dbBook.LastYearSale,
|
||
// &dbBook.TotalSale,
|
||
// &dbBook.SoldOutTimes,
|
||
// &dbBook.ShipmentCycle,
|
||
// &dbBook.BuyCount,
|
||
// &dbBook.SellCount,
|
||
// &dbBook.PublictionTimes,
|
||
// &dbBook.CatId,
|
||
// &dbBook.BuyCounts,
|
||
// &dbBook.SellCounts,
|
||
// &dbBook.CreateBy,
|
||
// &dbBook.CreateTime,
|
||
// &dbBook.UpdateBy,
|
||
// &dbBook.UpdateTime,
|
||
//)
|
||
err1 = bc.db.QueryRow(`
|
||
SELECT
|
||
id, category, book_name, book_pic,
|
||
COALESCE(book_pic_s, '""') AS book_pic_s, -- 如果 NULL,返回 '{}'
|
||
isbn, author, editor, binding_layout, publisher,
|
||
edition, format, languages,
|
||
COALESCE(publication_time, 0) AS publication_time, -- 如果 NULL,返回 0
|
||
COALESCE(print_time, 0) AS print_time,
|
||
COALESCE(paper, '') AS paper,
|
||
COALESCE(pages, 0) AS pages,
|
||
COALESCE(wordage, '') AS wordage,
|
||
COALESCE(fix_price, 0) AS fix_price,
|
||
COALESCE(content, '') AS content,
|
||
COALESCE(remark, '') AS remark,
|
||
COALESCE(vio_book, 0) AS vio_book,
|
||
COALESCE(book_set, 0) AS book_set,
|
||
COALESCE(onenum_mbooks, 0) AS onenum_mbooks,
|
||
COALESCE(ill_publisher, 0) AS ill_publisher,
|
||
COALESCE(ill_author, 0) AS ill_author,
|
||
COALESCE(day_sale_7, 0) AS day_sale_7,
|
||
COALESCE(day_sale_15, 0) AS day_sale_15,
|
||
COALESCE(day_sale_30, 0) AS day_sale_30,
|
||
COALESCE(day_sale_60, 0) AS day_sale_60,
|
||
COALESCE(day_sale_90, 0) AS day_sale_90,
|
||
COALESCE(day_sale_180, 0) AS day_sale_180,
|
||
COALESCE(day_sale_365, 0) AS day_sale_365,
|
||
COALESCE(this_year_sale, 0) AS this_year_sale,
|
||
COALESCE(last_year_sale, 0) AS last_year_sale,
|
||
COALESCE(total_sale, 0) AS total_sale,
|
||
COALESCE(sold_out_times, '') AS sold_out_times,
|
||
COALESCE(shipment_cycle, 0) AS shipment_cycle,
|
||
COALESCE(buy_counts, '') AS buy_count,
|
||
COALESCE(sell_counts, '') AS sell_count,
|
||
COALESCE(publiction_times, 0) AS publiction_times,
|
||
COALESCE(cat_id, 0) AS cat_id,
|
||
COALESCE(buy_counts, 0) AS buy_counts,
|
||
COALESCE(sell_counts, 0) AS sell_counts,
|
||
COALESCE(create_by, 0) AS create_by,
|
||
COALESCE(create_time, 0) AS create_time,
|
||
COALESCE(update_by, 0) AS update_by,
|
||
COALESCE(update_time, 0) AS update_time
|
||
FROM book_center
|
||
WHERE isbn = ?`,
|
||
book.Isbn,
|
||
).Scan(
|
||
&dbBook.Id,
|
||
&dbBook.Category,
|
||
&dbBook.BookName,
|
||
&dbBook.BookPic,
|
||
&dbBook.BookPicS,
|
||
&dbBook.Isbn,
|
||
&dbBook.Author,
|
||
&dbBook.Editor,
|
||
&dbBook.BindingLayout,
|
||
&dbBook.Publisher,
|
||
&dbBook.Edition,
|
||
&dbBook.Format,
|
||
&dbBook.Languages,
|
||
&dbBook.PublicationTime,
|
||
&dbBook.PrintTime,
|
||
&dbBook.Paper,
|
||
&dbBook.Pages,
|
||
&dbBook.Wordage,
|
||
&dbBook.FixPrice,
|
||
&dbBook.Content,
|
||
&dbBook.Remark,
|
||
&dbBook.VioBook,
|
||
&dbBook.BookSet,
|
||
&dbBook.OnenumMbooks,
|
||
&dbBook.IllPublisher,
|
||
&dbBook.IllAuthor,
|
||
&dbBook.DaySale7,
|
||
&dbBook.DaySale15,
|
||
&dbBook.DaySale30,
|
||
&dbBook.DaySale60,
|
||
&dbBook.DaySale90,
|
||
&dbBook.DaySale180,
|
||
&dbBook.DaySale365,
|
||
&dbBook.ThisYearSale,
|
||
&dbBook.LastYearSale,
|
||
&dbBook.TotalSale,
|
||
&dbBook.SoldOutTimes,
|
||
&dbBook.ShipmentCycle,
|
||
&dbBook.BuyCount,
|
||
&dbBook.SellCount,
|
||
&dbBook.PublictionTimes,
|
||
&dbBook.CatId,
|
||
&dbBook.BuyCounts,
|
||
&dbBook.SellCounts,
|
||
&dbBook.CreateBy,
|
||
&dbBook.CreateTime,
|
||
&dbBook.UpdateBy,
|
||
&dbBook.UpdateTime,
|
||
)
|
||
|
||
//if err != nil {
|
||
// c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败: " + err.Error()})
|
||
// return
|
||
//}
|
||
|
||
// 4. 事务处理MySQL操作
|
||
tx, err := bc.db.Begin()
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库事务启动失败: " + err.Error()})
|
||
return
|
||
}
|
||
mergeBooks(&book, &dbBook)
|
||
|
||
var action string
|
||
if book.BookName != "" {
|
||
dbBook.BookName = book.BookName
|
||
}
|
||
if book.Author != "" {
|
||
dbBook.Author = book.Author
|
||
}
|
||
if book.Publisher != "" {
|
||
dbBook.Publisher = book.Publisher
|
||
}
|
||
if book.PublicationTime != 0 {
|
||
dbBook.PublicationTime = book.PublicationTime
|
||
}
|
||
if book.BindingLayout != "" {
|
||
dbBook.BindingLayout = book.BindingLayout
|
||
}
|
||
if book.FixPrice != 0 {
|
||
dbBook.FixPrice = book.FixPrice
|
||
}
|
||
if book.BookPic != "" {
|
||
dbBook.BookPic = book.BookPic
|
||
}
|
||
if book.BookPicS != "" {
|
||
dbBook.BookPicS = book.BookPicS
|
||
}
|
||
|
||
if err1 == nil {
|
||
// ISBN已存在,执行更新操作
|
||
_, err = tx.Exec(`
|
||
UPDATE book_center SET
|
||
category = ?, book_name = ?, book_pic = ?, book_pic_s = ?,
|
||
author = ?, editor = ?, binding_layout = ?, publisher = ?,
|
||
edition = ?, format = ?, languages = ?, publication_time = ?,
|
||
print_time = ?, paper = ?, pages = ?, wordage = ?, fix_price = ?,
|
||
content = ?, remark = ?, vio_book = ?, book_set = ?,
|
||
onenum_mbooks = ?, ill_publisher = ?, ill_author = ?,
|
||
update_by = ?, update_time = ?
|
||
WHERE isbn = ?`,
|
||
dbBook.Category, dbBook.BookName, dbBook.BookPic, book.BookPicS,
|
||
dbBook.Author, dbBook.Editor, dbBook.BindingLayout, dbBook.Publisher,
|
||
dbBook.Edition, dbBook.Format, dbBook.Languages, dbBook.PublicationTime,
|
||
dbBook.PrintTime, dbBook.Paper, dbBook.Pages, dbBook.Wordage, dbBook.FixPrice,
|
||
dbBook.Content, dbBook.Remark, dbBook.VioBook, dbBook.BookSet,
|
||
dbBook.OnenumMbooks, dbBook.IllPublisher, dbBook.IllAuthor,
|
||
dbBook.UpdateBy, time.Now().Unix(),
|
||
dbBook.Isbn)
|
||
action = "更新"
|
||
} else if err1 == sql.ErrNoRows {
|
||
// ISBN不存在,执行插入操作
|
||
_, err = tx.Exec(`
|
||
INSERT INTO book_center (
|
||
category, book_name, book_pic, book_pic_s, isbn, author, editor,
|
||
binding_layout, publisher, edition, format, languages, publication_time,
|
||
print_time, paper, pages, wordage, fix_price, content, remark,
|
||
vio_book, book_set, onenum_mbooks, ill_publisher, ill_author,
|
||
create_by, create_time, update_by, update_time
|
||
)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
book.Category, book.BookName, book.BookPic, book.BookPicS, book.Isbn,
|
||
book.Author, book.Editor, book.BindingLayout, book.Publisher, book.Edition,
|
||
book.Format, book.Languages, book.PublicationTime, book.PrintTime,
|
||
book.Paper, book.Pages, book.Wordage, book.FixPrice, book.Content, book.Remark,
|
||
book.VioBook, book.BookSet, book.OnenumMbooks, book.IllPublisher, book.IllAuthor,
|
||
book.CreateBy, time.Now().Unix(), book.UpdateBy, time.Now().Unix())
|
||
action = "新增"
|
||
} else {
|
||
tx.Rollback()
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库查询失败: " + err1.Error()})
|
||
return
|
||
}
|
||
|
||
if err != nil {
|
||
tx.Rollback()
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库操作失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 5. 更新Redis数据
|
||
redisKey := fmt.Sprintf(book.Isbn)
|
||
bookMap := map[string]interface{}{
|
||
"id": dbBook.Id,
|
||
"book_name": dbBook.BookName, // 手动改为小写下划线
|
||
"isbn": dbBook.Isbn,
|
||
"author": dbBook.Author,
|
||
"book_pic": dbBook.BookPic,
|
||
"book_pic_s": dbBook.BookPicS,
|
||
"editor": dbBook.Editor,
|
||
"publisher": dbBook.Publisher,
|
||
"edition": dbBook.Edition,
|
||
"format": dbBook.Format,
|
||
"languages": dbBook.Languages,
|
||
"publication_time": dbBook.PublicationTime,
|
||
"print_time": dbBook.PrintTime,
|
||
"paper": dbBook.Paper,
|
||
"pages": dbBook.Pages,
|
||
"wordage": dbBook.Wordage,
|
||
"fix_price": dbBook.FixPrice,
|
||
"content": dbBook.Content,
|
||
"remark": dbBook.Remark,
|
||
"vio_book": dbBook.VioBook,
|
||
"book_set": dbBook.BookSet,
|
||
"onenum_mbooks": dbBook.OnenumMbooks,
|
||
"ill_publisher": dbBook.IllPublisher,
|
||
"ill_author": dbBook.IllAuthor,
|
||
"day_sale_7": dbBook.DaySale7,
|
||
"day_sale_15": dbBook.DaySale15,
|
||
"day_sale_30": dbBook.DaySale30,
|
||
"day_sale_60": dbBook.DaySale60,
|
||
"day_sale_90": dbBook.DaySale90,
|
||
"day_sale_180": dbBook.DaySale180,
|
||
"day_sale_365": dbBook.DaySale365,
|
||
"this_year_sale": dbBook.ThisYearSale,
|
||
"last_year_sale": dbBook.LastYearSale,
|
||
"total_sale": dbBook.TotalSale,
|
||
"sold_out_times": dbBook.SoldOutTimes,
|
||
"shipment_cycle": dbBook.ShipmentCycle,
|
||
"buy_count": dbBook.BuyCount,
|
||
"sell_count": dbBook.SellCount,
|
||
"publiction_times": dbBook.PublictionTimes,
|
||
"cat_id": dbBook.CatId,
|
||
"buy_counts": dbBook.BuyCounts,
|
||
"sell_counts": dbBook.SellCounts,
|
||
"create_by": dbBook.CreateBy,
|
||
"create_time": dbBook.CreateTime,
|
||
"update_by": dbBook.UpdateBy,
|
||
"update_time": dbBook.UpdateTime,
|
||
"category": dbBook.Category,
|
||
}
|
||
bookJSON, err := json.Marshal(bookMap)
|
||
if err != nil {
|
||
tx.Rollback()
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据序列化失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 修改点4:添加Redis操作日志便于调试
|
||
log.Printf("Updating Redis key: %s with data: %s", redisKey, string(bookJSON))
|
||
|
||
// 更新Redis - 现在会在第一次请求时就执行
|
||
err = bc.redis.Set(context.Background(), redisKey, bookJSON, 0).Err()
|
||
if err != nil {
|
||
tx.Rollback()
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Redis操作失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 6. 提交事务
|
||
if err := tx.Commit(); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库事务提交失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 7. 返回成功响应
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": fmt.Sprintf("图书信息%s成功", action),
|
||
"isbn": dbBook.Isbn,
|
||
})
|
||
}
|
||
|
||
func mergeBooks(dest, src *BookBaseInfo) {
|
||
destVal := reflect.ValueOf(dest).Elem()
|
||
srcVal := reflect.ValueOf(src).Elem()
|
||
|
||
for i := 0; i < destVal.NumField(); i++ {
|
||
destField := destVal.Field(i)
|
||
srcField := srcVal.Field(i)
|
||
|
||
// 只处理可设置字段且源值不为零值的情况
|
||
if destField.CanSet() && !reflect.DeepEqual(srcField.Interface(), reflect.Zero(srcField.Type()).Interface()) {
|
||
// 如果目标值为零值,则用源值替换
|
||
if reflect.DeepEqual(destField.Interface(), reflect.Zero(destField.Type()).Interface()) {
|
||
destField.Set(srcField)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 导出Excel表
|
||
func (bc *BookCenterController) exportISBNs(c *gin.Context) {
|
||
// 查询所有ISBN记录(不限制数量)
|
||
query := "SELECT isbn FROM book_center WHERE book_pic_s IS NOT NULL AND book_pic_s != ''"
|
||
rows, err := bc.db.Query(query)
|
||
if err != nil {
|
||
log.Printf("查询执行失败: %v", err)
|
||
c.JSON(500, gin.H{"error": "数据库查询失败"})
|
||
return
|
||
}
|
||
defer rows.Close()
|
||
|
||
// 创建Excel文件
|
||
f := excelize.NewFile()
|
||
defer func() {
|
||
if err := f.Close(); err != nil {
|
||
log.Printf("关闭Excel文件时出错: %v", err)
|
||
}
|
||
}()
|
||
|
||
// 创建工作表
|
||
sheetName := "ISBN数据"
|
||
index, err := f.NewSheet(sheetName)
|
||
if err != nil {
|
||
log.Printf("创建工作表失败: %v", err)
|
||
c.JSON(500, gin.H{"error": "创建Excel工作表失败"})
|
||
return
|
||
}
|
||
|
||
// 设置表头样式
|
||
headerStyle, _ := f.NewStyle(&excelize.Style{
|
||
Font: &excelize.Font{Bold: true, Color: "#FFFFFF"},
|
||
Fill: excelize.Fill{Type: "pattern", Color: []string{"#4F81BD"}, Pattern: 1},
|
||
Alignment: &excelize.Alignment{Horizontal: "center"},
|
||
})
|
||
f.SetCellStyle(sheetName, "A1", "A1", headerStyle)
|
||
|
||
// 设置表头
|
||
f.SetCellValue(sheetName, "A1", "ISBN")
|
||
|
||
// 设置列宽
|
||
f.SetColWidth(sheetName, "A", "A", 25)
|
||
|
||
// 写入数据(最多5万条)
|
||
rowNum := 2
|
||
var count int
|
||
maxRecords := 50000
|
||
|
||
for rows.Next() {
|
||
var isbn string
|
||
if err := rows.Scan(&isbn); err != nil {
|
||
log.Printf("扫描行数据失败: %v", err)
|
||
continue
|
||
}
|
||
|
||
f.SetCellValue(sheetName, fmt.Sprintf("A%d", rowNum), isbn)
|
||
rowNum++
|
||
count++
|
||
|
||
// 进度显示
|
||
if count%1000 == 0 {
|
||
log.Printf("已处理 %d 条ISBN...", count)
|
||
}
|
||
|
||
// 达到最大限制时停止
|
||
if count >= maxRecords {
|
||
break
|
||
}
|
||
}
|
||
|
||
// 设置活动工作表
|
||
f.SetActiveSheet(index)
|
||
|
||
// 生成文件名
|
||
fileName := fmt.Sprintf("C:\\Users\\pc\\Desktop\\book_isbns_%s_%d条.xlsx",
|
||
time.Now().Format("20060102_150477"), count)
|
||
|
||
// 保存Excel文件
|
||
if err := f.SaveAs(fileName); err != nil {
|
||
log.Printf("保存Excel文件失败: %v", err)
|
||
c.JSON(500, gin.H{"error": "保存Excel文件失败"})
|
||
return
|
||
}
|
||
|
||
log.Printf("成功导出 %d 条ISBN到 %s", count, fileName)
|
||
c.JSON(200, gin.H{
|
||
"message": "ISBN导出成功",
|
||
"count": count,
|
||
"filePath": fileName,
|
||
})
|
||
}
|
||
|
||
// func (bc *BookCenterController) updateBooks(c *gin.Context) {
|
||
// // 1. 连接到目标数据库
|
||
// targetDSN := "book:xTxZDK3fxri8pNNW@tcp(118.195.145.133:3306)/book?charset=utf8mb4&parseTime=True&loc=Local"
|
||
// db, err := sql.Open("mysql", targetDSN)
|
||
// if err != nil {
|
||
// c.JSON(500, gin.H{"error": "数据库连接失败"})
|
||
// return
|
||
// }
|
||
// defer db.Close()
|
||
// db.SetMaxOpenConns(10)
|
||
//
|
||
// // 2. 查询书名和ISBN
|
||
// rows, err := db.Query("SELECT id, book_name, isbn FROM book_center")
|
||
// if err != nil {
|
||
// c.JSON(500, gin.H{"error": "查询失败"})
|
||
// return
|
||
// }
|
||
// defer rows.Close()
|
||
//
|
||
// const basePath = "/file/goods_img"
|
||
// var success, fail int
|
||
//
|
||
// // 创建Redis管道批量操作
|
||
// pipe := bc.redis.Pipeline()
|
||
// defer pipe.Close()
|
||
//
|
||
// for rows.Next() {
|
||
// var id int
|
||
// var name, isbn string
|
||
// if err := rows.Scan(&id, &name, &isbn); err != nil {
|
||
// log.Printf("Error scanning row: %v", err)
|
||
// fail++
|
||
// continue
|
||
// }
|
||
//
|
||
// // 3. 通过MD5获取首个字符
|
||
// hash := md5.Sum([]byte(name))
|
||
// firstChar := hex.EncodeToString(hash[:])[0:1]
|
||
// imageFile := fmt.Sprintf("%s_01.jpg", isbn)
|
||
//
|
||
// // 4. 使用filepath构造路径
|
||
// localFilePath := filepath.Join(basePath, firstChar, imageFile)
|
||
//
|
||
// // 5. 检查文件是否存在
|
||
// if _, err := os.Stat(localFilePath); err != nil {
|
||
// log.Printf("File not found for book %s (ISBN: %s) at path: %s", name, isbn, localFilePath)
|
||
// fail++
|
||
// continue
|
||
// }
|
||
//
|
||
// imageFileUrl := fmt.Sprintf("https://book.center.image.buzhiyushu.cn/%s/%s", firstChar, imageFile)
|
||
//
|
||
// // 6. 开启事务处理MySQL和Redis
|
||
// tx, err := db.Begin()
|
||
// if err != nil {
|
||
// log.Printf("Error starting transaction for book %s: %v", name, err)
|
||
// fail++
|
||
// continue
|
||
// }
|
||
//
|
||
// // 更新MySQL
|
||
// if _, err = tx.Exec("UPDATE book_center SET book_pic = ? WHERE id = ?", imageFileUrl, id); err != nil {
|
||
// tx.Rollback()
|
||
// log.Printf("Error updating book_pic for book %s: %v", name, err)
|
||
// fail++
|
||
// continue
|
||
// }
|
||
// //
|
||
// // 更新Redis
|
||
// fmt.Sprintf("isbn:%s book_pic:%s", isbn, imageFileUrl)
|
||
// // 提交事务
|
||
// if err = tx.Commit(); err != nil {
|
||
// bc.sendErrorResponse(c, http.StatusInternalServerError, 500, "事务提交失败", err.Error())
|
||
// return
|
||
// }
|
||
//
|
||
// success++
|
||
// log.Printf("Updated book_pic for: %s (ISBN: %s)", name, isbn)
|
||
// }
|
||
//
|
||
// if err = rows.Err(); err != nil {
|
||
// c.JSON(500, gin.H{"error": "遍历结果集失败"})
|
||
// return
|
||
// }
|
||
//
|
||
// c.JSON(200, gin.H{
|
||
// "message": "更新完成",
|
||
// "success": success,
|
||
// "fail": fail,
|
||
// })
|
||
// }
|
||
//func (bc *BookCenterController) updateBooks(c *gin.Context) {
|
||
//
|
||
// // 1. 从ApiFox获取起始页参数
|
||
// startPage, err := strconv.Atoi(c.DefaultQuery("startPage", "1"))
|
||
// if err != nil || startPage < 1 {
|
||
// startPage = 1
|
||
// }
|
||
// const pageSize = 100 // 固定每页100条数据
|
||
//
|
||
// // 1. 连接到目标MySQL数据库
|
||
// targetDSN := "book:xTxZDK3fxri8pNNW@tcp(118.195.145.133:3306)/book?charset=utf8mb4&parseTime=True&loc=Local"
|
||
// db, err := sql.Open("mysql", targetDSN)
|
||
// if err != nil {
|
||
// c.JSON(500, gin.H{"error": "数据库连接失败"})
|
||
// return
|
||
// }
|
||
// defer db.Close()
|
||
// db.SetMaxOpenConns(10)
|
||
// // 4. 自动分页处理循环
|
||
// for page := startPage; ; page++ {
|
||
// offset := (page - 1) * pageSize
|
||
// log.Printf("正在处理第 %d 页,偏移量: %d", page, offset)
|
||
// // 2. 查询书籍完整信息
|
||
// rows, err := db.Query(`SELECT
|
||
// id, category, book_name, book_pic,
|
||
// COALESCE(book_pic_s, '{}') AS book_pic_s, -- 如果 NULL,返回 '{}'
|
||
// isbn, author, editor, binding_layout, publisher,
|
||
// edition, format, languages,
|
||
// COALESCE(publication_time, 0) AS publication_time, -- 如果 NULL,返回 0
|
||
//COALESCE(print_time, 0) AS print_time,
|
||
// COALESCE(paper, '') AS paper,
|
||
// COALESCE(pages, 0) AS pages,
|
||
// COALESCE(wordage, '') AS wordage,
|
||
// COALESCE(fix_price, 0) AS fix_price,
|
||
// COALESCE(content, '') AS content,
|
||
// COALESCE(remark, '') AS remark,
|
||
// COALESCE(vio_book, 0) AS vio_book,
|
||
// COALESCE(book_set, 0) AS book_set,
|
||
// COALESCE(onenum_mbooks, 0) AS onenum_mbooks,
|
||
// COALESCE(ill_publisher, 0) AS ill_publisher,
|
||
// COALESCE(ill_author, 0) AS ill_author,
|
||
// COALESCE(day_sale_7, 0) AS day_sale_7,
|
||
// COALESCE(day_sale_15, 0) AS day_sale_15,
|
||
// COALESCE(day_sale_30, 0) AS day_sale_30,
|
||
// COALESCE(day_sale_60, 0) AS day_sale_60,
|
||
// COALESCE(day_sale_90, 0) AS day_sale_90,
|
||
// COALESCE(day_sale_180, 0) AS day_sale_180,
|
||
// COALESCE(day_sale_365, 0) AS day_sale_365,
|
||
// COALESCE(this_year_sale, 0) AS this_year_sale,
|
||
// COALESCE(last_year_sale, 0) AS last_year_sale,
|
||
// COALESCE(total_sale, 0) AS total_sale,
|
||
// COALESCE(sold_out_times, '') AS sold_out_times,
|
||
// COALESCE(shipment_cycle, 0) AS shipment_cycle,
|
||
// COALESCE(buy_count, '') AS buy_count,
|
||
// COALESCE(sell_count, '') AS sell_count,
|
||
// COALESCE(publiction_times, 0) AS publiction_times,
|
||
// COALESCE(cat_id, 0) AS cat_id,
|
||
// COALESCE(buy_counts, 0) AS buy_counts,
|
||
// COALESCE(sell_counts, 0) AS sell_counts,
|
||
// COALESCE(create_by, 0) AS create_by,
|
||
// COALESCE(create_time, 0) AS create_time,
|
||
// COALESCE(update_by, 0) AS update_by,
|
||
// COALESCE(update_time, 0) AS update_time
|
||
// FROM book_center LIMIT ? OFFSET ?`, pageSize, offset)
|
||
// if err != nil {
|
||
// c.JSON(500, gin.H{"error": "查询失败"})
|
||
// return
|
||
// }
|
||
// defer rows.Close()
|
||
//
|
||
// const basePath = "/file/goods_img"
|
||
// var success, fail int
|
||
//
|
||
// // 获取列名信息
|
||
// columns, err := rows.Columns()
|
||
// if err != nil {
|
||
// c.JSON(500, gin.H{"error": "获取列名失败"})
|
||
// return
|
||
// }
|
||
//
|
||
// // 准备值容器
|
||
// values := make([]interface{}, len(columns))
|
||
// valuePtrs := make([]interface{}, len(columns))
|
||
// for i := range columns {
|
||
// valuePtrs[i] = &values[i]
|
||
// }
|
||
//
|
||
// for rows.Next() {
|
||
// // 扫描行数据
|
||
// if err := rows.Scan(valuePtrs...); err != nil {
|
||
// log.Printf("扫描行数据失败: %v", err)
|
||
// fail++
|
||
// continue
|
||
// }
|
||
//
|
||
// // 将行数据转换为map
|
||
// bookData := make(map[string]interface{})
|
||
// for i, col := range columns {
|
||
// val := values[i]
|
||
// b, ok := val.([]byte)
|
||
// if ok {
|
||
// bookData[col] = string(b)
|
||
// } else {
|
||
// bookData[col] = val
|
||
// }
|
||
// }
|
||
//
|
||
// // 获取书名和ISBN
|
||
// name, _ := bookData["book_name"].(string)
|
||
// isbn, _ := bookData["isbn"].(string)
|
||
// //id, _ := bookData["id"].(int64)
|
||
// var id int64
|
||
// switch v := bookData["id"].(type) {
|
||
// case int64:
|
||
// id = v
|
||
// case uint64:
|
||
// id = int64(v)
|
||
// case int32:
|
||
// id = int64(v)
|
||
// case uint32:
|
||
// id = int64(v)
|
||
// case int:
|
||
// id = int64(v)
|
||
// case uint:
|
||
// id = int64(v)
|
||
// case []byte:
|
||
// id, _ = strconv.ParseInt(string(v), 10, 64)
|
||
// case string:
|
||
// id, _ = strconv.ParseInt(v, 10, 64)
|
||
// default:
|
||
// log.Printf("无法解析 ID 类型: %T,值: %v", v, v)
|
||
// fail++
|
||
// continue
|
||
// }
|
||
//
|
||
// // 3. 通过MD5获取首个字符
|
||
// hash := md5.Sum([]byte(name))
|
||
// firstChar := hex.EncodeToString(hash[:])[0:1]
|
||
// imageFile := fmt.Sprintf("%s_01.jpg", isbn)
|
||
//
|
||
// // 4. 构造本地文件路径
|
||
// localFilePath := filepath.Join(basePath, firstChar, imageFile)
|
||
//
|
||
// // 5. 检查文件是否存在
|
||
// if _, err := os.Stat(localFilePath); err != nil {
|
||
// log.Printf("书籍图片未找到: %s (ISBN: %s) 路径: %s", name, isbn, localFilePath)
|
||
// fail++
|
||
// continue
|
||
// }
|
||
//
|
||
// // 构造图片URL
|
||
// imageFileUrl := fmt.Sprintf("https://book.center.image.buzhiyushu.cn/%s/%s", firstChar, imageFile)
|
||
// bookData["book_pic"] = imageFileUrl
|
||
// // 6. 开启事务处理MySQL和Redis
|
||
// tx, err := db.Begin()
|
||
// if err != nil {
|
||
// log.Printf("事务开启失败: %s: %v", name, err)
|
||
// fail++
|
||
// continue
|
||
// }
|
||
//
|
||
// // 修改点1: 添加MySQL更新日志
|
||
// log.Printf("准备更新MySQL记录 ID: %d, ISBN: %s, 新图片URL: %s", id, isbn, imageFileUrl)
|
||
//
|
||
// // 更新MySQL中的book_pic字段
|
||
// result, err := tx.Exec("UPDATE book_center SET book_pic = ? WHERE id = ?", imageFileUrl, id)
|
||
// if err != nil {
|
||
// tx.Rollback()
|
||
// log.Printf("更新book_pic失败: %s: %v", name, err)
|
||
// fail++
|
||
// continue
|
||
// }
|
||
//
|
||
// // 修改点2: 检查实际影响的行数
|
||
// rowsAffected, _ := result.RowsAffected()
|
||
// log.Printf("MySQL更新影响行数: %d (ID: %d)", rowsAffected, id)
|
||
//
|
||
// // 将书籍数据转换为JSON格式
|
||
// bookJSON, err := json.Marshal(bookData)
|
||
// if err != nil {
|
||
// tx.Rollback()
|
||
// log.Printf("JSON序列化失败: %s: %v", name, err)
|
||
// fail++
|
||
// continue
|
||
// }
|
||
//
|
||
// // 修改点3: 添加Redis操作日志便于调试
|
||
// log.Printf("准备更新Redis键: %s 数据: %s", isbn, string(bookJSON))
|
||
//
|
||
// // 更新Redis
|
||
// err = bc.redis.Set(context.Background(), isbn, bookJSON, 0).Err()
|
||
// if err != nil {
|
||
// tx.Rollback()
|
||
// log.Printf("Redis更新失败: %s: %v", name, err)
|
||
// fail++
|
||
// continue
|
||
// }
|
||
//
|
||
// // 修改点4: 添加事务提交日志
|
||
// log.Printf("准备提交事务 (ID: %d, ISBN: %s)", id, isbn)
|
||
// if err := tx.Commit(); err != nil {
|
||
// log.Printf("事务提交失败: %s: %v", name, err)
|
||
// fail++
|
||
// continue
|
||
// }
|
||
//
|
||
// success++
|
||
// log.Printf("更新成功: %s (ISBN: %s)", name, isbn)
|
||
//
|
||
// //// 6. 开启事务处理MySQL和Redis
|
||
// //tx, err := db.Begin()
|
||
// //if err != nil {
|
||
// // log.Printf("事务开启失败: %s: %v", name, err)
|
||
// // fail++
|
||
// // continue
|
||
// //}
|
||
// //
|
||
// //// 更新MySQL中的book_pic字段
|
||
// //if _, err = tx.Exec("UPDATE book_center SET book_pic = ? WHERE id = ?", imageFileUrl, id); err != nil {
|
||
// // tx.Rollback()
|
||
// // log.Printf("更新book_pic失败: %s: %v", name, err)
|
||
// // fail++
|
||
// // continue
|
||
// //}
|
||
// //
|
||
// //// 将书籍数据转换为JSON格式
|
||
// //bookJSON, err := json.Marshal(bookData)
|
||
// //if err != nil {
|
||
// // tx.Rollback()
|
||
// // log.Printf("JSON序列化失败: %s: %v", name, err)
|
||
// // fail++
|
||
// // continue
|
||
// //}
|
||
// //
|
||
// //// 修改点4:添加Redis操作日志便于调试
|
||
// //log.Printf("Updating Redis key: %s with data: %s", isbn, string(bookJSON))
|
||
// //
|
||
// //// 更新Redis - 现在会在第一次请求时就执行
|
||
// //err = bc.redis.Set(context.Background(), isbn, bookJSON, 0).Err()
|
||
// //if err != nil {
|
||
// // tx.Rollback()
|
||
// // c.JSON(http.StatusInternalServerError, gin.H{"error": "Redis操作失败: " + err.Error()})
|
||
// // return
|
||
// //}
|
||
// //
|
||
// //// 6. 提交事务
|
||
// //if err := tx.Commit(); err != nil {
|
||
// // c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库事务提交失败: " + err.Error()})
|
||
// // return
|
||
// //}
|
||
// //
|
||
// //success++
|
||
// //log.Printf("更新成功: %s (ISBN: %s)", name, isbn)
|
||
// }
|
||
//
|
||
// if err = rows.Err(); err != nil {
|
||
// c.JSON(500, gin.H{"error": "遍历结果集失败"})
|
||
// return
|
||
// }
|
||
// }
|
||
//
|
||
// c.JSON(200, gin.H{
|
||
// "message": "更新完成",
|
||
// "success": success,
|
||
// "fail": fail,
|
||
// })
|
||
//}
|
||
|
||
func (bc *BookCenterController) updateBooks(c *gin.Context) {
|
||
// 1. 从ApiFox获取起始页参数
|
||
startPage, err := strconv.Atoi(c.DefaultQuery("startPage", "1"))
|
||
if err != nil || startPage < 1 {
|
||
startPage = 1
|
||
}
|
||
const pageSize = 100 // 固定每页100条数据
|
||
|
||
// 2. 连接到目标MySQL数据库
|
||
targetDSN := "book:xTxZDK3fxri8pNNW@tcp(118.195.145.133:3306)/book?charset=utf8mb4&parseTime=True&loc=Local"
|
||
db, err := sql.Open("mysql", targetDSN)
|
||
if err != nil {
|
||
c.JSON(500, gin.H{"error": "数据库连接失败"})
|
||
return
|
||
}
|
||
defer db.Close()
|
||
db.SetMaxOpenConns(10)
|
||
|
||
// 3. 初始化统计变量
|
||
var globalSuccess, globalFail int
|
||
const basePath = "/file/goods_img"
|
||
|
||
// 4. 自动分页处理循环
|
||
for page := startPage; ; page++ {
|
||
offset := (page - 1) * pageSize
|
||
log.Printf("正在处理第 %d 页,偏移量: %d", page, offset)
|
||
|
||
// 5. 分页查询书籍信息
|
||
rows, err := db.Query(`SELECT
|
||
id, category, book_name, book_pic,
|
||
COALESCE(book_pic_s, '""""') AS book_pic_s,
|
||
isbn, author, editor, binding_layout, publisher,
|
||
edition, format, languages,
|
||
COALESCE(publication_time, 0) AS publication_time,
|
||
COALESCE(print_time, 0) AS print_time,
|
||
COALESCE(paper, '') AS paper,
|
||
COALESCE(pages, 0) AS pages,
|
||
COALESCE(wordage, '') AS wordage,
|
||
COALESCE(fix_price, 0) AS fix_price,
|
||
COALESCE(content, '') AS content,
|
||
COALESCE(remark, '') AS remark,
|
||
COALESCE(vio_book, 0) AS vio_book,
|
||
COALESCE(book_set, 0) AS book_set,
|
||
COALESCE(onenum_mbooks, 0) AS onenum_mbooks,
|
||
COALESCE(ill_publisher, 0) AS ill_publisher,
|
||
COALESCE(ill_author, 0) AS ill_author,
|
||
COALESCE(day_sale_7, 0) AS day_sale_7,
|
||
COALESCE(day_sale_15, 0) AS day_sale_15,
|
||
COALESCE(day_sale_30, 0) AS day_sale_30,
|
||
COALESCE(day_sale_60, 0) AS day_sale_60,
|
||
COALESCE(day_sale_90, 0) AS day_sale_90,
|
||
COALESCE(day_sale_180, 0) AS day_sale_180,
|
||
COALESCE(day_sale_365, 0) AS day_sale_365,
|
||
COALESCE(this_year_sale, 0) AS this_year_sale,
|
||
COALESCE(last_year_sale, 0) AS last_year_sale,
|
||
COALESCE(total_sale, 0) AS total_sale,
|
||
COALESCE(sold_out_times, '') AS sold_out_times,
|
||
COALESCE(shipment_cycle, 0) AS shipment_cycle,
|
||
COALESCE(buy_counts, '') AS buy_count,
|
||
COALESCE(sell_counts, '') AS sell_count,
|
||
COALESCE(publiction_times, 0) AS publiction_times,
|
||
COALESCE(cat_id, 0) AS cat_id,
|
||
COALESCE(buy_counts, 0) AS buy_counts,
|
||
COALESCE(sell_counts, 0) AS sell_counts,
|
||
COALESCE(create_by, 0) AS create_by,
|
||
COALESCE(create_time, 0) AS create_time,
|
||
COALESCE(update_by, 0) AS update_by,
|
||
COALESCE(update_time, 0) AS update_time
|
||
FROM book_center LIMIT ? OFFSET ?`, pageSize, offset)
|
||
|
||
if err != nil {
|
||
log.Printf("第 %d 页查询失败: %v", page, err)
|
||
break
|
||
}
|
||
|
||
// 6. 处理当前页数据
|
||
var pageSuccess, pageFail int
|
||
columns, err := rows.Columns()
|
||
if err != nil {
|
||
rows.Close()
|
||
log.Printf("获取列名失败: %v", err)
|
||
break
|
||
}
|
||
|
||
values := make([]interface{}, len(columns))
|
||
valuePtrs := make([]interface{}, len(columns))
|
||
for i := range columns {
|
||
valuePtrs[i] = &values[i]
|
||
}
|
||
|
||
hasData := false
|
||
for rows.Next() {
|
||
hasData = true
|
||
|
||
// 扫描行数据
|
||
if err := rows.Scan(valuePtrs...); err != nil {
|
||
log.Printf("扫描行数据失败: %v", err)
|
||
pageFail++
|
||
continue
|
||
}
|
||
|
||
// 将行数据转换为map
|
||
bookData := make(map[string]interface{})
|
||
for i, col := range columns {
|
||
val := values[i]
|
||
b, ok := val.([]byte)
|
||
if ok {
|
||
bookData[col] = string(b)
|
||
} else {
|
||
bookData[col] = val
|
||
}
|
||
}
|
||
|
||
// 获取书名和ISBN
|
||
name, _ := bookData["book_name"].(string)
|
||
isbn, _ := bookData["isbn"].(string)
|
||
var id int64
|
||
switch v := bookData["id"].(type) {
|
||
case int64:
|
||
id = v
|
||
case uint64:
|
||
id = int64(v)
|
||
case int32:
|
||
id = int64(v)
|
||
case uint32:
|
||
id = int64(v)
|
||
case int:
|
||
id = int64(v)
|
||
case uint:
|
||
id = int64(v)
|
||
case []byte:
|
||
id, _ = strconv.ParseInt(string(v), 10, 64)
|
||
case string:
|
||
id, _ = strconv.ParseInt(v, 10, 64)
|
||
default:
|
||
log.Printf("无法解析 ID 类型: %T,值: %v", v, v)
|
||
pageFail++
|
||
continue
|
||
}
|
||
|
||
// 处理图片逻辑
|
||
hash := md5.Sum([]byte(name))
|
||
firstChar := hex.EncodeToString(hash[:])[0:1]
|
||
imageFile := fmt.Sprintf("%s_01.jpg", isbn)
|
||
localFilePath := filepath.Join(basePath, firstChar, imageFile)
|
||
|
||
if _, err := os.Stat(localFilePath); err != nil {
|
||
log.Printf("书籍图片未找到: %s (ISBN: %s) 路径: %s", name, isbn, localFilePath)
|
||
pageFail++
|
||
continue
|
||
}
|
||
|
||
imageFileUrl := fmt.Sprintf("https://book.center.image.buzhiyushu.cn/%s/%s", firstChar, imageFile)
|
||
bookData["book_pic"] = imageFileUrl
|
||
|
||
// 事务处理
|
||
tx, err := db.Begin()
|
||
if err != nil {
|
||
log.Printf("事务开启失败: %s: %v", name, err)
|
||
pageFail++
|
||
continue
|
||
}
|
||
|
||
// 更新MySQL
|
||
_, err = tx.Exec("UPDATE book_center SET book_pic = ? WHERE id = ?", imageFileUrl, id)
|
||
if err != nil {
|
||
tx.Rollback()
|
||
log.Printf("更新book_pic失败: %s: %v", name, err)
|
||
pageFail++
|
||
continue
|
||
}
|
||
|
||
// 更新Redis
|
||
bookJSON, err := json.Marshal(bookData)
|
||
if err != nil {
|
||
tx.Rollback()
|
||
log.Printf("JSON序列化失败: %s: %v", name, err)
|
||
pageFail++
|
||
continue
|
||
}
|
||
|
||
err = bc.redis.Set(context.Background(), isbn, bookJSON, 0).Err()
|
||
if err != nil {
|
||
tx.Rollback()
|
||
log.Printf("Redis更新失败: %s: %v", name, err)
|
||
pageFail++
|
||
continue
|
||
}
|
||
|
||
if err := tx.Commit(); err != nil {
|
||
log.Printf("事务提交失败: %s: %v", name, err)
|
||
pageFail++
|
||
continue
|
||
}
|
||
|
||
pageSuccess++
|
||
log.Printf("更新成功: %s (ISBN: %s)", name, isbn)
|
||
}
|
||
|
||
rows.Close()
|
||
if err = rows.Err(); err != nil {
|
||
log.Printf("遍历结果集失败: %v", err)
|
||
}
|
||
|
||
globalSuccess += pageSuccess
|
||
globalFail += pageFail
|
||
log.Printf("第 %d 页处理完成: 成功 %d, 失败 %d", page, pageSuccess, pageFail)
|
||
|
||
// 7. 如果没有数据,结束循环
|
||
if !hasData {
|
||
log.Printf("第 %d 页无数据,处理结束", page)
|
||
break
|
||
}
|
||
}
|
||
|
||
// 8. 返回最终结果
|
||
c.JSON(200, gin.H{
|
||
"message": "批量更新完成",
|
||
"success": globalSuccess,
|
||
"fail": globalFail,
|
||
"processedFromPage": startPage,
|
||
"timestamp": time.Now().Format(time.RFC3339),
|
||
})
|
||
}
|
||
|
||
// GetBookPicByISBN 根据ISBN获取书籍小图
|
||
func (bc *BookCenterController) GetBookPicByISBN(c *gin.Context) {
|
||
// 获取ISBN参数
|
||
isbn := c.Query("isbn")
|
||
if isbn == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "ISBN参数不能为空"})
|
||
return
|
||
}
|
||
|
||
// 验证ISBN格式
|
||
if !isValidISBN(isbn) {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "ISBN格式不正确"})
|
||
return
|
||
}
|
||
|
||
// 查询数据库中的book_pic_s字段
|
||
var bookPicS sql.NullString
|
||
query := "SELECT book_pic_s FROM book_center WHERE isbn = ?"
|
||
|
||
err := bc.db.QueryRow(query, isbn).Scan(&bookPicS)
|
||
if err != nil {
|
||
if err == sql.ErrNoRows {
|
||
c.JSON(http.StatusNotFound, gin.H{"error": "未找到该ISBN对应的图书"})
|
||
return
|
||
}
|
||
log.Printf("数据库查询失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库查询失败"})
|
||
return
|
||
}
|
||
|
||
// 如果book_pic_s为空或null,返回空字符串
|
||
if !bookPicS.Valid || bookPicS.String == "" {
|
||
c.JSON(http.StatusOK, gin.H{"localPath": ""})
|
||
return
|
||
}
|
||
|
||
// 解析JSON数据
|
||
var picData map[string]interface{}
|
||
if err := json.Unmarshal([]byte(bookPicS.String), &picData); err != nil {
|
||
log.Printf("解析book_pic_s JSON失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "图片数据格式错误"})
|
||
return
|
||
}
|
||
|
||
// 提取localPath字段
|
||
localPath := ""
|
||
if path, exists := picData["localPath"]; exists {
|
||
if pathStr, ok := path.(string); ok {
|
||
localPath = pathStr
|
||
}
|
||
}
|
||
|
||
// 返回localPath值
|
||
c.JSON(http.StatusOK, gin.H{"localPath": localPath})
|
||
}
|
||
|
||
// parseISBNs 解析ISBNs参数
|
||
func (bc *BookCenterController) parseISBNs(isbns interface{}) ([]string, error) {
|
||
var result []string
|
||
|
||
switch v := isbns.(type) {
|
||
case string:
|
||
if v == "" {
|
||
return nil, fmt.Errorf("必备参数为空isbns")
|
||
}
|
||
strISBNs := strings.Split(v, ",")
|
||
for _, strISBN := range strISBNs {
|
||
strISBN = strings.TrimSpace(strISBN)
|
||
if strISBN == "" {
|
||
continue
|
||
}
|
||
// 验证ISBN格式
|
||
if !isValidISBN(strISBN) {
|
||
return nil, fmt.Errorf("ISBN格式错误: %s", strISBN)
|
||
}
|
||
result = append(result, strISBN)
|
||
}
|
||
case []interface{}:
|
||
if len(v) == 0 {
|
||
return nil, fmt.Errorf("必备参数为空isbns")
|
||
}
|
||
for _, isbn := range v {
|
||
switch isbnVal := isbn.(type) {
|
||
case string:
|
||
isbnVal = strings.TrimSpace(isbnVal)
|
||
if isbnVal == "" {
|
||
continue
|
||
}
|
||
// 验证ISBN格式
|
||
if !isValidISBN(isbnVal) {
|
||
return nil, fmt.Errorf("ISBN格式错误: %s", isbnVal)
|
||
}
|
||
result = append(result, isbnVal)
|
||
default:
|
||
return nil, fmt.Errorf("ISBN类型错误,必须是字符串")
|
||
}
|
||
}
|
||
default:
|
||
return nil, fmt.Errorf("ISBN类型错误,必须是字符串或数组")
|
||
}
|
||
|
||
if len(result) == 0 {
|
||
return nil, fmt.Errorf("未提供有效的ISBN")
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// prepareUpdateDataByISBN 准备要更新的数据(根据ISBN)
|
||
func (bc *BookCenterController) prepareUpdateDataByISBN(request SetBookBaseInfoToIllByISBNRequest) map[string]interface{} {
|
||
updateData := make(map[string]interface{})
|
||
|
||
if request.VioBook != nil {
|
||
updateData["vio_book"] = *request.VioBook
|
||
}
|
||
if request.BookSet != nil {
|
||
updateData["book_set"] = *request.BookSet
|
||
}
|
||
if request.OnenumMbooks != nil {
|
||
updateData["onenum_mbooks"] = *request.OnenumMbooks
|
||
}
|
||
if request.IllPublisher != nil {
|
||
updateData["ill_publisher"] = *request.IllPublisher
|
||
}
|
||
if request.IllAuthor != nil {
|
||
updateData["ill_author"] = *request.IllAuthor
|
||
}
|
||
|
||
return updateData
|
||
}
|
||
|
||
// updateMySQLByISBN 根据ISBN更新MySQL数据库
|
||
func (bc *BookCenterController) updateMySQLByISBN(tx *sql.Tx, isbns []string, updateData map[string]interface{}) (int64, error) {
|
||
log.Printf("updateMySQLByISBN: 开始构建SQL查询,ISBNs: %v, updateData: %+v", isbns, updateData)
|
||
|
||
// 构建SQL查询
|
||
query := "UPDATE book_center SET "
|
||
var setParts []string
|
||
var args []interface{}
|
||
|
||
for field, value := range updateData {
|
||
setParts = append(setParts, field+" = ?")
|
||
args = append(args, value)
|
||
log.Printf("updateMySQLByISBN: 添加更新字段: %s = %v", field, value)
|
||
}
|
||
query += strings.Join(setParts, ", ") + " WHERE isbn IN ("
|
||
|
||
// 添加ISBN占位符
|
||
placeholders := make([]string, len(isbns))
|
||
for i := range isbns {
|
||
placeholders[i] = "?"
|
||
args = append(args, isbns[i])
|
||
}
|
||
query += strings.Join(placeholders, ",") + ")"
|
||
|
||
log.Printf("updateMySQLByISBN: 构建的SQL查询: %s", query)
|
||
log.Printf("updateMySQLByISBN: SQL参数: %v", args)
|
||
|
||
// 执行MySQL更新
|
||
log.Printf("updateMySQLByISBN: 开始执行SQL更新")
|
||
result, err := tx.Exec(query, args...)
|
||
if err != nil {
|
||
log.Printf("updateMySQLByISBN: ❌ SQL执行失败: %v", err)
|
||
return 0, fmt.Errorf("数据库更新错误: %v", err)
|
||
}
|
||
log.Printf("updateMySQLByISBN: ✅ SQL执行成功")
|
||
|
||
// 检查影响的行数
|
||
rowsAffected, err := result.RowsAffected()
|
||
if err != nil {
|
||
log.Printf("updateMySQLByISBN: ❌ 获取影响行数失败: %v", err)
|
||
return 0, fmt.Errorf("获取影响行数错误: %v", err)
|
||
}
|
||
|
||
log.Printf("updateMySQLByISBN: ✅ 更新完成,影响行数: %d", rowsAffected)
|
||
return rowsAffected, nil
|
||
}
|
||
|
||
// UploadBookPic 上传图书图片并更新book_pic_new字段
|
||
// UploadBookPic 上传图书图片并更新book_pic_new字段(支持文件上传和网络URL)
|
||
// UploadBookPic 上传图书图片并更新book_pic_new字段(支持网络URL)【JSON方式】
|
||
// UploadBookPic 仅保存图书图片URL到 book_pic_new 字段(结构仅包含 pddPath)
|
||
func (bc *BookCenterController) UploadBookPic(c *gin.Context) {
|
||
type UploadRequest struct {
|
||
Isbn string `json:"isbn"`
|
||
File string `json:"file"` // 实际上传的是URL
|
||
}
|
||
|
||
var req UploadRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"success": false,
|
||
"message": "请求参数解析失败",
|
||
"error": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
isbn := req.Isbn
|
||
fileURL := req.File
|
||
|
||
if isbn == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"success": false,
|
||
"message": "ISBN参数不能为空",
|
||
})
|
||
return
|
||
}
|
||
if !isValidISBN(isbn) {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"success": false,
|
||
"message": "ISBN格式不正确",
|
||
})
|
||
return
|
||
}
|
||
if fileURL == "" || !isValidURL(fileURL) || !isImageURL(fileURL) {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"success": false,
|
||
"message": "请提供有效的图片URL",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 检查图书是否存在
|
||
var bookID int
|
||
err := bc.db.QueryRow("SELECT id FROM book_center WHERE isbn = ? LIMIT 1", isbn).Scan(&bookID)
|
||
if err != nil {
|
||
if err == sql.ErrNoRows {
|
||
c.JSON(http.StatusNotFound, gin.H{
|
||
"success": false,
|
||
"message": "未找到该ISBN对应的图书",
|
||
})
|
||
} else {
|
||
log.Printf("数据库查询失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"success": false,
|
||
"message": "数据库查询失败",
|
||
})
|
||
}
|
||
return
|
||
}
|
||
|
||
// 构造 JSON 结构,仅包含 pddPath 字段
|
||
bookPicNew := map[string]string{
|
||
"pddPath": fileURL,
|
||
}
|
||
bookPicNewJSON, err := json.Marshal(bookPicNew)
|
||
if err != nil {
|
||
log.Printf("序列化 book_pic_new 失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"success": false,
|
||
"message": "数据序列化失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 写入数据库
|
||
tx, err := bc.db.Begin()
|
||
if err != nil {
|
||
log.Printf("开启事务失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"success": false,
|
||
"message": "开启事务失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
updateQuery := "UPDATE book_center SET book_pic_new = ?, update_time = ? WHERE isbn = ?"
|
||
result, err := tx.Exec(updateQuery, string(bookPicNewJSON), time.Now().Unix(), isbn)
|
||
if err != nil {
|
||
tx.Rollback()
|
||
log.Printf("更新数据库失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"success": false,
|
||
"message": "更新数据库失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
rowsAffected, err := result.RowsAffected()
|
||
if err != nil || rowsAffected == 0 {
|
||
tx.Rollback()
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"success": false,
|
||
"message": "更新失败或未找到记录",
|
||
})
|
||
return
|
||
}
|
||
|
||
if err := tx.Commit(); err != nil {
|
||
log.Printf("提交事务失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"success": false,
|
||
"message": "提交事务失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 异步更新 Redis 缓存(可选)
|
||
go func() {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
log.Printf("更新Redis缓存发生panic: %v", r)
|
||
}
|
||
}()
|
||
|
||
redisKey := isbn
|
||
bookData, err := bc.redis.Get(context.Background(), redisKey).Result()
|
||
if err == nil {
|
||
var result map[string]interface{}
|
||
if err := json.Unmarshal([]byte(bookData), &result); err == nil {
|
||
result["book_pic_new"] = bookPicNew
|
||
updatedData, _ := json.Marshal(result)
|
||
bc.redis.Set(context.Background(), redisKey, updatedData, 0)
|
||
}
|
||
}
|
||
}()
|
||
|
||
// 返回成功响应
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"message": "图片URL保存成功",
|
||
"data": gin.H{
|
||
"isbn": isbn,
|
||
"book_pic_new": bookPicNew,
|
||
},
|
||
})
|
||
}
|
||
|
||
func (bc *BookCenterController) getBookByIsbnXcx(c *gin.Context) {
|
||
// 解析请求参数
|
||
var request struct {
|
||
BuyCount int `json:"buyCount"`
|
||
Price float64 `json:"price"`
|
||
Author string `json:"author"`
|
||
BookPicNew string `json:"book_pic_new"`
|
||
ISBN string `json:"isbn"`
|
||
SellCount int `json:"sellCount"`
|
||
Publisher string `json:"publisher"`
|
||
BookName string `json:"book_name"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&request); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数解析错误", "message": err.Error()})
|
||
return
|
||
}
|
||
|
||
// 验证ISBN格式
|
||
if request.ISBN == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "ISBN不能为空"})
|
||
return
|
||
}
|
||
|
||
// 1. 先尝试从Redis获取数据
|
||
redisKey := request.ISBN
|
||
bookData, err := bc.redis.Get(context.Background(), redisKey).Result()
|
||
|
||
if err == nil {
|
||
// Redis中存在数据,解析并检查是否需要更新
|
||
var bookInfo map[string]interface{}
|
||
if err := json.Unmarshal([]byte(bookData), &bookInfo); err == nil {
|
||
// 检查price, author, publisher是否有值
|
||
needUpdate := false
|
||
|
||
// 检查price字段
|
||
if price, exists := bookInfo["fix_price"]; !exists || price == nil || price == "" || price == 0 {
|
||
bookInfo["fix_price"] = request.Price
|
||
needUpdate = true
|
||
}
|
||
|
||
// 检查author字段
|
||
if author, exists := bookInfo["author"]; !exists || author == nil || author == "" {
|
||
bookInfo["author"] = request.Author
|
||
needUpdate = true
|
||
}
|
||
|
||
// 检查publisher字段
|
||
if publisher, exists := bookInfo["publisher"]; !exists || publisher == nil || publisher == "" {
|
||
bookInfo["publisher"] = request.Publisher
|
||
needUpdate = true
|
||
}
|
||
|
||
// 如果需要更新,更新Redis和数据库
|
||
if needUpdate {
|
||
// 更新Redis
|
||
updatedData, err := json.Marshal(bookInfo)
|
||
if err == nil {
|
||
bc.redis.Set(context.Background(), redisKey, updatedData, 0)
|
||
}
|
||
|
||
// 更新数据库
|
||
query := `UPDATE book_center SET fix_price = ?, author = ?, publisher = ? WHERE isbn = ?`
|
||
_, err = bc.db.Exec(query, request.Price, request.Author, request.Publisher, request.ISBN)
|
||
if err != nil {
|
||
log.Printf("更新数据库失败: %v", err)
|
||
}
|
||
}
|
||
|
||
// 返回Redis中的数据
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"data": bookInfo,
|
||
"source": "redis",
|
||
})
|
||
return
|
||
}
|
||
}
|
||
|
||
// 2. Redis中没有数据,尝试从数据库获取
|
||
query := `SELECT
|
||
id, book_name, book_pic, book_pic_s, book_pic_new, isbn, author, category, publisher,
|
||
publication_time, binding_layout, fix_price, buy_counts, sell_counts
|
||
FROM book_center
|
||
WHERE isbn = ?`
|
||
|
||
var (
|
||
id, bookName, bookPic, dbIsbn, author, category, publisher string
|
||
bindingLayout sql.NullString
|
||
bookPicS, bookPicNew sql.NullString
|
||
buyCount, sellCount sql.NullString
|
||
publicationTime interface{}
|
||
fixPrice int64
|
||
)
|
||
|
||
err = bc.db.QueryRow(query, request.ISBN).Scan(
|
||
&id, &bookName, &bookPic, &bookPicS, &bookPicNew, &dbIsbn, &author, &category, &publisher,
|
||
&publicationTime, &bindingLayout, &fixPrice, &buyCount, &sellCount,
|
||
)
|
||
|
||
if err == nil {
|
||
// 数据库中存在数据,构建响应并缓存到Redis
|
||
bookInfo := map[string]interface{}{
|
||
"id": id,
|
||
"book_name": bookName,
|
||
"book_pic": bookPic,
|
||
"book_pic_s": bookPicS.String,
|
||
"book_pic_new": bookPicNew.String,
|
||
"isbn": dbIsbn,
|
||
"author": author,
|
||
"category": category,
|
||
"publisher": publisher,
|
||
"publication_time": publicationTime,
|
||
"binding_layout": bindingLayout.String,
|
||
"fix_price": fixPrice,
|
||
"buy_counts": buyCount.String,
|
||
"sell_counts": sellCount.String,
|
||
}
|
||
|
||
// 缓存到Redis
|
||
if jsonData, err := json.Marshal(bookInfo); err == nil {
|
||
bc.redis.Set(context.Background(), redisKey, jsonData, 0)
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"data": bookInfo,
|
||
"source": "database",
|
||
})
|
||
return
|
||
} else if err == sql.ErrNoRows {
|
||
// 3. 数据库中也没有数据,插入新数据
|
||
insertQuery := `INSERT INTO book_center (
|
||
book_name, isbn, author, publisher, fix_price, book_pic_new, total_sale,create_time, update_time
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||
|
||
now := time.Now().Unix()
|
||
_, err = bc.db.Exec(insertQuery,
|
||
request.BookName,
|
||
request.ISBN,
|
||
request.Author,
|
||
request.Publisher,
|
||
request.Price,
|
||
request.BookPicNew,
|
||
request.BuyCount,
|
||
now,
|
||
now,
|
||
)
|
||
|
||
if err != nil {
|
||
log.Printf("插入数据失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "插入数据失败", "message": err.Error()})
|
||
return
|
||
}
|
||
|
||
// 构建新数据并缓存到Redis
|
||
newBookInfo := map[string]interface{}{
|
||
"book_name": request.BookName,
|
||
"isbn": request.ISBN,
|
||
"author": request.Author,
|
||
"publisher": request.Publisher,
|
||
"fix_price": request.Price,
|
||
"buy_counts": request.BuyCount,
|
||
"sell_counts": request.SellCount,
|
||
"book_pic_new": request.BookPicNew,
|
||
"create_time": now,
|
||
"update_time": now,
|
||
}
|
||
|
||
// 缓存到Redis
|
||
if jsonData, err := json.Marshal(newBookInfo); err == nil {
|
||
bc.redis.Set(context.Background(), redisKey, jsonData, 0)
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"data": newBookInfo,
|
||
"source": "created",
|
||
})
|
||
return
|
||
} else {
|
||
// 数据库查询出错
|
||
log.Printf("数据库查询失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库查询失败", "message": err.Error()})
|
||
return
|
||
}
|
||
}
|
||
|
||
// isImageFile 检查文件是否为图片类型
|
||
func isImageFile(filename string) bool {
|
||
ext := strings.ToLower(filepath.Ext(filename))
|
||
allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"}
|
||
|
||
for _, allowedExt := range allowedExts {
|
||
if ext == allowedExt {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
} // is
|
||
// ValidURL 检查字符串是否为有效的URL
|
||
func isValidURL(str string) bool {
|
||
u, err := url.Parse(str)
|
||
return err == nil && u.Scheme != "" && u.Host != ""
|
||
}
|
||
|
||
// isImageURL 检查URL是否指向图片文件
|
||
func isImageURL(urlStr string) bool {
|
||
u, err := url.Parse(urlStr)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
|
||
// 获取路径部分的文件扩展名
|
||
path := u.Path
|
||
ext := strings.ToLower(filepath.Ext(path))
|
||
allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"}
|
||
|
||
for _, allowedExt := range allowedExts {
|
||
if ext == allowedExt {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// UploadBookImage 调用上传接口 (GET 请求)
|
||
func UploadBookImage(imageURL string) (string, error) {
|
||
// 拼接 URL 参数并进行转义
|
||
reqURL := fmt.Sprintf("%s/upload?token=%s&imageURL=%s",
|
||
bookUploadUrl,
|
||
url.QueryEscape(fixedToken),
|
||
url.QueryEscape(imageURL),
|
||
)
|
||
|
||
// 发送 GET 请求
|
||
resp, err := http.Get(reqURL)
|
||
if err != nil {
|
||
return "", fmt.Errorf("请求失败: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 读取响应内容
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return "", fmt.Errorf("读取响应失败: %v", err)
|
||
}
|
||
|
||
// 检查状态码
|
||
if resp.StatusCode != http.StatusOK {
|
||
return "", fmt.Errorf("上传失败,状态码: %d,响应: %s", resp.StatusCode, string(body))
|
||
}
|
||
|
||
return string(body), nil
|
||
}
|
||
|
||
// processBookImageUpload 处理图书图片上传和字段补全的辅助函数//
|
||
// processBookImageUpload 处理图书图片上传和字段补全的辅助函数
|
||
func (bc *BookCenterController) processBookImageUpload(bookData map[string]interface{}, isbn string) {
|
||
getString := func(data map[string]interface{}, key string) string {
|
||
if v, ok := data[key]; ok && v != nil {
|
||
if str, ok2 := v.(string); ok2 {
|
||
return str
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
needsUpload := false
|
||
bookPicNewEmpty := false
|
||
bookPicSEmpty := false
|
||
needsPddResponseFill := false
|
||
|
||
var bookPicNewPddPath string
|
||
var bookPicNewMap map[string]interface{}
|
||
var bookPicSMap map[string]interface{}
|
||
|
||
// ✅ 兼容字符串/Map 两种格式的 book_pic_new
|
||
switch v := bookData["book_pic_new"].(type) {
|
||
case string:
|
||
if v != "" {
|
||
json.Unmarshal([]byte(v), &bookPicNewMap)
|
||
}
|
||
case map[string]interface{}:
|
||
bookPicNewMap = v
|
||
}
|
||
|
||
if bookPicNewMap == nil || bookPicNewMap["pddPath"] == nil || bookPicNewMap["pddPath"] == "" {
|
||
bookPicNewEmpty = true
|
||
needsUpload = true
|
||
} else if pathStr, ok := bookPicNewMap["pddPath"].(string); ok && pathStr != "" {
|
||
bookPicNewPddPath = strings.TrimSpace(pathStr)
|
||
}
|
||
|
||
// ✅ 兼容字符串/Map 两种格式的 book_pic_s
|
||
switch v := bookData["book_pic_s"].(type) {
|
||
case string:
|
||
if v != "" {
|
||
json.Unmarshal([]byte(v), &bookPicSMap)
|
||
}
|
||
case map[string]interface{}:
|
||
bookPicSMap = v
|
||
}
|
||
|
||
if bookPicSMap == nil || bookPicSMap["pddResponse"] == nil || bookPicSMap["pddResponse"] == "" {
|
||
bookPicSEmpty = true
|
||
if bookPicNewPddPath != "" {
|
||
needsPddResponseFill = true
|
||
} else {
|
||
needsUpload = true
|
||
}
|
||
}
|
||
|
||
// ✅ 如果只需复制 pddResponse
|
||
if needsPddResponseFill && !needsUpload {
|
||
log.Printf("📸 ISBN %s 的 book_pic_new 有 pddPath,将其复制到 book_pic_s.pddResponse", isbn)
|
||
|
||
if bookPicSMap == nil {
|
||
bookPicSMap = make(map[string]interface{})
|
||
}
|
||
bookPicSMap["pddResponse"] = bookPicNewPddPath
|
||
bookData["book_pic_s"] = bookPicSMap
|
||
|
||
go func() {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
log.Printf("异步更新数据库和Redis过程发生panic: %v, ISBN: %s", r, isbn)
|
||
}
|
||
}()
|
||
|
||
bookPicSJSON, _ := json.Marshal(bookPicSMap)
|
||
updateQuery := "UPDATE book_center SET book_pic_s = ? WHERE isbn = ?"
|
||
if _, err := bc.db.Exec(updateQuery, string(bookPicSJSON), isbn); err != nil {
|
||
log.Printf("❌ 更新数据库 book_pic_s 失败: %v, ISBN: %s", err, isbn)
|
||
} else {
|
||
log.Printf("✅ 成功更新数据库 book_pic_s 字段, ISBN: %s", isbn)
|
||
}
|
||
|
||
if resultJSON, err := json.Marshal(bookData); err == nil {
|
||
if err := bc.redis.Set(context.Background(), isbn, resultJSON, 0).Err(); err != nil {
|
||
log.Printf("❌ 更新Redis缓存失败: %v, ISBN: %s", err, isbn)
|
||
} else {
|
||
log.Printf("✅ 成功更新Redis缓存, ISBN: %s", isbn)
|
||
}
|
||
}
|
||
}()
|
||
return
|
||
}
|
||
|
||
if !needsUpload {
|
||
log.Printf("📸 ISBN %s 的图片字段已完整,无需上传", isbn)
|
||
return
|
||
}
|
||
|
||
// ✅ 获取可上传的图片路径
|
||
var imageToUpload string
|
||
|
||
if path, ok := bookPicNewMap["pddPath"].(string); ok && path != "" {
|
||
imageToUpload = path
|
||
}
|
||
if imageToUpload == "" {
|
||
if localPath, ok := bookPicSMap["localPath"].(string); ok && localPath != "" {
|
||
imageToUpload = localPath
|
||
}
|
||
}
|
||
if imageToUpload == "" {
|
||
bookPic := getString(bookData, "book_pic")
|
||
if bookPic != "" {
|
||
if !strings.HasPrefix(bookPic, "http://") && !strings.HasPrefix(bookPic, "https://") {
|
||
bookPic = "https://" + bookPic
|
||
log.Printf("🔗 自动补全为完整URL: %s", bookPic)
|
||
}
|
||
imageToUpload = bookPic
|
||
}
|
||
}
|
||
|
||
log.Printf("📸 选中的上传图片路径: %s", imageToUpload)
|
||
if imageToUpload == "" {
|
||
log.Printf("⚠️ 没有找到可上传的图片路径,跳过上传")
|
||
return
|
||
}
|
||
|
||
uploadResp, err := UploadBookImage(imageToUpload)
|
||
if err != nil {
|
||
log.Printf("❌ 图片上传失败: %v", err)
|
||
return
|
||
}
|
||
log.Printf("✅ 图片上传成功,返回: %s", uploadResp)
|
||
|
||
if bookPicNewEmpty {
|
||
if bookPicNewMap == nil {
|
||
bookPicNewMap = make(map[string]interface{})
|
||
}
|
||
bookPicNewMap["pddPath"] = uploadResp
|
||
bookData["book_pic_new"] = bookPicNewMap
|
||
log.Printf("✅ 已更新 book_pic_new.pddPath: %s", uploadResp)
|
||
}
|
||
|
||
if bookPicSEmpty {
|
||
if bookPicSMap == nil {
|
||
bookPicSMap = make(map[string]interface{})
|
||
}
|
||
bookPicSMap["pddResponse"] = uploadResp
|
||
bookData["book_pic_s"] = bookPicSMap
|
||
log.Printf("✅ 已更新 book_pic_s.pddResponse: %s", uploadResp)
|
||
}
|
||
|
||
go func() {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
log.Printf("异步更新数据库和Redis过程发生panic: %v, ISBN: %s", r, isbn)
|
||
}
|
||
}()
|
||
|
||
if bookPicNewEmpty || bookPicSEmpty {
|
||
updateQuery := "UPDATE book_center SET "
|
||
updateParams := []interface{}{}
|
||
updateFields := []string{}
|
||
|
||
if bookPicNewEmpty {
|
||
bookPicNewJSON, _ := json.Marshal(bookPicNewMap)
|
||
updateFields = append(updateFields, "book_pic_new = ?")
|
||
updateParams = append(updateParams, string(bookPicNewJSON))
|
||
}
|
||
if bookPicSEmpty {
|
||
bookPicSJSON, _ := json.Marshal(bookPicSMap)
|
||
updateFields = append(updateFields, "book_pic_s = ?")
|
||
updateParams = append(updateParams, string(bookPicSJSON))
|
||
}
|
||
updateQuery += strings.Join(updateFields, ", ") + " WHERE isbn = ?"
|
||
updateParams = append(updateParams, isbn)
|
||
|
||
if _, err := bc.db.Exec(updateQuery, updateParams...); err != nil {
|
||
log.Printf("❌ 更新数据库失败: %v, ISBN: %s", err, isbn)
|
||
} else {
|
||
log.Printf("✅ 成功更新数据库图片字段, ISBN: %s", isbn)
|
||
}
|
||
}
|
||
|
||
if resultJSON, err := json.Marshal(bookData); err == nil {
|
||
if err := bc.redis.Set(context.Background(), isbn, resultJSON, 0).Err(); err != nil {
|
||
log.Printf("❌ 更新Redis缓存失败: %v, ISBN: %s", err, isbn)
|
||
} else {
|
||
log.Printf("✅ 成功更新Redis缓存, ISBN: %s", isbn)
|
||
}
|
||
}
|
||
}()
|
||
}
|
||
|
||
// 查询数据库
|
||
func (bc *BookCenterController) queryBookByIsbn(isbn string) (map[string]interface{}, error) {
|
||
query := `SELECT isbn, book_name, author, publisher, fix_price, book_pic_s, book_pic_new
|
||
FROM book_center WHERE isbn = ?`
|
||
|
||
row := bc.db.QueryRow(query, isbn)
|
||
var isbnVal, bookName, author, publisher string
|
||
var fixPrice sql.NullInt64
|
||
var bookPicS, bookPicNew sql.NullString
|
||
|
||
err := row.Scan(&isbnVal, &bookName, &author, &publisher, &fixPrice, &bookPicS, &bookPicNew)
|
||
if err != nil {
|
||
if err == sql.ErrNoRows {
|
||
return nil, nil
|
||
}
|
||
return nil, err
|
||
}
|
||
|
||
result := map[string]interface{}{
|
||
"isbn": isbnVal,
|
||
"book_name": bookName,
|
||
"author": author,
|
||
"publisher": publisher,
|
||
"fix_price": fixPrice.Int64,
|
||
"book_pic_s": bookPicS.String,
|
||
"book_pic_new": bookPicNew.String,
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// 工具:转换 Excel 单元格为 int
|
||
func toInt(s string) int {
|
||
v, _ := strconv.Atoi(s)
|
||
return v
|
||
}
|
||
|
||
type BookPicS struct {
|
||
LocalPath string
|
||
PddResponse string
|
||
}
|
||
|
||
// 自定义类型 + UnmarshalJSON
|
||
func (b *BookPicS) UnmarshalJSON(data []byte) error {
|
||
// 尝试解析为对象
|
||
var obj struct {
|
||
LocalPath string `json:"localPath"`
|
||
PddResponse string `json:"pddResponse"`
|
||
}
|
||
if err := json.Unmarshal(data, &obj); err == nil {
|
||
*b = BookPicS{
|
||
LocalPath: obj.LocalPath,
|
||
PddResponse: obj.PddResponse,
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// 尝试解析为 string
|
||
var s string
|
||
if err := json.Unmarshal(data, &s); err == nil {
|
||
*b = BookPicS{
|
||
LocalPath: s,
|
||
PddResponse: "",
|
||
}
|
||
return nil
|
||
}
|
||
|
||
return fmt.Errorf("无法解析 BookPicS: %s", string(data))
|
||
}
|