资金账户模块表设计

This commit is contained in:
2025-08-18 19:20:51 +08:00
parent 02becac11a
commit caaa1737e9
21 changed files with 7129 additions and 84 deletions

Binary file not shown.

View File

@@ -10,7 +10,7 @@ import (
)
// SaveMomentWithImages 新建动态
func SaveMomentWithImages(moment *model.Moment, imageURLs []string) error {
func SaveMomentWithImages(moment *model.Moment, files []*user.MomentFile) error {
db := DB
// 使用事务确保数据一致性
return db.Transaction(func(tx *gorm.DB) error {
@@ -20,12 +20,13 @@ func SaveMomentWithImages(moment *model.Moment, imageURLs []string) error {
}
// 保存图片(如果有)
if len(imageURLs) > 0 {
images := make([]model.MomentImage, 0, len(imageURLs))
for _, url := range imageURLs {
images = append(images, model.MomentImage{
if len(files) > 0 {
images := make([]model.MomentFile, 0, len(files))
for _, url := range files {
images = append(images, model.MomentFile{
MomentID: moment.ID,
ImageURL: url,
FileURL: url.FileURL,
FileType: url.FileType,
})
}
if err := tx.Create(&images).Error; err != nil {
@@ -101,19 +102,21 @@ func convertUserModelToDTO(u model.User) user.UserInfoReq {
}
// GetMomentsImage 获取指定动态的图像列表
func GetMomentsImage(ids uint) ([]*user.MomentImage, error) {
func GetMomentsImage(ids uint) ([]*user.MomentFile, error) {
db := DB
var images []model.MomentImage
var data []*user.MomentImage
err := db.Model(&model.MomentImage{}).Where("moment_id = ?", ids).Find(&images).Error
var images []model.MomentFile
var data []*user.MomentFile
err := db.Model(&model.MomentFile{}).Where("moment_id = ?", ids).Find(&images).Error
if err != nil {
return nil, err
}
for _, v := range images {
data = append(data, &user.MomentImage{
ID: int64(v.ID),
MomentID: int64(v.MomentID),
ImageURL: v.ImageURL,
data = append(data, &user.MomentFile{
ID: int64(v.ID),
MomentID: int64(v.MomentID),
FileURL: v.FileURL,
FileType: v.FileType,
SortOrder: int8(v.SortOrder),
})
}
return data, nil
@@ -192,7 +195,7 @@ func GetFriendsMoments(req user.ListMomentsRequest) ([]*user.Moment, int32, erro
Status: user.ContentStatus(int64(v.Status)),
LikeCount: int32(v.LikeCount),
CommentCount: int32(v.CommentCount),
Images: images,
Files: images,
CreatedAt: v.CreatedAt.Format("2006-01-02 15:04"),
})
}
@@ -243,7 +246,7 @@ func GetMomentsAppoint(req user.ListMomentsAppointRequest) ([]*user.Moment, int3
Status: user.ContentStatus(int64(v.Status)),
LikeCount: int32(v.LikeCount),
CommentCount: int32(v.CommentCount),
Images: images,
Files: images,
CreatedAt: v.CreatedAt.Format("2006-01-02 15:04"),
})
}
@@ -275,7 +278,7 @@ func DeleteMoment(req user.DeleteMomentRequest) error {
}
// 2. 删除动态图片
if err := tx.Where("moment_id = ?", req.MomentID).Delete(&model.MomentImage{}).Error; err != nil {
if err := tx.Where("moment_id = ?", req.MomentID).Delete(&model.MomentFile{}).Error; err != nil {
return fmt.Errorf("删除动态图片失败: %w", err)
}

View File

@@ -33,7 +33,7 @@ func Init() {
panic(err)
}
// 自动迁移
err = DB.AutoMigrate(model.User{}, model.Country{}, model.UserRelations{}, model.FriendRelationship{}, model.ChatGroupInfo{}, model.GroupUserRelation{}, model.Moment{}, model.MomentImage{}, model.MomentLike{}, model.MomentComment{})
err = DB.AutoMigrate(model.User{}, model.Country{}, model.UserRelations{}, model.FriendRelationship{}, model.ChatGroupInfo{}, model.GroupUserRelation{}, model.Moment{}, model.MomentFile{}, model.MomentLike{}, model.MomentComment{})
if err != nil {
hlog.Error("AutoMigrate failed: " + err.Error())
return

View File

@@ -43,7 +43,7 @@ func CreateMoment(ctx context.Context, c *app.RequestContext) {
}
// 保存动态到数据库
if err = mysql.SaveMomentWithImages(&moment, req.ImageUrls); err != nil {
if err = mysql.SaveMomentWithImages(&moment, req.FileUrls); err != nil {
c.JSON(consts.StatusInternalServerError, &user.CreateMomentResponse{Code: user.Code_DBErr, Msg: err.Error()})
return
}

View File

@@ -0,0 +1,123 @@
// Code generated by hertz generator.
package user_assets
import (
"context"
user_assets "acquaintances/biz/model/user_assets"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)
// GetUserAssets .
// @router /v1/user_assets/ [GET]
func GetUserAssets(ctx context.Context, c *app.RequestContext) {
var err error
var req user_assets.GetUserAssetsRequest
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp := new(user_assets.GetUserAssetsResponse)
c.JSON(consts.StatusOK, resp)
}
// AddDiamond .
// @router /v1/user_assets/diamond/ [POST]
func AddDiamond(ctx context.Context, c *app.RequestContext) {
var err error
var req user_assets.AddDiamondRequest
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp := new(user_assets.AddDiamondResponse)
c.JSON(consts.StatusOK, resp)
}
// AddGoldBean .
// @router /v1/user_assets/gold_bean/ [POST]
func AddGoldBean(ctx context.Context, c *app.RequestContext) {
var err error
var req user_assets.AddGoldBeanRequest
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp := new(user_assets.AddGoldBeanResponse)
c.JSON(consts.StatusOK, resp)
}
// AddGoldLeaf .
// @router /v1/user_assets/gold_leaf/ [POST]
func AddGoldLeaf(ctx context.Context, c *app.RequestContext) {
var err error
var req user_assets.AddGoldLeafRequest
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp := new(user_assets.AddGoldLeafResponse)
c.JSON(consts.StatusOK, resp)
}
// AddBankcard .
// @router /v1/user_assets/bankcard/ [POST]
func AddBankcard(ctx context.Context, c *app.RequestContext) {
var err error
var req user_assets.AddBankcardRequest
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp := new(user_assets.AddBankcardResponse)
c.JSON(consts.StatusOK, resp)
}
// RemoveBankcard .
// @router /v1/user_assets/bankcard/ [DELETE]
func RemoveBankcard(ctx context.Context, c *app.RequestContext) {
var err error
var req user_assets.RemoveBankcardRequest
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp := new(user_assets.RemoveBankcardResponse)
c.JSON(consts.StatusOK, resp)
}
// ListBankcardByUser .
// @router /v1/user_assets/bankcard/ [GET]
func ListBankcardByUser(ctx context.Context, c *app.RequestContext) {
var err error
var req user_assets.ListBankcardByUserRequest
err = c.BindAndValidate(&req)
if err != nil {
c.String(consts.StatusBadRequest, err.Error())
return
}
resp := new(user_assets.ListBankcardByUserResponse)
c.JSON(consts.StatusOK, resp)
}

View File

@@ -23,10 +23,12 @@ type Moment struct {
}
// 朋友圈图片表
type MomentImage struct {
type MomentFile struct {
gorm.Model
MomentID uint `gorm:"column:moment_id;type:int unsigned;not null;comment:'朋友圈动态ID'"`
ImageURL string `gorm:"column:image_url;type:varchar(255);not null;comment:'图片URL'"`
MomentID uint `gorm:"column:moment_id;type:int unsigned;not null;comment:'动态ID'"`
FileURL string `gorm:"column:file_url;type:varchar(255);not null;comment:'文件URL'"`
FileType string `gorm:"column:file_type;type:varchar(15);not null;comment:'文件类型'"`
SortOrder int `gorm:"column:sort_order;type:int;default:0;comment:'文件排序'"`
}
// 朋友圈点赞表

View File

@@ -1825,7 +1825,7 @@ type CreateUserRequest struct {
Name string `thrift:"name,1" form:"name" json:"name" vd:"(len($) > 0 && len($) < 100)"`
Gender Gender `thrift:"gender,2" form:"gender" json:"gender" vd:"($ == 1||$ == 2)"`
Age int64 `thrift:"age,3" form:"age" json:"age" vd:"$>0"`
Mobile string `thrift:"mobile,4" form:"mobile" form:"mobile" json:"mobile" vd:"(len($) > 0 && len($) < 12)"`
Mobile string `thrift:"mobile,4" form:"mobile" json:"mobile" vd:"(len($) > 0 && len($) < 12)"`
Area int64 `thrift:"area,5" form:"area" json:"area" vd:"$>0"`
UserPassword string `thrift:"user_password,6" form:"user_password" json:"user_password" vd:"(len($) > 0 && len($) < 15)"`
}
@@ -13144,7 +13144,7 @@ type Moment struct {
// 评论数
CommentCount int32 `thrift:"comment_count,9" form:"comment_count" json:"comment_count" query:"comment_count"`
// 动态图片列表
Images []*MomentImage `thrift:"images,10" form:"images" json:"images" query:"images"`
Files []*MomentFile `thrift:"files,10" form:"files" json:"files" query:"files"`
// 创建时间
CreatedAt string `thrift:"created_at,11" form:"created_at" json:"created_at" query:"created_at"`
// 更新时间
@@ -13199,8 +13199,8 @@ func (p *Moment) GetCommentCount() (v int32) {
return p.CommentCount
}
func (p *Moment) GetImages() (v []*MomentImage) {
return p.Images
func (p *Moment) GetFiles() (v []*MomentFile) {
return p.Files
}
func (p *Moment) GetCreatedAt() (v string) {
@@ -13221,7 +13221,7 @@ var fieldIDToName_Moment = map[int16]string{
7: "status",
8: "like_count",
9: "comment_count",
10: "images",
10: "files",
11: "created_at",
12: "updated_at",
}
@@ -13474,8 +13474,8 @@ func (p *Moment) ReadField10(iprot thrift.TProtocol) error {
if err != nil {
return err
}
_field := make([]*MomentImage, 0, size)
values := make([]MomentImage, size)
_field := make([]*MomentFile, 0, size)
values := make([]MomentFile, size)
for i := 0; i < size; i++ {
_elem := &values[i]
_elem.InitDefault()
@@ -13489,7 +13489,7 @@ func (p *Moment) ReadField10(iprot thrift.TProtocol) error {
if err := iprot.ReadListEnd(); err != nil {
return err
}
p.Images = _field
p.Files = _field
return nil
}
func (p *Moment) ReadField11(iprot thrift.TProtocol) error {
@@ -13732,13 +13732,13 @@ WriteFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T write field 9 end error: ", p), err)
}
func (p *Moment) writeField10(oprot thrift.TProtocol) (err error) {
if err = oprot.WriteFieldBegin("images", thrift.LIST, 10); err != nil {
if err = oprot.WriteFieldBegin("files", thrift.LIST, 10); err != nil {
goto WriteFieldBeginError
}
if err := oprot.WriteListBegin(thrift.STRUCT, len(p.Images)); err != nil {
if err := oprot.WriteListBegin(thrift.STRUCT, len(p.Files)); err != nil {
return err
}
for _, v := range p.Images {
for _, v := range p.Files {
if err := v.Write(oprot); err != nil {
return err
}
@@ -13796,6 +13796,331 @@ func (p *Moment) String() string {
}
// 动态图片结构体
type MomentFile struct {
// ID
ID int64 `thrift:"id,1" form:"id" json:"id" query:"id"`
// 所属动态ID
MomentID int64 `thrift:"moment_id,2" form:"moment_id" json:"moment_id" query:"moment_id"`
// 文件URL
FileURL string `thrift:"file_url,3" form:"file_url" json:"file_url" query:"file_url"`
// 文件类型
FileType string `thrift:"file_type,4" form:"file_type" json:"file_type" query:"file_type"`
//文件顺序
SortOrder int8 `thrift:"sort_order,5" form:"sort_order" json:"sort_order" query:"sort_order"`
}
func NewMomentFile() *MomentFile {
return &MomentFile{}
}
func (p *MomentFile) InitDefault() {
}
func (p *MomentFile) GetID() (v int64) {
return p.ID
}
func (p *MomentFile) GetMomentID() (v int64) {
return p.MomentID
}
func (p *MomentFile) GetFileURL() (v string) {
return p.FileURL
}
func (p *MomentFile) GetFileType() (v string) {
return p.FileType
}
func (p *MomentFile) GetSortOrder() (v int8) {
return p.SortOrder
}
var fieldIDToName_MomentFile = map[int16]string{
1: "id",
2: "moment_id",
3: "file_url",
4: "file_type",
5: "sort_order",
}
func (p *MomentFile) Read(iprot thrift.TProtocol) (err error) {
var fieldTypeId thrift.TType
var fieldId int16
if _, err = iprot.ReadStructBegin(); err != nil {
goto ReadStructBeginError
}
for {
_, fieldTypeId, fieldId, err = iprot.ReadFieldBegin()
if err != nil {
goto ReadFieldBeginError
}
if fieldTypeId == thrift.STOP {
break
}
switch fieldId {
case 1:
if fieldTypeId == thrift.I64 {
if err = p.ReadField1(iprot); err != nil {
goto ReadFieldError
}
} else if err = iprot.Skip(fieldTypeId); err != nil {
goto SkipFieldError
}
case 2:
if fieldTypeId == thrift.I64 {
if err = p.ReadField2(iprot); err != nil {
goto ReadFieldError
}
} else if err = iprot.Skip(fieldTypeId); err != nil {
goto SkipFieldError
}
case 3:
if fieldTypeId == thrift.STRING {
if err = p.ReadField3(iprot); err != nil {
goto ReadFieldError
}
} else if err = iprot.Skip(fieldTypeId); err != nil {
goto SkipFieldError
}
case 4:
if fieldTypeId == thrift.STRING {
if err = p.ReadField4(iprot); err != nil {
goto ReadFieldError
}
} else if err = iprot.Skip(fieldTypeId); err != nil {
goto SkipFieldError
}
case 5:
if fieldTypeId == thrift.BYTE {
if err = p.ReadField5(iprot); err != nil {
goto ReadFieldError
}
} else if err = iprot.Skip(fieldTypeId); err != nil {
goto SkipFieldError
}
default:
if err = iprot.Skip(fieldTypeId); err != nil {
goto SkipFieldError
}
}
if err = iprot.ReadFieldEnd(); err != nil {
goto ReadFieldEndError
}
}
if err = iprot.ReadStructEnd(); err != nil {
goto ReadStructEndError
}
return nil
ReadStructBeginError:
return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err)
ReadFieldBeginError:
return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err)
ReadFieldError:
return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_MomentFile[fieldId]), err)
SkipFieldError:
return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err)
ReadFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err)
ReadStructEndError:
return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err)
}
func (p *MomentFile) ReadField1(iprot thrift.TProtocol) error {
var _field int64
if v, err := iprot.ReadI64(); err != nil {
return err
} else {
_field = v
}
p.ID = _field
return nil
}
func (p *MomentFile) ReadField2(iprot thrift.TProtocol) error {
var _field int64
if v, err := iprot.ReadI64(); err != nil {
return err
} else {
_field = v
}
p.MomentID = _field
return nil
}
func (p *MomentFile) ReadField3(iprot thrift.TProtocol) error {
var _field string
if v, err := iprot.ReadString(); err != nil {
return err
} else {
_field = v
}
p.FileURL = _field
return nil
}
func (p *MomentFile) ReadField4(iprot thrift.TProtocol) error {
var _field string
if v, err := iprot.ReadString(); err != nil {
return err
} else {
_field = v
}
p.FileType = _field
return nil
}
func (p *MomentFile) ReadField5(iprot thrift.TProtocol) error {
var _field int8
if v, err := iprot.ReadByte(); err != nil {
return err
} else {
_field = v
}
p.SortOrder = _field
return nil
}
func (p *MomentFile) Write(oprot thrift.TProtocol) (err error) {
var fieldId int16
if err = oprot.WriteStructBegin("MomentFile"); err != nil {
goto WriteStructBeginError
}
if p != nil {
if err = p.writeField1(oprot); err != nil {
fieldId = 1
goto WriteFieldError
}
if err = p.writeField2(oprot); err != nil {
fieldId = 2
goto WriteFieldError
}
if err = p.writeField3(oprot); err != nil {
fieldId = 3
goto WriteFieldError
}
if err = p.writeField4(oprot); err != nil {
fieldId = 4
goto WriteFieldError
}
if err = p.writeField5(oprot); err != nil {
fieldId = 5
goto WriteFieldError
}
}
if err = oprot.WriteFieldStop(); err != nil {
goto WriteFieldStopError
}
if err = oprot.WriteStructEnd(); err != nil {
goto WriteStructEndError
}
return nil
WriteStructBeginError:
return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err)
WriteFieldError:
return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err)
WriteFieldStopError:
return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err)
WriteStructEndError:
return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err)
}
func (p *MomentFile) writeField1(oprot thrift.TProtocol) (err error) {
if err = oprot.WriteFieldBegin("id", thrift.I64, 1); err != nil {
goto WriteFieldBeginError
}
if err := oprot.WriteI64(p.ID); err != nil {
return err
}
if err = oprot.WriteFieldEnd(); err != nil {
goto WriteFieldEndError
}
return nil
WriteFieldBeginError:
return thrift.PrependError(fmt.Sprintf("%T write field 1 begin error: ", p), err)
WriteFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T write field 1 end error: ", p), err)
}
func (p *MomentFile) writeField2(oprot thrift.TProtocol) (err error) {
if err = oprot.WriteFieldBegin("moment_id", thrift.I64, 2); err != nil {
goto WriteFieldBeginError
}
if err := oprot.WriteI64(p.MomentID); err != nil {
return err
}
if err = oprot.WriteFieldEnd(); err != nil {
goto WriteFieldEndError
}
return nil
WriteFieldBeginError:
return thrift.PrependError(fmt.Sprintf("%T write field 2 begin error: ", p), err)
WriteFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T write field 2 end error: ", p), err)
}
func (p *MomentFile) writeField3(oprot thrift.TProtocol) (err error) {
if err = oprot.WriteFieldBegin("file_url", thrift.STRING, 3); err != nil {
goto WriteFieldBeginError
}
if err := oprot.WriteString(p.FileURL); err != nil {
return err
}
if err = oprot.WriteFieldEnd(); err != nil {
goto WriteFieldEndError
}
return nil
WriteFieldBeginError:
return thrift.PrependError(fmt.Sprintf("%T write field 3 begin error: ", p), err)
WriteFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T write field 3 end error: ", p), err)
}
func (p *MomentFile) writeField4(oprot thrift.TProtocol) (err error) {
if err = oprot.WriteFieldBegin("file_type", thrift.STRING, 4); err != nil {
goto WriteFieldBeginError
}
if err := oprot.WriteString(p.FileType); err != nil {
return err
}
if err = oprot.WriteFieldEnd(); err != nil {
goto WriteFieldEndError
}
return nil
WriteFieldBeginError:
return thrift.PrependError(fmt.Sprintf("%T write field 4 begin error: ", p), err)
WriteFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T write field 4 end error: ", p), err)
}
func (p *MomentFile) writeField5(oprot thrift.TProtocol) (err error) {
if err = oprot.WriteFieldBegin("sort_order", thrift.BYTE, 5); err != nil {
goto WriteFieldBeginError
}
if err := oprot.WriteByte(p.SortOrder); err != nil {
return err
}
if err = oprot.WriteFieldEnd(); err != nil {
goto WriteFieldEndError
}
return nil
WriteFieldBeginError:
return thrift.PrependError(fmt.Sprintf("%T write field 5 begin error: ", p), err)
WriteFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T write field 5 end error: ", p), err)
}
func (p *MomentFile) String() string {
if p == nil {
return "<nil>"
}
return fmt.Sprintf("MomentFile(%+v)", *p)
}
// 发布动态请求
type CreateMomentRequest struct {
// 发布者用户ID
@@ -13807,7 +14132,7 @@ type CreateMomentRequest struct {
// 发布地点
Location string `thrift:"location,4" form:"location" json:"location" query:"location"`
// 图片URL列表
ImageUrls []string `thrift:"image_urls,5" form:"image_urls" json:"image_urls" query:"image_urls"`
FileUrls []*MomentFile `thrift:"file_urls,5" form:"file_urls" json:"file_urls" query:"file_urls"`
}
func NewCreateMomentRequest() *CreateMomentRequest {
@@ -13833,8 +14158,8 @@ func (p *CreateMomentRequest) GetLocation() (v string) {
return p.Location
}
func (p *CreateMomentRequest) GetImageUrls() (v []string) {
return p.ImageUrls
func (p *CreateMomentRequest) GetFileUrls() (v []*MomentFile) {
return p.FileUrls
}
var fieldIDToName_CreateMomentRequest = map[int16]string{
@@ -13842,7 +14167,7 @@ var fieldIDToName_CreateMomentRequest = map[int16]string{
2: "content",
3: "visibility",
4: "location",
5: "image_urls",
5: "file_urls",
}
func (p *CreateMomentRequest) Read(iprot thrift.TProtocol) (err error) {
@@ -13981,14 +14306,14 @@ func (p *CreateMomentRequest) ReadField5(iprot thrift.TProtocol) error {
if err != nil {
return err
}
_field := make([]string, 0, size)
_field := make([]*MomentFile, 0, size)
values := make([]MomentFile, size)
for i := 0; i < size; i++ {
_elem := &values[i]
_elem.InitDefault()
var _elem string
if v, err := iprot.ReadString(); err != nil {
if err := _elem.Read(iprot); err != nil {
return err
} else {
_elem = v
}
_field = append(_field, _elem)
@@ -13996,7 +14321,7 @@ func (p *CreateMomentRequest) ReadField5(iprot thrift.TProtocol) error {
if err := iprot.ReadListEnd(); err != nil {
return err
}
p.ImageUrls = _field
p.FileUrls = _field
return nil
}
@@ -14109,14 +14434,14 @@ WriteFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T write field 4 end error: ", p), err)
}
func (p *CreateMomentRequest) writeField5(oprot thrift.TProtocol) (err error) {
if err = oprot.WriteFieldBegin("image_urls", thrift.LIST, 5); err != nil {
if err = oprot.WriteFieldBegin("file_urls", thrift.LIST, 5); err != nil {
goto WriteFieldBeginError
}
if err := oprot.WriteListBegin(thrift.STRING, len(p.ImageUrls)); err != nil {
if err := oprot.WriteListBegin(thrift.STRUCT, len(p.FileUrls)); err != nil {
return err
}
for _, v := range p.ImageUrls {
if err := oprot.WriteString(v); err != nil {
for _, v := range p.FileUrls {
if err := v.Write(oprot); err != nil {
return err
}
}

View File

@@ -0,0 +1,117 @@
package model
import (
"google.golang.org/genproto/googleapis/type/decimal"
"gorm.io/gorm"
)
// Asset Type
const (
diamond = iota
goldBean
goldLeaf
)
// source Type
const (
bankcard = iota
usdt
)
type UserAssets struct {
gorm.Model
UserID string `gorm:"column:user_id;type:varchar(32);not null;index:idx_user_id;comment:'用户ID'" json:"user_id"`
Status int8 `gorm:"column:status;type:tinyint unsigned;default:0;comment:'资金账户状态;0:正常,1:异常'" json:"status"`
Diamond decimal.Decimal `gorm:"-" ;json:"diamond"`
GoldBean decimal.Decimal `gorm:"-" ;json:"gold_bean"`
GoldLeaf decimal.Decimal `gorm:"-" ;json:"gold_leaf"`
}
func (*UserAssets) TableName() string {
return "user_assets"
}
type Diamond struct {
gorm.Model
UserID string `gorm:"column:user_id;type:varchar(32);not null;index:idx_user_id;comment:'用户ID'"`
Diamond decimal.Decimal `gorm:"column:diamond;type:decimal(15,2);not null;default:0.00;comment:'钻石余额'"`
}
func (*Diamond) TableName() string {
return "diamond"
}
type GoldBean struct {
gorm.Model
UserID string `gorm:"column:user_id;type:varchar(32);not null;index:idx_user_id;comment:'用户ID'"`
GoldBean decimal.Decimal `gorm:"column:gold_bean;type:decimal(15,2);not null;default:0.00;comment:'金豆余额'"`
}
func (*GoldBean) TableName() string {
return "gold_bean"
}
type GoldLeaf struct {
gorm.Model
UserID string `gorm:"column:user_id;type:varchar(32);not null;index:idx_user_id;comment:'用户ID'"`
GoldLeaf decimal.Decimal `gorm:"column:gold_leaf;type:decimal(15,2);not null;default:0.00;comment:'金叶余额'"`
}
func (*GoldLeaf) TableName() string {
return "gold_leaf"
}
type Bankcard struct {
gorm.Model
UserID string `gorm:"column:user_id;type:varchar(32);not null;index:idx_user_id;comment:'用户ID'"`
BankCardNumber string `gorm:"column:bankcard_number;type:varchar(19);not null;uniqueIndex:uniq_bank_card_number;comment:'银行卡号码'"`
BankName string `gorm:"column:bank_name;type:varchar(100);not null;index:idx_bank_name;comment:'银行名称'"`
AccountType string `gorm:"column:account_type;type:varchar(50);not null;default:'basic';comment:'账户类型'"`
IsDefault bool `gorm:"column:is_default;type:tinyint(1);not null;default:0;index:idx_is_default;comment:'是否默认银行卡'"`
}
func (*Bankcard) TableName() string {
return "bank_card"
}
type AssetHistory struct {
gorm.Model
UserID string `gorm:"column:user_id;type:varchar(32);not null;index:idx_user_id;comment:'用户ID'"`
TargetID string `gorm:"column:target_id;type:bigint;comment:'目标商品id'"`
AssetType int8 `gorm:"column:asset_type;type:tinyint;not null;comment:资产类型(0-钻石,1-金叶,2-金豆)"`
Amount decimal.Decimal `gorm:"column:amount;type:decimal(15,2);not null;default:0.00;comment:'数量'"`
SourceType int8 `gorm:"column:source_type;type:tinyint;not null;comment:来源类型(0-银行卡,1-usdt)"`
SourceAccount string `gorm:"column:source_account;type:varchar(50);not null;index:uniq_bank_card_number;comment:'来源账号'"`
}
func (*AssetHistory) TableName() string {
return "asset_history"
}
// 物品类型枚举,定义支持的消费物品类型
type ItemType uint8
const (
ItemTypeDiamond ItemType = 1 // 钻石
ItemTypeGoldBean ItemType = 2 // 金豆
ItemTypeGoldLeaf ItemType = 3 // 金叶
)
// ConsumptionRecord 物品消费记录表
type ConsumptionRecord struct {
gorm.Model
UserID string `gorm:"column:user_id;type:varchar(32);not null;index:idx_user_id;comment:'用户ID'"`
ItemType ItemType `gorm:"column:item_type;type:tinyint unsigned;not null;index:idx_item_type;comment:'物品类型 1:钻石 2:金豆 3:金叶'"`
Amount decimal.Decimal `gorm:"column:amount;type:decimal(15,2);not null;comment:'消费数量'"`
BeforeAmount decimal.Decimal `gorm:"column:before_amount;type:decimal(15,2);not null;comment:'操作前数量'"`
AfterAmount decimal.Decimal `gorm:"column:after_amount;type:decimal(15,2);not null;comment:'操作后数量'"`
BusinessType string `gorm:"column:business_type;type:varchar(32);not null;index:idx_business_type;comment:'业务类型,如:buy_vip购买会员、send_gift赠送礼物'"`
BusinessID string `gorm:"column:business_id;type:varchar(64);index:idx_business_id;comment:'关联业务ID如订单ID、礼物ID'"`
Remark string `gorm:"column:remark;type:varchar(255);comment:'备注信息'"`
Operator string `gorm:"column:operator;type:varchar(32);comment:'操作人系统操作填system'"`
}
// TableName 指定表名
func (*ConsumptionRecord) TableName() string {
return "consumption_records"
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,12 +5,15 @@ package router
import (
area "acquaintances/biz/router/area"
user "acquaintances/biz/router/user"
user_assets "acquaintances/biz/router/user_assets"
"github.com/cloudwego/hertz/pkg/app/server"
)
// GeneratedRegister registers routers generated by IDL.
func GeneratedRegister(r *server.Hertz) {
//INSERT_POINT: DO NOT DELETE THIS LINE!
user_assets.Register(r)
area.Register(r)
user.Register(r)

View File

@@ -0,0 +1,77 @@
// Code generated by hertz generator.
package user_assets
import (
"github.com/cloudwego/hertz/pkg/app"
)
func rootMw() []app.HandlerFunc {
// your code...
return nil
}
func _v1Mw() []app.HandlerFunc {
// your code...
return nil
}
func _user_assetsMw() []app.HandlerFunc {
// your code...
return nil
}
func _getuserassetsMw() []app.HandlerFunc {
// your code...
return nil
}
func _bankcardMw() []app.HandlerFunc {
// your code...
return nil
}
func _removebankcardMw() []app.HandlerFunc {
// your code...
return nil
}
func _listbankcardbyuserMw() []app.HandlerFunc {
// your code...
return nil
}
func _addbankcardMw() []app.HandlerFunc {
// your code...
return nil
}
func _diamondMw() []app.HandlerFunc {
// your code...
return nil
}
func _adddiamondMw() []app.HandlerFunc {
// your code...
return nil
}
func _gold_beanMw() []app.HandlerFunc {
// your code...
return nil
}
func _addgoldbeanMw() []app.HandlerFunc {
// your code...
return nil
}
func _gold_leafMw() []app.HandlerFunc {
// your code...
return nil
}
func _addgoldleafMw() []app.HandlerFunc {
// your code...
return nil
}

View File

@@ -0,0 +1,45 @@
// Code generated by hertz generator. DO NOT EDIT.
package user_assets
import (
user_assets "acquaintances/biz/handler/user_assets"
"github.com/cloudwego/hertz/pkg/app/server"
)
/*
This file will register all the routes of the services in the master idl.
And it will update automatically when you use the "update" command for the idl.
So don't modify the contents of the file, or your code will be deleted when it is updated.
*/
// Register register routes based on the IDL 'api.${HTTP Method}' annotation.
func Register(r *server.Hertz) {
root := r.Group("/", rootMw()...)
{
_v1 := root.Group("/v1", _v1Mw()...)
{
_user_assets := _v1.Group("/user_assets", _user_assetsMw()...)
_user_assets.GET("/", append(_getuserassetsMw(), user_assets.GetUserAssets)...)
{
_bankcard := _user_assets.Group("/bankcard", _bankcardMw()...)
_bankcard.DELETE("/", append(_removebankcardMw(), user_assets.RemoveBankcard)...)
_bankcard.GET("/", append(_listbankcardbyuserMw(), user_assets.ListBankcardByUser)...)
_bankcard.POST("/", append(_addbankcardMw(), user_assets.AddBankcard)...)
}
{
_diamond := _user_assets.Group("/diamond", _diamondMw()...)
_diamond.POST("/", append(_adddiamondMw(), user_assets.AddDiamond)...)
}
{
_gold_bean := _user_assets.Group("/gold_bean", _gold_beanMw()...)
_gold_bean.POST("/", append(_addgoldbeanMw(), user_assets.AddGoldBean)...)
}
{
_gold_leaf := _user_assets.Group("/gold_leaf", _gold_leafMw()...)
_gold_leaf.POST("/", append(_addgoldleafMw(), user_assets.AddGoldLeaf)...)
}
}
}
}

View File

@@ -147,6 +147,7 @@ struct SmsResponse{
struct CheckOtpRequest{
1: string phone (api.path="phone",api.body="phone")
2: string otp (api.path="otp",api.body="otp")
3: string type (api.path="type",api.body="type")
}
struct CheckOtpResponse{
@@ -436,18 +437,27 @@ struct Moment {
7: ContentStatus status // 状态
8: i32 like_count // 点赞数
9: i32 comment_count // 评论数
10: list<MomentImage> images // 动态图片列表
10: list<MomentFile> files // 动态图片列表
11: string created_at // 创建时间
12: string updated_at // 更新时间
}
// 动态图片结构体
struct MomentFile {
1: i64 id // ID
2: i64 moment_id // 所属动态ID
3: string file_url // 文件URL
4: string file_type // 文件类型
5: i8 sort_order //文件顺序
}
// 发布动态请求
struct CreateMomentRequest {
1: string user_id // 发布者用户ID
2: string content // 动态内容
3: MomentVisibility visibility // 可见性,默认公开
4: string location // 发布地点
5: list<string> image_urls // 图片URL列表
5: list<MomentFile> file_urls // 图片URL列表
}
// 发布动态响应

View File

@@ -0,0 +1,108 @@
namespace go user_assets
enum Code {
Success = 1
ParamInvalid = 2
DBErr = 3
ServerError = 4
}
struct UserAssets {
1: string user_id
2: string diamond
3: string gold_bean
4: string gold_leaf
}
struct GetUserAssetsRequest{
1: string user_id
}
struct GetUserAssetsResponse{
1: UserAssets user_assets
}
struct AddDiamondRequest {
1: string user_id
2: string bankcard_number
3: string diamond
}
struct AddDiamondResponse {
1: Code code
2: string msg
}
struct AddGoldBeanRequest {
1: string user_id
2: string bankcard_number
3: string diamond
}
struct AddGoldBeanResponse {
1: Code code
2: string msg
}
struct AddGoldLeafRequest {
1: string user_id
2: string bankcard_number
3: string diamond
}
struct AddGoldLeafResponse {
1: Code code
2: string msg
}
struct Bankcard {
1: string user_id
2: string bankcard_number
3: string bank_name
4: string account_type
5: bool is_default
}
struct AddBankcardRequest {
1: string user_id
2: string bankcard_number
3: string bank_name
4: string account_type
5: bool is_default
}
struct AddBankcardResponse {
1: Code code
2: string msg
}
struct RemoveBankcardRequest {
1: string user_id
2: string bankcard_number
}
struct RemoveBankcardResponse {
1: Code code
2: string msg
}
struct ListBankcardByUserRequest {
1: string user_id
}
struct ListBankcardByUserResponse {
1: Code code
2: string msg
3: list<Bankcard> bankcards
4: i32 total
}
service UserAssetsService {
GetUserAssetsResponse GetUserAssets(1:GetUserAssetsRequest req)(api.get="/v1/user_assets/") //获取指定用户银行卡列表
AddDiamondResponse AddDiamond(1:AddDiamondRequest req)(api.post="/v1/user_assets/diamond/") //充值钻石
AddGoldBeanResponse AddGoldBean(1:AddGoldBeanRequest req)(api.post="/v1/user_assets/gold_bean/") //充值金豆
AddGoldLeafResponse AddGoldLeaf(1:AddGoldLeafRequest req)(api.post="/v1/user_assets/gold_leaf/") //充值金叶
AddBankcardResponse AddBankcard(1:AddBankcardRequest req)(api.post="/v1/user_assets/bankcard/") //添加银行卡
RemoveBankcardResponse RemoveBankcard(1:RemoveBankcardRequest req)(api.delete="/v1/user_assets/bankcard/") //删除银行卡
ListBankcardByUserResponse ListBankcardByUser(1:ListBankcardByUserRequest req)(api.get="/v1/user_assets/bankcard/") //获取指定用户银行卡列表
}

View File

@@ -2,6 +2,7 @@ package redis
import (
"github.com/cloudwego/hertz/pkg/common/hlog"
"log"
"pushNotificationCenter/protocol"
)
@@ -13,13 +14,48 @@ func PublishToRedis(channel string, message []byte) {
}
// 订阅redis私聊通道消息
func SubscribeToUserChannel(sendChan chan []byte, userid string) {
//
// func SubscribeToUserChannel(sendChan chan []byte, userid string) {
// pubsub := rdb.Subscribe(ctx, protocol.TypePrivate+":"+userid)
// defer pubsub.Close()
//
// ch := pubsub.Channel()
// for msg := range ch {
// sendChan <- []byte(msg.Payload)
// }
// }
func SubscribeToUserChannel(sendChan chan []byte, userid string, done <-chan struct{}) {
// 订阅Redis频道
pubsub := rdb.Subscribe(ctx, protocol.TypePrivate+":"+userid)
defer pubsub.Close()
defer pubsub.Close() // 确保函数退出时关闭Redis订阅
ch := pubsub.Channel()
for msg := range ch {
sendChan <- []byte(msg.Payload)
// 使用for循环结合select同时监听Redis消息和退出信号
for {
select {
case msg, ok := <-ch:
// 1. 检查Redis订阅通道是否关闭
if !ok {
hlog.Errorf("用户 %s 的Redis订阅通道已关闭", userid)
return
}
// 2. 检查sendChan是否可写避免向关闭的通道发送
select {
case sendChan <- []byte(msg.Payload):
// 消息发送成功
case <-done:
// 收到退出信号,优先处理退出
log.Printf("用户 %s 的消息发送已终止", userid)
return
}
case <-done:
// 收到退出信号如WebSocket连接关闭立即退出
log.Printf("用户 %s 的订阅已取消", userid)
return
}
}
}

View File

@@ -5,23 +5,41 @@ import (
"github.com/cloudwego/hertz/pkg/common/hlog"
"net/http"
"pushNotificationCenter/biz/dal/redis"
"sync"
"time"
"pushNotificationCenter/protocol"
"github.com/gorilla/websocket"
)
// 客户端连接
// 定义全局互斥锁(确保已在代码中声明)
var clientsMu sync.Mutex
// Client结构体
type Client struct {
ID string
Conn *websocket.Conn
Send chan []byte
UserID string // 用户ID
ID string
Conn *websocket.Conn
Send chan []byte
UserID string
Done chan struct{}
isClosed bool // 标记连接是否已关闭
mu sync.Mutex // 保护isClosed和lastPingAt的并发读写
lastPingAt time.Time // 最后一次收到客户端ping的时间
}
// 客户端管理器
var clients = make(map[string]*Client) // 用户ID -> Client
// 封装安全关闭done通道的方法
func (c *Client) closeDone() {
c.mu.Lock()
defer c.mu.Unlock()
if !c.isClosed {
close(c.Done)
c.isClosed = true // 标记为已关闭
}
}
// clients映射和upgrader保持不变
var clients = make(map[string]*Client) // 用户ID -> Client
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 允许所有跨域请求
@@ -29,83 +47,243 @@ var upgrader = websocket.Upgrader{
}
func HandleConnections(w http.ResponseWriter, r *http.Request) {
// 升级为WebSocket连接
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
hlog.Error(err)
return
}
// 仅在HandleConnections中管理Conn的关闭避免重复关闭
defer ws.Close()
// 读取用户ID (实际应用中应从认证信息获取)
// 读取用户ID
userID := r.URL.Query().Get("user_id")
if userID == "" {
hlog.Info("未提供用户ID")
hlog.Info("未提供用户ID,关闭连接")
return
}
// 创建客户端
// 创建客户端实例
done := make(chan struct{})
client := &Client{
ID: userID,
UserID: userID,
Conn: ws,
Send: make(chan []byte, 256),
Done: done,
}
// 注册客户端
// 注册客户端(加锁确保并发安全)
clientsMu.Lock()
clients[userID] = client
clientsMu.Unlock()
hlog.Infof("用户 %s 已连接", userID)
// 订阅redis通道
go redis.SubscribeToUserChannel(client.Send, client.UserID)
//go redis.SubscribeToUserGroupChannel(client.Send)
go redis.SubscribeToSystemChannel(client.Send)
// 启动订阅协程传入done通道
go redis.SubscribeToUserChannel(client.Send, client.UserID, done)
// 写协程
// 启动写协程
go client.writePump()
// 启动应用层心跳检测协程
go client.startAppHeartbeat()
// 执行读协程(阻塞,退出时表示连接关闭)
client.readPump()
// 读协程退出后,执行统一清理
hlog.Infof("用户 %s 断开连接,开始清理资源", userID)
// 1. 关闭done通道通知所有关联协程退出
client.closeDone()
// 2. 从clients中删除加锁
clientsMu.Lock()
delete(clients, userID)
clientsMu.Unlock()
// 3. 关闭Send通道防止后续发送
close(client.Send)
}
// 示例:向所有在线用户广播消息(需加锁)
func Broadcast(message []byte) {
clientsMu.Lock()
defer clientsMu.Unlock() // 确保锁释放
for _, client := range clients {
// 向每个客户端的Send通道发送消息需检查通道状态
select {
case client.Send <- message:
default:
hlog.Warnf("用户 %s 的消息通道已满,消息丢弃", client.UserID)
}
}
}
func (c *Client) readPump() {
defer func() {
// 连接关闭时清理
delete(clients, c.UserID)
c.Conn.Close()
hlog.Infof("用户 %s 已断开", c.UserID)
}()
// 初始化最后心跳时间为连接建立时间
c.mu.Lock()
c.lastPingAt = time.Now()
c.mu.Unlock()
for {
_, message, err := c.Conn.ReadMessage()
mt, message, err := c.Conn.ReadMessage()
if err != nil {
// 连接错误,退出循环触发清理
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
hlog.Errorf("读取错误: %v", err)
hlog.Errorf("用户 %s 读取错误: %v", c.UserID, err)
}
break
}
// 处理应用层心跳ping请求
if mt == websocket.TextMessage && string(message) == protocol.TypePing {
c.mu.Lock()
c.lastPingAt = time.Now() // 更新最后心跳时间
c.mu.Unlock()
// 回复pong响应
if err := c.Conn.WriteMessage(websocket.TextMessage, []byte(protocol.TypePong)); err != nil {
hlog.Errorf("用户 %s 回复pong失败: %v", c.UserID, err)
return // 回复失败,断开连接
}
hlog.Debugf("用户 %s 收到ping已回复pong", c.UserID)
continue // 跳过后续消息处理
}
// 处理非心跳的业务消息
handleIncomingMessage(c.UserID, message)
}
}
// 应用层心跳检测检查客户端是否超时未发送ping
func (c *Client) startAppHeartbeat() {
// 心跳超时阈值如30秒未收到ping则断开
const timeout = 30 * time.Second
// 检测间隔每10秒检查一次
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-c.Done:
// 连接已关闭,退出检测
return
case <-ticker.C:
c.mu.Lock()
lastPing := c.lastPingAt
c.mu.Unlock()
// 计算距离上次ping的时间
elapsed := time.Since(lastPing)
if elapsed > timeout {
hlog.Warnf("用户 %s 心跳超时(%v未收到ping断开连接", c.UserID, elapsed)
c.closeDone() // 触发连接关闭
return
}
hlog.Debugf("用户 %s 心跳正常最后ping时间: %v", c.UserID, lastPing)
}
}
}
// 确保writePump正确处理done和Send通道关闭的情况参考之前的改进
func (c *Client) writePump() {
defer c.Conn.Close()
defer func() {
if r := recover(); r != nil {
hlog.Errorf("writePump panic: %v", r)
}
}()
for {
select {
case message, ok := <-c.Send:
if !ok {
// 通道关闭
c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
// Send通道关闭发送关闭消息
if c.Conn != nil {
_ = c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
}
return
}
if c.Conn == nil {
hlog.Errorf("用户 %s 连接已失效,跳过消息发送", c.UserID)
return
}
if err := c.Conn.WriteMessage(websocket.TextMessage, message); err != nil {
hlog.Errorf("发送错误: %v", err)
hlog.Errorf("用户 %s 发送错误: %v", c.UserID, err)
return
}
case <-c.Done:
// 收到退出信号,退出写协程
hlog.Infof("用户 %s writePump 退出", c.UserID)
return
}
}
}
// func (c *Client) writePump() {
// defer c.Conn.Close()
//
// for {
// select {
// case message, ok := <-c.Send:
// if !ok {
// // 通道关闭
// c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
// return
// }
//
// if err := c.Conn.WriteMessage(websocket.TextMessage, message); err != nil {
// hlog.Errorf("发送错误: %v", err)
// return
// }
// }
// }
// }
//func (c *Client) writePump() {
// // 移除defer c.Conn.Close(),避免重复关闭(由连接管理器统一关闭)
// defer func() {
// // 捕获panic避免单个连接错误导致整个服务崩溃
// if r := recover(); r != nil {
// hlog.Errorf("writePump panic: %v", r)
// }
// // 关闭Send通道通知其他协程停止发送消息
// close(c.Send)
// }()
//
// for {
// select {
// case message, ok := <-c.Send:
// // 1. 检查Send通道是否关闭
// if !ok {
// // 通道关闭时尝试发送CloseMessage但需先检查连接有效性
// if c.Conn == nil {
// hlog.Warnf("连接已关闭无法发送CloseMessage")
// return
// }
// // 发送CloseMessage时也可能失败需处理错误
// if err := c.Conn.WriteMessage(websocket.CloseMessage, []byte{}); err != nil {
// hlog.Errorf("发送CloseMessage失败: %v", err)
// }
// return
// }
//
// // 2. 检查连接是否已为nil避免空指针
// if c.Conn == nil {
// hlog.Errorf("连接已失效nil跳过消息发送")
// return
// }
//
// // 3. 尝试写入消息,处理可能的连接关闭错误
// if err := c.Conn.WriteMessage(websocket.TextMessage, message); err != nil {
// hlog.Errorf("发送错误: %v", err)
// // 连接已不可用,退出循环
// return
// }
// }
// }
//}
func handleIncomingMessage(senderID string, msg []byte) {
var message protocol.Message
if err := json.Unmarshal(msg, &message); err != nil {

View File

@@ -9,6 +9,8 @@ const (
TypeSys = "sys"
TypeAudioCall = "audioCall"
TypeVideoCall = "videoCall"
TypePing = "ping"
TypePong = "pong"
)
// 消息固定值

BIN
userAuthCenter.zip Normal file

Binary file not shown.

View File

@@ -88,5 +88,5 @@ func LoadRedis(file *ini.File) {
RedisHost = file.Section("redis").Key("RedisHost").MustString("127.0.0.1")
RedisPort = file.Section("redis").Key("RedisPort").MustString("6379")
RedisUser = file.Section("redis").Key("RedisUser").MustString("root")
RedisPassWord = file.Section("redis").Key("RedisPassWord").MustString("123456")
RedisPassWord = file.Section("redis").Key("RedisPassWord").MustString("cdsrh")
}

Binary file not shown.