Implement Product Visibility Control via Excel File Upload

我有一个接口现在需要实现以下的需求,我有一个产品列表,需要通过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 main

import (
"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"
)

// 定义数据库和OSS客户端
var (
db *sql.DB
ossBucket *oss.Bucket
)

// 初始化数据库和OSS连接
func init() {
// 初始化数据库连接
var err error
db, err = sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
if err != nil {
panic(err)
}

// 初始化OSS客户端
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
}

// 上传文件到OSS
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)
}

这段代码提供了一个基本的框架来实现您的需求。以下是主要功能的解释:

  1. 文件上传:使用 Gin 框架处理文件上传。

  2. 文件格式验证:validateFileFormat 函数检查上传的 Excel 文件格式是否正确。

  3. 数据存储:saveDataToDatabase 函数将 Excel 文件中的数据保存到数据库中。

  4. OSS 上传:uploadFileToOSS 函数将文件上传到阿里云 OSS。

要完成这个实现,您还需要:

  1. 安装必要的依赖:

    1
    2
    3
    go get github.com/gin-gonic/gin
    go get github.com/aliyun/aliyun-oss-go-sdk/oss
    go get github.com/xuri/excelize/v2
  2. 配置数据库连接和 OSS 客户端信息。

  3. 创建相应的数据库表结构。

  4. 实现产品列表接口,根据用户的手机号或账户 ID 来过滤可见的产品。

  5. 添加错误处理和日志记录。

  6. 实现文件下载功能,允许用户下载之前上传的文件。

这个实现提供了一个良好的起点。您可能需要根据具体的业务需求进行调整和扩展。如果您需要更详细的解释或有任何问题,请随时告诉我。

这句代码意思是文件通过请求头传过来吗?

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 请求上传的文件,但不是通过请求头传递的。这里的具体含义是:

  1. c.Request.FormFile("file") 是在尝试从 HTTP 请求的表单数据中获取一个名为 “file” 的文件。

  2. 文件通常是通过 HTTP POST 请求的请求体(body)中的 multipart/form-data 格式传输的,而不是通过请求头。

  3. FormFile 函数返回三个值:

    • file:一个 multipart.File 接口,可以用来读取文件内容。
    • header:一个 *multipart.FileHeader 结构,包含文件的元信息,如文件名等。
    • err:如果出现错误,这里会返回错误信息。
  4. 如果 err 不为 nil,表示获取文件失败,可能是因为没有文件被上传,或者文件名不是 “file”,或者其他原因。这时会返回一个错误响应。

  5. 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 main

import (
"database/sql"
"fmt"
"net/http"

"github.com/gin-gonic/gin"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/xuri/excelize/v2"
)

// 定义数据库和OSS客户端
var (
db *sql.DB
ossBucket *oss.Bucket
)

// 初始化数据库和OSS连接
func init() {
// ... (保持原有的初始化代码不变)
}

func main() {
r := gin.Default()
r.POST("/upload", handleFileUpload)
r.Run(":8080")
}

// UploadRequest 定义上传请求的结构
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
}

// 上传文件到OSS
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": "文件上传成功"})
}

// validateFileFormat, saveDataToDatabase, uploadFileToOSS 函数保持不变
// ...

这个更新后的版本有以下几个关键改变:

  1. 定义了 UploadRequest 结构体,明确了接口期望的请求参数:

    1
    2
    3
    type UploadRequest struct {
    File *multipart.FileHeader `form:"file" binding:"required"`
    }

    这里 form:"file" 指定了前端应该使用的字段名,binding:"required" 表示这是一个必须的字段。

  2. 使用 c.ShouldBind(&req) 来绑定请求参数,这样可以自动进行参数验证。

  3. 使用 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); // fileObject 是一个 File 对象

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) 主要用于绑定和验证请求参数,但它不能检查文件的具体内容或格式。让我解释一下它的功能和局限性:

  1. c.ShouldBind(&req) 的功能:

    • 它会检查请求中是否存在名为 “file” 的文件字段。
    • 它会验证文件字段是否为空(因为我们使用了 binding:"required")。
    • 它会将文件信息绑定到 UploadRequest 结构体的 File 字段上。
  2. 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 main

import (
"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"
)

// ... (保留之前的 import 和全局变量定义)

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)

// ... (保留后续的数据库保存和 OSS 上传逻辑)
}

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)
}
// 这里可以添加更多的验证,比如检查手机号格式、账户ID格式等
}

return nil
}

// ... (保留其他函数不变)

