Merge pull request #563 from rocboss/dev

sync source code
pull/572/head
北野 - Michael Li 1 year ago committed by GitHub
commit eb9fd0fbf5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

1
.gitattributes vendored

@ -1,3 +1,4 @@
*.ts linguist-language=go
*.js linguist-language=go
*.css linguist-language=go
*.vue linguist-language=go

@ -47,7 +47,7 @@ jobs:
name: Test
strategy:
matrix:
go-version: [ 1.20.x, 1.21.x ]
go-version: [ 1.21.x, 1.22.x ]
platform: [ ubuntu-latest, macos-latest ]
runs-on: ${{ matrix.platform }}
steps:
@ -66,7 +66,7 @@ jobs:
name: TestOnWindows
strategy:
matrix:
go-version: [ 1.20.x, 1.21.x ]
go-version: [ 1.21.x, 1.22.x ]
platform: [ windows-latest ]
runs-on: ${{ matrix.platform }}
steps:

@ -2,7 +2,11 @@
All notable changes to paopao-ce are documented in this file.
## 0.6.0+dev ([`dev`](https://github.com/rocboss/paopao-ce/tree/dev))
TODO;
### Added
- add all-in-one docker image build scripts.
- frontend: add tweets filter support use tag for home page and make it as default behavior.
- add pin topic support.
- support upload webp format image as picture when send tweet.
## 0.5.2
### Change

@ -1,4 +1,4 @@
# syntax=docker/dockerfile:experimental
# syntax=docker/dockerfile:1
# build frontend
FROM node:19-alpine as frontend
@ -22,9 +22,9 @@ ARG USE_DIST=no
WORKDIR /paopao-ce
COPY . .
COPY --from=frontend /web/dist ./web/dist
ENV GOPROXY=https://goproxy.cn
RUN [ $EMBED_UI != yes ] || make build TAGS='go_json'
RUN [ $EMBED_UI = yes ] || make build TAGS='slim embed go_json'
ENV GOPROXY=https://goproxy.cn,direct
RUN --mount=type=cache,target=$GOPATH/go/pkg,id=paopao-ce-gopkg [ $EMBED_UI != yes ] || make buildx TAGS='go_json'
RUN --mount=type=cache,target=$GOPATH/go/pkg,id=paopao-ce-gopkg [ $EMBED_UI = yes ] || make buildx TAGS='slim embed go_json'
FROM bitbus/paopao-ce-backend-runner:latest
ARG API_HOST

@ -0,0 +1,37 @@
# syntax=docker/dockerfile:1
# build frontend
FROM node:19-alpine as frontend
ARG API_HOST
ARG USE_API_HOST=yes
ARG EMBED_UI=yes
ARG USE_DIST=no
WORKDIR /web
COPY web/ ./
RUN [ $EMBED_UI != yes ] || [ $USE_API_HOST != yes ] || echo "VITE_HOST='$API_HOST'">.env.local
RUN [ $EMBED_UI != yes ] || [ $USE_DIST != no ] || (yarn && yarn build)
RUN [ $EMBED_UI = yes ] || mkdir dist || echo ""
# build backend
FROM bitbus/paopao-ce-backend-builder:latest AS backend
ARG API_HOST
ARG USE_API_HOST=yes
ARG EMBED_UI=yes
ARG USE_DIST=no
WORKDIR /paopao-ce
COPY . .
COPY --from=frontend /web/dist ./web/dist
ENV GOPROXY=https://goproxy.cn,direct
RUN --mount=type=cache,target=$GOPATH/go/pkg,id=paopao-ce-gopkg [ $EMBED_UI != yes ] || make buildx TAGS='go_json migration'
RUN --mount=type=cache,target=$GOPATH/go/pkg,id=paopao-ce-gopkg [ $EMBED_UI = yes ] || make buildx TAGS='slim embed go_json migration'
FROM bitbus/paopao-ce-allinone-runner:latest
ARG API_HOST
ARG USE_API_HOST=yes
ARG EMBED_UI=yes
ARG USE_DIST=no
ENV TZ=Asia/Shanghai
WORKDIR /app
COPY --from=backend /paopao-ce/release/paopao .

@ -32,6 +32,10 @@ LDFLAGS = -X "${MOD_NAME}/pkg/version.version=${BUILD_VERSION}" \
all: fmt build
build:
@echo Build paopao-ce
@go build -pgo=auto -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $(RELEASE_ROOT)/$(TARGET)
buildx:
@go mod download
@echo Build paopao-ce
@go build -pgo=auto -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $(RELEASE_ROOT)/$(TARGET)
@ -115,10 +119,10 @@ pre-commit: fmt
.PHONY: install-protobuf-plugins
install-protobuf-plugins:
@go install github.com/bufbuild/buf/cmd/buf@v1.25.0
@go install github.com/bufbuild/buf/cmd/protoc-gen-buf-breaking@v1.25.0
@go install github.com/bufbuild/buf/cmd/protoc-gen-buf-lint@v1.25.0
@go install github.com/bufbuild/connect-go/cmd/protoc-gen-connect-go@latest
@go install github.com/bufbuild/buf/cmd/buf@v1.28.1
@go install github.com/bufbuild/buf/cmd/protoc-gen-buf-breaking@v1.28.1
@go install github.com/bufbuild/buf/cmd/protoc-gen-buf-lint@v1.28.1
@go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest
@go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
@go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
@go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

