🎏FInish prefer List

This commit is contained in:
Havoc412 2024-11-07 21:28:41 +08:00
parent dc72569f32
commit 473eafc069
10 changed files with 338 additions and 20 deletions

View File

@ -12,6 +12,7 @@ import (
"github.com/yankeguo/zhipu"
"go.uber.org/zap"
"gorm.io/gorm"
)
var (

View File

@ -8,6 +8,8 @@ import (
"catface/app/model"
"catface/app/service/animals/curd"
"catface/app/service/upload_file"
"catface/app/utils/query_handler"
"catface/app/utils/redis_factory"
"catface/app/utils/response"
"os"
"path/filepath"
@ -19,6 +21,16 @@ import (
type Animals struct { // INFO 起到一个标记的作用,这样 web.xxx 的时候不同模块就不会命名冲突了。
}
// contains 检查 id 是否在 ids 切片中
func contains(ids []int, id int) bool {
for _, v := range ids {
if v == id {
return true
}
}
return false
}
func (a *Animals) List(context *gin.Context) {
// 1. Get Params
attrs := context.GetString(consts.ValidatorPrefix + "attrs")
@ -27,26 +39,95 @@ func (a *Animals) List(context *gin.Context) {
sterilization := context.GetString(consts.ValidatorPrefix + "sterilization")
status := context.GetString(consts.ValidatorPrefix + "status")
department := context.GetString(consts.ValidatorPrefix + "department")
num := context.GetFloat64(consts.ValidatorPrefix + "num")
skip := context.GetFloat64(consts.ValidatorPrefix + "skip")
num := int(context.GetFloat64(consts.ValidatorPrefix + "num"))
skip := int(context.GetFloat64(consts.ValidatorPrefix + "skip"))
userId := context.GetFloat64(consts.ValidatorPrefix + "user_id")
mode := context.GetString(consts.ValidatorPrefix + "mode")
var preferCatsId []int64
var redis_num int
var key int64
if mode == consts.AnimalPreferMode {
preferList(context)
} else { // 其余都是 默认模式。
animals := curd.CreateAnimalsCurdFactory().List(attrs, gender, breed, sterilization, status, department, int(num), int(skip), int(userId))
if animals != nil {
response.Success(context, consts.CurdStatusOkMsg, animals)
key = int64(context.GetFloat64(consts.ValidatorPrefix + "key"))
redisClient := redis_factory.GetOneRedisClient()
defer redisClient.ReleaseOneRedisClient()
if key != 0 {
redis_num, _ = redisClient.Int(redisClient.Execute("get", key))
} else {
response.Fail(context, errcode.AnimalNoFind, errcode.ErrMsg[errcode.AnimalNoFind], "")
key = variable.SnowFlake.GetId()
}
if redis_num == skip {
preferCatsId, _ = getPreferCatsId(int(userId), int(num))
redis_num += len(preferCatsId)
}
if _, err := redisClient.String(redisClient.Execute("set", key, redis_num)); err != nil {
}
}
var animalsWithLike []model.AnimalWithLikeList
if len(preferCatsId) > 0 {
// 创建一个 map 来存储查询结果
animalMap := make(map[int64]model.Animal, len(preferCatsId))
attrsSlice := query_handler.StringToStringArray(attrs)
attrsSlice = append(attrsSlice, "id")
animals := model.CreateAnimalFactory("").ShowByIDs(preferCatsId, attrsSlice...)
for _, v := range animals {
animalMap[v.Id] = v
}
// 根据 preferCatsId 的顺序构建最终结果列表
for _, id := range preferCatsId {
if animal, ok := animalMap[id]; ok {
animalsWithLike = append(animalsWithLike, model.AnimalWithLikeList{Animal: animal})
}
}
}
// 计算还需要多少动物
num -= len(animalsWithLike)
skip -= redis_num
if num > 0 {
additionalAnimals := curd.CreateAnimalsCurdFactory().List(attrs, gender, breed, sterilization, status, department, preferCatsId, num, skip, int(userId))
// 将 additionalAnimals 整合到 animalsWithLike 的后面
animalsWithLike = append(animalsWithLike, additionalAnimals...)
}
if animalsWithLike != nil {
response.Success(context, consts.CurdStatusOkMsg, gin.H{
"animals": animalsWithLike,
"key": key,
})
} else {
response.Fail(context, errcode.AnimalNoFind, errcode.ErrMsg[errcode.AnimalNoFind], "")
}
}
func preferList(context *gin.Context) {
// TODO 先去考虑一下前端筛选的实现方式。
// UPDATE 就先简单一些,主要就依靠 encounter - animal_id 来获取一个目标。
func getPreferCatsId(userId, num int) ([]int64, error) {
// STAGE check 一下 key 是否存在。
// if key != "" {
// redisClient := redis_factory.GetOneRedisClient()
// defer redisClient.ReleaseOneRedisClient()
// if res, err := redisClient.Bool(redisClient.Execute("get", key)); err != nil {
// if res { // 如果 redis 返回的是 1则代表 prefer 已经耗尽了。
// return nil, err
// }
// }
// }
// STAGE - 1 模块一,无视条件,获取路遇过的 id 列表;先获取 ID然后再去查询细节信息。
encounteredCats, err := model.CreateEncounterFactory("").EncounteredCats(userId, num)
if err != nil {
// variable.ZapLog.Error("获取用户浏览记录失败", Zap.Error(err))
return encounteredCats, err
}
return encounteredCats, nil
}
// v0.1

View File

@ -21,6 +21,7 @@ type List struct {
UserId int `form:"user_id" json:"user_id"`
Mode string `form:"mode" json:"mode"` // INFO 控制 animal_ctl 的查询模式。 // default: 简单调用 List 函数 || prefer: 优先返回和用户关联度更高的目标。
Ket string `form:"key" json:"key"` // redis の key 值。
}
func (l List) CheckParams(context *gin.Context) {

View File

@ -51,7 +51,7 @@ func (a *Animal) TableName() string {
return "animals"
}
func (a *Animal) Show(attrs []string, gender []uint8, breed []uint8, sterilization []uint8, status []uint8, department []uint8, num int, skip int) (temp []Animal) {
func (a *Animal) Show(attrs []string, gender []uint8, breed []uint8, sterilization []uint8, status []uint8, department []uint8, notInIds []int64, num int, skip int) (temp []Animal) {
db := a.DB.Table(a.TableName()).Limit(int(num)).Offset(int(skip)).Select(attrs)
// 创建条件映射
@ -65,6 +65,10 @@ func (a *Animal) Show(attrs []string, gender []uint8, breed []uint8, sterilizati
db = gorm_v2.BuildWhere(db, conditions) // TIP 这里的 Where 条件连接就很方便了。
if len(notInIds) > 0 {
db = db.Where("id not in (?)", notInIds)
}
err := db.Find(&temp).Error
if err != nil {
variable.ZapLog.Error("Animal Show Error", zap.Error(err))

View File

@ -9,7 +9,6 @@ import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gorm.io/gorm"
)
func CreateEncounterFactory(sqlType string) *Encounter {
@ -99,7 +98,7 @@ func formatEncounterList(rows *gorm.DB) (temp []EncounterList, err error) {
}
func (e *Encounter) Show(num, skip, user_id int, animals_id []int) (temp []EncounterList) {
// SATGE - 1build SQL
// STAGE - 1build SQL
var sqlBuilder strings.Builder
// 构建基础查询
@ -122,8 +121,8 @@ WHERE eal.animal_id IN (?)`)
// 添加排序和分页
sqlBuilder.WriteString(`
ORDER BY e.updated_at DESC
LIMIT ? OFFSET ?
ORDER BY e.updated_at DESC
LIMIT ? OFFSET ?
`)
sql := sqlBuilder.String() // 获取到 SQL
@ -169,3 +168,42 @@ func (e *Encounter) ShowByID(id int64) (temp *Encounter, err error) {
// // TODO 4. 然后整合
// return
}
func (e *Encounter) EncounteredCats(user_id, num int) ([]int64, error) {
sql := `SELECT eal.animal_id
FROM encounter_animal_links eal
JOIN encounters e
ON e.id = eal.encounter_id AND e.user_id = ?
ORDER BY e.updated_at DESC
LIMIT ?`
rows, err := e.Raw(sql, user_id, num).Rows()
if err != nil {
log.Println("查询失败:", err)
return nil, err
}
defer rows.Close()
// Scan 同时去重。
var temp []int64
seen := make(map[int64]bool)
for rows.Next() {
var animal_id int64
if err := rows.Scan(&animal_id); err != nil {
log.Println("扫描失败:", err)
return nil, err
}
if !seen[animal_id] {
seen[animal_id] = true
temp = append(temp, animal_id)
}
}
if err := rows.Err(); err != nil {
log.Println("遍历失败:", err)
return nil, err
}
return temp, nil
}

View File

@ -48,7 +48,7 @@ func getSelectAttrs(attrs string) (validSelectedFields []string) {
return
}
func (a *AnimalsCurd) List(attrs string, gender string, breed string, sterilization string, status string, department string, num int, skip int, userId int) (temp []model.AnimalWithLikeList) {
func (a *AnimalsCurd) List(attrs string, gender string, breed string, sterilization string, status string, department string, notInIds []int64, num int, skip int, userId int) (temp []model.AnimalWithLikeList) {
validSelectedFields := getSelectAttrs(attrs)
genderArray := query_handler.StringToUint8Array(gender)
breedArray := query_handler.StringToUint8Array(breed)
@ -60,7 +60,7 @@ func (a *AnimalsCurd) List(attrs string, gender string, breed string, sterilizat
num = 10
}
animals := model.CreateAnimalFactory("").Show(validSelectedFields, genderArray, breedArray, sterilizationArray, statusArray, departmentArray, num, skip)
animals := model.CreateAnimalFactory("").Show(validSelectedFields, genderArray, breedArray, sterilizationArray, statusArray, departmentArray, notInIds, num, skip)
// 状态记录
var likeRes []bool

View File

@ -4,6 +4,7 @@ import (
"catface/app/model"
"catface/app/utils/query_handler"
"strconv"
)
func CreateEncounterCurdFactory() *EncounterCurd {
@ -21,8 +22,8 @@ func (e *EncounterCurd) List(num, skip, user_id int, mode string) (result []mode
var likedAnimalIds []int
switch mode {
case "liked":
likedAnimalIds = model.CreateAnimalLikeFactory("").LikedCats(user_id)
case "liked":
likedAnimalIds = model.CreateAnimalLikeFactory("").LikedCats(user_id)
}
result = model.CreateEncounterFactory("").Show(num, skip, user_id, likedAnimalIds)
return

View File

@ -10,7 +10,6 @@ import (
"github.com/gomodule/redigo/redis"
"go.uber.org/zap"
)
var redisPool *redis.Pool

135
test/redis_test.go Normal file
View File

@ -0,0 +1,135 @@
package test
import (
"catface/app/global/variable"
"catface/app/utils/redis_factory"
_ "catface/bootstrap"
"fmt"
"testing"
"time"
"go.uber.org/zap"
)
// 普通的key value
func TestRedisKeyValue(t *testing.T) {
// 从连接池获取一个连接
redisClient := redis_factory.GetOneRedisClient()
// set 命令, 因为 set key value 在redis客户端执行以后返回的是 ok所以取回结果就应该是 string 格式
res, err := redisClient.String(redisClient.Execute("set", "key2020", "value202022"))
if err != nil {
t.Errorf("单元测试失败,%s\n", err.Error())
} else {
variable.ZapLog.Info("Info 日志", zap.String("key2020", res))
}
// get 命令分为两步1.执行get命令 2.将结果转为需要的格式
if res, err = redisClient.String(redisClient.Execute("get", "key2020")); err != nil {
t.Errorf("单元测试失败,%s\n", err.Error())
}
variable.ZapLog.Info("get key2020 ", zap.String("key2020", res))
//操作完毕记得释放连接官方明确说redis使用完毕必须释放
redisClient.ReleaseOneRedisClient()
}
func TestRedisKeyInt64(t *testing.T) {
redisClient := redis_factory.GetOneRedisClient()
defer redisClient.ReleaseOneRedisClient()
id := variable.SnowFlake.GetId()
res, err := redisClient.String(redisClient.Execute("set", id, 1))
if err != nil {
t.Errorf("单元测试失败,%s\n", err.Error())
}
if res, err = redisClient.String(redisClient.Execute("get", id)); err != nil {
t.Errorf("单元测试失败,%s\n", err.Error())
} else {
t.Logf("单元测试通过,%s\n", res)
}
}
// hash 键、值
func TestRedisHashKey(t *testing.T) {
redisClient := redis_factory.GetOneRedisClient()
// hash键 set 命令, 因为 hSet h_key key value 在redis客户端执行以后返回的是 1 或者 0所以按照int64格式取回
res, err := redisClient.Int64(redisClient.Execute("hSet", "h_key2020", "hKey2020", "value2020_hash"))
if err != nil {
t.Errorf("单元测试失败,%s\n", err.Error())
} else {
fmt.Println(res)
}
// hash键 get 命令分为两步1.执行get命令 2.将结果转为需要的格式
res2, err := redisClient.String(redisClient.Execute("hGet", "h_key2020", "hKey2020"))
if err != nil {
t.Errorf("单元测试失败,%s\n", err.Error())
}
fmt.Println(res2)
//官方明确说redis使用完毕必须释放
redisClient.ReleaseOneRedisClient()
}
// 测试 redis 连接池
func TestRedisConnPool(t *testing.T) {
for i := 1; i <= 20; i++ {
go func() {
redisClient := redis_factory.GetOneRedisClient()
fmt.Printf("获取的redis数据库连接池地址%p\n", redisClient)
time.Sleep(time.Second * 10)
fmt.Printf("阻塞过程中您可以通过redis命令 client list 查看链接的客户端")
redisClient.ReleaseOneRedisClient() // 释放从连接池获取的连接
}()
}
time.Sleep(time.Second * 20)
}
// 测试redis 网络中断自动重连机制
func TestRedisReConn(t *testing.T) {
redisClient := redis_factory.GetOneRedisClient()
res, err := redisClient.String(redisClient.Execute("set", "key2020", "测试网络抖动,自动重连机制"))
if err != nil {
t.Errorf("单元测试失败,%s\n", err.Error())
} else {
variable.ZapLog.Info("Info 日志", zap.String("key2020", res))
}
//官方明确说redis使用完毕必须释放
redisClient.ReleaseOneRedisClient()
// 以上内容输出后 拔掉网线, 模拟短暂的网络抖动
t.Log("请在 10秒之内拔掉网线")
time.Sleep(time.Second * 10)
// 断网情况下就会自动进行重连
redisClient = redis_factory.GetOneRedisClient()
if res, err = redisClient.String(redisClient.Execute("get", "key2020")); err != nil {
t.Errorf("单元测试失败,%s\n", err.Error())
} else {
t.Log("获取的值:", res)
}
redisClient.ReleaseOneRedisClient()
}
// 测试返回值为多值的情况
func TestRedisMulti(t *testing.T) {
redisClient := redis_factory.GetOneRedisClient()
if _, err := redisClient.String(redisClient.Execute("multi")); err == nil {
redisClient.Execute("hset", "mobile", "xiaomi", "1999")
redisClient.Execute("hset", "mobile", "oppo", "2999")
redisClient.Execute("hset", "mobile", "iphone", "3999")
if strs, err := redisClient.Int64s(redisClient.Execute("exec")); err == nil {
t.Logf("直接输出切片:%#+v\n", strs)
} else {
t.Errorf(err.Error())
}
} else {
t.Errorf(err.Error())
}
redisClient.ReleaseOneRedisClient()
}
// 其他请参照以上示例即可

58
test/snowflake_test.go Normal file
View File

@ -0,0 +1,58 @@
package test
import (
"catface/app/global/variable"
_ "catface/bootstrap"
"fmt"
"sync"
"testing"
)
// 雪花算法单元测试
func TestSnowFlake(t *testing.T) {
num := 3
// 并发 3万 测试,实际业务场景中,并发是不可能达到 3万 这个值的
var slice1 []int64
var vMuext sync.Mutex
var wg sync.WaitGroup
wg.Add(num)
for i := 1; i <= num; i++ {
go func() {
defer wg.Done()
//加锁操作主要是为了保证切片([]int64的并发安全
//我们本次测试的核心目的是雪花算法生成的ID必须是唯一的
vMuext.Lock()
slice1 = append(slice1, variable.SnowFlake.GetId())
fmt.Println(slice1)
vMuext.Unlock()
//fmt.Printf("%d\n", variable.SnowFlake.GetId())
}()
}
wg.Wait()
if lastLen := len(RemoveRepeatedElement(slice1)); lastLen == num {
t.Log("单元测试OK")
} else {
t.Errorf("雪花算法单元测试失败,并发 3万 生成的id经过去重之后小于预期个数去重后的个数%d\n", lastLen)
}
}
// 切片去重
func RemoveRepeatedElement(arr []int64) (newArr []int64) {
newArr = make([]int64, 0)
for i := 0; i < len(arr); i++ {
repeat := false
for j := i + 1; j < len(arr); j++ {
if arr[i] == arr[j] {
repeat = true
break
}
}
if !repeat {
newArr = append(newArr, arr[i])
}
}
return
}