这个更新版本增加了以下几个关键点:

  1. isValidFileExtension 函数检查文件扩展名是否为 .xlsx 或 .xls。

  2. validateFileContent 函数执行更详细的文件内容验证:

    • 检查是否能成功打开为 Excel 文件。
    • 验证文件是否至少包含标题行和一行数据。
    • 检查标题行是否符合预期格式(”手机号码” 和 “账户ID”)。
    • 检查每行数据是否包含两列。
  3. 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 main

import (
"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"
)

// ... (保留之前的 import 和全局变量定义)

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
}

// 将 JSON 数据保存到数据库
if err := saveVisibilityDataToDB(req.ProductID, visibilityData); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存数据失败"})
return
}

// ... (保留 OSS 上传逻辑)

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
}

// ... (保留其他函数不变)

这个更新版本添加了以下关键功能:

  1. 新增了 VisibilityDataUserInfo 结构体,用于表示可见性数据的 JSON 结构。

  2. 修改了 UploadRequest 结构体,添加了 ProductID 字段,用于指定要更新的产品。

  3. 新增了 parseExcelToJSON 函数,用于将 Excel 文件内容解析为 VisibilityData 结构。

  4. 新增了 saveVisibilityDataToDB 函数,用于将解析后的 JSON 数据保存到数据库的产品表中。

  5. 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
}

// 如果 AllowedUsers 为空,则所有用户可见
if len(visibilityData.AllowedUsers) == 0 {
return true, nil
}

// 检查用户是否在允许列表中
for _, user := range visibilityData.AllowedUsers {
if user.AccountID == userAccountID {
return true, nil
}
}

return false, nil
}

这种方法的优点是:

  1. 查询效率高,不需要关联其他表。
  2. 可以轻松地在应用层面处理可见性逻辑。
  3. 灵活性强,可以轻松添加更多的可见性规则。

缺点是:

  1. 数据冗余,同样的用户信息可能在多个产品中重复。
  2. 更新单个用户的可见性需要更新所有相关产品的 JSON 数据。

根据您的具体需求和数据规模,这种方法可能是一个很好的平衡。如果您预计频繁地更新单个用户的可见性,或者数据量非常大,可能需要考虑其他的数据模型。您对这个实现有什么看法或其他需求吗?

面对这个需求还有没有其他更好的设计方案

您提出了一个很好的问题。确实,对于这种需求,可能还有其他更优的设计方案。让我们探讨一下几种可能的替代方案,并分析它们的优缺点:

  1. 关联表方案
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 p
LEFT JOIN product_visibility pv ON p.id = pv.product_id
LEFT JOIN users u ON pv.user_id = u.id
WHERE u.account_id = ? OR pv.id IS NULL;

-- 查询某个产品对特定用户是否可见
SELECT COUNT(*) > 0 AS is_visible
FROM products p
LEFT JOIN product_visibility pv ON p.id = pv.product_id
LEFT JOIN users u ON pv.user_id = u.id
WHERE p.id = ? AND (u.account_id = ? OR pv.id IS NULL);

优点:

  • 数据一致性强,避免了数据冗余
  • 易于管理和更新单个用户或产品的可见性
  • 支持复杂的查询和分析

缺点:

  • 查询可能需要多表连接,在数据量大时可能影响性能
  • 实现稍微复杂一些
  1. 位图索引方案

对于大规模数据,我们可以考虑使用位图索引来优化性能:

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. 缓存层方案

我们还可以考虑添加一个缓存层来优化频繁的查询:

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.Client

func 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
}

优点:

  • 大幅提高查询性能,特别是对于频繁访问的数据
  • 减轻数据库负载
  • 可以与其他方案结合使用

缺点:

  • 需要额外的缓存基础设施
  • 需要处理缓存一致性问题
  • 增加了系统复杂性

建议:

  1. 对于中小规模数据,关联表方案可能是最佳选择,因为它简单直接,易于维护和扩展。

  2. 如果您的系统需要处理大规模数据和高并发查询,位图索引方案可能更合适。

  3. 无论选择哪种方案,添加缓存层都可以显著提升性能,特别是对于读多写少的场景。

  4. 您还可以考虑混合方案,例如使用关联表存储数据,但在应用层使用位图进行内存中的快速查询。

选择最佳方案需要考虑您的具体需求,包括数据规模、查询频率、写入频率、扩展性需求等。您可能需要进行一些性能测试来确定哪种方案最适合您的使用场景。

您对这些方案有什么看法?是否有任何特定的关注点或约束条件需要考虑?

位图索引方案如何实现我们这个需求呢

Implement Product Visibility Control via Excel File Upload

https://blog.jiejaitt.top/posts/cd8b1634.html

作者

JIeJaitt

发布于

2024-09-27

更新于

2024-09-27

许可协议

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×