This commit is contained in:
oneao committed 2026-01-27 22:47:55 +08:00
1 parent 83cbf18572
commit 5ca04f85ca
32 files changed
+1308 -1804

No files matched your search

@@ -12,9 +12,6 @@ import (
"allapp/utils/response"
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgtype"
money "allapp/biz/model/money"
@@ -31,85 +28,85 @@ func QueryMoneyRecord(ctx context.Context, c *app.RequestContext) {
return
}
// 解析年月
startTime, err := time.Parse("2006-01", req.RecordTime)
if err != nil {
errorx.AddError(c, fmt.Errorf("invalid record_time format: %v", err))
return
}
endTime := startTime.AddDate(0, 1, 0).Add(-time.Nanosecond)
//// 解析年月
//startTime, err := time.Parse("2006-01", req.RecordTime)
//if err != nil {
// errorx.AddError(c, fmt.Errorf("invalid record_time format: %v", err))
// return
//}
//endTime := startTime.AddDate(0, 1, 0).Add(-time.Nanosecond)
//
//// 获取 userID 和 spaceID
//userID := middleware.GetUserID(ctx)
//spaceIspaceID := middleware.GetSpaceID(ctx)
//
//// 定义统一的 records 切片
//var dbRecordsUser []repo.MoneyRecord
//var dbRecordsSpace []repo.FindMoneyRecordsBySpaceRow
//var errQuery error
//
//// 最终返回统一类型
//var records []*money.MoneyRecord
//
//// 有空间ID,查询空间内所有用户
//if ok {
// params := repo.FindMoneyRecordsBySpaceParams{
// SpaceID: spaceID,
// RecordTime: pgtype.Date{Time: startTime, Valid: true},
// RecordTime_2: pgtype.Date{Time: endTime, Valid: true},
// }
// dbRecordsSpace, errQuery = db.Queries.FindMoneyRecordsBySpace(ctx, params)
// if errQuery != nil {
// errorx.AddError(c, errQuery)
// return
// }
//
// for _, r := range dbRecordsSpace {
// records = append(records, &money.MoneyRecord{
// Id: r.ID,
// CategoryId: r.CategoryID,
// UserId: r.UserID,
// Username: r.Username.String,
// Amount: pgtypex.NumericToString(r.Amount),
// Remark: r.Remark.String,
// RecordTime: pgtypex.DateToString(r.RecordTime),
// CreateTime: pgtypex.TimestampToMillis(r.CreateTime),
// UpdateTime: pgtypex.TimestampToMillis(r.UpdateTime),
// })
// }
//} else {
// // 没有空间ID,只查询单用户
// params := repo.FindUserMoneyRecordsParams{
// UserID: userID,
// RecordTime: pgtype.Date{Time: startTime, Valid: true},
// RecordTime_2: pgtype.Date{Time: endTime, Valid: true},
// }
// dbRecordsUser, errQuery = db.Queries.FindUserMoneyRecords(ctx, params)
// if errQuery != nil {
// errorx.AddError(c, errQuery)
// return
// }
//
// for _, r := range dbRecordsUser {
// records = append(records, &money.MoneyRecord{
// Id: r.ID,
// CategoryId: r.CategoryID,
// UserId: r.UserID,
// Username: "",
// Amount: pgtypex.NumericToString(r.Amount),
// Remark: r.Remark.String,
// RecordTime: pgtypex.DateToString(r.RecordTime),
// CreateTime: pgtypex.TimestampToMillis(r.CreateTime),
// UpdateTime: pgtypex.TimestampToMillis(r.UpdateTime),
// })
// }
//}
//
//result := &money.QueryMoneyRecordsResult{
// Records: records,
//}
// 获取 userID 和 spaceID
userID := middleware.GetUserID(ctx)
spaceID, ok := middleware.GetSpaceID(ctx)
// 定义统一的 records 切片
var dbRecordsUser []repo.MoneyRecord
var dbRecordsSpace []repo.FindMoneyRecordsBySpaceRow
var errQuery error
// 最终返回统一类型
var records []*money.MoneyRecord
// 有空间ID,查询空间内所有用户
if ok {
params := repo.FindMoneyRecordsBySpaceParams{
SpaceID: spaceID,
RecordTime: pgtype.Date{Time: startTime, Valid: true},
RecordTime_2: pgtype.Date{Time: endTime, Valid: true},
}
dbRecordsSpace, errQuery = db.Queries.FindMoneyRecordsBySpace(ctx, params)
if errQuery != nil {
errorx.AddError(c, errQuery)
return
}
for _, r := range dbRecordsSpace {
records = append(records, &money.MoneyRecord{
Id: r.ID,
CategoryId: r.CategoryID,
UserId: r.UserID,
Username: r.Username.String,
Amount: pgtypex.NumericToString(r.Amount),
Remark: r.Remark.String,
RecordTime: pgtypex.DateToString(r.RecordTime),
CreateTime: pgtypex.TimestampToMillis(r.CreateTime),
UpdateTime: pgtypex.TimestampToMillis(r.UpdateTime),
})
}
} else {
// 没有空间ID,只查询单用户
params := repo.FindUserMoneyRecordsParams{
UserID: userID,
RecordTime: pgtype.Date{Time: startTime, Valid: true},
RecordTime_2: pgtype.Date{Time: endTime, Valid: true},
}
dbRecordsUser, errQuery = db.Queries.FindUserMoneyRecords(ctx, params)
if errQuery != nil {
errorx.AddError(c, errQuery)
return
}
for _, r := range dbRecordsUser {
records = append(records, &money.MoneyRecord{
Id: r.ID,
CategoryId: r.CategoryID,
UserId: r.UserID,
Username: "",
Amount: pgtypex.NumericToString(r.Amount),
Remark: r.Remark.String,
RecordTime: pgtypex.DateToString(r.RecordTime),
CreateTime: pgtypex.TimestampToMillis(r.CreateTime),
UpdateTime: pgtypex.TimestampToMillis(r.UpdateTime),
})
}
}
result := &money.QueryMoneyRecordsResult{
Records: records,
}
response.Success(c).Data(result).Send()
response.Success(c).Send()
}
// InsertMoneyRecord .
@@ -9,7 +9,7 @@ import (
"allapp/utils"
"allapp/utils/errorx"
"allapp/utils/idgen"
"allapp/utils/redisx"
"allapp/utils/pgtypex"
"allapp/utils/response"
"context"
@@ -19,70 +19,287 @@ import (
"github.com/cloudwego/hertz/pkg/protocol/consts"
)
// KickOutUser 踢出某人
// @router /space/kick [POST]
func KickOutUser(ctx context.Context, c *app.RequestContext) {
var req space.KickOutUserParams
if err := c.BindAndValidate(&req); err != nil {
// CreateSpace .
// @router /space/create [POST]
func CreateSpace(ctx context.Context, c *app.RequestContext) {
var err error
var req space.CreateSpaceParams
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
userID := middleware.GetUserID(ctx)
spaceID := middleware.GetSpaceID(ctx)
hasPermission, err := CheckUserPermission(ctx, userID, spaceID)
if err != nil {
errorx.AddError(c, err)
return
}
if !hasPermission {
response.Fail(c).Message("无权限操作").Send()
return
}
if err := LeaveSpace(ctx, req.UserId, spaceID); err != nil {
errorx.AddError(c, err)
return
}
response.Success(c).Send()
}
// QuitSpace .
// @router /space/quit [POST]
func QuitSpace(ctx context.Context, c *app.RequestContext) {
var req space.QuitSpaceParams
if err := c.BindAndValidate(&req); err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp := new(space.CreateSpaceResult)
userID := middleware.GetUserID(ctx)
spaceID := req.SpaceId
// 检查权限
role, exists, err := redisx.GetUserSpaceRole(ctx, userID, spaceID)
_, err = db.Queries.GetSpaceUser(ctx, userID)
var isExist = true
if err != nil {
errorx.AddError(c, err)
if errorx.IsNotFound(err) {
isExist = false
} else {
errorx.AddError(c, err)
return
}
}
if isExist {
response.Fail(c).Message("请先退出所在空间").Send()
return
}
if !exists {
response.Fail(c).Message("已不在该空间内").Send()
return
// 生成邀请码
var inviteCode string
for {
// 生成邀请码
code := utils.GenerateCode(6)
// 检查数据库是否存在
_, err = db.Queries.FindSpaceByInviteCode(ctx, code)
if err != nil {
if errorx.IsNotFound(err) {
// 不存在,唯一,直接使用
inviteCode = code
break
} else {
// 系统错误
errorx.AddError(c, err)
return
}
}
// 如果存在,继续循环重新生成
}
spaceId := idgen.NextId()
// 创建空间
err = db.WithTx(ctx, func(q *repo.Queries) error {
if role == 0 {
// 创建空间
params := repo.InsertSpaceParams{
ID: spaceId,
Name: pgtypex.StringToText(req.Name),
InviteCode: inviteCode,
}
err = q.InsertSpace(ctx, params)
if err != nil {
return err
}
// 将创建用户默认添加到该空间内
userParams := repo.InsertSpaceUserParams{
ID: idgen.NextId(),
SpaceID: spaceId,
UserID: userID,
Role: 0,
}
err := q.InsertSpaceUser(ctx, userParams)
if err != nil {
return err
}
return nil
})
response.Success(c).Send()
if err != nil {
errorx.AddError(c, err)
return
}
resp.Id = spaceId
resp.InviteCode = inviteCode
response.Success(c).Data(resp).Send()
}
// DissolveSpace .
// @router /space/dissolve [POST]
func DissolveSpace(ctx context.Context, c *app.RequestContext) {
var err error
var req space.DissolveSpaceParams
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp := new(space.DissolveSpaceResult)
spaceID := req.GetSpaceId()
userID := middleware.GetUserID(ctx)
// 检查权限
hasPermission, err := hasSpacePermission(ctx, userID, spaceID)
if err != nil {
errorx.AddError(c, err)
return
}
if !hasPermission {
response.Fail(c).Message("无权限操作该空间").Send()
return
}
// 解散空间
err = db.WithTx(ctx, func(q *repo.Queries) error {
err2 := q.DeleteSpace(ctx, spaceID)
if err2 != nil {
return err2
}
err2 = q.DeleteSpaceUsersBySpaceID(ctx, spaceID)
if err2 != nil {
return err2
}
return nil
})
if err != nil {
errorx.AddError(c, err)
return
}
response.Success(c).Data(resp).Send()
}
// ExitSpace .
// @router /space/exit [POST]
func ExitSpace(ctx context.Context, c *app.RequestContext) {
var err error
var req space.ExitSpaceParams
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp := new(space.ExitSpaceResult)
userID := middleware.GetUserID(ctx)
spaceUser, err := db.Queries.GetSpaceUser(ctx, userID)
if err != nil {
if errorx.IsNotFound(err) {
response.Success(c).Data(resp).Send()
} else {
errorx.AddError(c, err)
return
}
}
if spaceUser.Role == 0 {
response.Fail(c).Message("请选择解散空间").Send()
return
}
err = db.WithTx(ctx, func(q *repo.Queries) error {
err2 := q.DeleteSpaceUserByUserID(ctx, userID)
if err2 != nil {
return err2
}
return nil
})
if err != nil {
errorx.AddError(c, err)
return
}
response.Success(c).Data(resp).Send()
}
// ListSpaceUsers .
// @router /space/users [POST]
func ListSpaceUsers(ctx context.Context, c *app.RequestContext) {
var err error
var req space.ListSpaceUsersParams
if err = c.BindAndValidate(&req); err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp := new(space.ListSpaceUsersResult)
users, err := db.Queries.ListSpaceUsers(ctx, req.SpaceId)
if err != nil {
c.String(consts.StatusInternalServerError, err.Error())
return
}
resp.Users = make([]*space.SpaceUser, 0, len(users))
for _, u := range users {
resp.Users = append(resp.Users, &space.SpaceUser{
Id: u.UserID,
Username: u.UserName,
Avatar: u.UserAvatar,
Role: u.Role,
LastLoginTime: pgtypex.TimestamptzToMillis(u.LastLoginTime),
})
}
response.Success(c).Data(resp).Send()
}
// RemoveSpaceUser .
// @router /space/remove [POST]
func RemoveSpaceUser(ctx context.Context, c *app.RequestContext) {
var err error
var req space.RemoveSpaceUserParams
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp := new(space.RemoveSpaceUserResult)
userID := middleware.GetUserID(ctx)
spaceID := req.SpaceId
// 检查权限
hasPermission, err := hasSpacePermission(ctx, userID, spaceID)
if err != nil {
errorx.AddError(c, err)
return
}
if !hasPermission {
response.Fail(c).Message("无权限操作该空间").Send()
return
}
// 移除
err = db.WithTx(ctx, func(q *repo.Queries) error {
err2 := q.DeleteSpaceUserBySpaceIDAndUserID(ctx, repo.DeleteSpaceUserBySpaceIDAndUserIDParams{
SpaceID: spaceID,
UserID: req.UserId,
})
if err2 != nil {
return err2
}
return nil
})
if err != nil {
errorx.AddError(c, err)
return
}
response.Success(c).Data(resp).Send()
}
// JoinSpace .
@@ -99,243 +316,73 @@ func JoinSpace(ctx context.Context, c *app.RequestContext) {
resp := new(space.JoinSpaceResult)
userID := middleware.GetUserID(ctx)
oldSpaceID := middleware.GetSpaceID(ctx)
inviteCode := req.InviteCode
spaceByInviteCode, err := db.Queries.FindSpaceByInviteCode(ctx, inviteCode)
_, err = db.Queries.GetSpaceUser(ctx, userID)
if err != nil {
if errorx.IsNotFound(err) {
response.Fail(c).Message("邀请码错误或已过期").Send()
} else {
errorx.AddError(c, err)
return
}
errorx.AddError(c, err)
} else {
response.Fail(c).Message("请先退出已加入的空间").Send()
return
}
joinSpaceID := spaceByInviteCode.ID
inviteCode := req.GetInviteCode()
spaceByInviteCode, err := db.Queries.FindSpaceByInviteCode(ctx, inviteCode)
// 检查权限
role, exists, err := redisx.GetUserSpaceRole(ctx, userID, oldSpaceID)
if err != nil {
errorx.AddError(c, err)
return
if errorx.IsNotFound(err) {
response.Fail(c).Message("邀请码无效或过期").Send()
return
} else {
errorx.AddError(c, err)
return
}
}
err = db.WithTx(ctx, func(q *repo.Queries) error {
// 判断权限, 如果角色为 0 则解散
if exists && role == 0 {
users, err2 := q.ListSpaceUsers(ctx, oldSpaceID)
err2 := q.InsertSpaceUser(ctx, repo.InsertSpaceUserParams{
ID: idgen.NextId(),
UserID: userID,
SpaceID: spaceByInviteCode.ID,
Role: 1,
})
if err2 != nil {
return err2
}
for _, user := range users {
if err2 := LeaveSpace(ctx, user.UserID, oldSpaceID); err2 != nil {
return err2
}
}
} else {
// 退出就行
if err2 := q.DeleteSpaceUser(ctx, repo.DeleteSpaceUserParams{
SpaceID: oldSpaceID,
UserID: userID,
}); err2 != nil {
return err2
}
if err2 := redisx.RemoveUserSpace(ctx, userID, oldSpaceID); err2 != nil {
return err2
}
// 再加入
if err2 := q.InsertSpaceUser(ctx, repo.InsertSpaceUserParams{
ID: idgen.NextId(),
UserID: userID,
SpaceID: joinSpaceID,
Role: 0,
}); err2 != nil {
return err2
}
if err2 := redisx.AddOrUpdateUserSpace(ctx, userID, joinSpaceID, 0); err2 != nil {
return err2
}
if err2 != nil {
return err2
}
return nil
})
c.JSON(consts.StatusOK, resp)
}
// ListSpaceUsers .
// @router /space/users [POST]
func ListSpaceUsers(ctx context.Context, c *app.RequestContext) {
var err error
var req space.ListSpaceUsersParams
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp := new(space.ListSpaceUsersResult)
// 查询
users, err := db.Queries.ListSpaceUsers(ctx, req.SpaceId)
if err != nil {
errorx.AddError(c, err)
return
}
spaceUsers := make([]*space.SpaceUser, 0, len(users))
resp.Id = spaceByInviteCode.ID
resp.Name = spaceByInviteCode.Name.String
for _, user := range users {
spaceUsers = append(spaceUsers, &space.SpaceUser{
UserId: user.UserID,
UserName: user.UserName,
UserAvatar: user.UserAvatar,
Role: user.Role,
})
}
resp.Users = spaceUsers
response.Success(c).Data(spaceUsers).Send()
response.Success(c).Data(resp).Send()
}
// CheckUserPermission 检查是否有权限
func CheckUserPermission(ctx context.Context, userID int64, spaceID int64) (bool, error) {
inSpace, err := redisx.InSpace(ctx, userID, spaceID)
func hasSpacePermission(ctx context.Context, userID int64, spaceID int64) (bool, error) {
user, err := db.Queries.GetSpaceUser(ctx, userID)
if err != nil {
return false, err
}
if !inSpace {
return false, nil
}
role, exists, err := redisx.GetUserSpaceRole(ctx, userID, spaceID)
if err != nil {
return false, err
}
if !exists {
return false, nil
}
return role == 0, nil
}
// LeaveSpace 处理用户离开空间逻辑(包括删除空间用户、清理 Redis、创建新空间)
func LeaveSpace(ctx context.Context, userID, spaceID int64) error {
return db.WithTx(ctx, func(q *repo.Queries) error {
// 1. 删除数据库空间用户
if err := q.DeleteSpaceUser(ctx, repo.DeleteSpaceUserParams{
UserID: userID,
SpaceID: spaceID,
}); err != nil {
return err
}
// 确保该用户加入了缓存
err := InitUserSpaces(ctx, userID)
if err != nil {
return err
}
// 2. 删除 Redis 缓存
if err := redisx.RemoveUserSpace(ctx, userID, spaceID); err != nil {
return err
}
// 3. 确保用户至少有一个空间
if err := EnsureUserHasSpaceWithTx(ctx, q, userID); err != nil {
return err
}
return nil
})
}
// InitUserSpaces 初始化用户空间信息
func InitUserSpaces(ctx context.Context, userID int64) error {
hasSpace, err := redisx.HasUserSpaces(ctx, userID)
if err != nil {
return err
}
if hasSpace {
return nil
}
spaces, err := db.Queries.ListUserSpaces(ctx, userID)
if err != nil {
return err
}
spaceRoles := make(map[int64]int32, len(spaces))
for _, row := range spaces {
spaceRoles[row.ID] = row.Role
}
return redisx.SetUserSpaces(ctx, userID, spaceRoles)
}
// EnsureUserHasSpaceWithTx 确保用户至少有一个空间,如果没有则创建新空间
// 注意:不管理事务,需要在外层传入 q *repo.Queries
func EnsureUserHasSpaceWithTx(ctx context.Context, q *repo.Queries, userID int64) error {
// 1. 查询用户现有空间
spaces, err := redisx.GetUserSpaces(ctx, userID)
if err != nil {
return err
}
if len(spaces) > 0 {
// 已经有空间,无需创建
return nil
}
// 2. 没有空间则创建
var inviteCode string
for {
code := utils.GenerateCode(6)
_, err := q.FindSpaceByInviteCode(ctx, code)
if errorx.IsNotFound(err) {
inviteCode = code
break
} else if err != nil {
return err
return false, nil
}
return false, err
}
newSpaceID := idgen.NextId()
// 插入新空间
if err := q.InsertSpace(ctx, repo.InsertSpaceParams{
ID: newSpaceID,
InviteCode: inviteCode,
}); err != nil {
return err
if user.SpaceID != spaceID {
return false, nil
}
// 插入空间用户
if err := q.InsertSpaceUser(ctx, repo.InsertSpaceUserParams{
ID: idgen.NextId(),
SpaceID: newSpaceID,
UserID: userID,
}); err != nil {
return err
}
// 同步到 Redis
if err := redisx.AddOrUpdateUserSpace(ctx, userID, newSpaceID, 0); err != nil {
return err
}
return nil
return user.Role == 0, nil
}
@@ -15,40 +15,37 @@ import (
"github.com/cloudwego/hertz/pkg/protocol/consts"
)
// ListUserSpaces .
// @router /user/spaces [GET]
func ListUserSpaces(ctx context.Context, c *app.RequestContext) {
// GetUserSpace .
// @router /user/space [GET]
func GetUserSpace(ctx context.Context, c *app.RequestContext) {
var err error
var req user.ListUserSpacesParams
var req user.GetUserSpaceParams
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp := new(user.ListUserSpacesResult)
resp := new(user.UserSpace)
userID := middleware.GetUserID(ctx)
// 查询
spaces, err := db.Queries.ListUserSpaces(ctx, userID)
space, err := db.Queries.GetSpaceByUserId(ctx, userID)
if err != nil {
if errorx.IsNotFound(err) {
response.Success(c).Data(resp).Send()
return
}
errorx.AddError(c, err)
return
}
userSpaces := make([]*user.UserSpace, 0, len(spaces))
for _, space := range spaces {
userSpaces = append(userSpaces, &user.UserSpace{
Id: space.ID,
Name: space.Name.String,
InviteCode: space.InviteCode,
Role: space.Role,
})
}
resp.Spaces = userSpaces
resp.Id = space.ID
resp.Name = space.Name.String
resp.InviteCode = space.InviteCode
resp.Role = space.Role
response.Success(c).Data(resp).Send()
}