@ -52,7 +52,7 @@ PaoPao主要由以下优秀的开源项目/工具构建
* [Mir](https://github.com/alimy/mir 'go-mir')
* [Buf](https://github.com/bufbuild/buf 'buf')
* [gRPC](https://github.com/grpc/grpc-go 'grpc-go')
* [Zinc](https://zinclabs.io/ 'zinc')
* [Meilisearch](https://www.meilisearch.com/ 'meilisearch')
#### 前端:
* [Naive UI](https://www.naiveui.com/)
@ -69,9 +69,7 @@ PaoPao主要由以下优秀的开源项目/工具构建
* Node.js (14+)
* MySQL (5.7+)
* Redis
* Zinc
> Zinc是一款轻量级全文搜索引擎可以查阅 <https://zincsearch.com/> 安装
* Meilisearch
以上环境版本为PaoPao官方的开发版本仅供参考其他版本的环境未进行充分测试
@ -221,6 +219,23 @@ PaoPao主要由以下优秀的开源项目/工具构建
docker run -d -p 8010:80 your/paopao-ce:web
```
* All-In-One:
```sh
# 构建Image
docker buildx build --build-arg USE_DIST="yes" -t your/paopao-ce:all-in-one-latest -f Dockerfile.allinone .
# 运行
docker run --name paopao-ce-allinone -d -p 8000:8008 -p 7700:7700 -v ./data/custom:/app/custom -v ./data/meili_data:/app/meili_data your/paopao-ce:all-in-one-latest
# 或者使用官方Image运行
docker run --name paopao-ce-allinone -d -p 8000:8008 -p 7700:7700 -v ./data/custom:/app/custom -v ./data/meili_data:/app/meili_data bitbus/paopao-ce:all-in-one-latest
# 或者使用官方Image运行 + 自定义config.yaml
docker run --name paopao-ce-allinone -d -p 8000:8008 -p 7700:7700 -v ./config.yaml:/app/config.yaml -v ./data/custom:/app/custom -v ./data/meili_data:/app/meili_data bitbus/paopao-ce:all-in-one-latest
```
> 注意在`config.yaml` 中`Meili.ApiKey`的值必须与容器中meili启动时设定的`MEILI_MASTER_KEY`环境变量值相同,默认为`paopao-meilisearch`. 可以在docker启动容器时通过`-e MEILI_MASTER_KEY=<custom-key>`设置该值。
### 方式三. 使用 docker-compose 运行
```sh
git clone https://github.com/rocboss/paopao-ce.git

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v4.0.0
// - mir v4.2.0
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v4.0.0
// - mir v4.2.0
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v4.0.0
// - mir v4.2.0
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v4.0.0
// - mir v4.2.0
package v1
@ -22,6 +22,7 @@ type _render_ interface {
type _default_ interface {
Bind(*gin.Context, any) mir.Error
BindJson(*gin.Context, any) mir.Error
Render(*gin.Context, any, mir.Error)
}

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v4.0.0
// - mir v4.2.0
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v4.0.0
// - mir v4.2.0
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v4.0.0
// - mir v4.2.0
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v4.0.0
// - mir v4.2.0
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v4.0.0
// - mir v4.2.0
package v1
@ -56,7 +56,7 @@ func RegisterFriendshipServant(e *gin.Engine, s Friendship) {
default:
}
req := new(web.DeleteFriendReq)
if err := s.Bind(c, req); err != nil {
if err := s.BindJson(c, req); err != nil {
s.Render(c, nil, err)
return
}
@ -69,7 +69,7 @@ func RegisterFriendshipServant(e *gin.Engine, s Friendship) {
default:
}
req := new(web.RejectFriendReq)
if err := s.Bind(c, req); err != nil {
if err := s.BindJson(c, req); err != nil {
s.Render(c, nil, err)
return
}
@ -82,7 +82,7 @@ func RegisterFriendshipServant(e *gin.Engine, s Friendship) {
default:
}
req := new(web.AddFriendReq)
if err := s.Bind(c, req); err != nil {
if err := s.BindJson(c, req); err != nil {
s.Render(c, nil, err)
return
}
@ -95,7 +95,7 @@ func RegisterFriendshipServant(e *gin.Engine, s Friendship) {
default:
}
req := new(web.RequestingFriendReq)
if err := s.Bind(c, req); err != nil {
if err := s.BindJson(c, req); err != nil {
s.Render(c, nil, err)
return
}

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v4.0.0
// - mir v4.2.0
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v4.0.0
// - mir v4.2.0
package v1
@ -20,6 +20,7 @@ type Priv interface {
UnfollowTopic(*web.UnfollowTopicReq) mir.Error
FollowTopic(*web.FollowTopicReq) mir.Error
PinTopic(*web.PinTopicReq) (*web.PinTopicResp, mir.Error)
StickTopic(*web.StickTopicReq) (*web.StickTopicResp, mir.Error)
ThumbsDownTweetReply(*web.TweetReplyThumbsReq) mir.Error
ThumbsUpTweetReply(*web.TweetReplyThumbsReq) mir.Error
@ -91,6 +92,20 @@ func RegisterPrivServant(e *gin.Engine, s Priv, m ...PrivChain) {
}
s.Render(c, nil, s.FollowTopic(req))
})
router.Handle("POST", "/topic/pin", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
return
default:
}
req := new(web.PinTopicReq)
if err := s.Bind(c, req); err != nil {
s.Render(c, nil, err)
return
}
resp, err := s.PinTopic(req)
s.Render(c, resp, err)
})
router.Handle("POST", "/topic/stick", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
@ -406,6 +421,10 @@ func (UnimplementedPrivServant) FollowTopic(req *web.FollowTopicReq) mir.Error {
return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented))
}
func (UnimplementedPrivServant) PinTopic(req *web.PinTopicReq) (*web.PinTopicResp, mir.Error) {
return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented))
}
func (UnimplementedPrivServant) StickTopic(req *web.StickTopicReq) (*web.StickTopicResp, mir.Error) {
return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented))
}

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v4.0.0
// - mir v4.2.0
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v4.0.0
// - mir v4.2.0
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v4.0.0
// - mir v4.2.0
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v4.0.0
// - mir v4.2.0
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v4.0.0
// - mir v4.2.0
package v1

@ -5,9 +5,9 @@
package corev1connect
import (
connect "connectrpc.com/connect"
context "context"
errors "errors"
connect_go "github.com/bufbuild/connect-go"
v1 "github.com/rocboss/paopao-ce/auto/rpc/core/v1"
http "net/http"
strings "strings"
@ -18,7 +18,7 @@ import (
// generated with a version of connect newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of connect or updating the connect
// version compiled into your binary.
const _ = connect_go.IsAtLeastVersion0_1_0
const _ = connect.IsAtLeastVersion1_13_0
const (
// AuthenticateServiceName is the fully-qualified name of the AuthenticateService service.
@ -44,11 +44,19 @@ const (
AuthenticateServiceLogoutProcedure = "/core.v1.AuthenticateService/logout"
)
// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package.
var (
authenticateServiceServiceDescriptor = v1.File_core_v1_auth_proto.Services().ByName("AuthenticateService")
authenticateServicePreLoginMethodDescriptor = authenticateServiceServiceDescriptor.Methods().ByName("preLogin")
authenticateServiceLoginMethodDescriptor = authenticateServiceServiceDescriptor.Methods().ByName("login")
authenticateServiceLogoutMethodDescriptor = authenticateServiceServiceDescriptor.Methods().ByName("logout")
)
// AuthenticateServiceClient is a client for the core.v1.AuthenticateService service.
type AuthenticateServiceClient interface {
PreLogin(context.Context, *connect_go.Request[v1.User]) (*connect_go.Response[v1.ActionReply], error)
Login(context.Context, *connect_go.Request[v1.User]) (*connect_go.Response[v1.LoginReply], error)
Logout(context.Context, *connect_go.Request[v1.User]) (*connect_go.Response[v1.ActionReply], error)
PreLogin(context.Context, *connect.Request[v1.User]) (*connect.Response[v1.ActionReply], error)
Login(context.Context, *connect.Request[v1.User]) (*connect.Response[v1.LoginReply], error)
Logout(context.Context, *connect.Request[v1.User]) (*connect.Response[v1.ActionReply], error)
}
// NewAuthenticateServiceClient constructs a client for the core.v1.AuthenticateService service. By
@ -58,54 +66,57 @@ type AuthenticateServiceClient interface {
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewAuthenticateServiceClient(httpClient connect_go.HTTPClient, baseURL string, opts ...connect_go.ClientOption) AuthenticateServiceClient {
func NewAuthenticateServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) AuthenticateServiceClient {
baseURL = strings.TrimRight(baseURL, "/")
return &authenticateServiceClient{
preLogin: connect_go.NewClient[v1.User, v1.ActionReply](
preLogin: connect.NewClient[v1.User, v1.ActionReply](
httpClient,
baseURL+AuthenticateServicePreLoginProcedure,
opts...,
connect.WithSchema(authenticateServicePreLoginMethodDescriptor),
connect.WithClientOptions(opts...),
),
login: connect_go.NewClient[v1.User, v1.LoginReply](
login: connect.NewClient[v1.User, v1.LoginReply](
httpClient,
baseURL+AuthenticateServiceLoginProcedure,
opts...,
connect.WithSchema(authenticateServiceLoginMethodDescriptor),
connect.WithClientOptions(opts...),
),
logout: connect_go.NewClient[v1.User, v1.ActionReply](
logout: connect.NewClient[v1.User, v1.ActionReply](
httpClient,
baseURL+AuthenticateServiceLogoutProcedure,
opts...,
connect.WithSchema(authenticateServiceLogoutMethodDescriptor),
connect.WithClientOptions(opts...),
),
}
}
// authenticateServiceClient implements AuthenticateServiceClient.
type authenticateServiceClient struct {
preLogin *connect_go.Client[v1.User, v1.ActionReply]
login *connect_go.Client[v1.User, v1.LoginReply]
logout *connect_go.Client[v1.User, v1.ActionReply]
preLogin *connect.Client[v1.User, v1.ActionReply]
login *connect.Client[v1.User, v1.LoginReply]
logout *connect.Client[v1.User, v1.ActionReply]
}
// PreLogin calls core.v1.AuthenticateService.preLogin.
func (c *authenticateServiceClient) PreLogin(ctx context.Context, req *connect_go.Request[v1.User]) (*connect_go.Response[v1.ActionReply], error) {
func (c *authenticateServiceClient) PreLogin(ctx context.Context, req *connect.Request[v1.User]) (*connect.Response[v1.ActionReply], error) {
return c.preLogin.CallUnary(ctx, req)
}
// Login calls core.v1.AuthenticateService.login.
func (c *authenticateServiceClient) Login(ctx context.Context, req *connect_go.Request[v1.User]) (*connect_go.Response[v1.LoginReply], error) {
func (c *authenticateServiceClient) Login(ctx context.Context, req *connect.Request[v1.User]) (*connect.Response[v1.LoginReply], error) {
return c.login.CallUnary(ctx, req)
}
// Logout calls core.v1.AuthenticateService.logout.
func (c *authenticateServiceClient) Logout(ctx context.Context, req *connect_go.Request[v1.User]) (*connect_go.Response[v1.ActionReply], error) {
func (c *authenticateServiceClient) Logout(ctx context.Context, req *connect.Request[v1.User]) (*connect.Response[v1.ActionReply], error) {
return c.logout.CallUnary(ctx, req)
}
// AuthenticateServiceHandler is an implementation of the core.v1.AuthenticateService service.
type AuthenticateServiceHandler interface {
PreLogin(context.Context, *connect_go.Request[v1.User]) (*connect_go.Response[v1.ActionReply], error)
Login(context.Context, *connect_go.Request[v1.User]) (*connect_go.Response[v1.LoginReply], error)
Logout(context.Context, *connect_go.Request[v1.User]) (*connect_go.Response[v1.ActionReply], error)
PreLogin(context.Context, *connect.Request[v1.User]) (*connect.Response[v1.ActionReply], error)
Login(context.Context, *connect.Request[v1.User]) (*connect.Response[v1.LoginReply], error)
Logout(context.Context, *connect.Request[v1.User]) (*connect.Response[v1.ActionReply], error)
}
// NewAuthenticateServiceHandler builds an HTTP handler from the service implementation. It returns
@ -113,21 +124,24 @@ type AuthenticateServiceHandler interface {
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewAuthenticateServiceHandler(svc AuthenticateServiceHandler, opts ...connect_go.HandlerOption) (string, http.Handler) {
authenticateServicePreLoginHandler := connect_go.NewUnaryHandler(
func NewAuthenticateServiceHandler(svc AuthenticateServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
authenticateServicePreLoginHandler := connect.NewUnaryHandler(
AuthenticateServicePreLoginProcedure,
svc.PreLogin,
opts...,
connect.WithSchema(authenticateServicePreLoginMethodDescriptor),
connect.WithHandlerOptions(opts...),
)
authenticateServiceLoginHandler := connect_go.NewUnaryHandler(
authenticateServiceLoginHandler := connect.NewUnaryHandler(
AuthenticateServiceLoginProcedure,
svc.Login,
opts...,
connect.WithSchema(authenticateServiceLoginMethodDescriptor),
connect.WithHandlerOptions(opts...),
)
authenticateServiceLogoutHandler := connect_go.NewUnaryHandler(
authenticateServiceLogoutHandler := connect.NewUnaryHandler(
AuthenticateServiceLogoutProcedure,
svc.Logout,
opts...,
connect.WithSchema(authenticateServiceLogoutMethodDescriptor),
connect.WithHandlerOptions(opts...),
)
return "/core.v1.AuthenticateService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
@ -146,14 +160,14 @@ func NewAuthenticateServiceHandler(svc AuthenticateServiceHandler, opts ...conne
// UnimplementedAuthenticateServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedAuthenticateServiceHandler struct{}
func (UnimplementedAuthenticateServiceHandler) PreLogin(context.Context, *connect_go.Request[v1.User]) (*connect_go.Response[v1.ActionReply], error) {
return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("core.v1.AuthenticateService.preLogin is not implemented"))
func (UnimplementedAuthenticateServiceHandler) PreLogin(context.Context, *connect.Request[v1.User]) (*connect.Response[v1.ActionReply], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("core.v1.AuthenticateService.preLogin is not implemented"))
}
func (UnimplementedAuthenticateServiceHandler) Login(context.Context, *connect_go.Request[v1.User]) (*connect_go.Response[v1.LoginReply], error) {
return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("core.v1.AuthenticateService.login is not implemented"))
func (UnimplementedAuthenticateServiceHandler) Login(context.Context, *connect.Request[v1.User]) (*connect.Response[v1.LoginReply], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("core.v1.AuthenticateService.login is not implemented"))
}
func (UnimplementedAuthenticateServiceHandler) Logout(context.Context, *connect_go.Request[v1.User]) (*connect_go.Response[v1.ActionReply], error) {
return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("core.v1.AuthenticateService.logout is not implemented"))
func (UnimplementedAuthenticateServiceHandler) Logout(context.Context, *connect.Request[v1.User]) (*connect.Response[v1.ActionReply], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("core.v1.AuthenticateService.logout is not implemented"))
}

@ -5,9 +5,9 @@
package greetv1connect
import (
connect "connectrpc.com/connect"
context "context"
errors "errors"
connect_go "github.com/bufbuild/connect-go"
v1 "github.com/rocboss/paopao-ce/auto/rpc/greet/v1"
http "net/http"
strings "strings"
@ -18,7 +18,7 @@ import (
// generated with a version of connect newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of connect or updating the connect
// version compiled into your binary.
const _ = connect_go.IsAtLeastVersion0_1_0
const _ = connect.IsAtLeastVersion1_13_0
const (
// GreetServiceName is the fully-qualified name of the GreetService service.
@ -37,9 +37,15 @@ const (
GreetServiceGreetProcedure = "/greet.v1.GreetService/Greet"
)
// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package.
var (
greetServiceServiceDescriptor = v1.File_greet_v1_greet_proto.Services().ByName("GreetService")
greetServiceGreetMethodDescriptor = greetServiceServiceDescriptor.Methods().ByName("Greet")
)
// GreetServiceClient is a client for the greet.v1.GreetService service.
type GreetServiceClient interface {
Greet(context.Context, *connect_go.Request[v1.GreetRequest]) (*connect_go.Response[v1.GreetResponse], error)
Greet(context.Context, *connect.Request[v1.GreetRequest]) (*connect.Response[v1.GreetResponse], error)
}
// NewGreetServiceClient constructs a client for the greet.v1.GreetService service. By default, it
@ -49,30 +55,31 @@ type GreetServiceClient interface {
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewGreetServiceClient(httpClient connect_go.HTTPClient, baseURL string, opts ...connect_go.ClientOption) GreetServiceClient {
func NewGreetServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) GreetServiceClient {
baseURL = strings.TrimRight(baseURL, "/")
return &greetServiceClient{
greet: connect_go.NewClient[v1.GreetRequest, v1.GreetResponse](
greet: connect.NewClient[v1.GreetRequest, v1.GreetResponse](
httpClient,
baseURL+GreetServiceGreetProcedure,
opts...,
connect.WithSchema(greetServiceGreetMethodDescriptor),
connect.WithClientOptions(opts...),
),
}
}
// greetServiceClient implements GreetServiceClient.
type greetServiceClient struct {
greet *connect_go.Client[v1.GreetRequest, v1.GreetResponse]
greet *connect.Client[v1.GreetRequest, v1.GreetResponse]
}
// Greet calls greet.v1.GreetService.Greet.
func (c *greetServiceClient) Greet(ctx context.Context, req *connect_go.Request[v1.GreetRequest]) (*connect_go.Response[v1.GreetResponse], error) {
func (c *greetServiceClient) Greet(ctx context.Context, req *connect.Request[v1.GreetRequest]) (*connect.Response[v1.GreetResponse], error) {
return c.greet.CallUnary(ctx, req)
}
// GreetServiceHandler is an implementation of the greet.v1.GreetService service.
type GreetServiceHandler interface {
Greet(context.Context, *connect_go.Request[v1.GreetRequest]) (*connect_go.Response[v1.GreetResponse], error)
Greet(context.Context, *connect.Request[v1.GreetRequest]) (*connect.Response[v1.GreetResponse], error)
}
// NewGreetServiceHandler builds an HTTP handler from the service implementation. It returns the
@ -80,11 +87,12 @@ type GreetServiceHandler interface {
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewGreetServiceHandler(svc GreetServiceHandler, opts ...connect_go.HandlerOption) (string, http.Handler) {
greetServiceGreetHandler := connect_go.NewUnaryHandler(
func NewGreetServiceHandler(svc GreetServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
greetServiceGreetHandler := connect.NewUnaryHandler(
GreetServiceGreetProcedure,
svc.Greet,
opts...,
connect.WithSchema(greetServiceGreetMethodDescriptor),
connect.WithHandlerOptions(opts...),
)
return "/greet.v1.GreetService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
@ -99,6 +107,6 @@ func NewGreetServiceHandler(svc GreetServiceHandler, opts ...connect_go.HandlerO
// UnimplementedGreetServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedGreetServiceHandler struct{}
func (UnimplementedGreetServiceHandler) Greet(context.Context, *connect_go.Request[v1.GreetRequest]) (*connect_go.Response[v1.GreetResponse], error) {
return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("greet.v1.GreetService.Greet is not implemented"))
func (UnimplementedGreetServiceHandler) Greet(context.Context, *connect.Request[v1.GreetRequest]) (*connect.Response[v1.GreetResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("greet.v1.GreetService.Greet is not implemented"))
}

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc-gen-go v1.32.0
// protoc (unknown)
// source: core/v1/auth.proto

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc-gen-go v1.32.0
// protoc (unknown)
// source: greet/v1/greet.proto

@ -0,0 +1,26 @@
#!/bin/sh
# eg.1 : sh build-image.sh
# eg.2, set image: sh build-image.sh bitbus/paopao-ce
VERSION=`git describe --tags --always | cut -f1,2 -d "-"` # eg.: 0.2.5
IMAGE="bitbus/paopao-ce"
if [ -n "$1" ]; then
IMAGE="$1"
fi
if [ -n "$2" ]; then
VERSION="$2"
fi
# build image
docker buildx build \
--build-arg USE_DIST="yes" \
--tag "$IMAGE:all-in-one-${VERSION}" \
--tag "$IMAGE:all-in-one-latest" \
. -f Dockerfile.allinone
# push to image rep
# if [ -n "$1" ]; then
# docker push "$IMAGE:${VERSION}"
# docker push "$IMAGE:latest"
# fi

@ -2,7 +2,7 @@ version: '3.1'
services:
db:
image: mysql:8.0
image: mysql:${MYSQL_TAG:-8.0}
restart: always
environment:
MYSQL_DATABASE: paopao
@ -33,7 +33,7 @@ services:
# - paopao-network
# redis:
# image: redis:7.2.1-alpine
# image: redis:${REDIS_TAG:-7.2.1-alpine}
# restart: always
# ports:
# - 6379:6379
@ -41,7 +41,7 @@ services:
# - paopao-network
redis:
image: redis/redis-stack:7.2.0-v2
image: redis/redis-stack:${REDIS_STACK_TAG:-7.2.0-v6}
restart: always
ports:
- 6379:6379
@ -67,7 +67,7 @@ services:
# - paopao-network
meili:
image: getmeili/meilisearch:v1.4
image: getmeili/meilisearch:${MEILI_TAG:-v1.7}
restart: always
ports:
- 7700:7700
@ -87,7 +87,7 @@ services:
# - paopao-network
# openobserve:
# image: public.ecr.aws/zinclabs/openobserve:latest
# image: public.ecr.aws/zinclabs/openobserve:${OPENOBSERVE_TAG:-latest}
# restart: always
# ports:
# - 5080:5080
@ -101,7 +101,7 @@ services:
# - paopao-network
# pyroscope:
# image: pyroscope/pyroscope:latest
# image: pyroscope/pyroscope:${PYROSCOPE_TAG:-latest}
# restart: always
# ports:
# - 4040:4040
@ -111,7 +111,7 @@ services:
# - paopao-network
# phpmyadmin:
# image: phpmyadmin:5.2
# image: phpmyadmin:${PHPMYADMIN_TAG:-5.2}
# depends_on:
# - db
# ports:
@ -124,7 +124,7 @@ services:
# - paopao-network
backend:
image: bitbus/paopao-ce:0.5
image: bitbus/paopao-ce:${PAOPAOCE_TAG:-nightly}
restart: always
depends_on:
- db

@ -0,0 +1,3 @@
# Docker for paopao-ce
TODO;

@ -0,0 +1,13 @@
#!/bin/sh
set -xe
# Create paopao user for paopao-ce
addgroup -S paopao
adduser -G paopao -H -D -g 'paopao User' paopao -h /app -s /bin/sh && usermod -p '*' paopao && passwd -u paopao
# echo "export PAOPAO_CUSTOM=${PAOPAO_CUSTOM}" >> /etc/profile
# Final cleaning
mv /app/docker/config.yaml /app/config.yaml
rm -rf /app/docker/build
rm /app/docker/README.md

@ -0,0 +1,71 @@
App: # APP基础设置项
RunMode: debug
AttachmentIncomeRate: 0.8
MaxCommentCount: 10
DefaultContextTimeout: 60
DefaultPageSize: 10
MaxPageSize: 100
Features:
Default: ["Web", "Frontend:EmbedWeb", "Meili", "LocalOSS", "Sqlite3", "LoggerFile", "Migration"]
Sms: "SmsJuhe"
WebServer: # Web服务
HttpIp: 0.0.0.0
HttpPort: 8008
ReadTimeout: 60
WriteTimeout: 60
SmsJuhe:
Gateway: https://v.juhe.cn/sms/send
Key:
TplID:
TplVal: "#code#=%s&#m#=%d"
Logger: # 日志通用配置
Level: debug # 日志级别 panic|fatal|error|warn|info|debug|trace
LoggerFile: # 使用File写日志
SavePath: custom/logs
FileName: app
FileExt: .log
JWT: # 鉴权加密
Secret: 18a6413dc4fe394c66345ebe501b2f26
Issuer: paopao-api
Expire: 86400
Meili: # Meili搜索配置
Host: 127.0.0.1:7700
Index: paopao-data
ApiKey: paopao-meilisearch
Secure: False
ObjectStorage: # 对象存储通用配置
RetainInDays: 2 # 临时对象过期时间多少天
TempDir: tmp # 临时对象存放目录名
LocalOSS: # 本地文件OSS存储配置
SavePath: custom/oss
Secure: False
Bucket: paopao
Domain: 127.0.0.1:8008
Database: # Database通用配置
LogLevel: error # 日志级别 silent|error|warn|info
TablePrefix: p_ # 表名前缀
Sqlite3: # Sqlite3数据库
Path: custom/paopao-ce.db
Redis:
InitAddress:
- 127.0.0.1:6379
WebProfile:
UseFriendship: true # 前端是否使用好友体系
EnableTrendsBar: true # 广场页面是否开启动态条栏功能
EnableWallet: false # 是否开启钱包功能
AllowTweetAttachment: true # 是否允许推文附件
AllowTweetAttachmentPrice: true # 是否允许推文付费附件
AllowTweetVideo: true # 是否允许视频推文
AllowUserRegister: true # 是否允许用户注册
AllowPhoneBind: true # 是否允许手机绑定
DefaultTweetMaxLength: 2000 # 推文允许输入的最大长度, 默认2000字值的范围需要查询后端支持的最大字数
TweetWebEllipsisSize: 400 # Web端推文作为feed显示的最长字数默认400字
TweetMobileEllipsisSize: 300 # 移动端推文作为feed显示的最长字数默认300字
DefaultTweetVisibility: friend # 推文可见性,默认好友可见 值: public/following/friend/private
DefaultMsgLoopInterval: 5000 # 拉取未读消息的间隔,单位:毫秒, 默认5000ms
CopyrightTop: "2023 paopao.info"
CopyrightLeft: "Roc's Me"
CopyrightLeftLink: ""
CopyrightRight: "泡泡(PaoPao)开源社区"
CopyrightRightLink: "https://www.paopao.info"

@ -0,0 +1,3 @@
#!/bin/sh
# do nothing now

@ -0,0 +1,8 @@
#!/bin/sh
if test -f ./setup; then
# shellcheck disable=SC2039,SC1091,SC3046
source ./setup
fi
exec gosu ${USER} /bin/meilisearch

@ -0,0 +1,3 @@
#!/bin/sh
cd /app/meili_data || exit 1

@ -0,0 +1,8 @@
#!/bin/sh
if test -f ./setup; then
# shellcheck disable=SC2039,SC1091,SC3046
source ./setup
fi
exec gosu ${USER} /app/paopao serve

@ -0,0 +1,5 @@
#!/bin/sh
# sleep 10s等待meilisearch启动完成这是笨方法暂时先凑着后面再找更靠谱的法子这里之所以要等待meili启动完成是因为paopao-ce首次启动时需要初始化一次meili的index如果初始化失败就无法在后续使用。
sleep 10
cd /app || exit 1

@ -0,0 +1,8 @@
#!/bin/sh
if test -f ./setup; then
# shellcheck disable=SC2039,SC1091,SC3046
source ./setup
fi
exec docker-entrypoint.sh redis-server

@ -0,0 +1,35 @@
#!/bin/sh
create_volume_subfolder() {
# only change ownership if needed, if using an nfs mount this could be expensive
if [ "$USER:$USER" != "$(stat /app -c '%U:%G')" ]
then
# Modify the owner of /app dir, make $USER(paopao) user have permission to create sub-dir in /app.
chown -R "$USER:$USER" /app
fi
# Create VOLUME subfolder
for f in /app/custom /app/meili_data; do
if ! test -d $f; then
gosu "$USER" mkdir -p $f
fi
done
}
setids() {
export USER=paopao
PUID=${PUID:-1000}
PGID=${PGID:-1000}
groupmod -o -g "$PGID" $USER
usermod -o -u "$PUID" $USER
}
setids
create_volume_subfolder
# Exec CMD or S6 by default if nothing present
if [ $# -gt 0 ];then
exec "$@"
else
exec /bin/s6-svscan /app/gogs/docker/s6/
fi

186
go.mod

@ -1,111 +1,112 @@
module github.com/rocboss/paopao-ce
go 1.20
go 1.21
require (
connectrpc.com/connect v1.16.2
github.com/Masterminds/semver/v3 v3.2.1
github.com/RoaringBitmap/roaring v1.5.0
github.com/RoaringBitmap/roaring v1.9.4
github.com/afocus/captcha v0.0.0-20191010092841-4bd1f21c8868
github.com/alimy/mir/v4 v4.0.0
github.com/alimy/tryst v0.8.3
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible
github.com/alimy/mir/v4 v4.2.0-alpha.5
github.com/alimy/tryst v0.21.0
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/allegro/bigcache/v3 v3.1.0
github.com/bufbuild/connect-go v1.10.0
github.com/bytedance/sonic v1.10.1
github.com/cockroachdb/errors v1.11.1
github.com/bytedance/sonic v1.11.9
github.com/cockroachdb/errors v1.11.3
github.com/disintegration/imaging v1.6.2
github.com/fatih/color v1.15.0
github.com/getsentry/sentry-go v0.24.1
github.com/gin-contrib/cors v1.4.0
github.com/gin-gonic/gin v1.9.1
github.com/go-resty/resty/v2 v2.9.1
github.com/goccy/go-json v0.10.2
github.com/gofrs/uuid/v5 v5.0.0
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/golang-migrate/migrate/v4 v4.15.2
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.23.9+incompatible
github.com/fatih/color v1.17.0
github.com/getsentry/sentry-go v0.28.1
github.com/gin-contrib/cors v1.7.2
github.com/gin-gonic/gin v1.10.0
github.com/go-resty/resty/v2 v2.12.0
github.com/goccy/go-json v0.10.3
github.com/gofrs/uuid/v5 v5.2.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/golang-migrate/migrate/v4 v4.17.1
github.com/grafana/pyroscope-go v1.1.2
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.6+incompatible
github.com/json-iterator/go v1.1.12
github.com/meilisearch/meilisearch-go v0.25.1
github.com/minio/minio-go/v7 v7.0.63
github.com/onsi/ginkgo/v2 v2.12.1
github.com/onsi/gomega v1.28.0
github.com/prometheus/client_golang v1.16.0
github.com/pyroscope-io/client v0.7.2
github.com/redis/rueidis v1.0.19
github.com/meilisearch/meilisearch-go v0.27.2
github.com/minio/minio-go/v7 v7.0.66
github.com/onsi/ginkgo/v2 v2.19.0
github.com/onsi/gomega v1.33.1
github.com/prometheus/client_golang v1.19.1
github.com/redis/rueidis v1.0.37
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.3
github.com/smartwalle/alipay/v3 v3.2.16
github.com/smartwalle/alipay/v3 v3.2.20
github.com/sourcegraph/conc v0.3.0
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.16.0
github.com/tencentyun/cos-go-sdk-v5 v0.7.43
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.18.2
github.com/tencentyun/cos-go-sdk-v5 v0.7.52
github.com/yinheli/mahonia v0.0.0-20131226213531-0eef680515cc
go.uber.org/automaxprocs v1.5.3
google.golang.org/grpc v1.58.2
google.golang.org/protobuf v1.31.0
golang.org/x/crypto v0.25.0
google.golang.org/grpc v1.64.0
google.golang.org/protobuf v1.34.1
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/resty.v1 v1.12.0
gorm.io/driver/mysql v1.5.1
gorm.io/driver/postgres v1.5.2
gorm.io/driver/sqlite v1.5.3
gorm.io/gorm v1.25.4
gorm.io/plugin/dbresolver v1.4.7
gorm.io/driver/mysql v1.5.6
gorm.io/driver/postgres v1.5.7
gorm.io/driver/sqlite v1.5.6
gorm.io/gorm v1.25.10
gorm.io/plugin/dbresolver v1.5.1
gorm.io/plugin/soft_delete v1.2.1
modernc.org/sqlite v1.26.0
modernc.org/sqlite v1.29.6
)
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.2.0 // indirect
github.com/bits-and-blooms/bitset v1.12.0 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect
github.com/clbanning/mxj v1.8.4 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.6 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.3.1 // indirect
github.com/jackc/pgx/v5 v5.5.4 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lib/pq v1.10.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -113,47 +114,46 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mozillazg/go-httpheader v0.2.1 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/pyroscope-io/godeltaprof v0.1.2 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/smartwalle/ncrypto v1.0.3 // indirect
github.com/smartwalle/ngx v1.0.7 // indirect
github.com/smartwalle/nsign v1.0.8 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/smartwalle/ncrypto v1.0.4 // indirect
github.com/smartwalle/ngx v1.0.9 // indirect
github.com/smartwalle/nsign v1.0.9 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.40.0 // indirect
github.com/valyala/fasthttp v1.52.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect
golang.org/x/image v0.0.0-20210216034530-4410531fe030 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.12.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.24.1 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.6.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.41.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)

2163
go.sum

File diff suppressed because it is too large Load Diff

@ -128,8 +128,8 @@ func setupSetting(suite []string, noDefault bool) error {
}
CacheSetting.CientSideCacheExpire *= time.Second
EventManagerSetting.TickWaitTime *= time.Second
MetricManagerSetting.TickWaitTime *= time.Second
EventManagerSetting.MaxIdleTime *= time.Second
MetricManagerSetting.MaxIdleTime *= time.Second
JWTSetting.Expire *= time.Second
SimpleCacheIndexSetting.CheckTickDuration *= time.Second
SimpleCacheIndexSetting.ExpireTickDuration *= time.Second

@ -22,16 +22,16 @@ Cache:
MessagesExpire: 60 # 消息列表过期时间,单位秒, 默认60s
EventManager: # 事件管理器的配置参数
MinWorker: 64 # 最小后台工作者, 设置范围[5, ++], 默认64
MaxTempWorker: -1 # 最大临时工作者, -1为无限制, 默认-1
MaxEventBuf: 128 # 最大log缓存条数, 设置范围[10, ++], 默认128
MaxTempEventBuf: 256 # 最大log缓存条数, 设置范围[10, ++], 默认256
MaxTickCount: 60 # 最大的循环周期, 设置范围[60, ++], 默认60
TickWaitTime: 1 # 一个周期的等待时间,单位:秒 默认1s
MaxIdleTime: 60 # 临时工作者最大空闲等待时间,单位:秒 默认60
MetricManager: # 指标监控管理器的配置参数
MinWorker: 32 # 最小后台工作者, 设置范围[5, ++], 默认32
MaxTempWorker: -1 # 最大临时工作者, -1为无限制, 默认-1
MaxEventBuf: 128 # 最大log缓存条数, 设置范围[10, ++], 默认128
MaxTempEventBuf: 256 # 最大log缓存条数, 设置范围[10, ++], 默认256
MaxTickCount: 60 # 最大的循环周期, 设置范围[60, ++], 默认60
TickWaitTime: 1 # 一个周期的等待时间,单位:秒 默认1s
MaxIdleTime: 60 # 临时工作者最大空闲等待时间,单位:秒 默认60
JobManager: # Cron Job理器的配置参数
MaxOnlineInterval: "@every 5m" # 更新最大在线人数默认每5分钟更新一次
UpdateMetricsInterval: "@every 5m" # 更新Prometheus指标默认每5分钟更新一次
@ -250,7 +250,7 @@ Redis:
ConnWriteTimeout: 60 # 连接写超时时间 多少秒 默认 60秒
WebProfile:
UseFriendship: true # 前端是否使用好友体系
EnableTrendsBar: true # 广场页面是否开启动态条栏功能
EnableTrendsBar: false # 广场页面是否开启动态条栏功能
EnableWallet: false # 是否开启钱包功能
AllowTweetAttachment: true # 是否允许推文附件
AllowTweetAttachmentPrice: true # 是否允许推文付费附件

@ -5,6 +5,7 @@
package conf
import (
"log"
"sync"
"time"
@ -27,13 +28,13 @@ func MustGormDB() *gorm.DB {
_onceGorm.Do(func() {
var err error
if _gormdb, err = newGormDB(); err != nil {
logrus.Fatalf("new gorm db failed: %s", err)
log.Fatalf("new gorm db failed: %s", err)
}
})
return _gormdb
}
func newGormDB() (*gorm.DB, error) {
func newGormDB() (db *gorm.DB, err error) {
newLogger := logger.New(
logrus.StandardLogger(), // io writer日志输出的目标前缀和日志包含的内容
logger.Config{
@ -58,15 +59,9 @@ func newGormDB() (*gorm.DB, error) {
SetMaxIdleConns(MysqlSetting.MaxIdleConns).
SetMaxOpenConns(MysqlSetting.MaxOpenConns)
var (
db *gorm.DB
err error
)
if cfg.If("MySQL") {
logrus.Debugln("use MySQL as db")
if db, err = gorm.Open(mysql.Open(MysqlSetting.Dsn()), config); err == nil {
db.Use(plugin)
}
db, err = gorm.Open(mysql.Open(MysqlSetting.Dsn()), config)
} else if cfg.If("Postgres") {
logrus.Debugln("use PostgreSQL as db")
db, err = gorm.Open(postgres.Open(PostgresSetting.Dsn()), config)
@ -75,10 +70,10 @@ func newGormDB() (*gorm.DB, error) {
db, err = gormOpenSqlite3(config)
} else {
logrus.Debugln("use default of MySQL as db")
if db, err = gorm.Open(mysql.Open(MysqlSetting.Dsn()), config); err == nil {
db.Use(plugin)
}
db, err = gorm.Open(mysql.Open(MysqlSetting.Dsn()), config)
}
return db, err
if err == nil {
err = db.Use(plugin)
}
return
}

@ -56,8 +56,7 @@ func newObserveLogHook() *observeLogHook {
MinWorker: s.MinWorker,
MaxRequestBuf: s.MaxLogBuffer,
MaxRequestTempBuf: 100,
MaxTickCount: 60,
TickWaitTime: time.Second,
MaxIdleTime: 60 * time.Second,
}
return &observeLogHook{
client: obx.NewClient(obc, acc, func(req *http.Request, resp *http.Response, err error) {

@ -11,7 +11,7 @@ import (
"strings"
"time"
"github.com/pyroscope-io/client/pyroscope"
pyroscope "github.com/grafana/pyroscope-go"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"gorm.io/gorm/logger"
@ -115,18 +115,18 @@ type cacheConf struct {
type eventManagerConf struct {
MinWorker int
MaxTempWorker int
MaxEventBuf int
MaxTempEventBuf int
MaxTickCount int
TickWaitTime time.Duration
MaxIdleTime time.Duration
}
type metricManagerConf struct {
MinWorker int
MaxTempWorker int
MaxEventBuf int
MaxTempEventBuf int
MaxTickCount int
TickWaitTime time.Duration
MaxIdleTime time.Duration
}
type jobManagerConf struct {

@ -116,3 +116,8 @@ type WebCache interface {
ExistUnreadMsgCountResp(uid int64) bool
PutHistoryMaxOnline(newScore int) (int, error)
}
type MetricCache interface {
SetEventTempWorkerCount(name string, count int32)
GetEventTempWorkerCount(name string) int32
}

@ -9,6 +9,7 @@ const (
TagTypeHot TagType = "hot"
TagTypeNew TagType = "new"
TagTypeFollow TagType = "follow"
TagTypePin TagType = "pin"
TagTypeHotExtral TagType = "hot_extral"
)
@ -40,6 +41,7 @@ type TagItem struct {
QuoteNum int64 `json:"quote_num"`
IsFollowing int8 `json:"is_following"`
IsTop int8 `json:"is_top"`
IsPin int8 `json:"is_pin"`
}
func (t *TagInfo) Format() *TagItem {
@ -51,5 +53,6 @@ func (t *TagInfo) Format() *TagItem {
QuoteNum: t.QuoteNum,
IsFollowing: 0,
IsTop: 0,
IsPin: 0,
}
}

@ -16,10 +16,11 @@ type TopicService interface {
TagsByKeyword(keyword string) (cs.TagInfoList, error)
GetHotTags(userId int64, limit int, offset int) (cs.TagList, error)
GetNewestTags(userId int64, limit int, offset int) (cs.TagList, error)
GetFollowTags(userId int64, limit int, offset int) (cs.TagList, error)
GetFollowTags(userId int64, isPin bool, limit int, offset int) (cs.TagList, error)
FollowTopic(userId int64, topicId int64) error
UnfollowTopic(userId int64, topicId int64) error
StickTopic(userId int64, topicId int64) (int8, error)
PinTopic(userId int64, topicId int64) (int8, error)
}
// TopicServantA 话题服务(版本A)

@ -148,7 +148,7 @@ func (s *commentSrv) GetCommentRepliesByID(ids []int64) ([]*ms.CommentReplyForma
return repliesFormated, nil
}
func (s *commentManageSrv) HighlightComment(userId, commentId int64) (res int8, err error) {
func (s *commentManageSrv) HighlightComment(userId, commentId int64) (isEssence int8, err error) {
post := &dbr.Post{}
comment := &dbr.Comment{}
db := s.db.Model(comment)
@ -161,8 +161,12 @@ func (s *commentManageSrv) HighlightComment(userId, commentId int64) (res int8,
if post.UserID != userId {
return 0, cs.ErrNoPermission
}
comment.IsEssence = 1 - comment.IsEssence
return comment.IsEssence, db.Save(comment).Error
isEssence = 1 - comment.IsEssence
err = s.db.Model(comment).UpdateColumns(map[string]any{
"is_essence": isEssence,
"modified_on": time.Now().Unix(),
}).Where("id=?", commentId).Error
return
}
func (s *commentManageSrv) DeleteComment(comment *ms.Comment) (err error) {

@ -25,6 +25,7 @@ type TopicUser struct {
Remark string `json:"-"`
QuoteNum int64 `json:"quote_num"`
IsTop int8 `json:"is_top"`
IsPin int8 `json:"is_pin"`
ReserveA string `json:"-"`
ReserveB string `json:"-"`
}

@ -33,6 +33,7 @@ type topicSrvA struct {
type topicInfo struct {
TopicId int64
IsTop int8
IsPin int8
}
func newTopicService(db *gorm.DB) core.TopicService {
@ -110,17 +111,18 @@ func (s *topicSrv) GetNewestTags(userId int64, limit int, offset int) (cs.TagLis
return s.tagsFormatA(userId, tags)
}
func (s *topicSrv) GetFollowTags(userId int64, limit int, offset int) (cs.TagList, error) {
func (s *topicSrv) GetFollowTags(userId int64, isPin bool, limit int, offset int) (cs.TagList, error) {
if userId < 0 {
return nil, nil
}
userTopics := []*topicInfo{}
err := s.db.Model(&dbr.TopicUser{}).
Where("user_id=?", userId).
Order("is_top DESC").
Limit(limit).
Offset(offset).
Find(&userTopics).Error
db := s.db.Model(&dbr.TopicUser{}).Order("is_top DESC").Limit(limit).Offset(offset)
if isPin {
db = db.Where("user_id=? AND is_pin=1", userId)
} else {
db = db.Where("user_id=?", userId)
}
err := db.Find(&userTopics).Error
if err != nil {
return nil, err
}
@ -207,7 +209,7 @@ func (s *topicSrv) tagsFormatA(userId int64, tags cs.TagList) (cs.TagList, error
}
for _, tag := range tags {
if info, exist := userTopicsMap[tag.ID]; exist {
tag.IsFollowing, tag.IsTop = 1, info.IsTop
tag.IsFollowing, tag.IsTop, tag.IsPin = 1, info.IsTop, info.IsPin
}
}
}
@ -238,7 +240,7 @@ func (s *topicSrv) tagsFormatB(userTopicsMap map[int64]*topicInfo, tags cs.TagIn
if len(userTopicsMap) > 0 {
for _, tag := range tagList {
if info, exist := userTopicsMap[tag.ID]; exist {
tag.IsFollowing, tag.IsTop = 1, info.IsTop
tag.IsFollowing, tag.IsTop, tag.IsPin = 1, info.IsTop, info.IsPin
}
}
}
@ -409,3 +411,27 @@ func (s *topicSrv) StickTopic(userId int64, topicId int64) (status int8, err err
db.Commit()
return
}
func (s *topicSrv) PinTopic(userId int64, topicId int64) (status int8, err error) {
db := s.db.Begin()
defer db.Rollback()
m := &dbr.TopicUser{}
err = db.Model(m).
Where("user_id=? and topic_id=?", userId, topicId).
UpdateColumn("is_pin", gorm.Expr("1-is_pin")).Error
if err != nil {
return
}
status = -1
err = db.Model(m).Where("user_id=? and topic_id=?", userId, topicId).Select("is_pin").Scan(&status).Error
if err != nil {
return
}
if status < 0 {
return -1, errors.New("topic not exist")
}
db.Commit()
return
}

@ -11,6 +11,7 @@ import (
"github.com/alimy/tryst/pool"
"github.com/robfig/cron/v3"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/internal/metrics/statistics"
"github.com/sirupsen/logrus"
)
@ -89,21 +90,24 @@ func initEventManager() {
var opts []pool.Option
s := conf.EventManagerSetting
if s.MinWorker > 5 {
opts = append(opts, pool.MinWorkerOpt(s.MinWorker))
opts = append(opts, pool.WithMinWorker(s.MinWorker))
} else {
opts = append(opts, pool.MinWorkerOpt(5))
opts = append(opts, pool.WithMinWorker(5))
}
if s.MaxEventBuf > 10 {
opts = append(opts, pool.MaxRequestBufOpt(s.MaxEventBuf))
opts = append(opts, pool.WithMaxRequestBuf(s.MaxEventBuf))
} else {
opts = append(opts, pool.MaxRequestBufOpt(10))
opts = append(opts, pool.WithMaxRequestBuf(10))
}
if s.MaxTempEventBuf > 10 {
opts = append(opts, pool.MaxRequestTempBufOpt(s.MaxTempEventBuf))
opts = append(opts, pool.WithMaxRequestTempBuf(s.MaxTempEventBuf))
} else {
opts = append(opts, pool.MaxRequestTempBufOpt(10))
opts = append(opts, pool.WithMaxRequestTempBuf(10))
}
opts = append(opts, pool.MaxTickCountOpt(s.MaxTickCount), pool.TickWaitTimeOpt(s.TickWaitTime))
if cfg.If("Metrics") {
opts = append(opts, pool.WithWorkerHook(NewEventWorkerHook("default", statistics.NewMetricCache())))
}
opts = append(opts, pool.WithMaxIdelTime(s.MaxIdleTime), pool.WithMaxTempWorker(s.MaxTempWorker))
_defaultEventManager = NewEventManager(func(req Event, err error) {
if err != nil {
logrus.Errorf("handle event[%s] occurs error: %s", req.Name(), err)

@ -7,6 +7,7 @@ package events
import (
"github.com/alimy/tryst/event"
"github.com/alimy/tryst/pool"
"github.com/rocboss/paopao-ce/internal/core"
)
type Event = event.Event
@ -17,6 +18,11 @@ type EventManager interface {
OnEvent(event Event)
}
type simpleWorkerHook struct {
name string
cache core.MetricCache
}
type simpleEventManager struct {
em event.EventManager
}
@ -33,8 +39,23 @@ func (s *simpleEventManager) OnEvent(event Event) {
s.em.OnEvent(event)
}
func (h *simpleWorkerHook) OnJoin(count int32) {
h.cache.SetEventTempWorkerCount(h.name, count)
}
func (h *simpleWorkerHook) OnLeave(count int32) {
h.cache.SetEventTempWorkerCount(h.name, count)
}
func NewEventManager(fn pool.RespFn[Event], opts ...pool.Option) EventManager {
return &simpleEventManager{
em: event.NewEventManager(fn, opts...),
}
}
func NewEventWorkerHook(name string, mc core.MetricCache) pool.WorkerHook {
return &simpleWorkerHook{
name: name,
cache: mc,
}
}

@ -49,23 +49,23 @@ func Initial() {
func initMetricManager() {
var opts []pool.Option
s := conf.EventManagerSetting
s := conf.MetricManagerSetting
if s.MinWorker > 5 {
opts = append(opts, pool.MinWorkerOpt(s.MinWorker))
opts = append(opts, pool.WithMinWorker(s.MinWorker))
} else {
opts = append(opts, pool.MinWorkerOpt(5))
opts = append(opts, pool.WithMinWorker(5))
}
if s.MaxEventBuf > 10 {
opts = append(opts, pool.MaxRequestBufOpt(s.MaxEventBuf))
opts = append(opts, pool.WithMaxRequestBuf(s.MaxEventBuf))
} else {
opts = append(opts, pool.MaxRequestBufOpt(10))
opts = append(opts, pool.WithMaxRequestBuf(10))
}
if s.MaxTempEventBuf > 10 {
opts = append(opts, pool.MaxRequestTempBufOpt(s.MaxTempEventBuf))
opts = append(opts, pool.WithMaxRequestTempBuf(s.MaxTempEventBuf))
} else {
opts = append(opts, pool.MaxRequestTempBufOpt(10))
opts = append(opts, pool.WithMaxRequestTempBuf(10))
}
opts = append(opts, pool.MaxTickCountOpt(s.MaxTickCount), pool.TickWaitTimeOpt(s.TickWaitTime))
opts = append(opts, pool.WithMaxIdelTime(s.MaxIdleTime), pool.WithMaxTempWorker(s.MaxTempWorker))
_defaultMetricManager = event.NewEventManager(func(req Metric, err error) {
if err != nil {
logrus.Errorf("handle event[%s] occurs error: %s", req.Name(), err)

@ -13,8 +13,10 @@ import (
type metrics struct {
siteInfo *prometheus.GaugeVec
runtime *prometheus.GaugeVec
ds core.DataService
wc core.WebCache
mc core.MetricCache
}
func (m *metrics) updateSiteInfo() {
@ -31,13 +33,21 @@ func (m *metrics) updateSiteInfo() {
}
}
func (m *metrics) updateRuntime() {
m.runtime.With(prometheus.Labels{"name": "default"}).Set(float64(m.mc.GetEventTempWorkerCount("default")))
}
func (m *metrics) onUpdate() {
logrus.Debugf("update promethues metrics job running")
m.updateSiteInfo()
m.updateRuntime()
}
func newMetrics(reg prometheus.Registerer, ds core.DataService, wc core.WebCache) *metrics {
func newMetrics(reg prometheus.Registerer, ds core.DataService, wc core.WebCache, mc core.MetricCache) *metrics {
m := &metrics{
ds: ds,
wc: wc,
mc: mc,
siteInfo: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: "paopao",
@ -49,9 +59,18 @@ func newMetrics(reg prometheus.Registerer, ds core.DataService, wc core.WebCache
// metric name
"name",
}),
ds: ds,
wc: wc,
runtime: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: "runtime",
Subsystem: "event_manager",
Name: "temp_worker_count",
Help: "runtime's event manager temp worker count info",
},
[]string{
// metric name
"name",
}),
}
reg.MustRegister(m.siteInfo)
reg.MustRegister(m.siteInfo, m.runtime)
return m
}

@ -27,7 +27,7 @@ func scheduleJobs(metrics *metrics) {
logrus.Debug("shedule prometheus metrics update jobs complete")
}
func NewHandler(ds core.DataService, wc core.WebCache) http.Handler {
func NewHandler(ds core.DataService, wc core.WebCache, mc core.MetricCache) http.Handler {
// Create non-global registry.
registry := prometheus.NewRegistry()
// Add go runtime metrics and process collectors.
@ -35,7 +35,7 @@ func NewHandler(ds core.DataService, wc core.WebCache) http.Handler {
collectors.NewGoCollector(),
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
)
metrics := newMetrics(registry, ds, wc)
metrics := newMetrics(registry, ds, wc, mc)
scheduleJobs(metrics)
return promhttp.HandlerFor(registry, promhttp.HandlerOpts{EnableOpenMetrics: true})
}

@ -0,0 +1,38 @@
// Copyright 2023 ROC. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package statistics
import (
"sync"
"github.com/rocboss/paopao-ce/internal/core"
)
var (
_metricCache core.MetricCache
_onceMetricCache sync.Once
)
type metricCache struct {
eventTempWorkerCount map[string]int32
}
func (m *metricCache) SetEventTempWorkerCount(name string, count int32) {
// 直接赋值,不需要加锁,因为这仅仅是一个统计信息
m.eventTempWorkerCount[name] = count
}
func (m *metricCache) GetEventTempWorkerCount(name string) int32 {
return m.eventTempWorkerCount[name]
}
func NewMetricCache() core.MetricCache {
_onceMetricCache.Do(func() {
_metricCache = &metricCache{
eventTempWorkerCount: make(map[string]int32),
}
})
return _metricCache
}

@ -20,6 +20,7 @@ const (
TagTypeHot = cs.TagTypeHot
TagTypeNew = cs.TagTypeNew
TagTypeFollow = cs.TagTypeFollow
TagTypePin = cs.TagTypePin
TagTypeHotExtral = cs.TagTypeHotExtral
)

@ -207,6 +207,15 @@ type StickTopicResp struct {
StickStatus int8 `json:"top_status"`
}
type PinTopicReq struct {
SimpleInfo `json:"-" binding:"-"`
TopicId int64 `json:"topic_id" binding:"required"`
}
type PinTopicResp struct {
PinStatus int8 `json:"pin_status"`
}
type FollowTopicReq struct {
SimpleInfo `json:"-" binding:"-"`
TopicId int64 `json:"topic_id" binding:"required"`

@ -24,6 +24,8 @@ func fileCheck(uploadType string, size int64) mir.Error {
func getFileExt(s string) (string, mir.Error) {
switch s {
case "image/webp":
return ".webp", nil
case "image/png":
return ".png", nil
case "image/jpg":
@ -42,6 +44,6 @@ func getFileExt(s string) (string, mir.Error) {
"application/x-zip-compressed":
return ".zip", nil
default:
return "", ErrFileInvalidExt.WithDetails("仅允许 png/jpg/gif/mp4/mov/zip 类型")
return "", ErrFileInvalidExt.WithDetails("仅允许 webp/png/jpg/gif/mp4/mov/zip 类型")
}
}

@ -95,6 +95,7 @@ var (
ErrFollowTopicFailed = xerror.NewError(90001, "关注话题失败")
ErrUnfollowTopicFailed = xerror.NewError(90002, "取消关注话题失败")
ErrStickTopicFailed = xerror.NewError(90003, "更行话题置顶状态失败")
ErrPinTopicFailed = xerror.NewError(90005, "更行话题钉住状态失败")
ErrThumbsUpTweetComment = xerror.NewError(90101, "评论点赞失败")
ErrThumbsDownTweetComment = xerror.NewError(90102, "评论点踩失败")
ErrThumbsUpTweetReply = xerror.NewError(90103, "评论回复点赞失败")

@ -29,7 +29,8 @@ import (
)
type BaseServant struct {
bindAny func(c *gin.Context, obj any) mir.Error
bindAny func(c *gin.Context, obj any) mir.Error
bindJson func(c *gin.Context, obj any) mir.Error
}
type DaoServant struct {
@ -158,6 +159,10 @@ func (s *BaseServant) Bind(c *gin.Context, obj any) mir.Error {
return s.bindAny(c, obj)
}
func (s *BaseServant) BindJson(c *gin.Context, obj any) mir.Error {
return s.bindJson(c, obj)
}
func (s *BaseServant) Render(c *gin.Context, data any, err mir.Error) {
if err == nil {
c.JSON(http.StatusOK, &joint.JsonResp{
@ -420,9 +425,17 @@ func NewBindAnyFn() func(c *gin.Context, obj any) mir.Error {
return bindAny
}
func NewBindJsonFn() func(c *gin.Context, obj any) mir.Error {
if conf.UseSentryGin() {
return bindAnySentry
}
return bindAny
}
func NewBaseServant() *BaseServant {
return &BaseServant{
bindAny: NewBindAnyFn(),
bindAny: NewBindAnyFn(),
bindJson: NewBindJsonFn(),
}
}

@ -7,8 +7,8 @@ package servants
import (
"net/http"
"connectrpc.com/connect"
"github.com/alimy/tryst/cfg"
"github.com/bufbuild/connect-go"
"github.com/gin-gonic/gin"
"github.com/rocboss/paopao-ce/internal/servants/admin"
"github.com/rocboss/paopao-ce/internal/servants/bot"

@ -7,7 +7,7 @@ package triplet
import (
"net/http"
"github.com/bufbuild/connect-go"
"connectrpc.com/connect"
api "github.com/rocboss/paopao-ce/auto/connect/core/v1/corev1connect"
)

@ -384,7 +384,9 @@ func (s *looseSrv) TopicList(req *web.TopicListReq) (*web.TopicListResp, mir.Err
case web.TagTypeNew:
tags, err = s.Ds.GetNewestTags(req.Uid, num, 0)
case web.TagTypeFollow:
tags, err = s.Ds.GetFollowTags(req.Uid, num, 0)
tags, err = s.Ds.GetFollowTags(req.Uid, false, num, 0)
case web.TagTypePin:
tags, err = s.Ds.GetFollowTags(req.Uid, true, num, 0)
case web.TagTypeHotExtral:
extralNum := req.ExtralNum
if extralNum <= 0 {
@ -392,7 +394,7 @@ func (s *looseSrv) TopicList(req *web.TopicListReq) (*web.TopicListResp, mir.Err
}
tags, err = s.Ds.GetHotTags(req.Uid, num, 0)
if err == nil {
extralTags, err = s.Ds.GetFollowTags(req.Uid, extralNum, 0)
extralTags, err = s.Ds.GetFollowTags(req.Uid, false, extralNum, 0)
}
default:
// TODO: return good error

@ -6,6 +6,7 @@ package web
import (
"image"
"io"
"strings"
"time"
@ -124,13 +125,28 @@ func (s *privSrv) StickTopic(req *web.StickTopicReq) (*web.StickTopicResp, mir.E
}, nil
}
func (s *privSrv) PinTopic(req *web.PinTopicReq) (*web.PinTopicResp, mir.Error) {
status, err := s.Ds.PinTopic(req.Uid, req.TopicId)
if err != nil {
logrus.Errorf("user(%d) pin topic(%d) failed: %s", req.Uid, req.TopicId, err)
return nil, web.ErrPinTopicFailed
}
return &web.PinTopicResp{
PinStatus: status,
}, nil
}
func (s *privSrv) UploadAttachment(req *web.UploadAttachmentReq) (*web.UploadAttachmentResp, mir.Error) {
defer req.File.Close()
// 生成随机路径
randomPath := uuid.Must(uuid.NewV4()).String()
ossSavePath := req.UploadType + "/" + generatePath(randomPath[:8]) + "/" + randomPath[9:] + req.FileExt
objectUrl, err := s.oss.PutObject(ossSavePath, req.File, req.FileSize, req.ContentType, false)
// NOTE: 注意这里将req.File Wrap到一个io.Reader的实例对象中是为了避免下游接口去主动调Closereq.File本身是实现了
// io.Closer接口的有的下游接口会断言传参是否实现了io.Closer接口如果实现了会主动去调我们这里因为下文中可能还要继续
// 使用req.File所以应避免下游Close否则会出现潜在的bug比如这里的场景就是传一个超大的图片(>10MB)可能就会触发bug了。
data := io.NopCloser(req.File)
objectUrl, err := s.oss.PutObject(ossSavePath, data, req.FileSize, req.ContentType, false)
if err != nil {
logrus.Errorf("oss.putObject err: %s", err)
return nil, web.ErrFileUploadFailed

@ -8,7 +8,7 @@ import (
"context"
"net/http"
"github.com/bufbuild/connect-go"
"connectrpc.com/connect"
hx "github.com/rocboss/paopao-ce/pkg/http"
)

@ -7,7 +7,7 @@ package service
import (
"net/http"
"github.com/bufbuild/connect-go"
"connectrpc.com/connect"
)
type baseConnectService struct {

@ -14,6 +14,7 @@ import (
"github.com/rocboss/paopao-ce/internal/dao"
"github.com/rocboss/paopao-ce/internal/dao/cache"
"github.com/rocboss/paopao-ce/internal/metrics/prometheus"
"github.com/rocboss/paopao-ce/internal/metrics/statistics"
)
var (
@ -44,9 +45,9 @@ func (s *metricsService) String() string {
func newMetricsService() Service {
addr := conf.MetricsServerSetting.HttpIp + ":" + conf.MetricsServerSetting.HttpPort
server := httpServers.from(addr, func() *httpServer {
ds, wc := dao.DataService(), cache.NewWebCache()
ds, wc, mc := dao.DataService(), cache.NewWebCache(), statistics.NewMetricCache()
mux := http.NewServeMux()
mux.Handle("/metrics", prometheus.NewHandler(ds, wc))
mux.Handle("/metrics", prometheus.NewHandler(ds, wc, mc))
return &httpServer{
baseServer: newBaseServe(),
server: &http.Server{

@ -16,16 +16,16 @@ type Friendship struct {
Group `mir:"v1"`
// RequestingFriend 请求添加朋友
RequestingFriend func(Post, web.RequestingFriendReq) `mir:"/friend/requesting"`
RequestingFriend func(Post, web.RequestingFriendReq) `mir:"/friend/requesting" binding:"json"`
// AddFriend 同意添加好友
AddFriend func(Post, web.AddFriendReq) `mir:"/friend/add"`
AddFriend func(Post, web.AddFriendReq) `mir:"/friend/add" binding:"json"`
// RejectFriend 拒绝添加好友
RejectFriend func(Post, web.RejectFriendReq) `mir:"/friend/reject"`
RejectFriend func(Post, web.RejectFriendReq) `mir:"/friend/reject" binding:"json"`
// DeleteFriend 删除好友
DeleteFriend func(Post, web.DeleteFriendReq) `mir:"/friend/delete"`
DeleteFriend func(Post, web.DeleteFriendReq) `mir:"/friend/delete" binding:"json"`
// GetContacts 获取好友列表
GetContacts func(Get, web.GetContactsReq) web.GetContactsResp `mir:"/user/contacts"`

@ -75,9 +75,12 @@ type Priv struct {
// ThumbsDownTweetReply 点踩评论回复
ThumbsDownTweetReply func(Post, web.TweetReplyThumbsReq) `mir:"/tweet/reply/thumbsdown"`
// StickTopic 置顶动态
// StickTopic 置顶话题
StickTopic func(Post, web.StickTopicReq) web.StickTopicResp `mir:"/topic/stick"`
// PinTopic 钉住话题
PinTopic func(Post, web.PinTopicReq) web.PinTopicResp `mir:"/topic/pin"`
// FollowTopic 关注话题
FollowTopic func(Post, web.FollowTopicReq) `mir:"/topic/follow"`

@ -55,8 +55,7 @@ func ParseToken(token string) (res *Claims, err error) {
func IssuerFrom(data string) string {
contents := make([]byte, 0, len(conf.JWTSetting.Issuer)+len(data))
copy(contents, []byte(conf.JWTSetting.Issuer))
contents = append(contents, []byte(data)...)
contents = append(append(contents, []byte(conf.JWTSetting.Issuer)...), []byte(data)...)
res := md5.Sum(contents)
return hex.EncodeToString(res[:])
}

@ -11,7 +11,7 @@ import (
"os"
"github.com/alimy/tryst/cfg"
"github.com/pyroscope-io/client/pyroscope"
pyroscope "github.com/grafana/pyroscope-go"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/sirupsen/logrus"
)

@ -34,8 +34,7 @@ type AsyncClientConf struct {
MinWorker int
MaxRequestBuf int
MaxRequestTempBuf int
MaxTickCount int
TickWaitTime time.Duration
MaxIdleTime time.Duration
}
type wormClient struct {
@ -64,11 +63,10 @@ func NewAsyncClient(client *http.Client, conf *AsyncClientConf) AsyncClient {
pool: gp.NewGoroutinePool(func(req *http.Request) (*http.Response, error) {
return client.Do(req)
},
gp.MinWorkerOpt(minWorker),
gp.MaxRequestBufOpt(maxRequestBuf),
gp.MaxRequestTempBufOpt(maxRequestTempBuf),
gp.MaxTickCountOpt(conf.MaxTickCount),
gp.TickWaitTimeOpt(conf.TickWaitTime),
gp.WithMinWorker(minWorker),
gp.WithMaxRequestBuf(maxRequestBuf),
gp.WithMaxRequestTempBuf(maxRequestTempBuf),
gp.WithMaxIdelTime(conf.MaxIdleTime),
),
}
}

@ -0,0 +1,157 @@
// Copyright 2023 ROC. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package types
import (
"github.com/RoaringBitmap/roaring"
"github.com/RoaringBitmap/roaring/roaring64"
"github.com/alimy/tryst/types"
)
type roaringBitmap struct {
Map *roaring.Bitmap
}
type roaring64Bitmap struct {
Map *roaring64.Bitmap
}
func (m *roaringBitmap) MarshalBinary() ([]byte, error) {
if m == nil {
return []byte{}, nil
}
return m.Map.MarshalBinary()
}
func (m *roaringBitmap) UnmarshalBinary(data []byte) (res *roaringBitmap, err error) {
res = &roaringBitmap{
Map: roaring.New(),
}
err = res.Map.UnmarshalBinary(data)
return
}
func (m *roaring64Bitmap) MarshalBinary() ([]byte, error) {
if m == nil {
return []byte{}, nil
}
return m.Map.MarshalBinary()
}
func (m *roaring64Bitmap) UnmarshalBinary(data []byte) (res *roaring64Bitmap, err error) {
res = &roaring64Bitmap{
Map: roaring64.New(),
}
err = res.Map.UnmarshalBinary(data)
return
}
// Bitmap alias type of types.Binary[*roaringBitmap]
type Bitmap = types.Binary[*roaringBitmap]
// NullBitmap alias type of types.NullBinary[*roaringBitmap]
type NullBitmap = types.NullBinary[*roaringBitmap]
// Bitmap64 alias type of types.Binary[*roaring64Bitmap]
type Bitmap64 = types.Binary[*roaring64Bitmap]
// NullBitmap64 alias type of types.NullBinary[*roaring64Bitmap]
type NullBitmap64 = types.NullBinary[*roaring64Bitmap]
// NewBitmap create a Bitmap instance
func NewBitmap() (res *Bitmap) {
return &types.Binary[*roaringBitmap]{
Data: &roaringBitmap{
Map: roaring.New(),
},
}
}
// NewNullBitmap create a NullBitmap instance
func NewNullBitmap() *NullBitmap {
return &types.NullBinary[*roaringBitmap]{
Data: &roaringBitmap{
Map: roaring.New(),
},
}
}
// NewBitmap64 create a Bitmap64 instance
func NewBitmap64() *Bitmap64 {
return &types.Binary[*roaring64Bitmap]{
Data: &roaring64Bitmap{
Map: roaring64.New(),
},
}
}
// NewNullBitmap64 create a NullBitmap64 instance
func NewNullBitmap64() *NullBitmap64 {
return &types.NullBinary[*roaring64Bitmap]{
Data: &roaring64Bitmap{
Map: roaring64.New(),
},
}
}
// MustBitmap create a Bitmap instance
func MustBitmap(data ...[]byte) (res *Bitmap) {
res = &types.Binary[*roaringBitmap]{
Data: &roaringBitmap{
Map: roaring.New(),
},
}
if len(data) > 0 && len(data[0]) > 0 {
if err := res.Data.Map.UnmarshalBinary(data[0]); err != nil {
panic(err)
}
}
return
}
// MustNullBitmap create a NullBitmap instance
func MustNullBitmap(data ...[]byte) (res *NullBitmap) {
res = &types.NullBinary[*roaringBitmap]{
Data: &roaringBitmap{
Map: roaring.New(),
},
}
if len(data) > 0 && len(data[0]) > 0 {
if err := res.Data.Map.UnmarshalBinary(data[0]); err != nil {
panic(err)
}
}
return
}
// MustBitmap64 create a Bitmap64 instance
func MustBitmap64(data ...[]byte) (res *Bitmap64) {
res = &types.Binary[*roaring64Bitmap]{
Data: &roaring64Bitmap{
Map: roaring64.New(),
},
}
if len(data) > 0 && len(data[0]) > 0 {
if err := res.Data.Map.UnmarshalBinary(data[0]); err != nil {
panic(err)
}
}
return
}
// MustNullBitmap64 create a NullBitmap64 instance
func MustNullBitmap64(data ...[]byte) (res *NullBitmap64) {
res = &types.NullBinary[*roaring64Bitmap]{
Data: &roaring64Bitmap{
Map: roaring64.New(),
},
}
if len(data) > 0 && len(data[0]) > 0 {
if err := res.Data.Map.UnmarshalBinary(data[0]); err != nil {
panic(err)
}
}
return
}

@ -0,0 +1,36 @@
// Copyright 2023 ROC. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package types_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/rocboss/paopao-ce/pkg/types"
)
var _ = Describe("Bitmap", Ordered, func() {
It("sql scaner for Bitmap", func() {
bitmap := types.NewBitmap()
bitmap.Data.Map.Add(128)
value, err := bitmap.Value()
Expect(err).To(BeNil())
var bm types.Bitmap
bm.Scan(value)
Expect(bm.Data.Map.Contains(128)).To(BeTrue())
})
It("sql scaner for Bitmap64", func() {
bitmap := types.NewBitmap64()
bitmap.Data.Map.Add(128)
value, err := bitmap.Value()
Expect(err).To(BeNil())
var bm types.Bitmap64
bm.Scan(value)
Expect(bm.Data.Map.Contains(128)).To(BeTrue())
})
})

@ -0,0 +1,24 @@
// Copyright 2024 ROC. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package types
import (
"io"
)
type readerWrap struct {
r io.Reader
}
func (rw *readerWrap) Read(p []byte) (n int, err error) {
return rw.r.Read(p)
}
// PureReader wrap a pure io.Reader object
func PureReader(r io.Reader) io.Reader {
return &readerWrap{
r: r,
}
}

@ -0,0 +1,32 @@
// Copyright 2024 ROC. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package types
import (
"golang.org/x/crypto/bcrypt"
)
type PasswordProvider interface {
Generate(password []byte) ([]byte, error)
Compare(hashedPassword, password []byte) error
}
func NewBcryptPasswordProvider(cost int) PasswordProvider {
return &bcryptPasswordProvider{
cost: cost,
}
}
type bcryptPasswordProvider struct {
cost int
}
func (p *bcryptPasswordProvider) Generate(password []byte) ([]byte, error) {
return bcrypt.GenerateFromPassword(password, p.cost)
}
func (p *bcryptPasswordProvider) Compare(hashedPassword, password []byte) error {
return bcrypt.CompareHashAndPassword(hashedPassword, password)
}

@ -9,7 +9,7 @@ import (
)
const (
series = "v0.5"
series = "v0.6-dev"
)
var (

@ -0,0 +1,37 @@
# syntax=docker/dockerfile:1
FROM getmeili/meilisearch:v1.5 as meilisearch
FROM redis:7.2-alpine3.18
ENV TZ=Asia/Shanghai
RUN apk update && apk add --no-cache ca-certificates && update-ca-certificates
RUN apk update --quiet \
&& apk -q --no-cache --no-progress add \
ca-certificates \
libgcc \
curl \
s6 \
&& update-ca-certificates
WORKDIR /app
COPY ./docker ./docker
# add meilisearch and meilitool to the `/bin` so you can run it from anywhere
# and it's easy to find.
COPY --from=meilisearch /bin/meilisearch /bin/meilisearch
COPY --from=meilisearch /bin/meilitool /bin/meilitool
# To stay compatible with the older version of the container (pre v0.27.0) we're
# going to symlink the meilisearch binary in the path to `/meilisearch`
RUN ln -s /bin/meilisearch /meilisearch
ENV MEILI_HTTP_ADDR 0.0.0.0:7700
ENV MEILI_SERVER_PROVIDER docker
ENV MEILI_DB_PATH=/app/meili_data
ENV MEILI_MASTER_KEY=paopao-meilisearch
RUN ./docker/build/finalize.sh
# Configure Docker Container
VOLUME ["/app/meili_data", "/app/custom"]
EXPOSE 7700/tcp 6379 8008
HEALTHCHECK --interval=5s --timeout=3s --retries=3 CMD ps -ef | grep paopao || exit 1
ENTRYPOINT ["/app/docker/start.sh"]
CMD ["/bin/s6-svscan", "/app/docker/s6/"]

@ -1,4 +1,5 @@
FROM golang:1.21-alpine
# syntax=docker/dockerfile:1
FROM golang:1.21-alpine3.18
RUN apk --no-cache --no-progress add --virtual \
build-deps \
build-base \

@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1
FROM alpine:3.18
ENV TZ=Asia/Shanghai
RUN apk update && apk add --no-cache ca-certificates && update-ca-certificates

@ -1,6 +1,7 @@
### Dockerfile builer pre-build images
```sh
docker build -t bitbus/paopao-ce-backend-builder:latest -f Dockerfile-backend-builder .
docker build -t bitbus/paopao-ce-backend-runner:latest -f Dockerfile-backend-runner .
docker buildx build -t bitbus/paopao-ce-backend-builder:latest -f Dockerfile.backend-builder .
docker buildx build -t bitbus/paopao-ce-backend-runner:latest -f Dockerfile.backend-runner .
cd ../../ && docker buildx build -t bitbus/paopao-ce-allinone-runner:latest -f scripts/docker/Dockerfile.allinone-runner .
```

@ -0,0 +1,2 @@
ALTER TABLE `p_topic_user` DROP COLUMN `is_pin`;
DROP INDEX IF EXISTS `idx_topic_user_uid_ispin`;

@ -0,0 +1,2 @@
ALTER TABLE `p_topic_user` ADD COLUMN `is_pin` TINYINT NOT NULL DEFAULT 0 COMMENT '是否钉住 0 为未钉住、1 为已钉住';
CREATE INDEX `idx_topic_user_uid_ispin` ON `p_topic_user` (`user_id`, `is_pin`) USING BTREE;

@ -0,0 +1,2 @@
ALTER TABLE p_topic_user DROP COLUMN is_pin;
DROP INDEX IF EXISTS idx_topic_user_uid_ispin;

@ -0,0 +1,2 @@
ALTER TABLE p_topic_user ADD COLUMN is_pin SMALLINT NOT NULL DEFAULT 0; -- 是否钉住 0 为未钉住、1 为已钉住
CREATE INDEX idx_topic_user_uid_ispin ON p_topic_user USING btree ( user_id, is_pin );

@ -1,7 +1,5 @@
PRAGMA foreign_keys=off;
BEGIN TRANSACTION;
ALTER TABLE p_post_content RENAME TO _p_post_content;
ALTER TABLE p_comment_content RENAME TO _p_comment_content;
@ -53,46 +51,6 @@ CREATE TABLE p_comment_reply
PRIMARY KEY ("id")
);
-- ----------------------------
-- Indexes structure for table p_post_content
-- ----------------------------
CREATE INDEX "idx_post_content_post_id"
ON "p_post_content" (
"post_id" ASC
);
CREATE INDEX "idx_post_content_user_id"
ON "p_post_content" (
"user_id" ASC
);
-- ----------------------------
-- Indexes structure for table p_comment_reply
-- ----------------------------
CREATE INDEX "idx_comment_reply_comment_id"
ON "p_comment_reply" (
"comment_id" ASC
);
-- ----------------------------
-- Indexes structure for table p_comment_content
-- ----------------------------
CREATE INDEX "idx_comment_content_comment_id"
ON "p_comment_content" (
"comment_id" ASC
);
CREATE INDEX "idx_comment_content_sort"
ON "p_comment_content" (
"sort" ASC
);
CREATE INDEX "idx_comment_content_type"
ON "p_comment_content" (
"type" ASC
);
CREATE INDEX "idx_comment_content_user_id"
ON "p_comment_content" (
"user_id" ASC
);
INSERT INTO p_post_content (id, post_id, user_id, content, `type`, sort, created_on, modified_on, deleted_on, is_del)
SELECT id, post_id, user_id, content, `type`, sort, created_on, modified_on, deleted_on, is_del
FROM _p_post_content;
@ -112,10 +70,12 @@ DROP TABLE _p_comment_reply;
-- ----------------------------
-- Indexes structure for table p_post_content
-- ----------------------------
DROP INDEX IF EXISTS "idx_post_content_post_id";
CREATE INDEX "idx_post_content_post_id"
ON "p_post_content" (
"post_id" ASC
);
DROP INDEX IF EXISTS "idx_post_content_user_id";
CREATE INDEX "idx_post_content_user_id"
ON "p_post_content" (
"user_id" ASC
@ -124,6 +84,7 @@ ON "p_post_content" (
-- ----------------------------
-- Indexes structure for table p_comment_reply
-- ----------------------------
DROP INDEX IF EXISTS "idx_comment_reply_comment_id";
CREATE INDEX "idx_comment_reply_comment_id"
ON "p_comment_reply" (
"comment_id" ASC
@ -132,23 +93,25 @@ ON "p_comment_reply" (
-- ----------------------------
-- Indexes structure for table p_comment_content
-- ----------------------------
DROP INDEX IF EXISTS "idx_comment_content_comment_id";
CREATE INDEX "idx_comment_content_comment_id"
ON "p_comment_content" (
"comment_id" ASC
);
DROP INDEX IF EXISTS "idx_comment_content_sort";
CREATE INDEX "idx_comment_content_sort"
ON "p_comment_content" (
"sort" ASC
);
DROP INDEX IF EXISTS "idx_comment_content_type";
CREATE INDEX "idx_comment_content_type"
ON "p_comment_content" (
"type" ASC
);
DROP INDEX IF EXISTS "idx_comment_content_user_id";
CREATE INDEX "idx_comment_content_user_id"
ON "p_comment_content" (
"user_id" ASC
);
COMMIT;
PRAGMA foreign_keys=on;

@ -0,0 +1,2 @@
ALTER TABLE "p_topic_user" DROP COLUMN "is_pin";
DROP INDEX IF EXISTS "idx_topic_user_uid_ispin";

@ -0,0 +1,6 @@
ALTER TABLE "p_topic_user" ADD COLUMN "is_pin" integer NOT NULL DEFAULT 0;
CREATE INDEX "main"."idx_topic_user_uid_ispin"
ON "p_topic_user" (
"user_id" ASC,
"is_pin" ASC
);

@ -324,6 +324,7 @@ CREATE TABLE `p_topic_user` (
`remark` VARCHAR ( 512 ) COMMENT '备注',
`quote_num` BIGINT COMMENT '引用数',
`is_top` TINYINT NOT NULL DEFAULT '0' COMMENT '是否置顶 0 为未置顶、1 为已置顶',
`is_pin` TINYINT NOT NULL DEFAULT '0' COMMENT '是否钉住 0 为未钉住、1 为已钉住',
`created_on` BIGINT NOT NULL DEFAULT '0' COMMENT '创建时间',
`modified_on` BIGINT NOT NULL DEFAULT '0' COMMENT '修改时间',
`deleted_on` BIGINT NOT NULL DEFAULT '0' COMMENT '删除时间',
@ -331,7 +332,8 @@ CREATE TABLE `p_topic_user` (
`reserve_a` VARCHAR ( 255 ) COMMENT '保留字段a',
`reserve_b` VARCHAR ( 255 ) COMMENT '保留字段b',
PRIMARY KEY ( `id` ) USING BTREE,
UNIQUE KEY `idx_topic_user_uid_tid` ( `topic_id`, `user_id` ) USING BTREE
UNIQUE KEY `idx_topic_user_uid_tid` ( `topic_id`, `user_id` ) USING BTREE,
KEY `idx_topic_user_uid_ispin` ( `user_id`, `is_pin`) USING BTREE
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户话题';
-- ----------------------------

@ -265,6 +265,7 @@ CREATE TABLE p_topic_user (
remark VARCHAR ( 512 ),-- 备注
quote_num BIGINT,-- 引用数
is_top SMALLINT NOT NULL DEFAULT 0,-- 是否置顶 0 为未置顶、1 为已置顶
is_pin SMALLINT NOT NULL DEFAULT 0,-- 是否钉住 0 为未钉住、1 为已钉住
created_on BIGINT NOT NULL DEFAULT 0,-- 创建时间
modified_on BIGINT NOT NULL DEFAULT 0,-- 修改时间
deleted_on BIGINT NOT NULL DEFAULT 0,-- 删除时间
@ -273,6 +274,7 @@ CREATE TABLE p_topic_user (
reserve_b VARCHAR ( 255 ) -- 保留字段b
);
CREATE UNIQUE INDEX idx_topic_user_uid_tid ON p_topic_user USING btree ( topic_id, user_id );
CREATE INDEX idx_topic_user_uid_ispin ON p_topic_user USING btree ( user_id, is_pin );
CREATE SEQUENCE IF NOT EXISTS user_id_seq AS BIGINT MINVALUE 100058 NO MAXVALUE;
DROP TABLE IF EXISTS p_user;

@ -341,6 +341,7 @@ CREATE TABLE "p_topic_user" (
"remark" text ( 512 ),-- 备注
"quote_num" integer,-- 引用数
"is_top" integer NOT NULL DEFAULT 0,-- 是否置顶 0 为未置顶、1 为已置顶
"is_pin" integer NOT NULL DEFAULT 0,-- 是否钉住 0 为未钉住、1 为已钉住
"created_on" integer NOT NULL DEFAULT 0,-- 创建时间
"modified_on" integer NOT NULL DEFAULT 0,-- 修改时间
"deleted_on" integer NOT NULL DEFAULT 0,-- 删除时间
@ -680,6 +681,11 @@ ON "p_topic_user" (
"topic_id",
"user_id"
);
CREATE INDEX "main"."idx_topic_user_uid_ispin"
ON "p_topic_user" (
"user_id" ASC,
"is_pin" ASC
);
-- ----------------------------
-- Indexes structure for table p_user

@ -7,7 +7,7 @@ VITE_USE_WEB_PROFILE=true # 是否使用后端web服务提供的WebPro
# 模块开启
VITE_ENABLE_ANOUNCEMENT=false
VITE_ENABLE_WALLET=false
VITE_ENABLE_TRENDS_BAR=true
VITE_ENABLE_TRENDS_BAR=false
# 功能开启
VITE_ALLOW_TWEET_ATTACHMENT=true
@ -37,6 +37,6 @@ VITE_COPYRIGHT_RIGHT_LINK="https://www.paopao.info"
# 图片推文参数
VITE_DEFAULT_TWEET_IMAGE_404="https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/404.png"
# AliOSS 图片thumbnail参数
VITE_TWEET_IMAGE_THUMBNAIL="?x-oss-process=image/resize,m_fill,w_300,h_300,limit_0/auto-orient,1/format,png"
VITE_TWEET_IMAGE_THUMBNAIL="?x-oss-process=image/resize,m_fill,w_300,h_300,limit_0/auto-orient,1/format,webp"
# MinIOn 图片thumbnail参数
# VITE_TWEET_IMAGE_THUMBNAIL="?x-oss-process=image/resize,m_fill,w_300,h_300,limit_0/auto-orient,1/format,png"

@ -1 +0,0 @@
import{_ as s}from"./main-nav.vue_vue_type_style_index_0_lang-0ad26703.js";import{u as i}from"./vue-router-e5a2430e.js";import{G as a,e as c,a2 as u}from"./naive-ui-eecf2ec3.js";import{d as l,f as d,k as t,w as o,e as f,A as x}from"./@vue-a481fc63.js";import{_ as g}from"./index-1e276b8f.js";import"./vuex-44de225f.js";import"./vooks-6d99783e.js";import"./evtd-b614532e.js";import"./@vicons-f0266f88.js";import"./seemly-76b7b838.js";import"./vueuc-7c8d4b48.js";import"./@css-render-7124a1a5.js";import"./vdirs-b0483831.js";import"./@juggle-41516555.js";import"./css-render-6a5c5852.js";import"./@emotion-8a8e73f6.js";import"./lodash-es-8412e618.js";import"./treemate-25c27bff.js";import"./async-validator-dee29e8b.js";import"./date-fns-975a2d8f.js";import"./axios-4a70c6fc.js";import"./moment-2ab8298d.js";/* empty css */const v=l({__name:"404",setup(h){const e=i(),_=()=>{e.push({path:"/"})};return(k,w)=>{const n=s,p=c,r=u,m=a;return f(),d("div",null,[t(n,{title:"404"}),t(m,{class:"main-content-wrap wrap404",bordered:""},{default:o(()=>[t(r,{status:"404",title:"404 资源不存在",description:"再看看其他的吧"},{footer:o(()=>[t(p,{onClick:_},{default:o(()=>[x("回主页")]),_:1})]),_:1})]),_:1})])}}});const O=g(v,[["__scopeId","data-v-e62daa85"]]);export{O as default};

@ -0,0 +1 @@
import{_ as i}from"./main-nav.vue_vue_type_style_index_0_lang-DmAlkpQJ.js";import{u as s}from"./vue-router-zwGLnBy5.js";import{G as a,e as c,a2 as u}from"./naive-ui-DNcWoFGl.js";import{d as l,f as d,k as t,w as o,e as f,A as x}from"./@vue-CQsYufSu.js";import{_ as g}from"./index-v3l9hw1O.js";import"./vuex-DNAxYlmG.js";import"./vooks-BQzJqMzq.js";import"./evtd-CI_DDEu_.js";import"./@vicons-C3A8jsfr.js";import"./seemly-B7f2tHrf.js";import"./vueuc-CbQ6ZCvR.js";import"./@css-render-CQdyXCYJ.js";import"./vdirs-DL8EOfHr.js";import"./@juggle-C8OzoCMD.js";import"./css-render-Ct37b3-v.js";import"./@emotion-WldOFDRm.js";import"./lodash-es-i05dkx59.js";import"./treemate-HRdUPn5m.js";import"./async-validator-DKvM95Vc.js";import"./date-fns-x7VUUoCw.js";import"./axios-Bo0ATomq.js";import"./moment-BqTRGcJI.js";/* empty css */const h=l({__name:"404",setup(k){const n=s(),e=()=>{n.push({path:"/"})};return(w,v)=>{const r=i,p=c,_=u,m=a;return f(),d("div",null,[t(r,{title:"404"}),t(m,{class:"main-content-wrap wrap404",bordered:""},{default:o(()=>[t(_,{status:"404",title:"404 资源不存在",description:"再看看其他的吧"},{footer:o(()=>[t(p,{onClick:e},{default:o(()=>[x("回主页")]),_:1})]),_:1})]),_:1})])}}}),O=g(h,[["__scopeId","data-v-e62daa85"]]);export{O as default};

@ -1,3 +0,0 @@
import{i as d}from"./@vue-a481fc63.js";function C(i){let r=".",s="__",m="--",f;if(i){let e=i.blockPrefix;e&&(r=e),e=i.elementPrefix,e&&(s=e),e=i.modifierPrefix,e&&(m=e)}const b={install(e){f=e.c;const l=e.context;l.bem={},l.bem.b=null,l.bem.els=null}};function y(e){let l,n;return{before(t){l=t.bem.b,n=t.bem.els,t.bem.els=null},after(t){t.bem.b=l,t.bem.els=n},$({context:t,props:u}){return e=typeof e=="string"?e:e({context:t,props:u}),t.bem.b=e,`${(u==null?void 0:u.bPrefix)||r}${t.bem.b}`}}}function v(e){let l;return{before(n){l=n.bem.els},after(n){n.bem.els=l},$({context:n,props:t}){return e=typeof e=="string"?e:e({context:n,props:t}),n.bem.els=e.split(",").map(u=>u.trim()),n.bem.els.map(u=>`${(t==null?void 0:t.bPrefix)||r}${n.bem.b}${s}${u}`).join(", ")}}}function P(e){return{$({context:l,props:n}){e=typeof e=="string"?e:e({context:l,props:n});const t=e.split(",").map(o=>o.trim());function u(o){return t.map(x=>`&${(n==null?void 0:n.bPrefix)||r}${l.bem.b}${o!==void 0?`${s}${o}`:""}${m}${x}`).join(", ")}const c=l.bem.els;return c!==null?u(c[0]):u()}}}function _(e){return{$({context:l,props:n}){e=typeof e=="string"?e:e({context:l,props:n});const t=l.bem.els;return`&:not(${(n==null?void 0:n.bPrefix)||r}${l.bem.b}${t!==null&&t.length>0?`${s}${t[0]}`:""}${m}${e})`}}}return Object.assign(b,{cB:(...e)=>f(y(e[0]),e[1],e[2]),cE:(...e)=>f(v(e[0]),e[1],e[2]),cM:(...e)=>f(P(e[0]),e[1],e[2]),cNotM:(...e)=>f(_(e[0]),e[1],e[2])}),b}const $=Symbol("@css-render/vue3-ssr");function M(i,r){return`<style cssr-id="${i}">
${r}
</style>`}function S(i,r){const s=d($,null);if(s===null){console.error("[css-render/vue3-ssr]: no ssr context found.");return}const{styles:m,ids:f}=s;f.has(i)||m!==null&&(f.add(i),m.push(M(i,r)))}const j=typeof document<"u";function N(){if(j)return;const i=d($,null);if(i!==null)return{adapter:S,context:i}}export{C as p,N as u};

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save