最终需求描述 我有一个接口现在需要实现以下的需求,我有一个产品列表,需要通过excel文件实现指定客户可以查看还是所有客户可以查看,现在默认是所有客户都可以查看。允许用户上传文件,在用户上传完文件之后就对excel的文件的格式进行校验,文件第一列手机号码,第二列账户id。如果有问题需要报错提示文件格式错误供前端使用,如果没问题就把文件数据拿出来存到数据库里面去,供文件列表接口使用判断某个用户能看到哪些产品列表;还需要把文件存到oss里面去,方便客户下载文件重新上传。项目语言为go语言,数据库框架使用Gorm,我希望使用两个字段来解决,用户信息从excel解码之后存json存到数据库字段 customerManager JSON 里面,默认为NULL,还有一个字段表示该产品是全量客户可以查看指定客户可以查看,1表示全量,2表示指定,默认为1。
需求简单描述 我有一个接口现在需要实现以下的需求,我有一个产品列表,需要通过excel文件实现指定客户可以查看还是所有客户可以查看,现在默认是所有客户都可以查看。允许用户上传文件,在用户上传完文件之后就对excel的文件的格式进行校验,文件第一列手机号码,第二列账户id。如果有问题需要报错提示文件格式错误供前端使用,如果没问题就把文件数据拿出来存到数据库里面去,供文件列表接口使用判断某个用户能看到哪些产品列表;还需要把文件存到oss里面去,方便客户下载文件重新上传。项目语言为go语言
我理解您的需求。让我为您提供一个 Go 语言实现的大致框架和主要步骤。这个实现将包括文件上传、格式验证、数据存储和 OSS 上传功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 package mainimport ( "database/sql" "encoding/csv" "fmt" "io" "mime/multipart" "net/http" "github.com/gin-gonic/gin" "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/xuri/excelize/v2" )var ( db *sql.DB ossBucket *oss.Bucket )func init () { var err error db, err = sql.Open("mysql" , "user:password@tcp(127.0.0.1:3306)/dbname" ) if err != nil { panic (err) } client, err := oss.New("endpoint" , "accessKeyId" , "accessKeySecret" ) if err != nil { panic (err) } ossBucket, err = client.Bucket("bucketName" ) if err != nil { panic (err) } }func main () { r := gin.Default() r.POST("/upload" , handleFileUpload) r.Run(":8080" ) }func handleFileUpload (c *gin.Context) { file, header, err := c.Request.FormFile("file" ) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "文件上传失败" }) return } defer file.Close() if !validateFileFormat(file) { c.JSON(http.StatusBadRequest, gin.H{"error" : "文件格式错误" }) return } file.Seek(0 , 0 ) if err := saveDataToDatabase(file); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "保存数据失败" }) return } ossPath := fmt.Sprintf("uploads/%s" , header.Filename) if err := uploadFileToOSS(file, ossPath); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "上传到OSS失败" }) return } c.JSON(http.StatusOK, gin.H{"message" : "文件上传成功" }) }func validateFileFormat (file multipart.File) bool { xlsx, err := excelize.OpenReader(file) if err != nil { return false } sheet := xlsx.GetActiveSheetIndex() rows, err := xlsx.GetRows(xlsx.GetSheetName(sheet)) if err != nil || len (rows) < 1 { return false } if len (rows[0 ]) != 2 { return false } return true }func saveDataToDatabase (file multipart.File) error { xlsx, err := excelize.OpenReader(file) if err != nil { return err } sheet := xlsx.GetActiveSheetIndex() rows, err := xlsx.GetRows(xlsx.GetSheetName(sheet)) if err != nil { return err } tx, err := db.Begin() if err != nil { return err } stmt, err := tx.Prepare("INSERT INTO product_visibility (phone, account_id) VALUES (?, ?)" ) if err != nil { tx.Rollback() return err } defer stmt.Close() for i, row := range rows { if i == 0 { continue } if len (row) != 2 { tx.Rollback() return fmt.Errorf("行 %d 格式错误" , i+1 ) } _, err = stmt.Exec(row[0 ], row[1 ]) if err != nil { tx.Rollback() return err } } return tx.Commit() }func uploadFileToOSS (file multipart.File, ossPath string ) error { return ossBucket.PutObject(ossPath, file) }
这段代码提供了一个基本的框架来实现您的需求。以下是主要功能的解释:
文件上传:使用 Gin 框架处理文件上传。
文件格式验证:validateFileFormat
函数检查上传的 Excel 文件格式是否正确。
数据存储:saveDataToDatabase
函数将 Excel 文件中的数据保存到数据库中。
OSS 上传:uploadFileToOSS
函数将文件上传到阿里云 OSS。
要完成这个实现,您还需要:
安装必要的依赖:
1 2 3 go get github.com /gin-gonic/gingo get github.com /aliyun/aliyun-oss-go -sdk/ossgo get github.com /xuri/excelize/v2
配置数据库连接和 OSS 客户端信息。
创建相应的数据库表结构。
实现产品列表接口,根据用户的手机号或账户 ID 来过滤可见的产品。
添加错误处理和日志记录。
实现文件下载功能,允许用户下载之前上传的文件。
这个实现提供了一个良好的起点。您可能需要根据具体的业务需求进行调整和扩展。如果您需要更详细的解释或有任何问题,请随时告诉我。
这句代码意思是文件通过请求头传过来吗? 1 file, header, err := c.Request.FormFile("file" ) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "文件上传失败" }) return } defer file.Close()
您的理解不完全准确。让我为您解释这段代码:
1 2 3 4 5 6 file, header, err := c.Request.FormFile("file" )if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "文件上传失败" }) return }defer file.Close()
这段代码是在处理通过 HTTP 请求上传的文件,但不是通过请求头传递的。这里的具体含义是:
c.Request.FormFile("file")
是在尝试从 HTTP 请求的表单数据中获取一个名为 “file” 的文件。
文件通常是通过 HTTP POST 请求的请求体(body)中的 multipart/form-data 格式传输的,而不是通过请求头。
FormFile
函数返回三个值:
file
:一个 multipart.File 接口,可以用来读取文件内容。header
:一个 *multipart.FileHeader 结构,包含文件的元信息,如文件名等。err
:如果出现错误,这里会返回错误信息。如果 err
不为 nil,表示获取文件失败,可能是因为没有文件被上传,或者文件名不是 “file”,或者其他原因。这时会返回一个错误响应。
defer file.Close()
确保在函数结束时关闭文件,防止资源泄露。
总的来说,这段代码是在处理通过 HTTP 请求体上传的文件,而不是通过请求头。文件数据是包含在请求的主体部分,通常使用 multipart/form-data
编码格式。
如果您想了解更多关于如何在前端发送这样的请求,或者如何在 Postman 等工具中测试这个接口,我可以为您提供更多信息。您有什么具体想了解的吗?
我这里是接口,应该提供一个字段给前端做请求参数使用吧 您说得对。作为一个接口,我们确实应该提供明确的参数给前端使用。我为您修改一下这部分的实现,使其更符合接口的标准做法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 package mainimport ( "database/sql" "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/xuri/excelize/v2" )var ( db *sql.DB ossBucket *oss.Bucket )func init () { }func main () { r := gin.Default() r.POST("/upload" , handleFileUpload) r.Run(":8080" ) }type UploadRequest struct { File *multipart.FileHeader `form:"file" binding:"required"` }func handleFileUpload (c *gin.Context) { var req UploadRequest if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "无效的请求参数" }) return } file, err := req.File.Open() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "无法打开文件" }) return } defer file.Close() if !validateFileFormat(file) { c.JSON(http.StatusBadRequest, gin.H{"error" : "文件格式错误" }) return } file.Seek(0 , 0 ) if err := saveDataToDatabase(file); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "保存数据失败" }) return } ossPath := fmt.Sprintf("uploads/%s" , req.File.Filename) if err := uploadFileToOSS(file, ossPath); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "上传到OSS失败" }) return } c.JSON(http.StatusOK, gin.H{"message" : "文件上传成功" }) }
这个更新后的版本有以下几个关键改变:
定义了 UploadRequest
结构体,明确了接口期望的请求参数:
1 2 3 type UploadRequest struct { File *multipart.FileHeader `form:"file" binding:"required"` }
这里 form:"file"
指定了前端应该使用的字段名,binding:"required"
表示这是一个必须的字段。
使用 c.ShouldBind(&req)
来绑定请求参数,这样可以自动进行参数验证。
使用 req.File.Open()
来打开上传的文件,而不是直接从请求中获取。
这样修改后,前端在调用这个接口时,需要以 multipart/form-data
格式发送请求,并且文件字段名必须为 “file”。例如,使用 JavaScript 的 Fetch API 调用这个接口可能是这样的:
1 2 3 4 5 6 7 8 9 10 const formData = new FormData (); formData.append ('file' , fileObject); fetch ('http://your-api-url/upload' , { method : 'POST' , body : formData }) .then (response => response.json ()) .then (data => console .log (data)) .catch (error => console .error ('Error:' , error));
这种方式更加明确和标准化,让前端开发者能够更容易地理解如何使用这个接口。同时,它也提供了更好的错误处理和参数验证。
如果您需要添加其他参数,比如用户ID或者其他元数据,您可以在 UploadRequest
结构体中添加更多字段。例如:
1 2 3 4 type UploadRequest struct { File *multipart.FileHeader `form:"file" binding:"required"` UserID string `form:"userId" binding:"required"` }
这样,前端就需要同时提供文件和用户ID。您对这个修改有什么看法或其他需求吗?
c.ShouldBind(&req);能检查出我的文件有没有格式问题吗? 您提出了一个很好的问题。c.ShouldBind(&req)
主要用于绑定和验证请求参数,但它不能检查文件的具体内容或格式。让我解释一下它的功能和局限性:
c.ShouldBind(&req)
的功能:
它会检查请求中是否存在名为 “file” 的文件字段。 它会验证文件字段是否为空(因为我们使用了 binding:"required"
)。 它会将文件信息绑定到 UploadRequest
结构体的 File
字段上。 c.ShouldBind(&req)
的局限性:
它不会检查文件的内容。 它不会验证文件的格式(例如是否为 Excel 文件)。 它不会检查文件的大小或其他具体属性。 要检查文件的具体格式和内容,我们需要在 c.ShouldBind(&req)
之后添加额外的验证步骤。我们可以修改代码来包含这些检查:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 package mainimport ( "database/sql" "fmt" "net/http" "path/filepath" "github.com/gin-gonic/gin" "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/xuri/excelize/v2" )type UploadRequest struct { File *multipart.FileHeader `form:"file" binding:"required"` }func handleFileUpload (c *gin.Context) { var req UploadRequest if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "无效的请求参数" }) return } if !isValidFileExtension(req.File.Filename) { c.JSON(http.StatusBadRequest, gin.H{"error" : "无效的文件类型,请上传 Excel 文件" }) return } file, err := req.File.Open() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "无法打开文件" }) return } defer file.Close() if err := validateFileContent(file); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : err.Error()}) return } file.Seek(0 , 0 ) }func isValidFileExtension (filename string ) bool { ext := filepath.Ext(filename) return ext == ".xlsx" || ext == ".xls" }func validateFileContent (file multipart.File) error { xlsx, err := excelize.OpenReader(file) if err != nil { return fmt.Errorf("无法读取 Excel 文件:%v" , err) } sheet := xlsx.GetActiveSheetIndex() rows, err := xlsx.GetRows(xlsx.GetSheetName(sheet)) if err != nil { return fmt.Errorf("无法获取工作表数据:%v" , err) } if len (rows) < 2 { return fmt.Errorf("文件内容为空或只有标题行" ) } if len (rows[0 ]) != 2 || rows[0 ][0 ] != "手机号码" || rows[0 ][1 ] != "账户ID" { return fmt.Errorf("文件格式错误:第一行应为 '手机号码' 和 '账户ID'" ) } for i, row := range rows[1 :] { if len (row) != 2 { return fmt.Errorf("第 %d 行数据格式错误" , i+2 ) } } return nil }
这个更新版本增加了以下几个关键点:
isValidFileExtension
函数检查文件扩展名是否为 .xlsx 或 .xls。
validateFileContent
函数执行更详细的文件内容验证:
检查是否能成功打开为 Excel 文件。 验证文件是否至少包含标题行和一行数据。 检查标题行是否符合预期格式(”手机号码” 和 “账户ID”)。 检查每行数据是否包含两列。 在 handleFileUpload
函数中,我们现在进行了两步验证:
这样,我们不仅验证了请求参数的存在性(通过 c.ShouldBind
),还验证了文件的格式和内容。这种方法可以有效地捕获各种可能的文件格式问题,并给出具体的错误信息。
您可以根据具体需求进一步调整验证逻辑,比如添加对手机号码格式的检查,或者对账户ID的特定要求等。这样可以确保上传的文件完全符合您的业务需求。
这种实现方式能够更好地保护您的系统,避免处理格式不正确的文件,同时也为用户提供了明确的错误反馈。您对这个实现有什么看法或其他需求吗?
假如这个文件是第一列手机号,第二列账户id,我能把这个excel的文件里面的字段解析出来当作json存到产品表的一个字段里面吗?这样我每次查询产品详情的时候或者查询产品列表的时候就可以直接通过这个字段查出来解析之后的账户id来判断这个产品是应该展示给所有用户看的还是应该只展示给某些用户看的 当然可以。这是一个很好的想法,可以提高查询效率。我们可以将Excel文件中的数据解析为JSON格式,然后存储在产品表的一个JSON类型字段中。这样做的好处是可以快速查询,而不需要每次都关联其他表。
让我们修改代码来实现这个功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 package mainimport ( "database/sql" "encoding/json" "fmt" "net/http" "path/filepath" "github.com/gin-gonic/gin" "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/xuri/excelize/v2" )type UploadRequest struct { File *multipart.FileHeader `form:"file" binding:"required"` ProductID int64 `form:"productId" binding:"required"` }type VisibilityData struct { AllowedUsers []UserInfo `json:"allowedUsers"` }type UserInfo struct { Phone string `json:"phone"` AccountID string `json:"accountId"` }func handleFileUpload (c *gin.Context) { var req UploadRequest if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "无效的请求参数" }) return } if !isValidFileExtension(req.File.Filename) { c.JSON(http.StatusBadRequest, gin.H{"error" : "无效的文件类型,请上传 Excel 文件" }) return } file, err := req.File.Open() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : "无法打开文件" }) return } defer file.Close() visibilityData, err := parseExcelToJSON(file) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : err.Error()}) return } if err := saveVisibilityDataToDB(req.ProductID, visibilityData); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error" : "保存数据失败" }) return } c.JSON(http.StatusOK, gin.H{"message" : "文件上传成功,产品可见性数据已更新" }) }func parseExcelToJSON (file multipart.File) (VisibilityData, error ) { xlsx, err := excelize.OpenReader(file) if err != nil { return VisibilityData{}, fmt.Errorf("无法读取 Excel 文件:%v" , err) } sheet := xlsx.GetActiveSheetIndex() rows, err := xlsx.GetRows(xlsx.GetSheetName(sheet)) if err != nil { return VisibilityData{}, fmt.Errorf("无法获取工作表数据:%v" , err) } if len (rows) < 2 { return VisibilityData{}, fmt.Errorf("文件内容为空或只有标题行" ) } var visibilityData VisibilityData for i, row := range rows[1 :] { if len (row) != 2 { return VisibilityData{}, fmt.Errorf("第 %d 行数据格式错误" , i+2 ) } visibilityData.AllowedUsers = append (visibilityData.AllowedUsers, UserInfo{ Phone: row[0 ], AccountID: row[1 ], }) } return visibilityData, nil }func saveVisibilityDataToDB (productID int64 , data VisibilityData) error { jsonData, err := json.Marshal(data) if err != nil { return fmt.Errorf("JSON 序列化失败:%v" , err) } _, err = db.Exec("UPDATE products SET visibility_data = ? WHERE id = ?" , jsonData, productID) if err != nil { return fmt.Errorf("更新数据库失败:%v" , err) } return nil }
这个更新版本添加了以下关键功能:
新增了 VisibilityData
和 UserInfo
结构体,用于表示可见性数据的 JSON 结构。
修改了 UploadRequest
结构体,添加了 ProductID
字段,用于指定要更新的产品。
新增了 parseExcelToJSON
函数,用于将 Excel 文件内容解析为 VisibilityData
结构。
新增了 saveVisibilityDataToDB
函数,用于将解析后的 JSON 数据保存到数据库的产品表中。
在 handleFileUpload
函数中,我们现在解析 Excel 文件并将数据保存到数据库,而不是之前的逐行保存方式。
使用这种方法,您可以在产品表中添加一个名为 visibility_data
的 JSON 类型字段(在 MySQL 中可以使用 JSON 类型,在其他数据库中可能需要使用 TEXT 或 JSONB 类型)。
现在,当您查询产品详情或产品列表时,可以直接从这个 JSON 字段中获取可见性信息。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func getProductVisibility (productID int64 , userAccountID string ) (bool , error ) { var visibilityDataJSON string err := db.QueryRow("SELECT visibility_data FROM products WHERE id = ?" , productID).Scan(&visibilityDataJSON) if err != nil { return false , err } var visibilityData VisibilityData if err := json.Unmarshal([]byte (visibilityDataJSON), &visibilityData); err != nil { return false , err } if len (visibilityData.AllowedUsers) == 0 { return true , nil } for _, user := range visibilityData.AllowedUsers { if user.AccountID == userAccountID { return true , nil } } return false , nil }
这种方法的优点是:
查询效率高,不需要关联其他表。 可以轻松地在应用层面处理可见性逻辑。 灵活性强,可以轻松添加更多的可见性规则。 缺点是:
数据冗余,同样的用户信息可能在多个产品中重复。 更新单个用户的可见性需要更新所有相关产品的 JSON 数据。 根据您的具体需求和数据规模,这种方法可能是一个很好的平衡。如果您预计频繁地更新单个用户的可见性,或者数据量非常大,可能需要考虑其他的数据模型。您对这个实现有什么看法或其他需求吗?
面对这个需求还有没有其他更好的设计方案 您提出了一个很好的问题。确实,对于这种需求,可能还有其他更优的设计方案。让我们探讨一下几种可能的替代方案,并分析它们的优缺点:
关联表方案 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 CREATE TABLE products ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR (255 ), );CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, phone VARCHAR (20 ), account_id VARCHAR (50 ) UNIQUE );CREATE TABLE product_visibility ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT , user_id INT , FOREIGN KEY (product_id) REFERENCES products(id), FOREIGN KEY (user_id) REFERENCES users(id), UNIQUE KEY (product_id, user_id) );SELECT p.* FROM products pLEFT JOIN product_visibility pv ON p.id = pv.product_idLEFT JOIN users u ON pv.user_id = u.idWHERE u.account_id = ? OR pv.id IS NULL ;SELECT COUNT (* ) > 0 AS is_visibleFROM products pLEFT JOIN product_visibility pv ON p.id = pv.product_idLEFT JOIN users u ON pv.user_id = u.idWHERE p.id = ? AND (u.account_id = ? OR pv.id IS NULL );
优点:
数据一致性强,避免了数据冗余 易于管理和更新单个用户或产品的可见性 支持复杂的查询和分析 缺点:
查询可能需要多表连接,在数据量大时可能影响性能 实现稍微复杂一些 位图索引方案 对于大规模数据,我们可以考虑使用位图索引来优化性能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import ( "github.com/RoaringBitmap/roaring" "encoding/binary" )type ProductVisibility struct { ProductID int64 UserBitmap *roaring.Bitmap }func SaveVisibility (productID int64 , userIDs []int32 ) error { bitmap := roaring.New() bitmap.AddMany(userIDs) data, err := bitmap.MarshalBinary() if err != nil { return err } _, err = db.Exec("INSERT INTO product_visibility (product_id, user_bitmap) VALUES (?, ?) ON DUPLICATE KEY UPDATE user_bitmap = ?" , productID, data, data) return err }func IsProductVisible (productID int64 , userID int32 ) (bool , error ) { var bitmapData []byte err := db.QueryRow("SELECT user_bitmap FROM product_visibility WHERE product_id = ?" , productID).Scan(&bitmapData) if err != nil { if err == sql.ErrNoRows { return true , nil } return false , err } bitmap := roaring.New() err = bitmap.UnmarshalBinary(bitmapData) if err != nil { return false , err } return bitmap.Contains(userID), nil }
优点:
非常高效的存储和查询,特别是对于大规模数据 支持快速的集合操作(如交集、并集) 缺点:
实现相对复杂 可能需要额外的库支持 不易于直接在数据库层面进行复杂查询 缓存层方案 我们还可以考虑添加一个缓存层来优化频繁的查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import ( "github.com/go-redis/redis/v8" "encoding/json" "context" )var redisClient *redis.Clientfunc init () { redisClient = redis.NewClient(&redis.Options{ Addr: "localhost:6379" , }) }func IsProductVisible (productID int64 , userAccountID string ) (bool , error ) { ctx := context.Background() key := fmt.Sprintf("product_visibility:%d" , productID) visibilityData, err := redisClient.Get(ctx, key).Result() if err == nil { var allowedUsers []string err = json.Unmarshal([]byte (visibilityData), &allowedUsers) if err != nil { return false , err } if len (allowedUsers) == 0 { return true , nil } for _, allowedUser := range allowedUsers { if allowedUser == userAccountID { return true , nil } } return false , nil } var allowedUsers []string err = db.QueryRow("SELECT allowed_users FROM product_visibility WHERE product_id = ?" , productID).Scan(&allowedUsers) if err != nil { if err == sql.ErrNoRows { return true , nil } return false , err } allowedUsersJSON, _ := json.Marshal(allowedUsers) redisClient.Set(ctx, key, allowedUsersJSON, 24 *time.Hour) if len (allowedUsers) == 0 { return true , nil } for _, allowedUser := range allowedUsers { if allowedUser == userAccountID { return true , nil } } return false , nil }
优点:
大幅提高查询性能,特别是对于频繁访问的数据 减轻数据库负载 可以与其他方案结合使用 缺点:
需要额外的缓存基础设施 需要处理缓存一致性问题 增加了系统复杂性 建议:
对于中小规模数据,关联表方案可能是最佳选择,因为它简单直接,易于维护和扩展。
如果您的系统需要处理大规模数据和高并发查询,位图索引方案可能更合适。
无论选择哪种方案,添加缓存层都可以显著提升性能,特别是对于读多写少的场景。
您还可以考虑混合方案,例如使用关联表存储数据,但在应用层使用位图进行内存中的快速查询。
选择最佳方案需要考虑您的具体需求,包括数据规模、查询频率、写入频率、扩展性需求等。您可能需要进行一些性能测试来确定哪种方案最适合您的使用场景。
您对这些方案有什么看法?是否有任何特定的关注点或约束条件需要考虑?
位图索引方案如何实现我们这个需求呢 我有一个接口现在需要实现以下的需求,我有一个产品列表,需要通过excel文件实现指定客户可以查看还是所有客户可以查看,现在默认是所有客户都可以查看。允许用户上传文件,在用户上传完文件之后就对excel的文件的格式进行校验,文件第一列手机号码,第二列账户id。如果有问题需要报错提示文件格式错误供前端使用,如果没问题就把文件数据拿出来存到数据库里面去,供文件列表接口使用判断某个用户能看到哪些产品列表;还需要把文件存到oss里面去,方便客户下载文件重新上传。项目语言为go语言 我来帮你设计这个接口的实现方案。以下是详细的代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 package handlerimport ( "github.com/gin-gonic/gin" "github.com/xuri/excelize/v2" "github.com/aliyun/aliyun-oss-go-sdk/oss" "database/sql" "fmt" "time" )type ProductVisibility struct { Phone string `json:"phone"` AccountID string `json:"account_id"` }type UploadResponse struct { Code int `json:"code"` Message string `json:"message"` Data any `json:"data"` }func HandleFileUpload (c *gin.Context) { file, header, err := c.Request.FormFile("file" ) if err != nil { c.JSON(400 , UploadResponse{ Code: 400 , Message: "文件上传失败" , }) return } defer file.Close() if !isExcelFile(header.Filename) { c.JSON(400 , UploadResponse{ Code: 400 , Message: "请上传Excel文件" , }) return } xlsx, err := excelize.OpenReader(file) if err != nil { c.JSON(400 , UploadResponse{ Code: 400 , Message: "无法读取Excel文件" , }) return } rows, err := validateAndParseExcel(xlsx) if err != nil { c.JSON(400 , UploadResponse{ Code: 400 , Message: err.Error(), }) return } ossPath, err := saveToOSS(file, header.Filename) if err != nil { c.JSON(500 , UploadResponse{ Code: 500 , Message: "文件存储失败" , }) return } err = saveToDatabase(rows, ossPath) if err != nil { c.JSON(500 , UploadResponse{ Code: 500 , Message: "数据保存失败" , }) return } c.JSON(200 , UploadResponse{ Code: 200 , Message: "上传成功" , }) }func isExcelFile (filename string ) bool { return strings.HasSuffix(filename, ".xlsx" ) || strings.HasSuffix(filename, ".xls" ) }func validateAndParseExcel (xlsx *excelize.File) ([]ProductVisibility, error ) { rows, err := xlsx.GetRows("Sheet1" ) if err != nil { return nil , fmt.Errorf("无法读取工作表" ) } if len (rows) < 2 { return nil , fmt.Errorf("文件内容为空" ) } var results []ProductVisibility for i, row := range rows { if i == 0 { continue } if len (row) < 2 { return nil , fmt.Errorf("第%d行数据格式错误" , i+1 ) } if !validatePhone(row[0 ]) { return nil , fmt.Errorf("第%d行手机号格式错误" , i+1 ) } if !validateAccountID(row[1 ]) { return nil , fmt.Errorf("第%d行账户ID格式错误" , i+1 ) } results = append (results, ProductVisibility{ Phone: row[0 ], AccountID: row[1 ], }) } return results, nil }func saveToOSS (file io.Reader, filename string ) (string , error ) { client, err := oss.New("endpoint" , "accessKeyId" , "accessKeySecret" ) if err != nil { return "" , err } bucket, err := client.Bucket("bucketName" ) if err != nil { return "" , err } ossPath := fmt.Sprintf("product_visibility/%s_%s" , time.Now().Format("20060102150405" ), filename) err = bucket.PutObject(ossPath, file) if err != nil { return "" , err } return ossPath, nil }func saveToDatabase (rows []ProductVisibility, ossPath string ) error { db, err := sql.Open("mysql" , "user:password@tcp(host:port)/dbname" ) if err != nil { return err } defer db.Close() tx, err := db.Begin() if err != nil { return err } stmt, err := tx.Prepare(` INSERT INTO product_visibility (phone, account_id, file_path, created_at) VALUES (?, ?, ?, NOW()) ` ) if err != nil { tx.Rollback() return err } defer stmt.Close() for _, row := range rows { _, err := stmt.Exec(row.Phone, row.AccountID, ossPath) if err != nil { tx.Rollback() return err } } return tx.Commit() }func validatePhone (phone string ) bool { matched, _ := regexp.MatchString(`^1[3-9]\d{9}$` , phone) return matched }func validateAccountID (accountID string ) bool { return len (accountID) > 0 }
以上代码实现了以下功能:
文件上传处理:
验证上传的文件是否为Excel格式 读取Excel文件内容 验证文件格式和数据有效性 数据验证:
验证Excel文件必须包含两列:手机号和账户ID 验证手机号格式是否正确 验证账户ID是否有效 OSS存储:
数据库存储:
使用事务保证数据一致性 保存用户可见性配置 保存文件路径信息 使用该接口的步骤:
前端上传Excel文件到该接口 接口返回处理结果: 数据库表结构建议:
1 2 3 4 5 6 7 8 9 10 CREATE TABLE product_visibility ( id BIGINT PRIMARY KEY AUTO_INCREMENT, phone VARCHAR (20 ) NOT NULL , account_id VARCHAR (50 ) NOT NULL , file_path VARCHAR (255 ) NOT NULL , created_at DATETIME NOT NULL , updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP , INDEX idx_phone (phone), INDEX idx_account_id (account_id) );
前端可以这样使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const formData = new FormData (); formData.append ('file' , file);fetch ('/api/upload-visibility' , { method : 'POST' , body : formData }) .then (response => response.json ()) .then (data => { if (data.code === 200 ) { alert ('上传成功' ); } else { alert (data.message ); } }) .catch (error => { console .error ('上传失败:' , error); });
需要注意的点:
需要配置正确的OSS和数据库连接信息 根据实际需求调整文件格式验证规则 可以添加文件大小限制 考虑添加并发控制 可以添加日志记录 建议添加超时处理 可以考虑添加异步处理大文件的机制 这个实现提供了基本的功能,你可以根据具体需求进行调整和扩展。
这个是什么方案?除了这个方案还有没有其他方案 我刚才介绍的是直接处理方案,就是同步处理Excel文件。下面我介绍几种其他可行的方案:
异步处理方案(推荐): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package handlerimport ( "github.com/gin-gonic/gin" "github.com/go-redis/redis/v8" )type FileTask struct { ID string `json:"id"` FilePath string `json:"file_path"` Status string `json:"status"` }func HandleAsyncFileUpload (c *gin.Context) { file, header, err := c.Request.FormFile("file" ) if err != nil { c.JSON(400 , gin.H{"error" : "文件上传失败" }) return } taskID := generateTaskID() ossPath, err := saveToOSS(file, header.Filename) if err != nil { c.JSON(500 , gin.H{"error" : "文件存储失败" }) return } task := FileTask{ ID: taskID, FilePath: ossPath, Status: "pending" , } err = publishTask(task) if err != nil { c.JSON(500 , gin.H{"error" : "任务创建失败" }) return } c.JSON(200 , gin.H{ "task_id" : taskID, "message" : "文件已上传,正在处理中" , }) }func processFileTask (task FileTask) { }
分片上传方案(适合大文件): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 type ChunkInfo struct { ChunkNumber int `json:"chunk_number"` TotalChunks int `json:"total_chunks"` FileHash string `json:"file_hash"` }func HandleChunkUpload (c *gin.Context) { chunkInfo := ChunkInfo{} if err := c.ShouldBind(&chunkInfo); err != nil { c.JSON(400 , gin.H{"error" : "参数错误" }) return } chunk, err := c.FormFile("chunk" ) if err != nil { c.JSON(400 , gin.H{"error" : "分片上传失败" }) return } err = saveChunk(chunk, chunkInfo) if err != nil { c.JSON(500 , gin.H{"error" : "分片保存失败" }) return } if chunkInfo.ChunkNumber == chunkInfo.TotalChunks { err = mergeChunks(chunkInfo) if err != nil { c.JSON(500 , gin.H{"error" : "文件合并失败" }) return } } c.JSON(200 , gin.H{"message" : "分片上传成功" }) }
数据库直接导入方案: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func HandleDBImport (c *gin.Context) { file, header, err := c.Request.FormFile("file" ) if err != nil { c.JSON(400 , gin.H{"error" : "文件上传失败" }) return } tempFile := fmt.Sprintf("/tmp/%s" , header.Filename) err = saveToTemp(file, tempFile) if err != nil { c.JSON(500 , gin.H{"error" : "临时文件保存失败" }) return } err = importToDB(tempFile) if err != nil { c.JSON(500 , gin.H{"error" : "数据导入失败" }) return } c.JSON(200 , gin.H{"message" : "数据导入成功" }) }
使用ETL工具方案: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func HandleETLImport (c *gin.Context) { file, header, err := c.Request.FormFile("file" ) if err != nil { c.JSON(400 , gin.H{"error" : "文件上传失败" }) return } etlPath := fmt.Sprintf("/etl/input/%s" , header.Filename) err = saveToETLPath(file, etlPath) if err != nil { c.JSON(500 , gin.H{"error" : "文件保存失败" }) return } c.JSON(200 , gin.H{"message" : "文件已提交到ETL处理" }) }
各方案比较:
异步处理方案:
分片上传方案:
数据库直接导入方案:
ETL工具方案:
推荐方案选择:
如果数据量小(<1000行):使用同步处理方案 如果数据量中等(1000-10000行):使用异步处理方案 如果数据量大(>10000行):使用ETL工具方案 如果文件很大(>100MB):使用分片上传方案 具体实现建议:
异步处理方案实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 type FileProcessor struct { redis *redis.Client queue string }func NewFileProcessor () *FileProcessor { return &FileProcessor{ redis: redis.NewClient(&redis.Options{ Addr: "localhost:6379" , }), queue: "file_tasks" , } }func (p *FileProcessor) Start() { for { result, err := p.redis.BLPop(context.Background(), 0 , p.queue).Result() if err != nil { continue } var task FileTask json.Unmarshal([]byte (result[1 ]), &task) go p.processTask(task) } }
分片上传实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function uploadChunk(file, chunkSize) { const chunks = Math.ceil(file.size / chunkSize); for (let i = 0 ; i < chunks; i++) { const chunk = file.slice(i * chunkSize, (i + 1 ) * chunkSize); const formData = new FormData(); formData.append ('chunk' , chunk); formData.append ('chunk_number' , i + 1 ); formData.append ('total_chunks' , chunks); await fetch('/api/upload-chunk' , { method: 'POST' , body: formData }); } }
这些方案都有其适用场景,建议根据具体需求选择合适的方案:
如果要求实时性:选择同步处理方案 如果要求用户体验:选择异步处理方案 如果数据量大:选择ETL工具方案 如果文件大:选择分片上传方案 我有一个接口现在需要实现以下的需求,我有一个产品列表,需要通过excel文件实现指定客户可以查看还是所有客户可以查看,现在默认是所有客户都可以查看。允许用户上传文件,在用户上传完文件之后就对excel的文件的格式进行校验,文件第一列手机号码,第二列账户id。如果有问题需要报错提示文件格式错误供前端使用,如果没问题就把文件数据拿出来存到数据库里面去,供文件列表接口使用判断某个用户能看到哪些产品列表;还需要把文件存到oss里面去,方便客户下载文件重新上传。项目语言为go语言,数据库框架使用Gorm,我希望使用两个字段来解决,用户信息从excel解码之后存json存到数据库字段 customerManager JSON 里面,默认为NULL,还有一个字段表示该产品是全量客户可以查看指定客户可以查看,1表示全量,2表示指定,默认为1。 我来帮你设计这个功能的实现方案。以下是详细的代码实现:
首先定义相关的结构体: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Product struct { ID uint `gorm:"primaryKey"` Name string CustomerManager *string `gorm:"type:json"` ViewType int `gorm:"default:1"` }type CustomerInfo struct { Phone string `json:"phone"` AccountID string `json:"account_id"` }
实现文件上传和验证的处理函数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 import ( "github.com/xuri/excelize/v2" "encoding/json" "errors" )func HandleCustomerUpload (file *multipart.FileHeader, productID uint ) error { src, err := file.Open() if err != nil { return errors.New("无法打开文件" ) } defer src.Close() ossPath, err := uploadToOSS(src) if err != nil { return errors.New("文件上传OSS失败" ) } f, err := excelize.OpenReader(src) if err != nil { return errors.New("无法解析Excel文件" ) } defer f.Close() rows, err := f.GetRows("Sheet1" ) if err != nil { return errors.New("读取Excel内容失败" ) } var customers []CustomerInfo for i, row := range rows { if i == 0 { continue } if len (row) < 2 { return errors.New("文件格式错误:每行必须包含手机号和账户ID" ) } if !validatePhoneNumber(row[0 ]) { return errors.New("文件格式错误:无效的手机号码" ) } customers = append (customers, CustomerInfo{ Phone: row[0 ], AccountID: row[1 ], }) } customerJSON, err := json.Marshal(customers) if err != nil { return errors.New("数据处理失败" ) } db := GetDB() jsonStr := string (customerJSON) err = db.Model(&Product{}).Where("id = ?" , productID).Updates(map [string ]interface {}{ "customer_manager" : &jsonStr, "view_type" : 2 , }).Error if err != nil { return errors.New("数据库更新失败" ) } return nil }
验证手机号的辅助函数: 1 2 3 4 5 func validatePhoneNumber (phone string ) bool { matched, _ := regexp.MatchString(`^1[3-9]\d{9}$` , phone) return matched }
OSS上传函数示例: 1 2 3 4 5 6 func uploadToOSS (file io.Reader) (string , error ) { return "" , nil }
查询产品列表的接口: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func GetProductList (userPhone, userAccountID string ) ([]Product, error ) { db := GetDB() var products []Product query := db.Model(&Product{}) query = query.Where("view_type = 1 OR (view_type = 2 AND customer_manager IS NOT NULL AND (customer_manager->>'$[*].phone' LIKE ? OR customer_manager->>'$[*].account_id' LIKE ?))" , "%" +userPhone+"%" , "%" +userAccountID+"%" ) err := query.Find(&products).Error if err != nil { return nil , err } return products, nil }
使用说明:
前端上传Excel文件时,调用HandleCustomerUpload函数处理文件。 文件格式验证包括:检查是否有足够的列 验证手机号格式 验证Excel基本格式 数据存储:customerManager字段以JSON格式存储客户信息 viewType字段标识查看权限类型 查询产品列表时,根据用户信息筛选可见产品 注意事项:
需要安装必要的依赖:
1 go get github.com/xuri/excelize/v2
需要配置OSS相关的认证信息和配置。
数据库需要支持JSON类型查询。
建议添加适当的错误处理和日志记录。
可以根据实际需求添加更多的验证规则和错误提示。
这个实现方案提供了基本的功能框架,你可以根据具体需求进行修改和扩展。如果有任何特殊需求或需要更详细的说明,请告诉我。
当一个客户想要查看列表的时候,默认可以查看全部的viewType=1的产品,然后去查找viewType = 2 的产品里面有没有这个用户的账号id就行了 明白了,我来修改查询产品列表的实现,让它更符合你的需求。以下是优化后的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 type Product struct { ID uint `gorm:"primaryKey"` Name string CustomerManager *string `gorm:"type:json"` ViewType int `gorm:"default:1"` }type CustomerInfo struct { AccountID string `json:"account_id"` }func GetProductList (accountID string ) ([]Product, error ) { db := GetDB() var products []Product err := db.Where("view_type = 1 OR (view_type = 2 AND JSON_CONTAINS(customer_manager, ?, '$.account_id'))" , accountID). Find(&products).Error if err != nil { return nil , fmt.Errorf("查询产品列表失败: %w" , err) } return products, nil }func GetProductListPostgres (accountID string ) ([]Product, error ) { db := GetDB() var products []Product err := db.Where("view_type = 1 OR (view_type = 2 AND customer_manager::jsonb @> ?::jsonb)" , fmt.Sprintf(`[{"account_id": "%s"}]` , accountID)). Find(&products).Error if err != nil { return nil , fmt.Errorf("查询产品列表失败: %w" , err) } return products, nil }
对应的文件上传处理函数也需要相应修改,只存储账号ID:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 func HandleCustomerUpload (file *multipart.FileHeader, productID uint ) error { src, err := file.Open() if err != nil { return errors.New("无法打开文件" ) } defer src.Close() ossPath, err := uploadToOSS(src) if err != nil { return errors.New("文件上传OSS失败" ) } f, err := excelize.OpenReader(src) if err != nil { return errors.New("无法解析Excel文件" ) } defer f.Close() rows, err := f.GetRows("Sheet1" ) if err != nil { return errors.New("读取Excel内容失败" ) } var customers []CustomerInfo for i, row := range rows { if i == 0 { continue } if len (row) < 2 { return errors.New("文件格式错误:每行必须包含账户ID" ) } customers = append (customers, CustomerInfo{ AccountID: row[1 ], }) } customerJSON, err := json.Marshal(customers) if err != nil { return errors.New("数据处理失败" ) } db := GetDB() jsonStr := string (customerJSON) err = db.Model(&Product{}).Where("id = ?" , productID).Updates(map [string ]interface {}{ "customer_manager" : &jsonStr, "view_type" : 2 , }).Error if err != nil { return errors.New("数据库更新失败" ) } return nil }
主要改动说明:
简化了数据结构,只存储账号ID,不再存储手机号 查询逻辑更清晰:ViewType = 1 的产品所有人可见 ViewType = 2 的产品只对指定账号可见 使用数据库的 JSON 查询功能来检查用户是否在允许列表中 使用示例:
1 2 3 4 5 6 7 8 9 10 11 products, err := GetProductList("user123" )if err != nil { return }for _, product := range products { }
注意事项:
确保数据库支持 JSON 查询操作 MySQL 和 PostgreSQL 的 JSON 查询语法有所不同,需要根据实际使用的数据库选择对应的查询方式 建议给 customer_manager 字段建立索引以提高查询性能 可以根据需求添加分页功能 建议添加适当的错误处理和日志记录 这个实现更加简洁和高效,直接根据 ViewType 和用户账号ID进行查询,避免了复杂的字符串匹配操作。如果你有任何其他需求或需要更详细的说明,请告诉我。
JSON_CONTAINS的功能是什么?查询效率怎么样 JSON_CONTAINS 是 MySQL 提供的一个 JSON 操作函数,用于检查一个 JSON 文档是否包含另一个 JSON 文档或值。
基本语法:
1 JSON_CONTAINS(target, candidate[, path])
target: 要搜索的 JSON 文档 candidate: 要查找的值 path: 可选参数,指定在 JSON 文档中的搜索路径 但对于你的场景,使用 JSON_CONTAINS 可能不是最优的选择,因为:
查询效率问题:
JSON_CONTAINS 需要扫描整个 JSON 数组 即使建立了索引,JSON 查询的效率也不如普通字段 当数据量大时,性能会下降明显 不同数据库的兼容性:
MySQL 和 PostgreSQL 的 JSON 查询语法不同 迁移数据库时需要修改代码 建议的优化方案:
使用关联表方案(推荐): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 type Product struct { ID uint `gorm:"primaryKey"` Name string ViewType int `gorm:"default:1"` }type ProductVisibility struct { ID uint `gorm:"primaryKey"` ProductID uint `gorm:"index"` AccountID string `gorm:"index"` }func GetProductList (accountID string ) ([]Product, error ) { db := GetDB() var products []Product err := db.Distinct("products.*" ). Joins("LEFT JOIN product_visibilities ON products.id = product_visibilities.product_id" ). Where("products.view_type = 1 OR (products.view_type = 2 AND product_visibilities.account_id = ?)" , accountID). Find(&products).Error if err != nil { return nil , fmt.Errorf("查询产品列表失败: %w" , err) } return products, nil }func HandleCustomerUpload (file *multipart.FileHeader, productID uint ) error { tx := db.Begin() if err := tx.Model(&Product{}).Where("id = ?" , productID). Update("view_type" , 2 ).Error; err != nil { tx.Rollback() return err } if err := tx.Where("product_id = ?" , productID). Delete(&ProductVisibility{}).Error; err != nil { tx.Rollback() return err } var visibilities []ProductVisibility for _, row := range rows { if len (row) < 2 { continue } visibilities = append (visibilities, ProductVisibility{ ProductID: productID, AccountID: row[1 ], }) } if len (visibilities) > 0 { if err := tx.CreateInBatches(visibilities, 100 ).Error; err != nil { tx.Rollback() return err } } return tx.Commit().Error }
使用关联表的优势:
更好的查询性能:
可以充分利用数据库索引 支持高效的联表查询 适合大数据量场景 更好的维护性:
数据结构清晰 方便添加和删除单个账号的权限 便于统计和分析 更好的扩展性:
可以轻松添加新的权限相关字段 支持更复杂的权限规则 方便实现其他相关功能 如果确实需要使用 JSON 存储,这里有一个优化的查询方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func GetProductListWithJSON (accountID string ) ([]Product, error ) { db := GetDB() var products []Product err := db.Where("view_type = 1 OR (view_type = 2 AND JSON_SEARCH(customer_manager, 'one', ?) IS NOT NULL)" , accountID). Find(&products).Error if err != nil { return nil , fmt.Errorf("查询产品列表失败: %w" , err) } return products, nil }
性能优化建议:
无论选择哪种方案,都要建立适当的索引:
1 2 3 4 5 6 CREATE INDEX idx_product_visibility ON product_visibilities(product_id, account_id);ALTER TABLE products ADD INDEX idx_view_type (view_type);ALTER TABLE products ADD INDEX idx_customer_manager ((CAST (customer_manager AS CHAR (512 ))));
考虑添加缓存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func GetProductListWithCache (accountID string ) ([]Product, error ) { cacheKey := fmt.Sprintf("product_list:%s" , accountID) if cached, found := cache.Get(cacheKey); found { return cached.([]Product), nil } products, err := GetProductList(accountID) if err != nil { return nil , err } cache.Set(cacheKey, products, time.Minute*5 ) return products, nil }
总的来说,我建议使用关联表方案,因为它提供了更好的性能、维护性和扩展性。如果业务确实需要使用 JSON 存储,那么要注意:
控制 JSON 数据的大小 合理使用索引 考虑添加缓存机制 定期监控查询性能 问题是我的一个产品如果是指定客户可以查看,那么他指定的客户不止一个,可能会有多个用户,比如某个产品指定了10个客户。你现在觉得用什么方案比较好 对于一个产品指定多个客户的场景,我依然建议使用关联表的方案,因为这是处理多对多关系的最佳实践。以下是详细的实现方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type Product struct { ID uint `gorm:"primaryKey"` Name string ViewType int `gorm:"default:1"` }type ProductCustomer struct { ID uint `gorm:"primaryKey"` ProductID uint `gorm:"index"` AccountID string `gorm:"index"` CreatedAt time.Time }
处理Excel上传的实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 func HandleCustomerUpload (file *multipart.FileHeader, productID uint ) error { db := GetDB() src, err := file.Open() if err != nil { return errors.New("无法打开文件" ) } defer src.Close() ossPath, err := uploadToOSS(src) if err != nil { return errors.New("文件上传OSS失败" ) } f, err := excelize.OpenReader(src) if err != nil { return errors.New("无法解析Excel文件" ) } defer f.Close() rows, err := f.GetRows("Sheet1" ) if err != nil { return errors.New("读取Excel内容失败" ) } tx := db.Begin() if tx.Error != nil { return tx.Error } defer func () { if r := recover (); r != nil { tx.Rollback() } }() if err := tx.Model(&Product{}).Where("id = ?" , productID). Update("view_type" , 2 ).Error; err != nil { tx.Rollback() return err } if err := tx.Where("product_id = ?" , productID). Delete(&ProductCustomer{}).Error; err != nil { tx.Rollback() return err } batchSize := 100 var batch []ProductCustomer for i := 1 ; i < len (rows); i++ { row := rows[i] if len (row) < 2 { continue } batch = append (batch, ProductCustomer{ ProductID: productID, AccountID: row[1 ], CreatedAt: time.Now(), }) if len (batch) >= batchSize { if err := tx.CreateInBatches(batch, batchSize).Error; err != nil { tx.Rollback() return err } batch = batch[:0 ] } } if len (batch) > 0 { if err := tx.CreateInBatches(batch, batchSize).Error; err != nil { tx.Rollback() return err } } return tx.Commit().Error }
查询产品列表的实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func GetProductList (accountID string ) ([]Product, error ) { db := GetDB() var products []Product err := db.Distinct("products.*" ). Select("products.*" ). Joins("LEFT JOIN product_customers ON products.id = product_customers.product_id" ). Where( db.Where("products.view_type = ?" , 1 ).Or( db.Where("products.view_type = ? AND product_customers.account_id = ?" , 2 , accountID), ), ). Find(&products).Error if err != nil { return nil , fmt.Errorf("查询产品列表失败: %w" , err) } return products, nil }
添加必要的索引: 1 2 CREATE INDEX idx_product_customer ON product_customers(product_id, account_id);
如果需要分页查询: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 type Pagination struct { PageNum int PageSize int }func GetProductListWithPagination (accountID string , page Pagination) ([]Product, int64 , error ) { db := GetDB() var products []Product var total int64 query := db.Model(&Product{}). Distinct("products.*" ). Joins("LEFT JOIN product_customers ON products.id = product_customers.product_id" ). Where( db.Where("products.view_type = ?" , 1 ).Or( db.Where("products.view_type = ? AND product_customers.account_id = ?" , 2 , accountID), ), ) if err := query.Count(&total).Error; err != nil { return nil , 0 , err } err := query.Offset((page.PageNum - 1 ) * page.PageSize). Limit(page.PageSize). Find(&products).Error if err != nil { return nil , 0 , fmt.Errorf("查询产品列表失败: %w" , err) } return products, total, nil }
可以添加缓存优化: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func GetProductListWithCache (accountID string ) ([]Product, error ) { cacheKey := fmt.Sprintf("product_list:%s" , accountID) if cached, found := cache.Get(cacheKey); found { return cached.([]Product), nil } products, err := GetProductList(accountID) if err != nil { return nil , err } cache.Set(cacheKey, products, 5 *time.Minute) return products, nil }
使用关联表方案的优势:
性能优势:
可以充分利用数据库索引 支持高效的批量操作 查询性能稳定,不受JSON数据大小影响 维护优势:
方便修改单个客户的权限 容易统计和分析权限数据 数据结构清晰,易于调试 扩展优势:
容易添加新的权限相关字段(如过期时间、授权时间等) 支持更复杂的权限规则 便于实现权限变更历史记录 操作优势:
支持单个添加/删除客户权限 方便实现权限批量操作 便于实现权限查询和验证 建议:
定期清理无效的关联记录 根据实际情况调整批量插入的大小 考虑添加创建时间和更新时间字段 可以添加软删除功能 建议添加适当的缓存机制 这个方案可以很好地处理一个产品指定多个客户的场景,而且性能和可维护性都比较好。如果你有任何特殊需求或需要更详细的说明,请告诉我。
产品-客户关联表的字段AccountID怎么存储多个值,也就是多个用户? 产品-客户关联表不需要在 AccountID 字段存储多个值。关联表的设计思路是:一个产品对应多个用户,就在表中存储多条记录,每条记录对应一个用户。举个例子:
假设有一个产品ID为1的产品,需要指定给账号ID为”user001”、”user002”、”user003”这三个用户查看,那么在关联表中会存储3条记录:
1 2 3 4 5 6 | ID | ProductID | AccountID | | | 1 | 1 | user001 | | 2 | 1 | user002 | | 3 | 1 | user003 |
完整的代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 type Product struct { ID uint `gorm:"primaryKey"` Name string ViewType int `gorm:"default:1"` }type ProductCustomer struct { ID uint `gorm:"primaryKey"` ProductID uint `gorm:"index"` AccountID string `gorm:"index"` CreatedAt time.Time }func HandleCustomerUpload (file *multipart.FileHeader, productID uint ) error { db := GetDB() tx := db.Begin() if tx.Error != nil { return tx.Error } defer func () { if r := recover (); r != nil { tx.Rollback() } }() f, err := excelize.OpenFile(file.Filename) if err != nil { tx.Rollback() return err } defer f.Close() if err := tx.Model(&Product{}).Where("id = ?" , productID). Update("view_type" , 2 ).Error; err != nil { tx.Rollback() return err } if err := tx.Where("product_id = ?" , productID). Delete(&ProductCustomer{}).Error; err != nil { tx.Rollback() return err } rows, err := f.GetRows("Sheet1" ) if err != nil { tx.Rollback() return err } var customers []ProductCustomer for i, row := range rows { if i == 0 { continue } if len (row) < 2 { continue } customers = append (customers, ProductCustomer{ ProductID: productID, AccountID: row[1 ], CreatedAt: time.Now(), }) } if len (customers) > 0 { if err := tx.CreateInBatches(customers, 100 ).Error; err != nil { tx.Rollback() return err } } return tx.Commit().Error }func GetProductList (accountID string ) ([]Product, error ) { db := GetDB() var products []Product err := db.Distinct("products.*" ). Joins("LEFT JOIN product_customers ON products.id = product_customers.product_id" ). Where("products.view_type = ? OR (products.view_type = ? AND product_customers.account_id = ?)" , 1 , 2 , accountID). Find(&products).Error if err != nil { return nil , err } return products, nil }func GetProductCustomers (productID uint ) ([]string , error ) { db := GetDB() var customers []ProductCustomer var accountIDs []string err := db.Where("product_id = ?" , productID). Select("account_id" ). Find(&customers).Error if err != nil { return nil , err } for _, customer := range customers { accountIDs = append (accountIDs, customer.AccountID) } return accountIDs, nil }func AddProductCustomer (productID uint , accountID string ) error { db := GetDB() var count int64 db.Model(&ProductCustomer{}). Where("product_id = ? AND account_id = ?" , productID, accountID). Count(&count) if count > 0 { return nil } customer := ProductCustomer{ ProductID: productID, AccountID: accountID, CreatedAt: time.Now(), } return db.Create(&customer).Error }func RemoveProductCustomer (productID uint , accountID string ) error { db := GetDB() return db.Where("product_id = ? AND account_id = ?" , productID, accountID). Delete(&ProductCustomer{}).Error }
这种设计的优点:
数据结构清晰:
每条记录代表一个产品-用户的关联关系 便于理解和维护 操作灵活:
可以方便地添加/删除单个用户的权限 支持批量操作 方便查询某个产品的所有可见用户 查询效率高:
扩展性好:
可以轻松添加更多字段(如权限级别、过期时间等) 支持添加权限变更历史记录 方便实现权限审计功能 使用建议:
创建合适的索引:
1 2 CREATE INDEX idx_product_customer ON product_customers(product_id, account_id);CREATE INDEX idx_account_id ON product_customers(account_id);
考虑添加软删除功能:
1 2 3 4 5 6 7 type ProductCustomer struct { ID uint `gorm:"primaryKey"` ProductID uint `gorm:"index"` AccountID string `gorm:"index"` CreatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` }
可以添加批量操作的方法:
1 2 3 4 5 6 7 func BatchAddProductCustomers (productID uint , accountIDs []string ) error { }func BatchRemoveProductCustomers (productID uint , accountIDs []string ) error { }
这种设计可以很好地满足你的需求,既保证了数据的完整性,又提供了良好的性能和可维护性。
假设有一个产品ID为1的产品,需要指定给账号ID为”user001”、”user002”、”user003”这三个用户查看,那么在关联表中会存储3条记录这样设计的查询效率怎么样呢 对于这种设计,让我分析一下查询效率和优化方案:
查询效率分析: 1 2 3 4 5 6 7 SELECT DISTINCT products.* FROM products LEFT JOIN product_customers ON products.id = product_customers.product_id WHERE products.view_type = 1 OR (products.view_type = 2 AND product_customers.account_id = 'user001' );
可能存在的性能问题:
表数据量大时,JOIN 操作可能会影响性能 需要使用 DISTINCT 去重 关联表记录数量会随着产品和用户的增加呈线性增长 让我们看看优化方案:
方案一:使用JSON字段存储(适合用户量不大的情况) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 type Product struct { ID uint `gorm:"primaryKey"` Name string ViewType int `gorm:"default:1"` CustomerIDs string `gorm:"type:json"` }func HandleCustomerUpload (file *multipart.FileHeader, productID uint ) error { var accountIDs []string for i, row := range rows { if i == 0 { continue } if len (row) < 2 { continue } accountIDs = append (accountIDs, row[1 ]) } customerIDsJSON, err := json.Marshal(accountIDs) if err != nil { return err } return db.Model(&Product{}). Where("id = ?" , productID). Updates(map [string ]interface {}{ "view_type" : 2 , "customer_ids" : string (customerIDsJSON), }).Error }func GetProductList (accountID string ) ([]Product, error ) { var products []Product err := db.Where("view_type = 1 OR (view_type = 2 AND JSON_CONTAINS(customer_ids, ?)))" , fmt.Sprintf("\"%s\"" , accountID)). Find(&products).Error return products, err }
方案二:使用位图标记(适合用户量固定且较小的情况) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 type Product struct { ID uint `gorm:"primaryKey"` Name string ViewType int `gorm:"default:1"` CustomerBits []byte `gorm:"type:blob"` }var userIDToPosition map [string ]int func SetProductCustomers (productID uint , accountIDs []string ) error { customerBits := make ([]byte , (len (userIDToPosition)+7 )/8 ) for _, accountID := range accountIDs { if pos, ok := userIDToPosition[accountID]; ok { bytePos := pos / 8 bitPos := pos % 8 customerBits[bytePos] |= 1 << bitPos } } return db.Model(&Product{}). Where("id = ?" , productID). Updates(map [string ]interface {}{ "view_type" : 2 , "customer_bits" : customerBits, }).Error }
方案三:分区表(适合大规模数据) 1 2 3 4 5 6 7 8 9 10 11 12 13 CREATE TABLE product_customers ( id BIGINT PRIMARY KEY, product_id BIGINT , account_id VARCHAR (64 ), created_at TIMESTAMP ) PARTITION BY RANGE (product_id) ( PARTITION p0 VALUES LESS THAN (1000 ), PARTITION p1 VALUES LESS THAN (2000 ), PARTITION p2 VALUES LESS THAN (3000 ), PARTITION pmax VALUES LESS THAN MAXVALUE );
方案四:使用缓存优化(推荐) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 type ProductService struct { db *gorm.DB cache *redis.Client }func (s *ProductService) GetProductList(accountID string ) ([]Product, error ) { cacheKey := fmt.Sprintf("product_list:%s" , accountID) if cached, err := s.cache.Get(ctx, cacheKey).Result(); err == nil { var products []Product if err := json.Unmarshal([]byte (cached), &products); err == nil { return products, nil } } var products []Product err := s.db.Distinct("products.*" ). Joins("LEFT JOIN product_customers ON products.id = product_customers.product_id" ). Where("products.view_type = ? OR (products.view_type = ? AND product_customers.account_id = ?)" , 1 , 2 , accountID). Find(&products).Error if err != nil { return nil , err } if productsJSON, err := json.Marshal(products); err == nil { s.cache.Set(ctx, cacheKey, productsJSON, time.Minute*5 ) } return products, nil }
性能优化建议:
索引优化:
1 2 3 4 CREATE INDEX idx_product_account ON product_customers(product_id, account_id);CREATE INDEX idx_view_type ON products(view_type);
查询优化:
1 2 3 4 5 6 7 8 9 10 11 12 13 func GetProductListWithPagination (accountID string , page, pageSize int ) ([]Product, error ) { var products []Product offset := (page - 1 ) * pageSize return db.Distinct("products.*" ). Joins("LEFT JOIN product_customers ON products.id = product_customers.product_id" ). Where("products.view_type = ? OR (products.view_type = ? AND product_customers.account_id = ?)" , 1 , 2 , accountID). Offset(offset). Limit(pageSize). Find(&products).Error }
缓存策略:
1 2 3 4 5 6 7 8 9 10 11 func cacheProductCustomers (productID uint , accountIDs []string ) error { key := fmt.Sprintf("product_customers:%d" , productID) return redis.SAdd(key, accountIDs...).Err() }func checkPermission (productID uint , accountID string ) bool { key := fmt.Sprintf("product_customers:%d" , productID) return redis.SIsMember(key, accountID).Val() }
综合建议:
如果产品和用户数量不是特别大(比如产品小于10万,用户小于100万),建议使用原始的关联表方案 + 缓存:
如果数据量特别大:
考虑分库分表 使用位图存储 或者采用其他NoSQL解决方案 针对原方案的优化:
添加适当的索引 实现合理的缓存策略 使用分页查询 定期清理无效数据 监控和优化:
选择哪种方案主要取决于:
数据规模 访问模式 实时性要求 维护成本 建议从最简单的关联表方案开始,配合缓存使用,当发现性能问题时再进行优化。
如果使用关联表方案配合缓存使用的话,那么他们之间的数据一致性怎么保证? 在使用关联表配合缓存时,确实需要考虑数据一致性的问题。让我展示一个完整的解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 type ProductService struct { db *gorm.DB redis *redis.Client }const ( ProductListCacheKeyPrefix = "product:list:" ProductPermissionCacheKeyPrefix = "product:permission:" CacheExpiration = time.Minute * 30 )func generateProductListKey (accountID string ) string { return ProductListCacheKeyPrefix + accountID }func generateProductPermissionKey (productID uint ) string { return ProductPermissionCacheKeyPrefix + strconv.FormatUint(uint64 (productID), 10 ) }func (s *ProductService) HandleCustomerUpload(ctx context.Context, file *multipart.FileHeader, productID uint ) error { tx := s.db.Begin() if tx.Error != nil { return tx.Error } defer func () { if r := recover (); r != nil { tx.Rollback() } }() customers, err := s.readAndValidateExcel(file) if err != nil { tx.Rollback() return err } if err := tx.Model(&Product{}).Where("id = ?" , productID). Update("view_type" , 2 ).Error; err != nil { tx.Rollback() return err } if err := tx.Where("product_id = ?" , productID). Delete(&ProductCustomer{}).Error; err != nil { tx.Rollback() return err } if err := tx.CreateInBatches(customers, 100 ).Error; err != nil { tx.Rollback() return err } if err := tx.Commit().Error; err != nil { return err } if err := s.invalidateCache(ctx, productID, customers); err != nil { log.Printf("Failed to invalidate cache: %v" , err) } return nil }func (s *ProductService) invalidateCache(ctx context.Context, productID uint , customers []ProductCustomer) error { pipe := s.redis.Pipeline() permissionKey := generateProductPermissionKey(productID) pipe.Del(ctx, permissionKey) for _, customer := range customers { listKey := generateProductListKey(customer.AccountID) pipe.Del(ctx, listKey) } _, err := pipe.Exec(ctx) return err }func (s *ProductService) GetProductList(ctx context.Context, accountID string ) ([]Product, error ) { products, err := s.getProductListFromCache(ctx, accountID) if err == nil { return products, nil } products, err = s.getProductListFromDB(accountID) if err != nil { return nil , err } if err := s.cacheProductList(ctx, accountID, products); err != nil { log.Printf("Failed to cache product list: %v" , err) } return products, nil }func (s *ProductService) getProductListFromCache(ctx context.Context, accountID string ) ([]Product, error ) { key := generateProductListKey(accountID) data, err := s.redis.Get(ctx, key).Bytes() if err != nil { return nil , err } var products []Product if err := json.Unmarshal(data, &products); err != nil { return nil , err } return products, nil }func (s *ProductService) getProductListFromDB(accountID string ) ([]Product, error ) { var products []Product err := s.db.Distinct("products.*" ). Joins("LEFT JOIN product_customers ON products.id = product_customers.product_id" ). Where("products.view_type = ? OR (products.view_type = ? AND product_customers.account_id = ?)" , 1 , 2 , accountID). Find(&products).Error return products, err }func (s *ProductService) cacheProductList(ctx context.Context, accountID string , products []Product) error { data, err := json.Marshal(products) if err != nil { return err } key := generateProductListKey(accountID) return s.redis.Set(ctx, key, data, CacheExpiration).Err() }func (s *ProductService) CheckPermission(ctx context.Context, productID uint , accountID string ) (bool , error ) { hasPermission, err := s.checkPermissionFromCache(ctx, productID, accountID) if err == nil { return hasPermission, nil } hasPermission, err = s.checkPermissionFromDB(productID, accountID) if err != nil { return false , err } if err := s.cachePermission(ctx, productID, accountID, hasPermission); err != nil { log.Printf("Failed to cache permission: %v" , err) } return hasPermission, nil }func (s *ProductService) withRetry(operation func () error ) error { maxRetries := 3 for i := 0 ; i < maxRetries; i++ { err := operation() if err == nil { return nil } if i == maxRetries-1 { return err } time.Sleep(time.Millisecond * 100 * time.Duration(i+1 )) } return nil }
数据一致性保证的关键点:
缓存更新策略:使用 Cache-Aside 模式
1 2 3 4 5 6 7 8 1. 更新数据库2. 删除相关缓存1. 查询缓存2. 缓存未命中时查询数据库3. 更新缓存
事务处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 tx := db.Begin()defer func () { if r := recover (); r != nil { tx.Rollback() } }()if err := tx.Commit().Error; err != nil { tx.Rollback() return err }
缓存失效策略:
1 2 3 4 5 6 7 const CacheExpiration = time.Minute * 30 1. 数据更新时2. 数据删除时3. 发生错误时
并发控制:
1 2 3 4 5 6 7 8 9 10 func (s *ProductService) UpdateWithLock(ctx context.Context, productID uint , update func () error ) error { lock := s.redisLock.NewLock(fmt.Sprintf("lock:product:%d" , productID)) if err := lock.Lock(); err != nil { return err } defer lock.Unlock() return update() }
错误处理:
1 2 3 4 5 6 7 8 9 10 11 func (s *ProductService) withRetry(operation func () error ) error { maxRetries := 3 for i := 0 ; i < maxRetries; i++ { if err := operation(); err == nil { return nil } time.Sleep(time.Millisecond * 100 * time.Duration(i+1 )) } return errors.New("operation failed after retries" ) }
监控和告警:
1 2 3 4 5 6 7 8 9 10 11 12 13 type Metrics struct { CacheHits prometheus.Counter CacheMisses prometheus.Counter DatabaseErrors prometheus.Counter CacheErrors prometheus.Counter InconsistencyDetected prometheus.Counter }func (s *ProductService) recordMetrics(metric prometheus.Counter) { metric.Inc() }
最佳实践建议:
缓存设计:
设置合理的过期时间 使用前缀区分不同类型的缓存 实现缓存预热机制 错误处理:
缓存操作错误不应影响主流程 实现重试机制 记录详细的错误日志 监控和维护:
性能优化:
使用管道批量处理缓存操作 合理设置缓存大小 实现缓存预热机制 其他建议:
定期验证数据一致性 实现缓存降级机制 保持代码的可测试性 这样的设计可以在保证数据一致性的同时,提供良好的性能和可维护性。当然,具体的实现还需要根据实际业务需求进行调整。
产品-客户关联表建表语句麻烦你写一下,还有这个需求牵涉到的其他的字段添加的sql语句也麻烦你写一下 好的,我来编写相关的SQL语句。这里包括产品表的修改和产品-客户关联表的创建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 ALTER TABLE productsADD COLUMN view_type tinyint(1 ) NOT NULL DEFAULT 1 COMMENT '查看权限类型:1全量可见 2指定客户可见' ;CREATE TABLE `product_customers` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID' , `product_id` bigint unsigned NOT NULL COMMENT '产品ID' , `account_id` varchar (64 ) NOT NULL COMMENT '客户账号ID' , `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' , `created_by` varchar (64 ) DEFAULT NULL COMMENT '创建人' , `updated_by` varchar (64 ) DEFAULT NULL COMMENT '更新人' , `is_deleted` tinyint(1 ) NOT NULL DEFAULT 0 COMMENT '是否删除:0未删除 1已删除' , PRIMARY KEY (`id`), KEY `idx_product_account` (`product_id`, `account_id`), KEY `idx_account` (`account_id`), KEY `idx_created_at` (`created_at`), CONSTRAINT `fk_product_customers_product` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT= '产品-客户关联表' ;CREATE TABLE `product_customer_uploads` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID' , `product_id` bigint unsigned NOT NULL COMMENT '产品ID' , `file_name` varchar (255 ) NOT NULL COMMENT '文件名称' , `file_path` varchar (255 ) NOT NULL COMMENT 'OSS文件路径' , `file_size` bigint NOT NULL COMMENT '文件大小(字节)' , `status` tinyint(1 ) NOT NULL DEFAULT 1 COMMENT '状态:1成功 2失败' , `error_msg` text DEFAULT NULL COMMENT '错误信息' , `affected_rows` int DEFAULT 0 COMMENT '影响的记录数' , `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' , `created_by` varchar (64 ) DEFAULT NULL COMMENT '创建人' , `updated_by` varchar (64 ) DEFAULT NULL COMMENT '更新人' , `is_deleted` tinyint(1 ) NOT NULL DEFAULT 0 COMMENT '是否删除:0未删除 1已删除' , PRIMARY KEY (`id`), KEY `idx_product_id` (`product_id`), KEY `idx_created_at` (`created_at`), CONSTRAINT `fk_uploads_product` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT= '产品客户Excel上传记录表' ;CREATE TABLE `product_customer_logs` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID' , `product_id` bigint unsigned NOT NULL COMMENT '产品ID' , `account_id` varchar (64 ) NOT NULL COMMENT '客户账号ID' , `operation_type` tinyint(1 ) NOT NULL COMMENT '操作类型:1添加权限 2删除权限' , `operation_desc` varchar (255 ) DEFAULT NULL COMMENT '操作描述' , `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `created_by` varchar (64 ) DEFAULT NULL COMMENT '操作人' , PRIMARY KEY (`id`), KEY `idx_product_account` (`product_id`, `account_id`), KEY `idx_created_at` (`created_at`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT= '产品客户权限变更日志表' ;ALTER TABLE productsADD INDEX idx_products_created_at (created_at);ALTER TABLE productsADD INDEX idx_view_type (view_type);ALTER TABLE products MODIFY COLUMN view_type tinyint(1 ) NOT NULL DEFAULT 1 COMMENT '查看权限类型:1全量可见 2指定客户可见' ;
相关的数据库设计说明:
product_customers
表:
使用软删除(is_deleted字段) 添加创建和更新时间 添加操作人字段 使用复合索引优化查询 添加外键约束保证数据完整性 product_customer_uploads
表:
记录文件上传历史 保存文件在OSS的路径 记录处理结果和错误信息 记录影响的记录数 product_customer_logs
表:
记录所有权限变更操作 便于审计和问题追踪 保存详细的操作信息 其他可能需要的SQL操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 SELECT DISTINCT p.* FROM products pLEFT JOIN product_customers pc ON p.id = pc.product_idWHERE p.view_type = 1 OR (p.view_type = 2 AND pc.account_id = 'user001' AND pc.is_deleted = 0 );SELECT pc.account_idFROM product_customers pcWHERE pc.product_id = 1 AND pc.is_deleted = 0 ;SELECT * FROM product_customer_uploadsWHERE product_id = 1 ORDER BY created_at DESC ;SELECT * FROM product_customer_logsWHERE product_id = 1 ORDER BY created_at DESC ;
索引使用建议:
主要查询模式:
1 2 3 4 5 6 7 SELECT ... FROM products WHERE view_type = 1 OR (view_type = 2 AND EXISTS ( SELECT 1 FROM product_customers WHERE product_id = products.id AND account_id = ? ));
批量操作时的索引注意事项:
1 2 3 4 SET UNIQUE_CHECKS= 0 ;SET UNIQUE_CHECKS= 1 ;
定期维护建议:
1 2 3 4 5 ANALYZE TABLE product_customers; OPTIMIZE TABLE product_customers;
这些SQL语句提供了完整的数据库结构支持,包括:
基本的权限控制 文件上传记录 操作日志 必要的索引优化 数据完整性保证 你可以根据实际需求调整字段类型、长度和索引设计。如果还需要其他的SQL语句或者有特殊的查询需求,请告诉我。
还需要一个SQL将全表的历史产品查出来,将他们的数据viewType设置为全量客户 好的,我来写这个SQL语句。为了安全起见,我会提供两种方案:一种是直接更新,另一种是分批更新。
直接更新所有历史产品(适用于数据量较小的情况): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 UPDATE products SET view_type = 1 , updated_at = NOW(), updated_by = 'SYSTEM' WHERE 1 = 1 ; DELETE FROM product_customers WHERE product_id IN ( SELECT id FROM products WHERE view_type = 1 );INSERT INTO product_customer_logs (product_id, account_id, operation_type, operation_desc, created_by)SELECT id, 'ALL' , 2 , '批量更新产品为全量可见' , 'SYSTEM' FROM products;
分批更新方案(推荐,适用于大数据量): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 DELIMITER / / CREATE PROCEDURE batch_update_products_view_type()BEGIN DECLARE done INT DEFAULT FALSE ; DECLARE batch_size INT DEFAULT 1000 ; DECLARE last_id BIGINT DEFAULT 0 ; CREATE TEMPORARY TABLE IF NOT EXISTS tmp_process_log ( id BIGINT PRIMARY KEY, processed BOOLEAN DEFAULT FALSE ); INSERT INTO tmp_process_log (id) SELECT id FROM products; REPEAT START TRANSACTION; UPDATE products SET view_type = 1 , updated_at = NOW(), updated_by = 'SYSTEM' WHERE id IN ( SELECT id FROM tmp_process_log WHERE processed = FALSE AND id > last_id ORDER BY id LIMIT batch_size ); UPDATE tmp_process_log SET processed = TRUE WHERE id IN ( SELECT id FROM products WHERE id > last_id ORDER BY id LIMIT batch_size ); SELECT MAX (id) INTO last_id FROM tmp_process_log WHERE processed = TRUE ; INSERT INTO product_customer_logs (product_id, account_id, operation_type, operation_desc, created_by) SELECT id, 'ALL' , 2 , '批量更新产品为全量可见' , 'SYSTEM' FROM products WHERE id > (last_id - batch_size) AND id <= last_id; COMMIT ; DO SLEEP(0.1 ); UNTIL last_id IS NULL OR (SELECT COUNT (* ) FROM tmp_process_log WHERE processed = FALSE ) = 0 END REPEAT; DELETE FROM product_customers WHERE product_id IN ( SELECT id FROM products WHERE view_type = 1 ); DROP TEMPORARY TABLE IF EXISTS tmp_process_log;END / / DELIMITER ;CALL batch_update_products_view_type();DROP PROCEDURE IF EXISTS batch_update_products_view_type;
更新前后的验证SQL: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 SELECT view_type, COUNT (* ) as countFROM productsGROUP BY view_type;SELECT COUNT (* ) as relation_countFROM product_customers;SELECT view_type, COUNT (* ) as countFROM productsGROUP BY view_type;SELECT COUNT (* ) as remaining_relationsFROM product_customers;SELECT operation_type, COUNT (* ) as log_countFROM product_customer_logsWHERE operation_desc = '批量更新产品为全量可见' GROUP BY operation_type;
回滚脚本(以防需要回滚): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 CREATE TABLE products_backup AS SELECT * FROM products;CREATE TABLE product_customers_backup AS SELECT * FROM product_customers;UPDATE products pINNER JOIN products_backup pb ON p.id = pb.idSET p.view_type = pb.view_type, p.updated_at = pb.updated_at, p.updated_by = pb.updated_by;INSERT INTO product_customersSELECT * FROM product_customers_backup;INSERT INTO product_customer_logs (product_id, account_id, operation_type, operation_desc, created_by)SELECT id, 'ALL' , 1 , '回滚批量更新操作' , 'SYSTEM' FROM products;
使用建议:
在执行更新前:
备份相关表的数据 在测试环境验证SQL 选择业务低峰期执行 预估执行时间 执行更新时:
执行更新后:
其他注意事项:
建议使用分批更新方案,这样可以:
避免长事务 控制系统负载 便于监控进度 出问题时容易处理 如果还需要其他SQL或者有特殊需求,请告诉我。
产品-客户关联表的这两段代码有用吗?KEY idx_product_account
(product_id
, account_id
), – 复合索引,用于权限查询 KEY idx_account
(account_id
), – 单独的账号索引,用于查询用户可见产品 这两个索引的作用性需要根据具体的查询场景来分析。让我们看看常见的查询场景:
查询某个用户可见的产品列表:1 2 3 4 5 SELECT DISTINCT products.* FROM products LEFT JOIN product_customers ON products.id = product_customers.product_id WHERE products.view_type = 1 OR (products.view_type = 2 AND product_customers.account_id = 'user001' );
这个查询中: idx_account
(account_id) 索引会被用到,因为我们需要根据 account_id 来查找记录查询某个产品的可见用户列表:1 2 3 SELECT account_id FROM product_customers WHERE product_id = 1 ;
这个查询中: idx_product_account
(product_id, account_id) 索引会被用到,因为是先根据 product_id 查询所以这两个索引的选择应该是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 CREATE TABLE `product_customers` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID' , `product_id` bigint unsigned NOT NULL COMMENT '产品ID' , `account_id` varchar (64 ) NOT NULL COMMENT '客户账号ID' , `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' , `created_by` varchar (64 ) DEFAULT NULL COMMENT '创建人' , `updated_by` varchar (64 ) DEFAULT NULL COMMENT '更新人' , `is_deleted` tinyint(1 ) NOT NULL DEFAULT 0 COMMENT '是否删除:0未删除 1已删除' , PRIMARY KEY (`id`), KEY `idx_product_account` (`product_id`, `account_id`), KEY `idx_created_at` (`created_at`), CONSTRAINT `fk_product_customers_product` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT= '产品-客户关联表' ;ALTER TABLE productsADD INDEX idx_view_type (view_type);
更好的索引设计:
如果主要查询场景是查询用户可见的产品列表:
1 2 3 4 5 CREATE INDEX idx_account_product ON product_customers(account_id, product_id);CREATE INDEX idx_view_type ON products(view_type);
如果经常需要同时查询产品信息:
1 CREATE INDEX idx_product_account_status ON product_customers(product_id, account_id, is_deleted);
要判断索引是否有效,可以:
使用 EXPLAIN 分析查询:
1 2 3 4 5 EXPLAIN SELECT DISTINCT products.* FROM products LEFT JOIN product_customers ON products.id = product_customers.product_id WHERE products.view_type = 1 OR (products.view_type = 2 AND product_customers.account_id = 'user001' );
查看索引使用情况:
1 SHOW INDEX FROM product_customers;
查看实际的查询性能:
1 2 3 SET long_query_time = 1 ;SET slow_query_log = 1 ;
优化建议:
对于主查询,可以这样优化:
1 2 3 4 5 6 7 8 9 10 11 12 WITH user_products AS ( SELECT DISTINCT product_id FROM product_customers WHERE account_id = 'user001' AND is_deleted = 0 )SELECT p.* FROM products pWHERE p.view_type = 1 OR (p.view_type = 2 AND p.id IN (SELECT product_id FROM user_products));
添加合适的索引:
1 2 3 4 5 6 7 ALTER TABLE productsADD INDEX idx_view_type_id (view_type, id);CREATE INDEX idx_account_product_status ON product_customers(account_id, product_id, is_deleted);
如果需要分页,可以这样优化:
1 2 3 4 5 6 7 8 9 10 11 12 WITH product_ids AS ( SELECT p.id FROM products p LEFT JOIN product_customers pc ON p.id = pc.product_id AND pc.account_id = 'user001' WHERE p.view_type = 1 OR (p.view_type = 2 AND pc.id IS NOT NULL ) LIMIT 10 OFFSET 0 )SELECT p.* FROM products pJOIN product_ids pi ON p.id = pi.id;
所以总结一下:
idx_product_account
是有用的,它支持按产品ID查询用户的场景idx_account
单独使用的场景较少,可以考虑删除应该根据实际的查询场景来设计索引 必要时可以通过修改查询语句来更好地利用索引 推荐的最终索引设计:
1 2 3 4 5 6 7 CREATE TABLE `product_customers` ( ... 其他字段 ..., PRIMARY KEY (`id`), KEY `idx_product_account_status` (`product_id`, `account_id`, `is_deleted`), KEY `idx_created_at` (`created_at`), CONSTRAINT `fk_product_customers_product` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE );
这样的设计可以满足大多数查询场景,并且不会维护过多的索引。
在第一个例子中,你说这个查询中:idx_product_account (product_id, account_id) 索引会被用到,因为是先根据 product_id 查询的意思是我们设置了这个为索引之后,mysql在查询的时候会自己去使用这个索引吗? 是的,你理解得对。在MySQL中,当你创建了索引后,MySQL查询优化器会自动判断是否使用索引来优化查询。
让我详细解释一下这个过程:
当你执行一个查询时,MySQL的查询优化器会:
分析你的WHERE条件 检查表中可用的索引 评估使用不同索引的成本(比如扫描的行数) 选择一个最优的执行计划 以这个索引为例:
1 CREATE INDEX idx_product_account ON table_name(product_id, account_id);
当你执行类似这样的查询:
1 2 3 SELECT * FROM table_name WHERE product_id = 1 AND account_id = 100 ;SELECT * FROM table_name WHERE product_id = 1 ;
MySQL会发现:
这个查询条件匹配索引的列 使用索引可以减少需要扫描的行数 所以会选择使用这个索引 你可以通过 EXPLAIN 命令来查看MySQL是否使用了索引:1 EXPLAIN SELECT * FROM table_name WHERE product_id = 1 ;
不过需要注意的是:
MySQL不一定总是使用索引,如果优化器认为全表扫描更快,它会选择不使用索引 索引的使用还要考虑前缀原则,比如在复合索引中,只有遵循最左前缀原则才会使用到索引 某些操作符(如 LIKE ‘%abc’)可能会导致索引失效 所以简单来说,是的,MySQL会自动判断并使用合适的索引,但这个过程是由查询优化器根据具体情况来决定的。
我有一个接口现在需要实现以下的需求,我有一个产品列表,需要通过excel文件实现指定客户可以查看还是所有客户可以查看,现在默认是所有客户都可以查看。允许用户上传文件,在用户上传完文件之后就对excel的文件的格式进行校验,文件第一列手机号码,第二列账户id。如果有问题需要报错提示文件格式错误供前端使用,如果没问题就把文件数据拿出来存到数据库里面去,供文件列表接口使用判断某个用户能看到哪些产品列表;还需要把文件存到oss里面去,方便客户下载文件重新上传。项目语言为go语言。我希望使用关联表方案 我来帮你实现这个功能。我们可以分步骤来实现:
首先创建关联表的数据库结构: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 CREATE TABLE products ( id BIGINT PRIMARY KEY, name VARCHAR (255 ), );CREATE TABLE product_visibility ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT , phone VARCHAR (20 ), account_id VARCHAR (50 ), file_url VARCHAR (255 ), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP , FOREIGN KEY (product_id) REFERENCES products(id) );
实现处理上传文件的接口: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 package handlerimport ( "github.com/gin-gonic/gin" "github.com/xuri/excelize/v2" "github.com/aliyun/aliyun-oss-go-sdk/oss" )type ProductVisibilityHandler struct { db *gorm.DB oss *oss.Client }type UploadResponse struct { Code int `json:"code"` Message string `json:"message"` Data any `json:"data"` }func (h *ProductVisibilityHandler) UploadVisibilityFile(c *gin.Context) { file, header, err := c.Request.FormFile("file" ) if err != nil { c.JSON(400 , UploadResponse{ Code: 400 , Message: "文件上传失败" , }) return } defer file.Close() if !strings.HasSuffix(header.Filename, ".xlsx" ) { c.JSON(400 , UploadResponse{ Code: 400 , Message: "文件格式错误,请上传xlsx格式文件" , }) return } xlsx, err := excelize.OpenReader(file) if err != nil { c.JSON(400 , UploadResponse{ Code: 400 , Message: "文件读取失败" , }) return } rows, err := xlsx.GetRows("Sheet1" ) if err != nil { c.JSON(400 , UploadResponse{ Code: 400 , Message: "文件格式错误" , }) return } if len (rows) < 2 { c.JSON(400 , UploadResponse{ Code: 400 , Message: "文件内容为空" , }) return } ossPath := "product-visibility/" + time.Now().Format("20060102" ) + "/" + header.Filename err = h.uploadToOSS(file, ossPath) if err != nil { c.JSON(500 , UploadResponse{ Code: 500 , Message: "文件保存失败" , }) return } visibilityRecords := make ([]ProductVisibility, 0 ) for i := 1 ; i < len (rows); i++ { row := rows[i] if len (row) < 2 { continue } record := ProductVisibility{ Phone: row[0 ], AccountID: row[1 ], FileURL: ossPath, } visibilityRecords = append (visibilityRecords, record) } if err := h.db.Create(&visibilityRecords).Error; err != nil { c.JSON(500 , UploadResponse{ Code: 500 , Message: "数据保存失败" , }) return } c.JSON(200 , UploadResponse{ Code: 200 , Message: "上传成功" , Data: gin.H{ "file_url" : ossPath, }, }) }func (h *ProductVisibilityHandler) uploadToOSS(file multipart.File, ossPath string ) error { bucket, err := h.oss.Bucket("your-bucket-name" ) if err != nil { return err } return bucket.PutObject(ossPath, file) }func (h *ProductVisibilityHandler) CheckProductVisibility(productID int64 , phone, accountID string ) bool { var count int64 h.db.Model(&ProductVisibility{}). Where("product_id = ? AND (phone = ? OR account_id = ?)" , productID, phone, accountID). Count(&count) return count > 0 }func (h *ProductVisibilityHandler) GetProductList(c *gin.Context) { phone := c.Query("phone" ) accountID := c.Query("account_id" ) var products []Product query := h.db.Model(&Product{}) if phone != "" || accountID != "" { query = query.Joins("LEFT JOIN product_visibility pv ON products.id = pv.product_id" ). Where("pv.phone = ? OR pv.account_id = ? OR pv.id IS NULL" , phone, accountID) } if err := query.Find(&products).Error; err != nil { c.JSON(500 , gin.H{"error" : "获取产品列表失败" }) return } c.JSON(200 , products) }
定义相关的模型结构: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 type ProductVisibility struct { ID int64 `gorm:"primaryKey"` ProductID int64 `gorm:"index"` Phone string `gorm:"size:20"` AccountID string `gorm:"size:50"` FileURL string `gorm:"size:255"` CreatedAt time.Time `gorm:"autoCreateTime"` }type Product struct { ID int64 `gorm:"primaryKey"` Name string `gorm:"size:255"` }
路由配置: 1 2 3 4 func SetupRoutes (r *gin.Engine, h *ProductVisibilityHandler) { r.POST("/api/product/visibility/upload" , h.UploadVisibilityFile) r.GET("/api/products" , h.GetProductList) }
这个实现主要包含以下功能:
文件上传和格式验证 将文件保存到OSS 解析Excel文件内容 将数据保存到关联表 提供产品列表接口,支持根据用户权限过滤 使用说明:
前端上传Excel文件时,需要确保文件格式正确(第一列为手机号,第二列为账户ID) 文件会被保存到OSS中,方便后续下载和重新上传 产品列表接口会根据用户的手机号或账户ID来过滤可见的产品 需要注意的点:
需要proper的错误处理 可以添加更多的数据验证 可以添加事务处理 可以添加并发处理大文件的能力 可以添加日志记录 你可能需要根据实际情况调整以下内容:
数据库表结构 OSS配置 错误处理方式 文件格式验证规则 接口返回格式 希望这个实现对你有帮助!如果需要更详细的说明或有任何问题,请随时询问。
我现在想查询某个用户可以看见的所有的产品怎么查询 我来帮你实现查询用户可见产品的功能。这里提供两种查询方案:
直接使用SQL查询的方式: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 func (h *ProductVisibilityHandler) GetVisibleProducts(c *gin.Context) { phone := c.Query("phone" ) accountID := c.Query("account_id" ) if phone == "" && accountID == "" { c.JSON(400 , gin.H{ "code" : 400 , "message" : "请提供手机号或账户ID" , }) return } var products []Product query := h.db.Table("products p" ). Select("DISTINCT p.*" ). Joins("LEFT JOIN product_visibility pv ON p.id = pv.product_id" ). Where("pv.phone = ? OR pv.account_id = ? OR pv.id IS NULL" , phone, accountID) if err := query.Find(&products).Error; err != nil { c.JSON(500 , gin.H{ "code" : 500 , "message" : "查询失败" , "error" : err.Error(), }) return } c.JSON(200 , gin.H{ "code" : 200 , "message" : "success" , "data" : products, }) }
使用GORM的方式更结构化的查询: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 type ProductService struct { db *gorm.DB }type ProductResponse struct { ID int64 `json:"id"` Name string `json:"name"` Description string `json:"description"` Price float64 `json:"price"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` }func NewProductService (db *gorm.DB) *ProductService { return &ProductService{db: db} }func (s *ProductService) GetVisibleProducts(phone, accountID string ) ([]ProductResponse, error ) { var products []ProductResponse query := s.db.Model(&Product{}). Select("DISTINCT products.*" ). Joins("LEFT JOIN product_visibility pv ON products.id = pv.product_id" ). Where("pv.phone = ? OR pv.account_id = ? OR pv.id IS NULL" , phone, accountID) query = query.Order("products.created_at DESC" ) err := query.Find(&products).Error if err != nil { return nil , fmt.Errorf("查询产品失败: %w" , err) } return products, nil }func (h *ProductVisibilityHandler) GetVisibleProducts(c *gin.Context) { phone := c.Query("phone" ) accountID := c.Query("account_id" ) if phone == "" && accountID == "" { c.JSON(400 , gin.H{ "code" : 400 , "message" : "请提供手机号或账户ID" , }) return } productService := NewProductService(h.db) products, err := productService.GetVisibleProducts(phone, accountID) if err != nil { c.JSON(500 , gin.H{ "code" : 500 , "message" : "查询失败" , "error" : err.Error(), }) return } c.JSON(200 , gin.H{ "code" : 200 , "message" : "success" , "data" : gin.H{ "total" : len (products), "list" : products, }, }) }
如果需要分页查询: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 type PageQuery struct { Page int `form:"page" binding:"required,min=1"` PageSize int `form:"page_size" binding:"required,min=1,max=100"` Phone string `form:"phone"` AccountID string `form:"account_id"` }func (s *ProductService) GetVisibleProductsWithPagination(query PageQuery) ([]ProductResponse, int64 , error ) { var products []ProductResponse var total int64 db := s.db.Model(&Product{}). Select("DISTINCT products.*" ). Joins("LEFT JOIN product_visibility pv ON products.id = pv.product_id" ). Where("pv.phone = ? OR pv.account_id = ? OR pv.id IS NULL" , query.Phone, query.AccountID) err := db.Count(&total).Error if err != nil { return nil , 0 , fmt.Errorf("计算总数失败: %w" , err) } offset := (query.Page - 1 ) * query.PageSize err = db.Offset(offset). Limit(query.PageSize). Order("products.created_at DESC" ). Find(&products).Error if err != nil { return nil , 0 , fmt.Errorf("查询产品失败: %w" , err) } return products, total, nil }func (h *ProductVisibilityHandler) GetVisibleProductsWithPagination(c *gin.Context) { var query PageQuery if err := c.ShouldBindQuery(&query); err != nil { c.JSON(400 , gin.H{ "code" : 400 , "message" : "参数错误" , "error" : err.Error(), }) return } productService := NewProductService(h.db) products, total, err := productService.GetVisibleProductsWithPagination(query) if err != nil { c.JSON(500 , gin.H{ "code" : 500 , "message" : "查询失败" , "error" : err.Error(), }) return } c.JSON(200 , gin.H{ "code" : 200 , "message" : "success" , "data" : gin.H{ "total" : total, "page" : query.Page, "page_size" : query.PageSize, "list" : products, }, }) }
这个实现的主要特点:
通过LEFT JOIN和WHERE条件来查询用户可见的产品 使用DISTINCT避免重复数据 提供了分页查询的支持 支持通过手机号或账户ID查询 结构化的错误处理 清晰的代码组织结构 使用说明:
基础查询:
1 GET /api/products/visible?phone=13800138000
分页查询:
1 GET /api/products/visible?phone=13800138000 &page=1 &page_size=10
你可以根据实际需求进行以下调整:
添加更多的查询条件(如产品状态、类别等) 添加排序选项 优化查询性能(添加适当的索引) 添加缓存机制 添加更多的数据验证