fix version 1

This commit is contained in:
2026-09-22 21:15:34 +08:00
parent 9f86366638
commit d63d7e8b3a
277 changed files with 9959 additions and 1514 deletions

View File

@@ -21,7 +21,7 @@ func Run() {
impl.NewImpl()
// 初始化服务
s := server.New(config.Spec.Addr)
s := server.New(nil)
srv := service.New(
s.Grpc,
&service.Options{

View File

@@ -1,45 +1,35 @@
Name: {ServiceKey}
ListenOn: 0.0.0.0:12212
Service: feed
Port: 12212
Dsn: postgres://postgres:CHANGE_ME@47.108.57.74:5432/milu?sslmode=disable&TimeZone=Asia/Shanghai
Databases:
Driver: postgres
Source:
- postgres://postgres:CHANGE_ME@127.0.0.1:5432/bsm_dev?sslmode=disable&TimeZone=Asia/Shanghai
# cache DB的选择请在后面直接带参数,不带会自动HASH计算选择DB库。
# DB库设置例:redis://null:Weidong1214@139.159.232.50:6379/12,代表DB12
# Cache: redis://null:YouMake!*@139.159.232.50:6379/
# DB库设置例:redis://null:CHANGE_ME@139.159.232.50:6379/12,代表DB12
Cache: redis://null:CHANGE_ME@127.0.0.1:6379/
#Etcd:
# Hosts:
# - 127.0.0.1:2379
# Key: service.{Workspace}.{ServiceKey}.rpc
Etcd:
Endpoints:
- "127.0.0.1:2379"
# 匿名访问清单,自动注入网关层
Anonymous:
Key: anonymous.{Workspace}
Urls:
- pb.Check.Hello
# 微服务设置
MicroService:
Enable: false
# 日志记录
# Gateway 设置
Gateway:
Enable: true
Port: 12213
# 日志记录(Level:0-debug 1-info 2-warn 3-error 4-fatal)
Log:
ServiceName: {ServiceKey}
Mode: file
Path: logs/{ServiceKey}
Stat: false
Name: feed
Level: 0
Dir: logs/feed
Console: true
File: true
# 性能监控
Prometheus:
Host: 127.0.0.1
Port: 22212
Path: /metrics
# 链路追踪
Telemetry:
Name: {Workspace}.{ServiceKey}.{RuntimeMode}
Endpoint: http://139.159.232.50:14268/api/traces
Sampler: 1.0
Batcher: jaeger
# MQ Pulsar
#Pulsar:
# Endpoints: 127.0.0.1
# Token: 9091
# Namespaces: {Workspace}
# 微服务调用密钥
SecretKey: CHANGE_ME

View File

@@ -1,44 +1,34 @@
Name: {ServiceKey}
ListenOn: 0.0.0.0:12212
Service: feed
Port: 12212
Dsn: postgres://prod:MakeW2023~PROD@192.168.0.224:5432/scf?sslmode=disable&TimeZone=Asia/Shanghai
Databases:
Driver: postgres
Source:
- postgres://prod:CHANGE_ME@192.168.0.224:5432/scf?sslmode=disable&TimeZone=Asia/Shanghai
# cache DB的选择请在后面直接带参数,不带会自动HASH计算选择DB库。
Cache: redis://null:CHANGE_ME@192.168.0.43:6379/
Cache: redis://null:CHANGE_ME@192.168.0.43:6379/
Etcd:
Hosts:
- 192.168.0.83:2379
Key: service.{Workspace}.{ServiceKey}.rpc
Endpoints:
- "192.168.0.83:2379"
# 匿名访问清单,自动注入网关层
Anonymous:
Key: anonymous.{Workspace}
Urls:
- initialize.Check.Hello
# 微服务设置
MicroService:
Enable: false
# 日志记录
# Gateway 设置
Gateway:
Enable: true
Port: 12213
# 日志记录(Level:0-debug 1-info 2-warn 3-error 4-fatal)
Log:
ServiceName: {ServiceKey}
Mode: file
Path: logs/{ServiceKey}
Stat: false
Name: feed
Level: 1
Dir: logs/feed
Console: false
File: true
# 性能监控
Prometheus:
Host: 127.0.0.1
Port: 22212
Path: /metrics
# 链路追踪
Telemetry:
Name: {Workspace}.{ServiceKey}.{RuntimeMode}
Endpoint: http://139.159.232.50:14268/api/traces
Sampler: 1.0
Batcher: jaeger
# MQ Pulsar
#Pulsar:
# Endpoints: 127.0.0.1
# Token: 9091
# Namespaces: {Workspace}
# 微服务调用密钥
SecretKey: CHANGE_ME

View File

@@ -1,45 +1,34 @@
Name: {ServiceKey}
ListenOn: 0.0.0.0:12212
Service: feed
Port: 12212
Dsn: postgres://postgres:CHANGE_ME@47.108.57.74:5432/milu?sslmode=disable&TimeZone=Asia/Shanghai
Databases:
Driver: postgres
Source:
- postgres://postgres:CHANGE_ME@127.0.0.1:5432/bsm_test?sslmode=disable&TimeZone=Asia/Shanghai
# cache DB的选择请在后面直接带参数,不带会自动HASH计算选择DB库。
# DB库设置例:redis://null:Weidong1214@139.159.232.50:6379/12,代表DB12
Cache: redis://null:CHANGE_ME@127.0.0.1:6379/
Cache: redis://null:CHANGE_ME@127.0.0.1:6379/
Etcd:
Hosts:
- 127.0.0.1:2379
Key: service.{Workspace}.{ServiceKey}.rpc
Endpoints:
- "127.0.0.1:2379"
# 匿名访问清单,自动注入网关层
Anonymous:
Key: anonymous.{Workspace}
Urls:
- pb.Check.Hello
# 微服务设置
MicroService:
Enable: false
# 日志记录
# Gateway 设置
Gateway:
Enable: true
Port: 12213
# 日志记录(Level:0-debug 1-info 2-warn 3-error 4-fatal)
Log:
ServiceName: {ServiceKey}
Mode: file
Path: logs/{ServiceKey}
Stat: false
Name: feed
Level: 0
Dir: logs/feed
Console: true
File: true
# 性能监控
Prometheus:
Host: 127.0.0.1
Port: 22212
Path: /metrics
# 链路追踪
Telemetry:
Name: {Workspace}.{ServiceKey}.{RuntimeMode}
Endpoint: http://139.159.232.50:14268/api/traces
Sampler: 1.0
Batcher: jaeger
# MQ Pulsar
#Pulsar:
# Endpoints: 127.0.0.1
# Token: 9091
# Namespaces: {Workspace}
# 微服务调用密钥
SecretKey: CHANGE_ME

View File

@@ -22,6 +22,11 @@ func Action(ctx context.Context, in *pb.PostActionRequest) (reply *pb.DataStatus
if in.GetActionOp() == "" || in.GetActionType() == "" || in.GetIdentity() == "" {
return nil, errcode.ErrInvalidArgument
}
// 白名单校验,避免非法操作/类别拼出空列名或未绑定目标表的更新语句
if (in.ActionOp != "ilike" && in.ActionOp != "unlike") ||
(in.ActionType != "post" && in.ActionType != "comment") {
return nil, errcode.ErrInvalidArgument
}
if err := models.LikeAction(in.ActionOp, in.ActionType, in.Identity); err != nil {
printer.Error(err.Error())
return nil, err

View File

@@ -15,7 +15,7 @@ import (
// 修改推文
func Change(ctx context.Context, in *pb.PostItem) (reply *pb.DataStatusReply, err error) {
_, err = service.ParseMetaCtx(ctx, nil)
auth, err := service.ParseMetaCtx(ctx, nil)
if err != nil {
return nil, err
}
@@ -31,17 +31,22 @@ func Change(ctx context.Context, in *pb.PostItem) (reply *pb.DataStatusReply, er
if len(in.GetAttachs()) != 0 {
for _, val := range in.Attachs {
postData.Attachs = append(postData.Attachs, models.FeedRelateAttach{
Identity: utils.UUID(),
AttachType: val.AttachType,
Url: val.Url,
PostIdentity: in.Identity,
Identity: utils.UUID(),
AttachType: val.AttachType,
Url: val.Url,
})
}
}
err = models.ChargePost(&postData)
// 只能修改自己发布的动态
affected, err := models.ChargePost(&postData, auth.Identity)
if err != nil {
printer.Error(err.Error())
return nil, errcode.ErrDB
}
if affected == 0 {
return nil, errcode.ErrPermissionDenied
}
return &pb.DataStatusReply{
Data: postData.Identity,
Timeseq: time.Now().UnixNano(),

View File

@@ -14,18 +14,22 @@ import (
// 删除评论
func DeleteComment(ctx context.Context, in *pb.IdentRequest) (reply *pb.DataStatusReply, err error) {
_, err = service.ParseMetaCtx(ctx, nil)
auth, err := service.ParseMetaCtx(ctx, nil)
if err != nil {
return nil, err
}
if in.GetId() == 0 && in.GetIdentity() == "" {
return nil, errcode.ErrInvalidArgument
}
err = models.DeleteComment(in.Identity)
// 只能删除自己发表的评论
affected, err := models.DeleteComment(in.Identity, auth.Identity)
if err != nil {
printer.Error(err.Error())
return nil, errcode.ErrDB
}
if affected == 0 {
return nil, errcode.ErrPermissionDenied
}
return &pb.DataStatusReply{
Data: vars.OK,
Timeseq: time.Now().UnixNano(),

View File

@@ -10,20 +10,27 @@ import (
"git.apinb.com/bsm-sdk/core/service"
)
// maxPageSize 单页最大条数,避免一次请求拉取过多数据
const maxPageSize int64 = 50
// 推文列表
func Fetch(ctx context.Context, in *pb.FetchRequest) (reply *pb.PostListReply, err error) {
_, err = service.ParseMetaCtx(ctx, nil)
auth, err := service.ParseMetaCtx(ctx, nil)
if err != nil {
return nil, err
}
// 归一化分页参数:页码下限 1,单页条数上限 maxPageSize
if in.GetPageNo() <= 0 {
in.PageNo = 1
}
if in.GetPageSize() <= 0 {
in.PageSize = 10
}
if in.GetPageSize() > maxPageSize {
in.PageSize = maxPageSize
}
data, cnt, err := models.PostList(in.PageNo, in.PageSize, in.Params["key"])
data, cnt, err := models.PostList(in.PageNo, in.PageSize, in.Params["key"], auth.Identity)
if err != nil {
printer.Error(err.Error())
return nil, errcode.ErrDB

View File

@@ -14,7 +14,7 @@ import (
// 删除推文
func Remove(ctx context.Context, in *pb.IdentRequest) (reply *pb.DataStatusReply, err error) {
_, err = service.ParseMetaCtx(ctx, nil)
auth, err := service.ParseMetaCtx(ctx, nil)
if err != nil {
return nil, err
}
@@ -22,11 +22,15 @@ func Remove(ctx context.Context, in *pb.IdentRequest) (reply *pb.DataStatusReply
return nil, errcode.ErrInvalidArgument
}
err = models.DeletePost(in.Identity)
// 只能删除自己发布的动态
affected, err := models.DeletePost(in.Identity, auth.Identity)
if err != nil {
printer.Error(err.Error())
return nil, errcode.ErrDB
}
if affected == 0 {
return nil, errcode.ErrPermissionDenied
}
return &pb.DataStatusReply{
Data: vars.OK,

View File

@@ -1,12 +1,16 @@
package models
import (
"fmt"
"errors"
"bsm/full/module/social/feed/internal/impl"
"git.apinb.com/bsm-sdk/core/errcode"
"gorm.io/gorm"
)
// maxActionCounter 互动计数上限,避免计数被无条件刷高
const maxActionCounter int64 = 1000000
func InitData() error {
return nil
}
@@ -27,47 +31,159 @@ func CreatePost(data *FeedPost) error {
return err
}
func ChargePost(data *FeedPost) error {
err := impl.DBService.Where("identity = ?", data.Identity).Updates(&data).Error
return err
// ChargePost 修改动态,仅允许作者本人修改;is_open=false 需真正写入,附件变更需真正落库
func ChargePost(data *FeedPost, passportIdentity string) (affected int64, err error) {
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
// 用 map 更新,避免 GORM 结构体更新忽略 is_open=false 这类零值
res := tx.Model(&FeedPost{}).
Where("identity = ? AND passport_identity = ?", data.Identity, passportIdentity).
Updates(map[string]any{"content": data.Content, "is_open": data.IsOpen})
if res.Error != nil {
return res.Error
}
affected = res.RowsAffected
if affected == 0 {
// 动态不存在或非本人发布,直接结束,不再处理附件
return nil
}
// 附件变更真正落库:请求携带附件时整体替换原附件
if len(data.Attachs) > 0 {
if err := tx.Where("post_identity = ?", data.Identity).Delete(&FeedRelateAttach{}).Error; err != nil {
return err
}
if err := tx.Create(&data.Attachs).Error; err != nil {
return err
}
}
return nil
})
return
}
func DeletePost(identity string) error {
err := impl.DBService.Where("identity = ?", identity).Delete(&FeedPost{}).Error
return err
// DeletePost 删除动态,仅允许作者本人删除
func DeletePost(identity, passportIdentity string) (affected int64, err error) {
res := impl.DBService.Where("identity = ? AND passport_identity = ?", identity, passportIdentity).Delete(&FeedPost{})
return res.RowsAffected, res.Error
}
// 点赞/踩操作
func LikeAction(action_op, action_type, identity string) (err error) {
var column string
tx := impl.DBService
if action_type == "post" {
var (
column string
tx = impl.DBService
)
// 显式绑定目标表,避免 action_type 取任意值时目标表不确定
switch action_type {
case "post":
tx = tx.Model(&FeedPost{})
} else if action_type == "comment" {
case "comment":
tx = tx.Model(&FeedComment{})
default:
return errcode.ErrInvalidArgument
}
if action_op == "ilike" {
switch action_op {
case "ilike":
column = "cnt_like"
} else if action_type == "unlike" {
case "unlike":
column = "cnt_unlike"
default:
return errcode.ErrInvalidArgument
}
if err = tx.Where("identity = ?", identity).UpdateColumn(column, gorm.Expr(fmt.Sprintf("%s + ?", column), 1)).Error; err != nil {
return err
// 条件更新:目标必须存在且计数未达上限,避免计数被无限刷高
res := tx.Where("identity = ? AND "+column+" < ?", identity, maxActionCounter).
UpdateColumn(column, gorm.Expr(column+" + ?", 1))
if res.Error != nil {
return res.Error
}
return
if res.RowsAffected == 0 {
return errcode.ErrRecordNotFound
}
return nil
}
// PostList
func PostList(page, size int64, tag string) (list []*FeedPost, cnt int64, err error) {
tx := impl.DBService.Order("created_at desc")
// PostList 动态列表,仅返回公开动态或调用者本人发布的动态
func PostList(page, size int64, tag, viewerIdentity string) (list []*FeedPost, cnt int64, err error) {
list = make([]*FeedPost, 0)
tx := impl.DBService.Model(&FeedPost{}).
Where("status <> ?", -1).
Where("is_open = ? OR passport_identity = ?", true, viewerIdentity)
if tag != "" {
tx = tx.Joins("join feed_relate_tags on feed_relate_tags.post_identity = feed_post.identity", impl.DBService.Where(&FeedRelateTags{Key: tag}))
tx = tx.Where("key = ?", tag)
tx = tx.Joins("join feed_relate_tags on feed_relate_tags.post_identity = feed_post.identity").
Where("feed_relate_tags.key = ?", tag)
}
// 使句柄可重复使用,统计与列表查询互不影响
tx = tx.Session(&gorm.Session{})
if err = tx.Count(&cnt).Error; err != nil {
return nil, 0, err
}
tx.Limit(int(page)).Offset(int((page - 1) * size))
err = tx.Find(&list).Count(&cnt).Error
fmt.Print("posts", list)
return
err = tx.Order("created_at desc").
Limit(int(size)).
Offset(int((page - 1) * size)).
Find(&list).Error
if err != nil {
return nil, 0, err
}
// 标签与附件是 gorm:"-" 字段,GORM 不会加载,需二次查询回填
if err = fillRelates(list); err != nil {
return nil, 0, err
}
return list, cnt, nil
}
// fillRelates 按 post_identity 批量回填动态的标签与附件
func fillRelates(list []*FeedPost) error {
if len(list) == 0 {
return nil
}
identities := make([]string, 0, len(list))
for _, item := range list {
identities = append(identities, item.Identity)
}
var attachs []FeedRelateAttach
if err := impl.DBService.Where("post_identity IN ?", identities).Find(&attachs).Error; err != nil {
return err
}
var relates []FeedRelateTags
if err := impl.DBService.Where("post_identity IN ?", identities).Find(&relates).Error; err != nil {
return err
}
// 标签名称存于 feed_tags 字典,按 key 批量查出后回填
contents := map[string]string{}
if len(relates) > 0 {
keys := make([]string, 0, len(relates))
for _, relate := range relates {
keys = append(keys, relate.Key)
}
var dict []FeedTags
if err := impl.DBService.Where("key IN ?", keys).Find(&dict).Error; err != nil {
return err
}
for _, tag := range dict {
contents[tag.Key] = tag.Content
}
}
attachMap := map[string][]FeedRelateAttach{}
for _, attach := range attachs {
attachMap[attach.PostIdentity] = append(attachMap[attach.PostIdentity], attach)
}
tagMap := map[string][]FeedRelateTags{}
for _, relate := range relates {
relate.Content = contents[relate.Key]
tagMap[relate.PostIdentity] = append(tagMap[relate.PostIdentity], relate)
}
for _, item := range list {
item.Attachs = attachMap[item.Identity]
item.Tags = tagMap[item.Identity]
}
return nil
}
func AddTags(data *[]FeedTags) (err error) {
@@ -77,16 +193,19 @@ func AddTags(data *[]FeedTags) (err error) {
}
return err
}
// ModifyTags 按标签 key 定位并保存,feed_tags 表没有 identity 列
func ModifyTags(data *FeedTags) (err error) {
err = impl.DBService.Where("identity = ?", data.Key).Save(data).Error
err = impl.DBService.Where("key = ?", data.Key).Save(data).Error
if err != nil {
return err
}
return err
}
// DeleteTags 按标签 key 删除,feed_tags 表没有 identity 列
func DeleteTags(key string) (err error) {
err = impl.DBService.Where("identity = ?", key).Delete(&FeedTags{}).Error
err = impl.DBService.Where("key = ?", key).Delete(&FeedTags{}).Error
if err != nil {
return err
}
@@ -102,6 +221,8 @@ func TagsList() (list []*FeedTags, cnt int64, err error) {
func AddComment(comment *FeedComment, authorIdentity string) (err error) {
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
// 记录评论作者,供后续归属校验使用
comment.PassportIdentity = authorIdentity
// 添加评论,更新文章评论量
err := tx.Create(comment).Table("feed_post").Where("identity = ?", comment.PostIdentity).UpdateColumn("cnt_comment", gorm.Expr("cnt_comment + ?", 1)).Error
if err != nil {
@@ -119,7 +240,33 @@ func AddComment(comment *FeedComment, authorIdentity string) (err error) {
return
}
func DeleteComment(identity string) (err error) {
// DeleteComment 删除评论,仅允许评论作者本人删除,并同步递减所属动态的评论数
func DeleteComment(identity, passportIdentity string) (affected int64, err error) {
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
var comment FeedComment
// 只有作者本人的评论才允许删除
if err := tx.Where("identity = ? AND passport_identity = ?", identity, passportIdentity).First(&comment).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return err
}
res := tx.Delete(&FeedComment{}, "identity = ?", identity)
if res.Error != nil {
return res.Error
}
affected = res.RowsAffected
// 同步递减所属动态的评论数,并做下限保护避免出现负数
if comment.PostIdentity != "" {
if err := tx.Model(&FeedPost{}).
Where("identity = ? AND cnt_comment > 0", comment.PostIdentity).
UpdateColumn("cnt_comment", gorm.Expr("cnt_comment - ?", 1)).Error; err != nil {
return err
}
}
return nil
})
return
}

View File

@@ -5,6 +5,7 @@ import (
pb "bsm/full/module/social/feed/pb"
"context"
"git.apinb.com/bsm-sdk/core/printer"
gwRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
@@ -17,10 +18,16 @@ type Server struct {
grpcConns map[string]*grpc.ClientConn // 连接池
}
func New(addr string) *Server {
func New(grpcServ *grpc.Server) *Server {
// grpcServ 为空表示独立进程启动,自行创建 gRPC Server
standalone := grpcServ == nil
if standalone {
grpcServ = grpc.NewServer()
}
srv := &Server{
Ctx: context.Background(),
Grpc: grpc.NewServer(),
Grpc: grpcServ,
grpcConns: make(map[string]*grpc.ClientConn),
}
@@ -30,7 +37,28 @@ func New(addr string) *Server {
pb.RegisterTagServer(srv.Grpc, NewTagServer())
pb.RegisterTimelineServer(srv.Grpc, NewTimelineServer())
reflection.Register(srv.Grpc)
if standalone {
// 独立进程需自行初始化网关路由并注册 handler,否则 /feed.* 全部 404
srv.Mux = gwRuntime.NewServeMux()
if err := RegisterGateway(srv.Ctx, srv.Mux); err != nil {
printer.Error(err.Error())
}
reflection.Register(srv.Grpc)
}
return srv
}
// RegisterGateway 注册四个 service 的 gateway handler
func RegisterGateway(ctx context.Context, mux *gwRuntime.ServeMux) error {
if err := pb.RegisterPostHandlerServer(ctx, mux, NewPostServer()); err != nil {
return err
}
if err := pb.RegisterSettingHandlerServer(ctx, mux, NewSettingServer()); err != nil {
return err
}
if err := pb.RegisterTagHandlerServer(ctx, mux, NewTagServer()); err != nil {
return err
}
return pb.RegisterTimelineHandlerServer(ctx, mux, NewTimelineServer())
}

View File

@@ -0,0 +1,32 @@
package service
import (
"bsm/full/module/social/feed/internal/impl"
"git.apinb.com/bsm-sdk/core/cache/redis"
cache "github.com/patrickmn/go-cache"
clientv3 "go.etcd.io/etcd/client/v3"
"gorm.io/gorm"
)
// Dependencies 聚合宿主注入的共享基础设施
type Dependencies struct {
Redis *redis.RedisClient
Etcd *clientv3.Client
DB *gorm.DB
Cache *cache.Cache
}
func applyDependencies(deps Dependencies) {
if deps.Redis != nil {
impl.RedisService = deps.Redis
}
if deps.Etcd != nil {
impl.EtcdService = deps.Etcd
}
if deps.DB != nil {
impl.DBService = deps.DB
}
if deps.Cache != nil {
impl.MemorySerice = deps.Cache
}
}

View File

@@ -0,0 +1,26 @@
package service
import (
"context"
"bsm/full/module/social/feed/internal/server"
gwRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
)
type ExposeOptions struct {
Dependencies
GRPC *grpc.Server
Gateway *gwRuntime.ServeMux
}
// Expose 供聚合宿主接入:注册 gRPC service 与网关路由
func Expose(options ExposeOptions) error {
applyDependencies(options.Dependencies)
server.New(options.GRPC)
if options.Gateway == nil {
return nil
}
return server.RegisterGateway(context.Background(), options.Gateway)
}