merge from dev branch

pull/288/head
Michael Li 1 year ago
commit d34068a8c3
No known key found for this signature in database

@ -9,6 +9,7 @@ updates:
directory: "/"
schedule:
interval: "weekly"
target-branch: "dev"
reviewers:
- "rocboss"
- "alimy"

@ -9,6 +9,7 @@ on:
- 'feature/**'
- 'r/**'
- 'x/**'
- 't/**'
paths:
- '**.go'
- 'go.mod'
@ -46,7 +47,7 @@ jobs:
name: Test
strategy:
matrix:
go-version: [ 1.18.x, 1.19.x ]
go-version: [ 1.20.x ]
platform: [ ubuntu-latest, macos-latest ]
runs-on: ${{ matrix.platform }}
steps:
@ -65,7 +66,7 @@ jobs:
name: TestOnWindows
strategy:
matrix:
go-version: [ 1.18.x, 1.19.x ]
go-version: [ 1.20.x ]
platform: [ windows-latest ]
runs-on: ${{ matrix.platform }}
steps:

4
.gitignore vendored

@ -1,5 +1,6 @@
.DS_Store
.idea
.vscode
__debug_bin
!*.example
/config.yaml
*.log
@ -7,3 +8,4 @@ paopao-ce*
/release
/data
/custom
/.custom

@ -0,0 +1,4 @@
.DS_Store
*.log
__debug_bin
settings.json

@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "paopao-ce [debug]",
"type": "go",
"request": "launch",
"mode": "exec",
"program": "${workspaceFolder}/.vscode/__debug_bin",
"preLaunchTask": "go: build (debug)",
"cwd": "${workspaceFolder}"
}
]
}

@ -0,0 +1,4 @@
let launch = require('./launch.json');
launch.configurations.sort((a, b) => a.name.localeCompare(b.name));
let fs = require('fs');
fs.writeFileSync('launch.json', JSON.stringify(launch, null, 4));

21
.vscode/tasks.json vendored

@ -0,0 +1,21 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "go: build (debug)",
"type": "shell",
"command": "go",
"args": [
"build",
"-gcflags=all=-N -l",
"-tags",
"'embed go_json'",
"-o",
"${workspaceFolder}/.vscode/__debug_bin"
],
"options": {
"cwd": "${workspaceFolder}"
}
}
]
}

@ -0,0 +1,159 @@
# Changelog
All notable changes to paopao-ce are documented in this file.
## 0.3.0+dev ([`dev`](https://github.com/rocboss/paopao-ce/tree/dev))
### Added
- add custom comment sort strategy support [#243](https://github.com/rocboss/paopao-ce/pull/243)
- add `RedisCacheIndex` feature [#250](https://github.com/rocboss/paopao-ce/pull/250)
- add `Sentry` feature [#258](https://github.com/rocboss/paopao-ce/pull/258)
- add simple tweet share feature(just copy tweet link to clipboard now) support [#264](https://github.com/rocboss/paopao-ce/pull/264)
- add default tweet max length configure in web/.env support. [&a1160ca](https://github.com/rocboss/paopao-ce/commit/a1160ca79380445157146d9eae1710543c153cce 'commit a1160ca')
Set the value of `VITE_DEFAULT_TWEET_MAX_LENGTH` in file web/.env to change the tweet max default length.
- add custom whether provide user register configure in web/.env support. [#267](https://github.com/rocboss/paopao-ce/pull/267)
Set the value of `VITE_ALLOW_USER_REGISTER` in file web/.env to custom whether provide user register feature.
```
# file: web/.env or web/.env.local
...
# 局部参数
VITE_ALLOW_USER_REGISTER=true
...
```
and disallow user register in backend(add `Web:DisallowUserRegister` feature in `config.yaml`):
```yaml
# file config.yaml
...
Features:
Default: ["Base", "Postgres", "Zinc", "LocalOSS", "LoggerZinc", "BigCacheIndex", "Friendship", "Service", "Web:DisallowUserRegister"]
...
```
- add topic follow feature support [#273](https://github.com/rocboss/paopao-ce/pull/273)
mirgration database first(sql ddl file in `scripts/migration/**/*_topic_follow.up.sql`):
```sql
CREATE TABLE `p_topic_user` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`topic_id` BIGINT UNSIGNED NOT NULL COMMENT '标签ID',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '创建者ID',
`alias_name` VARCHAR ( 255 ) COMMENT '别名',
`remark` VARCHAR ( 512 ) COMMENT '备注',
`quote_num` BIGINT UNSIGNED COMMENT '引用数',
`is_top` TINYINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '是否置顶 0 为未置顶、1 为已置顶',
`created_on` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建时间',
`modified_on` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '修改时间',
`deleted_on` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '删除时间',
`is_del` TINYINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '是否删除 0 为未删除、1 为已删除',
`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
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户话题';
```
- add tweet comment thumbs up/down feature support [#275](https://github.com/rocboss/paopao-ce/pull/275)
mirgration database first(sql ddl file in `scripts/migration/**/*_comment_thumbs.up.sql`):
- add load more comments feature support [&60b217b](https://github.com/rocboss/paopao-ce/commit/60b217bcd950c69ba45cebcaa17efdf8048d5a4f 'commit 60b217b')
### Fixed
- fixed sql ddl p_contact's column `is_delete` define error (change to `is_del`) in scripts/paopao-mysql.sql [&afd8fe1](https://github.com/rocboss/paopao-ce/commit/afd8fe18d2dce08a4af846c2f822379d99a3d3b3 'commit afd8fe1')
- fixed cache index not expire in delete/add tweet error [#266](https://github.com/rocboss/paopao-ce/pull/266)
- fixed tweet's owner not allow star/collection action when tweet is private error [#274](https://github.com/rocboss/paopao-ce/pull/274)
- fixed user not list owner's collectioned private tweet error [#274](https://github.com/rocboss/paopao-ce/pull/274)
- fixed comments thumbs up/down state incorrect error [#283](https://github.com/rocboss/paopao-ce/pull/283)
### Changed
- use [github.com/rueian/rueidis](https://github.com/rueian/rueidis) as Redis client [#249](https://github.com/rocboss/paopao-ce/pull/249)
the **Old** redis client configure field
```yaml
...
Redis:
Host: redis:6379
Password:
DB:
```
the **New** redis client configure field
```yaml
...
Redis:
InitAddress:
- redis:6379
Username:
Password:
SelectDB:
ConnWriteTimeout: 60 # 连接写超时时间 多少秒 默认 60秒
```
- optimize web frontend dark theme [&b082a8f](https://github.com/rocboss/paopao-ce/commit/b082a8fa5e43dd6dacf459df93fa7e243dd901ea 'commit b082a8f')
- change web frontend main content layout default size to 544px [&b082a8f](https://github.com/rocboss/paopao-ce/commit/b082a8fa5e43dd6dacf459df93fa7e243dd901ea 'commit b082a8f')
- optimize web frontend in mobile environment use Drawer to display menu [#265](https://github.com/rocboss/paopao-ce/pull/265)
- optimize Dockerfile use pre-build builder/runner image to prevent network latency problem (`bitbus/paopao-ce-backend-builder` `bitbus/paopao-ce-backend-runner`) [#265](https://github.com/rocboss/paopao-ce/pull/265)
- optimize web ui in mobile environment [#280](https://github.com/rocboss/paopao-ce/pull/280)
### Removed
- remove `Deprecated:OldWeb` feature [#256](https://github.com/rocboss/paopao-ce/pull/256)
## 0.2.5
### Changed
- fixed sql ddl error for contact table [#281](https://github.com/rocboss/paopao-ce/pull/281)
## 0.2.4
### Added
- add PWA support for web frontend [#242](https://github.com/rocboss/paopao-ce/pull/242)
## 0.2.3
### Added
- add PostgreSQL DDL file [#229](https://github.com/rocboss/paopao-ce/pull/229)
### Changed
- optimize MySQL DDL file [#229](https://github.com/rocboss/paopao-ce/pull/229)
- optimize Sqlite3 DDL file [#229](https://github.com/rocboss/paopao-ce/pull/229)
## 0.2.2
### Fixed
- fixed add star to tweet error [#222](https://github.com/rocboss/paopao-ce/pull/222)
## 0.2.1
### Changed
- optimize docker-compose.yaml use bitbus/paopao-ce:latest as release image [#217](https://github.com/rocboss/paopao-ce/pull/217)
### Fixed
- fixed sql ddl in scripts/paopao-mysql.sql and scripts/paopao-sqlite3.sql [#217](https://github.com/rocboss/paopao-ce/pull/217)
## 0.2.0
### Added
- add `Friendship` feature [#192](https://github.com/rocboss/paopao-ce/pull/192)
- add `Lightship` feature [#198](https://github.com/rocboss/paopao-ce/pull/198)
- add `Pyroscope` feature [#199](https://github.com/rocboss/paopao-ce/pull/199)
- add new `Web` service [#196](https://github.com/rocboss/paopao-ce/pull/196)
- add `Frontend:Web` feature [#196](https://github.com/rocboss/paopao-ce/pull/196)
- add `Deprecated:OldWeb` feature [#196](https://github.com/rocboss/paopao-ce/pull/196)
### Changes
- support run multiple service in single paopao-ce instance [#196](https://github.com/rocboss/paopao-ce/pull/196)
- use [go-mir](https://github.com/alimy/mir) optimize paopao-ce source code architecture [#196](https://github.com/rocboss/paopao-ce/pull/196)
### Fixed
- some other features optimize and bug fix
---
**Older change logs can be found on [GitHub](https://github.com/rocboss/paopao-ce/releases?after=v0.2.0).**

@ -1,5 +1,7 @@
# syntax=docker/dockerfile:experimental
# build frontend
FROM node:18-alpine as frontend
FROM node:19-alpine as frontend
ARG API_HOST
ARG USE_API_HOST=yes
ARG EMBED_UI=yes
@ -11,15 +13,11 @@ RUN [ $EMBED_UI != yes ] || [ $USE_DIST != no ] || (yarn && yarn build)
RUN [ $EMBED_UI = yes ] || mkdir dist || echo ""
# build backend
FROM golang:1.18-alpine AS 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
RUN apk --no-cache --no-progress add --virtual \
build-deps \
build-base \
git
WORKDIR /paopao-ce
COPY . .
@ -28,19 +26,18 @@ ENV GOPROXY=https://goproxy.cn
RUN [ $EMBED_UI != yes ] || make build TAGS='embed go_json'
RUN [ $EMBED_UI = yes ] || make build TAGS='go_json'
FROM alpine:3.16
FROM bitbus/paopao-ce-backend-runner:latest
ARG API_HOST
ARG USE_API_HOST=yes
ARG EMBED_UI=yes
ARG USE_DIST=no
ENV TZ=Asia/Shanghai
RUN apk update && apk add --no-cache ca-certificates && update-ca-certificates
WORKDIR /app/paopao-ce
COPY --from=backend /paopao-ce/release/paopao-ce .
COPY configs ./configs
COPY --from=backend /paopao-ce/config.yaml.sample config.yaml
VOLUME ["/app/paopao-ce/configs"]
VOLUME ["/app/paopao-ce/custom"]
EXPOSE 8008
HEALTHCHECK --interval=5s --timeout=3s --retries=3 CMD ps -ef | grep paopao-ce || exit 1
ENTRYPOINT ["/app/paopao-ce/paopao-ce"]

@ -4,27 +4,28 @@ TARGET = paopao-ce
ifeq ($(OS),Windows_NT)
TARGET := $(TARGET).exe
endif
TARGET_BIN = $(basename $(TARGET))
ifeq (n$(CGO_ENABLED),n)
CGO_ENABLED := 1
CGO_ENABLED := 0
endif
RELEASE_ROOT = release
RELEASE_FILES = LICENSE README.md config.yaml.sample scripts configs
RELEASE_FILES = LICENSE README.md CHANGELOG.md config.yaml.sample docker-compose.yaml scripts docs
RELEASE_LINUX_AMD64 = $(RELEASE_ROOT)/linux-amd64/$(TARGET)
RELEASE_DARWIN_AMD64 = $(RELEASE_ROOT)/darwin-amd64/$(TARGET)
RELEASE_DARWIN_ARM64 = $(RELEASE_ROOT)/darwin-arm64/$(TARGET)
RELEASE_WINDOWS_AMD64 = $(RELEASE_ROOT)/windows-amd64/$(TARGET)
BUILD_VERSION := $(shell git describe --tags | cut -f 1 -d "-")
BUILD_VERSION := $(shell git describe --tags --always | cut -f1 -f2 -d "-")
BUILD_DATE := $(shell date +'%Y-%m-%d %H:%M:%S')
SHA_SHORT := $(shell git rev-parse --short HEAD)
TAGS = ""
MOD_NAME = github.com/rocboss/paopao-ce
LDFLAGS = -X "${MOD_NAME}/pkg/debug.version=${BUILD_VERSION}" \
-X "${MOD_NAME}/pkg/debug.buildDate=${BUILD_DATE}" \
-X "${MOD_NAME}/pkg/debug.commitID=${SHA_SHORT}" -w -s
LDFLAGS = -X "${MOD_NAME}/pkg/version.version=${BUILD_VERSION}" \
-X "${MOD_NAME}/pkg/version.buildDate=${BUILD_DATE}" \
-X "${MOD_NAME}/pkg/version.commitID=${SHA_SHORT}" -w -s
all: fmt build
@ -50,23 +51,23 @@ release: linux-amd64 darwin-amd64 darwin-arm64 windows-x64
.PHONY: linux-amd64
linux-amd64:
@echo Build paopao-ce [linux-amd64] CGO_ENABLED=$(CGO_ENABLED)
@CGO_ENABLED=$(CGO_ENABLED) GOOS=linux GOARCH=amd64 go build -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $(RELEASE_LINUX_AMD64)/$(TARGET)
@echo Build paopao-ce [linux-amd64] CGO_ENABLED=$(CGO_ENABLED) TAGS="'$(TAGS)'"
@CGO_ENABLED=$(CGO_ENABLED) GOOS=linux GOARCH=amd64 go build -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $(RELEASE_LINUX_AMD64)/$(TARGET_BIN)
.PHONY: darwin-amd64
darwin-amd64:
@echo Build paopao-ce [darwin-amd64] CGO_ENABLED=$(CGO_ENABLED)
@CGO_ENABLED=$(CGO_ENABLED) GOOS=darwin GOARCH=amd64 go build -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $(RELEASE_DARWIN_AMD64)/$(TARGET)
@echo Build paopao-ce [darwin-amd64] CGO_ENABLED=$(CGO_ENABLED) TAGS="'$(TAGS)'"
@CGO_ENABLED=$(CGO_ENABLED) GOOS=darwin GOARCH=amd64 go build -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $(RELEASE_DARWIN_AMD64)/$(TARGET_BIN)
.PHONY: darwin-arm64
darwin-arm64:
@echo Build paopao-ce [darwin-arm64] CGO_ENABLED=$(CGO_ENABLED)
@CGO_ENABLED=$(CGO_ENABLED) GOOS=darwin GOARCH=arm64 go build -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $(RELEASE_DARWIN_ARM64)/$(TARGET)
@echo Build paopao-ce [darwin-arm64] CGO_ENABLED=$(CGO_ENABLED) TAGS="'$(TAGS)'"
@CGO_ENABLED=$(CGO_ENABLED) GOOS=darwin GOARCH=arm64 go build -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $(RELEASE_DARWIN_ARM64)/$(TARGET_BIN)
.PHONY: windows-x64
windows-x64:
@echo Build paopao-ce [windows-x64] CGO_ENABLED=$(CGO_ENABLED)
@CGO_ENABLED=$(CGO_ENABLED) GOOS=windows GOARCH=amd64 go build -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $(RELEASE_WINDOWS_AMD64)/$(basename $(TARGET)).exe
@echo Build paopao-ce [windows-x64] CGO_ENABLED=$(CGO_ENABLED) TAGS="'$(TAGS)'"
@CGO_ENABLED=$(CGO_ENABLED) GOOS=windows GOARCH=amd64 go build -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $(RELEASE_WINDOWS_AMD64)/$(TARGET_BIN).exe
.PHONY: generate
generate: gen-mir gen-grpc
@ -109,9 +110,9 @@ pre-commit: fmt
.PHONY: install-protobuf-plugins
install-protobuf-plugins:
@go install github.com/bufbuild/buf/cmd/buf@v1.11.0
@go install github.com/bufbuild/buf/cmd/protoc-gen-buf-breaking@v1.11.0
@go install github.com/bufbuild/buf/cmd/protoc-gen-buf-lint@v1.11.0
@go install github.com/bufbuild/buf/cmd/buf@v1.15.1
@go install github.com/bufbuild/buf/cmd/protoc-gen-buf-breaking@v1.15.1
@go install github.com/bufbuild/buf/cmd/protoc-gen-buf-lint@v1.15.1
@go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
@go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

@ -65,7 +65,7 @@ PaoPao主要由以下优秀的开源项目/工具构建
### 环境要求
* Go (1.18+)
* Go (1.20+)
* Node.js (14+)
* MySQL (5.7+)
* Redis
@ -194,7 +194,10 @@ PaoPao主要由以下优秀的开源项目/工具构建
docker build -t your/paopao-ce:tag --build-arg EMBED_UI=no .
# 运行
docker run -d -p 8008:8008 -v ${PWD}/config.yaml.sample:/app/paopao-ce/config.yaml your/paopao-ce:tag
mkdir custom && docker run -d -p 8008:8008 -v ${PWD}/custom:/app/paopao-ce/custom -v ${PWD}/config.yaml.sample:/app/paopao-ce/config.yaml your/paopao-ce:tag
# 或者直接运行构建好的docker image
mkdir custom && docker run -d -p 8008:8008 -v ${PWD}/custom:/app/paopao-ce/custom -v ${PWD}/config.yaml.sample:/app/paopao-ce/config.yaml bitbus/paopao-ce:latest
```
* 前端:
@ -227,8 +230,7 @@ docker compose up --build
# file: docker-compose.yaml
...
backend:
build:
context: .
image: bitbus/paopao-ce:latest
restart: always
depends_on:
- db
@ -340,7 +342,6 @@ release/paopao-ce --no-default-features --features sqlite3,localoss,loggerfile,r
|`Docs` | 子服务 | WIP | 开启开发者文档服务|
|`Frontend:Web` | 子服务 | 稳定 | 开启独立前端服务|
|`Frontend:EmbedWeb` | 子服务 | 稳定 | 开启内嵌于后端Web API服务中的前端服务|
|`Deprecated:Web` | 子服务 | 稳定 | 开启旧的Web服务|
|`Gorm` | 数据库 | 稳定(默认) | 使用[gorm](https://github.com/go-gorm/gorm)作为数据库的ORM默认使用 `Gorm` + `MySQL`组合|
|`Sqlx`| 数据库 | WIP | 使用[sqlx](https://github.com/jmoiron/sqlx)作为数据库的ORM|
|`Sqlc`| 数据库 | WIP | 使用[sqlc](https://github.com/kyleconroy/sqlc)自动生成ORM代码|
@ -358,9 +359,11 @@ release/paopao-ce --no-default-features --features sqlite3,localoss,loggerfile,r
|`Redis` | 缓存 | 稳定 | Redis缓存功能 |
|`SimpleCacheIndex` | 缓存 | Deprecated | 提供简单的 广场推文列表 的缓存功能 |
|`BigCacheIndex` | 缓存 | 稳定(推荐) | 使用[BigCache](https://github.com/allegro/bigcache)缓存 广场推文列表,缓存每个用户每一页,简单做到千人千面 |
|`RedisCacheIndex` | 缓存 | 内测(推荐) | 使用Redis缓存 广场推文列表,缓存每个用户每一页,简单做到千人千面 |
|`Zinc` | 搜索 | 稳定(推荐) | 基于[Zinc](https://github.com/zinclabs/zinc)搜索引擎提供推文搜索服务 |
|`Meili` | 搜索 | 稳定(推荐) | 基于[Meilisearch](https://github.com/meilisearch/meilisearch)搜索引擎提供推文搜索服务 |
|`Bleve` | 搜索 | WIP | 基于[Bleve](https://github.com/blevesearch/bleve)搜索引擎提供推文搜索服务 |
|[`Sentry`](docs/proposal/23040412-关于使用sentry用于错误追踪与性能检测的设计.md) | 监控 | 内测 | 使用Sentry进行错误跟踪与性能监控 |
|`LoggerFile` | 日志 | 稳定 | 使用文件写日志 |
|`LoggerZinc` | 日志 | 稳定(推荐) | 使用[Zinc](https://github.com/zinclabs/zinc)写日志 |
|`LoggerMeili` | 日志 | 内测 | 使用[Meilisearch](https://github.com/meilisearch/meilisearch)写日志 |
@ -370,7 +373,9 @@ release/paopao-ce --no-default-features --features sqlite3,localoss,loggerfile,r
|`Alipay` | 支付 | 稳定 | 开启基于[支付宝开放平台](https://open.alipay.com/)的钱包功能 |
|`Sms` | 短信验证 | 稳定 | 开启短信验证码功能,用于手机绑定验证手机是否注册者的;功能如果没有开启,手机绑定时任意短信验证码都可以绑定手机 |
|`Docs:OpenAPI` | 开发文档 | 稳定 | 开启openapi文档功能提供web api文档说明(visit http://127.0.0.1:8008/docs/openapi) |
|[`Pyroscope`](docs/proposal/016-关于使用pyroscope用于性能调试的设计.md)| 性能优化 | 内测 | 开启Pyroscope功能用于性能调试 |
|`PhoneBind` | 其他 | 稳定 | 手机绑定功能 |
|`Web:DisallowUserRegister` | 功能特性 | 稳定 | 不允许用户注册 |
> 功能项状态详情参考 [features-status](features-status.md).
@ -492,6 +497,32 @@ MinIO: # MinIO 存储配置
...
```
#### [Pyroscope](https://github.com/pyroscope-io/pyroscope) 性能剖析
* Pyroscope运行
```sh
mkdir -p data/minio/data
# 使用Docker运行
docker run -it -p 4040:4040 pyroscope/pyroscope:latest server
# 使用docker compose运行 需要删除docker-compose.yaml中关于pyroscope的注释
docker compose up -d pyroscope
# visit http://loclahost:4040
```
* 修改Pyroscope配置
```yaml
# features中加上 MinIO
Features:
Default: ["Meili", "LoggerMeili", "Base", "Sqlite3", "BigCacheIndex", "Pyroscope"]
...
Pyroscope: # Pyroscope配置
AppName: "paopao-ce"
Endpoint: "http://localhost:4040" # Pyroscope server address
AuthToken: # Pyroscope authentication token
Logger: none # Pyroscope logger (standard | logrus | none)
...
```
### 源代码分支管理
**主代码库`github.com/rocboss/paopao-ce`**
```bash
@ -513,19 +544,23 @@ x/sqlx
| 名称 | 说明 | 备注|
| ----- | ----- | ----- |
| [`main`](https://github.com/rocboss/paopao-ce) | 主分支 |分支`main`是主分支也是paopao-ce的稳定版本发布分支只有经过内部测试没有重大bug出现的稳定代码才会推进到这个分支该分支主要由`beta`分支代码演进而来,原则上**只接受bug修复PR**。`rc版本/稳定版本` 发布都应该在`main`主分支中进行。|
| [`beta`](https://github.com/rocboss/paopao-ce/tree/beta) | 公测分支 |分支`beta`是公测分支,代码推进到`main`主分支的候选分支;该分支主要由`dev`分支代码演进而来,**接受bug修复以及新功能优化的PR**原则上不接受新功能PR。`alpha/beta版本` 发布都应该在`beta`公测分支下进行。|
| [`dev`](https://github.com/rocboss/paopao-ce/tree/dev) | 开发分支 | 分支`dev`是开发分支,**不定期频繁更新**,接受 *新功能PR、代码优化PR、bug修复PR***新功能PR** 都应该首先提交给`dev`分支进行合并bug修复/代码优化 后 **冻结新功能** 将代码演进合并到`beta`分支。|
| [`beta`](https://github.com/rocboss/paopao-ce/tree/beta) | 公测分支 |分支`beta`是公测分支,代码推进到`main`主分支的候选分支;该分支主要由`alpha`分支代码演进而来,**接受bug修复以及新功能优化的PR**原则上不接受新功能PR。`beta版本` 发布都应该在`beta`公测分支下进行。|
| [`alpha`](https://github.com/rocboss/paopao-ce/tree/alpha) | 内测分支 |分支`alpha`是内测分支,代码推进到`beta`分支的候选分支;该分支主要由`dev`分支代码演进而来,**接受bug修复以及新功能相关的PR**接受新功能PR。分支代码演进到一个里程碑式的阶段后**冻结所有新功能**,合并代码到`beta`公测分支进行下一阶段的持续演进。`alpha版本` 发布都应该在`alpha`内测分支下进行。|
| [`dev`](https://github.com/rocboss/paopao-ce/tree/dev) | 开发分支 | 分支`dev`是开发分支,**不定期频繁更新**,接受 *新功能PR、代码优化PR、bug修复PR***新功能PR** 都应该首先提交给`dev`分支进行合并bug修复/新功能开发/代码优化 **阶段性冻结** 后将代码演进合并到`alpha`分支。|
| `feature/*` | 子功能分支 |`feature/*`是新功能子分支,一般新功能子分支都是 *从`dev`开发分支fork出来的*;子功能分支 **只专注于该新功能** 代码的开发/优化,待开发接近内测阶段 *提交新功能PR给`dev`分支进行review/merge*,待新功能代码演进到`beta`分支后,原则上是可以删除该分支,但也可以保留到稳定版本发布。**该分支专注于新功能的开发只接受新功能的bug修复/优化PR**。|
| `jc/*` |维护者的开发分支|`jc/*`是代码库维护者的开发分支一般包含一些局部优化或者bug修复代码有时可以直接将代码merge到`dev/beta`分支原则上不允许直接merge代码到`main`主分支。|
| `x/*` |实验分支|`x/*`是技术实验分支某些技术的引入需要经过具体的代码实现与真实场景的测评考量评估后如果某项技术适合引入到paopao-ce就fork出一个`feature/*`分支作为新功能引入到paopao-ce。一般一些比较激进的技术从`dev`分支fork出一个新的`x/*`分支各种尝试、考量、评估后或丢弃、或引入到paopao-ce。|
| `t/*` | 临时分支 |`t/*`是临时发版本分支,一般 `beta` 分支演进到正式版本发布前的最后某个beta版本比如v0.2.0-beta)就从beta分支fork出一个 `t/*` 分支用于向 `main` 分支提交 PR 用于Review待 PR Reviewed 合并到 `main` 分支后,可以删除这个临时创建的分支。这样设计主要是考虑到有时合并到 `main` 分支时需要Review的时间可能会长一些而dev分支的代码又急需推进到beta分支以发布下一个alpha版本用于内测相当于为下一个测试版本发布腾地方。|
| `r/*` |发行版本分支|`r/*`是不同发行版本分支,不同发行版本各有不同的侧重点,可以根据需要选择适合的发行版本。|
**发行版本分支说明**
| 名称 | 说明 | 维护者 | 备注 |
| ----- | ----- | ----- | ----- |
|[`paopao-ce`](https://github.com/rocboss/paopao-ce/tree/dev)|paopao-ce 主发行版本|[ROC](https://github.com/rocboss 'ROC')|该分支 [数据逻辑层](https://github.com/rocboss/paopao-ce/tree/dev/internal/dao/jinzhu) 使用[gorm](https://github.com/go-gorm/gorm)作为数据逻辑层的ORM框架适配MySQL/PostgreSQL/Sqlite3数据库。|
|[`r/paopao-ce`](https://github.com/rocboss/paopao-ce/tree/r/paopao-ce)|paopao-ce 主分支预览版本|[ROC](https://github.com/rocboss 'ROC')<br/>[北野](https://github.com/alimy 'Michael Li')|该分支 [数据逻辑层](https://github.com/rocboss/paopao-ce/tree/dev/internal/dao/jinzhu) 使用[gorm](https://github.com/go-gorm/gorm)作为数据逻辑层的ORM框架适配MySQL/PostgreSQL/Sqlite3数据库。代码较`main`分支新,是主发行版本的前瞻预览版本。|
|[`r/paopao-ce-plus`](https://github.com/rocboss/paopao-ce/tree/r/paopao-ce-plus)|paopao-ce-plus 发行版本|[北野](https://github.com/alimy 'Michael Li')|该分支 [数据逻辑层](https://github.com/rocboss/paopao-ce/tree/r/paopao-ce-plus/internal/dao/sakila) 使用[sqlx](https://github.com/jmoiron/sqlx)作为数据逻辑层的ORM框架专注于为MySQL/PostgreSQL/Sqlite3使用更优化的查询语句以提升数据检索效率。建议熟悉[sqlx](https://github.com/jmoiron/sqlx)的开发人员可以基于此版本来做 二次开发。|
|[`r/paopao-ce-pro`](https://github.com/rocboss/paopao-ce/tree/r/paopao-ce-pro)|paopao-ce-pro 发行版本|[北野](https://github.com/alimy 'Michael Li')|该分支 [数据逻辑层](https://github.com/rocboss/paopao-ce/tree/r/paopao-ce-pro/internal/dao/slonik) 使用[sqlc](https://github.com/kyleconroy/sqlc)作为sql语句生成器自动生成ORM代码专门针对特定数据库MySQL/PostgreSQL进行查询优化熟悉[sqlc](https://github.com/kyleconroy/sqlc)的开发人员可以基于此版本来做 二次开发。(另:分支目前只使用[pgx-v5](https://github.com/jackc/pgx)适配了PostgreSQL数据库后续或许会适配MySQL/TiDB数据库。)|
|[`r/paopao-ce-xtra`](https://github.com/rocboss/paopao-ce/tree/r/paopao-ce-xtra)|paopao-ce-xtra 发行版本|[北野](https://github.com/alimy 'Michael Li')|该分支 是r/paopao-ce、r/paopao-ce-plus、r/paopao-ce-pro的合集|
**代码分支演进图**
![](docs/proposal/.assets/000-01.png)

@ -1,16 +1,28 @@
## Roadmap for paopao-ce
[paopao-ce](https://github.com/rocboss/paopao-ce/tree/dev)/[paopao-plus](https://github.com/rocboss/paopao-ce/tree/r/paopao-plus)/[paopao-pro](https://github.com/rocboss/paopao-ce/tree/r/paopao-pro) features develop or optimize and bug fix roadmap.
[paopao-ce](https://github.com/rocboss/paopao-ce/tree/dev)/[paopao-ce-plus](https://github.com/rocboss/paopao-ce/tree/r/paopao-ce-plus)/[paopao-ce-pro](https://github.com/rocboss/paopao-ce/tree/r/paopao-ce-pro) features develop or optimize and bug fix roadmap.
### paopao-ce roadmap
#### v0.3.0
## paopao-ce roadmap
#### dev+
* [ ] add `Followship` feature
* [ ] add `Auth:Bcrypt` feature
* [ ] add `Auth:MD5` feature (just for compatible)
* [x] add extend base ORM code for implement data logic base sqlx/sqlc
* [ ] optimize media tweet submit logic
* [ ] optimize search logic service
#### v0.3.0
* [x] remove `Deprecated:OldWeb` feature
* [x] add user topic follow feature support
* [x] add tweet link share support
* [ ] add comment thumbsUp/thumbsDown support
* [x] add `RedisCacheIndex` feature
* [x] add `Sentry` feature
#### v0.2.0
* [x] add `Friendship` feature
* [x] add `Lightship` feature
* [ ] add extend base ORM code for implement data logic base sqlx/sqlc
* [x] add `Pyroscope` feature
* [x] add new `Web` service
* [x] add `Frontend:Web` feature
* [x] add `Deprecated:OldWeb` feature
@ -33,14 +45,26 @@
* [ ] add reactions support
* [ ] add tweet thread like twitter support
* [ ] add short link support
* [ ] optimize current message push logic service use `ims` module
* [ ] optimize topics service
* [ ] optimize current message push logic service use `ims` module
* [ ] optimize backend data logic service(optimize database CRUD operate)
### paopao-plus roadmap
#### v0.3.0
## paopao-ce-plus roadmap
#### paopao-ce-plus/v0.4.0
* [ ] adapt for paopao-ce v0.4.0
#### paopao-ce-plus/v0.3.0
* [ ] adapt for paopao-ce v0.3.0
### paopao-pro roadmap
#### v0.3.0
* [ ] adapt for paopao-ce v0.3.0
#### paopao-ce-plus/v0.2.0
* [ ] adapt for paopao-ce v0.2.0
## paopao-ce-pro roadmap
#### paopao-ce-pro/v0.4.0
* [ ] adapt for paopao-ce v0.4.0
#### paopao-ce-pro/v0.3.0
* [ ] adapt for paopao-ce v0.3.0
#### paopao-ce-pro/v0.2.0
* [ ] adapt for paopao-ce v0.2.0

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v3.0.1
// - mir v3.1.1
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v3.0.1
// - mir v3.1.1
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v3.0.1
// - mir v3.1.1
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v3.0.1
// - mir v3.1.1
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v3.0.1
// - mir v3.1.1
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v3.0.1
// - mir v3.1.1
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v3.0.1
// - mir v3.1.1
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v3.0.1
// - mir v3.1.1
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v3.0.1
// - mir v3.1.1
package v1

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v3.0.1
// - mir v3.1.1
package v1
@ -16,6 +16,8 @@ type Loose interface {
// Chain provide handlers chain for gin
Chain() gin.HandlersChain
TweetComments(*web.TweetCommentsReq) (*web.TweetCommentsResp, mir.Error)
TopicList(*web.TopicListReq) (*web.TopicListResp, mir.Error)
GetUserProfile(*web.GetUserProfileReq) (*web.GetUserProfileResp, mir.Error)
GetUserTweets(*web.GetUserTweetsReq) (*web.GetUserTweetsResp, mir.Error)
Timeline(*web.TimelineReq) (*web.TimelineResp, mir.Error)
@ -24,6 +26,8 @@ type Loose interface {
}
type LooseBinding interface {
BindTweetComments(*gin.Context) (*web.TweetCommentsReq, mir.Error)
BindTopicList(*gin.Context) (*web.TopicListReq, mir.Error)
BindGetUserProfile(*gin.Context) (*web.GetUserProfileReq, mir.Error)
BindGetUserTweets(*gin.Context) (*web.GetUserTweetsReq, mir.Error)
BindTimeline(*gin.Context) (*web.TimelineReq, mir.Error)
@ -32,6 +36,8 @@ type LooseBinding interface {
}
type LooseRender interface {
RenderTweetComments(*gin.Context, *web.TweetCommentsResp, mir.Error)
RenderTopicList(*gin.Context, *web.TopicListResp, mir.Error)
RenderGetUserProfile(*gin.Context, *web.GetUserProfileResp, mir.Error)
RenderGetUserTweets(*gin.Context, *web.GetUserTweetsResp, mir.Error)
RenderTimeline(*gin.Context, *web.TimelineResp, mir.Error)
@ -47,6 +53,38 @@ func RegisterLooseServant(e *gin.Engine, s Loose, b LooseBinding, r LooseRender)
router.Use(middlewares...)
// register routes info to router
router.Handle("GET", "/post/comments", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
return
default:
}
req, err := b.BindTweetComments(c)
if err != nil {
r.RenderTweetComments(c, nil, err)
return
}
resp, err := s.TweetComments(req)
r.RenderTweetComments(c, resp, err)
})
router.Handle("GET", "/tags", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
return
default:
}
req, err := b.BindTopicList(c)
if err != nil {
r.RenderTopicList(c, nil, err)
return
}
resp, err := s.TopicList(req)
r.RenderTopicList(c, resp, err)
})
router.Handle("GET", "/user/profile", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
@ -105,6 +143,14 @@ func (UnimplementedLooseServant) Chain() gin.HandlersChain {
return nil
}
func (UnimplementedLooseServant) TweetComments(req *web.TweetCommentsReq) (*web.TweetCommentsResp, mir.Error) {
return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented))
}
func (UnimplementedLooseServant) TopicList(req *web.TopicListReq) (*web.TopicListResp, mir.Error) {
return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented))
}
func (UnimplementedLooseServant) GetUserProfile(req *web.GetUserProfileReq) (*web.GetUserProfileResp, mir.Error) {
return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented))
}
@ -124,6 +170,14 @@ type UnimplementedLooseRender struct {
RenderAny func(*gin.Context, any, mir.Error)
}
func (r *UnimplementedLooseRender) RenderTweetComments(c *gin.Context, data *web.TweetCommentsResp, err mir.Error) {
r.RenderAny(c, data, err)
}
func (r *UnimplementedLooseRender) RenderTopicList(c *gin.Context, data *web.TopicListResp, err mir.Error) {
r.RenderAny(c, data, err)
}
func (r *UnimplementedLooseRender) RenderGetUserProfile(c *gin.Context, data *web.GetUserProfileResp, err mir.Error) {
r.RenderAny(c, data, err)
}
@ -143,6 +197,18 @@ type UnimplementedLooseBinding struct {
BindAny func(*gin.Context, any) mir.Error
}
func (b *UnimplementedLooseBinding) BindTweetComments(c *gin.Context) (*web.TweetCommentsReq, mir.Error) {
obj := new(web.TweetCommentsReq)
err := b.BindAny(c, obj)
return obj, err
}
func (b *UnimplementedLooseBinding) BindTopicList(c *gin.Context) (*web.TopicListReq, mir.Error) {
obj := new(web.TopicListReq)
err := b.BindAny(c, obj)
return obj, err
}
func (b *UnimplementedLooseBinding) BindGetUserProfile(c *gin.Context) (*web.GetUserProfileReq, mir.Error) {
obj := new(web.GetUserProfileReq)
err := b.BindAny(c, obj)

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v3.0.1
// - mir v3.1.1
package v1
@ -16,6 +16,13 @@ type Priv interface {
// Chain provide handlers chain for gin
Chain() gin.HandlersChain
UnfollowTopic(*web.UnfollowTopicReq) mir.Error
FollowTopic(*web.FollowTopicReq) mir.Error
StickTopic(*web.StickTopicReq) (*web.StickTopicResp, mir.Error)
ThumbsDownTweetReply(*web.TweetReplyThumbsReq) mir.Error
ThumbsUpTweetReply(*web.TweetReplyThumbsReq) mir.Error
ThumbsDownTweetComment(*web.TweetCommentThumbsReq) mir.Error
ThumbsUpTweetComment(*web.TweetCommentThumbsReq) mir.Error
DeleteCommentReply(*web.DeleteCommentReplyReq) mir.Error
CreateCommentReply(*web.CreateCommentReplyReq) (*web.CreateCommentReplyResp, mir.Error)
DeleteComment(*web.DeleteCommentReq) mir.Error
@ -35,6 +42,13 @@ type Priv interface {
}
type PrivBinding interface {
BindUnfollowTopic(*gin.Context) (*web.UnfollowTopicReq, mir.Error)
BindFollowTopic(*gin.Context) (*web.FollowTopicReq, mir.Error)
BindStickTopic(*gin.Context) (*web.StickTopicReq, mir.Error)
BindThumbsDownTweetReply(*gin.Context) (*web.TweetReplyThumbsReq, mir.Error)
BindThumbsUpTweetReply(*gin.Context) (*web.TweetReplyThumbsReq, mir.Error)
BindThumbsDownTweetComment(*gin.Context) (*web.TweetCommentThumbsReq, mir.Error)
BindThumbsUpTweetComment(*gin.Context) (*web.TweetCommentThumbsReq, mir.Error)
BindDeleteCommentReply(*gin.Context) (*web.DeleteCommentReplyReq, mir.Error)
BindCreateCommentReply(*gin.Context) (*web.CreateCommentReplyReq, mir.Error)
BindDeleteComment(*gin.Context) (*web.DeleteCommentReq, mir.Error)
@ -54,6 +68,13 @@ type PrivBinding interface {
}
type PrivRender interface {
RenderUnfollowTopic(*gin.Context, mir.Error)
RenderFollowTopic(*gin.Context, mir.Error)
RenderStickTopic(*gin.Context, *web.StickTopicResp, mir.Error)
RenderThumbsDownTweetReply(*gin.Context, mir.Error)
RenderThumbsUpTweetReply(*gin.Context, mir.Error)
RenderThumbsDownTweetComment(*gin.Context, mir.Error)
RenderThumbsUpTweetComment(*gin.Context, mir.Error)
RenderDeleteCommentReply(*gin.Context, mir.Error)
RenderCreateCommentReply(*gin.Context, *web.CreateCommentReplyResp, mir.Error)
RenderDeleteComment(*gin.Context, mir.Error)
@ -80,6 +101,112 @@ func RegisterPrivServant(e *gin.Engine, s Priv, b PrivBinding, r PrivRender) {
router.Use(middlewares...)
// register routes info to router
router.Handle("POST", "/topic/unfollow", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
return
default:
}
req, err := b.BindUnfollowTopic(c)
if err != nil {
r.RenderUnfollowTopic(c, err)
return
}
r.RenderUnfollowTopic(c, s.UnfollowTopic(req))
})
router.Handle("POST", "/topic/follow", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
return
default:
}
req, err := b.BindFollowTopic(c)
if err != nil {
r.RenderFollowTopic(c, err)
return
}
r.RenderFollowTopic(c, s.FollowTopic(req))
})
router.Handle("POST", "/topic/stick", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
return
default:
}
req, err := b.BindStickTopic(c)
if err != nil {
r.RenderStickTopic(c, nil, err)
return
}
resp, err := s.StickTopic(req)
r.RenderStickTopic(c, resp, err)
})
router.Handle("POST", "/tweet/reply/thumbsdown", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
return
default:
}
req, err := b.BindThumbsDownTweetReply(c)
if err != nil {
r.RenderThumbsDownTweetReply(c, err)
return
}
r.RenderThumbsDownTweetReply(c, s.ThumbsDownTweetReply(req))
})
router.Handle("POST", "/tweet/reply/thumbsup", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
return
default:
}
req, err := b.BindThumbsUpTweetReply(c)
if err != nil {
r.RenderThumbsUpTweetReply(c, err)
return
}
r.RenderThumbsUpTweetReply(c, s.ThumbsUpTweetReply(req))
})
router.Handle("POST", "/tweet/comment/thumbsdown", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
return
default:
}
req, err := b.BindThumbsDownTweetComment(c)
if err != nil {
r.RenderThumbsDownTweetComment(c, err)
return
}
r.RenderThumbsDownTweetComment(c, s.ThumbsDownTweetComment(req))
})
router.Handle("POST", "/tweet/comment/thumbsup", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
return
default:
}
req, err := b.BindThumbsUpTweetComment(c)
if err != nil {
r.RenderThumbsUpTweetComment(c, err)
return
}
r.RenderThumbsUpTweetComment(c, s.ThumbsUpTweetComment(req))
})
router.Handle("DELETE", "/post/comment/reply", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
@ -206,7 +333,7 @@ func RegisterPrivServant(e *gin.Engine, s Priv, b PrivBinding, r PrivRender) {
r.RenderCollectionTweet(c, resp, err)
})
router.Handle("POST", "/post/start", func(c *gin.Context) {
router.Handle("POST", "/post/star", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
return
@ -311,6 +438,34 @@ func (UnimplementedPrivServant) Chain() gin.HandlersChain {
return nil
}
func (UnimplementedPrivServant) UnfollowTopic(req *web.UnfollowTopicReq) mir.Error {
return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented))
}
func (UnimplementedPrivServant) FollowTopic(req *web.FollowTopicReq) mir.Error {
return 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))
}
func (UnimplementedPrivServant) ThumbsDownTweetReply(req *web.TweetReplyThumbsReq) mir.Error {
return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented))
}
func (UnimplementedPrivServant) ThumbsUpTweetReply(req *web.TweetReplyThumbsReq) mir.Error {
return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented))
}
func (UnimplementedPrivServant) ThumbsDownTweetComment(req *web.TweetCommentThumbsReq) mir.Error {
return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented))
}
func (UnimplementedPrivServant) ThumbsUpTweetComment(req *web.TweetCommentThumbsReq) mir.Error {
return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented))
}
func (UnimplementedPrivServant) DeleteCommentReply(req *web.DeleteCommentReplyReq) mir.Error {
return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented))
}
@ -374,6 +529,34 @@ type UnimplementedPrivRender struct {
RenderAny func(*gin.Context, any, mir.Error)
}
func (r *UnimplementedPrivRender) RenderUnfollowTopic(c *gin.Context, err mir.Error) {
r.RenderAny(c, nil, err)
}
func (r *UnimplementedPrivRender) RenderFollowTopic(c *gin.Context, err mir.Error) {
r.RenderAny(c, nil, err)
}
func (r *UnimplementedPrivRender) RenderStickTopic(c *gin.Context, data *web.StickTopicResp, err mir.Error) {
r.RenderAny(c, data, err)
}
func (r *UnimplementedPrivRender) RenderThumbsDownTweetReply(c *gin.Context, err mir.Error) {
r.RenderAny(c, nil, err)
}
func (r *UnimplementedPrivRender) RenderThumbsUpTweetReply(c *gin.Context, err mir.Error) {
r.RenderAny(c, nil, err)
}
func (r *UnimplementedPrivRender) RenderThumbsDownTweetComment(c *gin.Context, err mir.Error) {
r.RenderAny(c, nil, err)
}
func (r *UnimplementedPrivRender) RenderThumbsUpTweetComment(c *gin.Context, err mir.Error) {
r.RenderAny(c, nil, err)
}
func (r *UnimplementedPrivRender) RenderDeleteCommentReply(c *gin.Context, err mir.Error) {
r.RenderAny(c, nil, err)
}
@ -437,6 +620,48 @@ type UnimplementedPrivBinding struct {
BindAny func(*gin.Context, any) mir.Error
}
func (b *UnimplementedPrivBinding) BindUnfollowTopic(c *gin.Context) (*web.UnfollowTopicReq, mir.Error) {
obj := new(web.UnfollowTopicReq)
err := b.BindAny(c, obj)
return obj, err
}
func (b *UnimplementedPrivBinding) BindFollowTopic(c *gin.Context) (*web.FollowTopicReq, mir.Error) {
obj := new(web.FollowTopicReq)
err := b.BindAny(c, obj)
return obj, err
}
func (b *UnimplementedPrivBinding) BindStickTopic(c *gin.Context) (*web.StickTopicReq, mir.Error) {
obj := new(web.StickTopicReq)
err := b.BindAny(c, obj)
return obj, err
}
func (b *UnimplementedPrivBinding) BindThumbsDownTweetReply(c *gin.Context) (*web.TweetReplyThumbsReq, mir.Error) {
obj := new(web.TweetReplyThumbsReq)
err := b.BindAny(c, obj)
return obj, err
}
func (b *UnimplementedPrivBinding) BindThumbsUpTweetReply(c *gin.Context) (*web.TweetReplyThumbsReq, mir.Error) {
obj := new(web.TweetReplyThumbsReq)
err := b.BindAny(c, obj)
return obj, err
}
func (b *UnimplementedPrivBinding) BindThumbsDownTweetComment(c *gin.Context) (*web.TweetCommentThumbsReq, mir.Error) {
obj := new(web.TweetCommentThumbsReq)
err := b.BindAny(c, obj)
return obj, err
}
func (b *UnimplementedPrivBinding) BindThumbsUpTweetComment(c *gin.Context) (*web.TweetCommentThumbsReq, mir.Error) {
obj := new(web.TweetCommentThumbsReq)
err := b.BindAny(c, obj)
return obj, err
}
func (b *UnimplementedPrivBinding) BindDeleteCommentReply(c *gin.Context) (*web.DeleteCommentReplyReq, mir.Error) {
obj := new(web.DeleteCommentReplyReq)
err := b.BindAny(c, obj)

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v3.0.1
// - mir v3.1.1
package v1
@ -13,8 +13,6 @@ import (
)
type Pub interface {
TopicList(*web.TopicListReq) (*web.TopicListResp, mir.Error)
TweetComments(*web.TweetCommentsReq) (*web.TweetCommentsResp, mir.Error)
TweetDetail(*web.TweetDetailReq) (*web.TweetDetailResp, mir.Error)
SendCaptcha(*web.SendCaptchaReq) mir.Error
GetCaptcha() (*web.GetCaptchaResp, mir.Error)
@ -26,8 +24,6 @@ type Pub interface {
}
type PubBinding interface {
BindTopicList(*gin.Context) (*web.TopicListReq, mir.Error)
BindTweetComments(*gin.Context) (*web.TweetCommentsReq, mir.Error)
BindTweetDetail(*gin.Context) (*web.TweetDetailReq, mir.Error)
BindSendCaptcha(*gin.Context) (*web.SendCaptchaReq, mir.Error)
BindRegister(*gin.Context) (*web.RegisterReq, mir.Error)
@ -37,8 +33,6 @@ type PubBinding interface {
}
type PubRender interface {
RenderTopicList(*gin.Context, *web.TopicListResp, mir.Error)
RenderTweetComments(*gin.Context, *web.TweetCommentsResp, mir.Error)
RenderTweetDetail(*gin.Context, *web.TweetDetailResp, mir.Error)
RenderSendCaptcha(*gin.Context, mir.Error)
RenderGetCaptcha(*gin.Context, *web.GetCaptchaResp, mir.Error)
@ -54,38 +48,6 @@ func RegisterPubServant(e *gin.Engine, s Pub, b PubBinding, r PubRender) {
router := e.Group("v1")
// register routes info to router
router.Handle("GET", "/tags", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
return
default:
}
req, err := b.BindTopicList(c)
if err != nil {
r.RenderTopicList(c, nil, err)
return
}
resp, err := s.TopicList(req)
r.RenderTopicList(c, resp, err)
})
router.Handle("GET", "/post/comments", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
return
default:
}
req, err := b.BindTweetComments(c)
if err != nil {
r.RenderTweetComments(c, nil, err)
return
}
resp, err := s.TweetComments(req)
r.RenderTweetComments(c, resp, err)
})
router.Handle("GET", "/post", func(c *gin.Context) {
select {
case <-c.Request.Context().Done():
@ -177,14 +139,6 @@ func RegisterPubServant(e *gin.Engine, s Pub, b PubBinding, r PubRender) {
type UnimplementedPubServant struct {
}
func (UnimplementedPubServant) TopicList(req *web.TopicListReq) (*web.TopicListResp, mir.Error) {
return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented))
}
func (UnimplementedPubServant) TweetComments(req *web.TweetCommentsReq) (*web.TweetCommentsResp, mir.Error) {
return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented))
}
func (UnimplementedPubServant) TweetDetail(req *web.TweetDetailReq) (*web.TweetDetailResp, mir.Error) {
return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented))
}
@ -216,14 +170,6 @@ type UnimplementedPubRender struct {
RenderAny func(*gin.Context, any, mir.Error)
}
func (r *UnimplementedPubRender) RenderTopicList(c *gin.Context, data *web.TopicListResp, err mir.Error) {
r.RenderAny(c, data, err)
}
func (r *UnimplementedPubRender) RenderTweetComments(c *gin.Context, data *web.TweetCommentsResp, err mir.Error) {
r.RenderAny(c, data, err)
}
func (r *UnimplementedPubRender) RenderTweetDetail(c *gin.Context, data *web.TweetDetailResp, err mir.Error) {
r.RenderAny(c, data, err)
}
@ -255,18 +201,6 @@ type UnimplementedPubBinding struct {
BindAny func(*gin.Context, any) mir.Error
}
func (b *UnimplementedPubBinding) BindTopicList(c *gin.Context) (*web.TopicListReq, mir.Error) {
obj := new(web.TopicListReq)
err := b.BindAny(c, obj)
return obj, err
}
func (b *UnimplementedPubBinding) BindTweetComments(c *gin.Context) (*web.TweetCommentsReq, mir.Error) {
obj := new(web.TweetCommentsReq)
err := b.BindAny(c, obj)
return obj, err
}
func (b *UnimplementedPubBinding) BindTweetDetail(c *gin.Context) (*web.TweetDetailReq, mir.Error) {
obj := new(web.TweetDetailReq)
err := b.BindAny(c, obj)

@ -1,6 +1,6 @@
// Code generated by go-mir. DO NOT EDIT.
// versions:
// - mir v3.0.1
// - mir v3.1.1
package v1

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

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc-gen-go-grpc v1.3.0
// - protoc (unknown)
// source: v1/auth.proto
@ -18,6 +18,12 @@ import (
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
const (
Authenticate_PreLogin_FullMethodName = "/auth.Authenticate/preLogin"
Authenticate_Login_FullMethodName = "/auth.Authenticate/login"
Authenticate_Logout_FullMethodName = "/auth.Authenticate/logout"
)
// AuthenticateClient is the client API for Authenticate service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
@ -37,7 +43,7 @@ func NewAuthenticateClient(cc grpc.ClientConnInterface) AuthenticateClient {
func (c *authenticateClient) PreLogin(ctx context.Context, in *User, opts ...grpc.CallOption) (*ActionReply, error) {
out := new(ActionReply)
err := c.cc.Invoke(ctx, "/auth.Authenticate/preLogin", in, out, opts...)
err := c.cc.Invoke(ctx, Authenticate_PreLogin_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
@ -46,7 +52,7 @@ func (c *authenticateClient) PreLogin(ctx context.Context, in *User, opts ...grp
func (c *authenticateClient) Login(ctx context.Context, in *User, opts ...grpc.CallOption) (*LoginReply, error) {
out := new(LoginReply)
err := c.cc.Invoke(ctx, "/auth.Authenticate/login", in, out, opts...)
err := c.cc.Invoke(ctx, Authenticate_Login_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
@ -55,7 +61,7 @@ func (c *authenticateClient) Login(ctx context.Context, in *User, opts ...grpc.C
func (c *authenticateClient) Logout(ctx context.Context, in *User, opts ...grpc.CallOption) (*ActionReply, error) {
out := new(ActionReply)
err := c.cc.Invoke(ctx, "/auth.Authenticate/logout", in, out, opts...)
err := c.cc.Invoke(ctx, Authenticate_Logout_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
@ -108,7 +114,7 @@ func _Authenticate_PreLogin_Handler(srv interface{}, ctx context.Context, dec fu
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/auth.Authenticate/preLogin",
FullMethod: Authenticate_PreLogin_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthenticateServer).PreLogin(ctx, req.(*User))
@ -126,7 +132,7 @@ func _Authenticate_Login_Handler(srv interface{}, ctx context.Context, dec func(
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/auth.Authenticate/login",
FullMethod: Authenticate_Login_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthenticateServer).Login(ctx, req.(*User))
@ -144,7 +150,7 @@ func _Authenticate_Logout_Handler(srv interface{}, ctx context.Context, dec func
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/auth.Authenticate/logout",
FullMethod: Authenticate_Logout_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthenticateServer).Logout(ctx, req.(*User))

@ -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 -f2 -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:${VERSION}" \
--tag "$IMAGE:latest" \
. -f Dockerfile
# push to image rep
# if [ -n "$1" ]; then
# docker push "$IMAGE:${VERSION}"
# docker push "$IMAGE:latest"
# fi

@ -0,0 +1,11 @@
#!/bin/sh
# eg.1 : sh build-image.sh
# eg.2, set tags: sh build-image.sh 'embed go_json'
TAGS='embed go_json'
if [ -n "$1" ]; then
TAGS="$1"
fi
make release CGO_ENABLED=0 TAGS="$TAGS"

@ -8,23 +8,23 @@ App: # APP基础设置项
Server: # 服务设置
RunMode: debug
HttpIp: 0.0.0.0
HttpPort: 8008
HttpPort: 8010
ReadTimeout: 60
WriteTimeout: 60
Features:
Default: ["Base", "MySQL", "Option", "Zinc", "LocalOSS", "LoggerFile", "Friendship", "Deprecated"]
Default: ["Web", "Frontend:EmbedWeb", "Zinc", "LocalOSS", "MySQL", "BigCacheIndex", "LoggerZinc", "Friendship"]
Develop: ["Base", "MySQL", "BigCacheIndex", "Meili", "Sms", "AliOSS", "LoggerMeili", "OSS:Retention"]
Demo: ["Base", "MySQL", "Option", "Zinc", "Sms", "MinIO", "LoggerZinc", "Migration"]
Slim: ["Base", "Sqlite3", "LocalOSS", "LoggerFile", "OSS:TempDir"]
Base: ["Redis", "PhoneBind"]
Docs: ["Docs:OpenAPI"]
Deprecated: ["Deprecated:OldWeb"]
Service: ["Web", "Admin", "SpaceX", "Bot", "LocalOSS", "Mobile", "Frontend:Web", "Fronetend:EmbedWeb", "Docs"]
Service: ["Web", "Admin", "SpaceX", "Bot", "LocalOSS", "Mobile", "Frontend:Web", "Frontend:EmbedWeb", "Docs"]
Option: ["SimpleCacheIndex"]
Sms: "SmsJuhe"
WebServer: # Web服务
HttpIp: 0.0.0.0
HttpPort: 8010
HttpPort: 8008
ReadTimeout: 60
WriteTimeout: 60
AdminServer: # Admin后台运维服务
@ -84,7 +84,7 @@ BigCacheIndex: # 使用BigCache缓存泡泡广场消息流
Logger: # 日志通用配置
Level: debug # 日志级别 panic|fatal|error|warn|info|debug|trace
LoggerFile: # 使用File写日志
SavePath: data/paopao-ce/logs
SavePath: custom/data/paopao-ce/logs
FileName: app
FileExt: .log
LoggerZinc: # 使用Zinc写日志
@ -154,7 +154,7 @@ S3: # Amazon S3 存储配置
Bucket: paopao
Domain:
LocalOSS: # 本地文件OSS存储配置
SavePath: data/paopao-ce/oss
SavePath: custom/data/paopao-ce/oss
Secure: False
Bucket: paopao
Domain: 127.0.0.1:8008
@ -179,9 +179,8 @@ Postgres: # PostgreSQL数据库
SSLMode: disable
TimeZone: Asia/Shanghai
Sqlite3: # Sqlite3数据库
Path: data/sqlite3/paopao-ce.db
Path: custom/data/sqlite3/paopao-ce.db
Redis:
Host: redis:6379
Password:
DB:
InitAddress:
- redis:6379

@ -4,4 +4,4 @@ PaoPao部署站点信息。
| 名称 | 网址 | 站长 | 备注 |
| ----- | ----- | ----- | ----- |
|泡泡|[www.paopao.info](https://www.paopao.info)|[ROC](https://www.paopao.info/#/user?username=roc 'roc(@paopao.info)')|PaoPao官方站点|
|布里塔|[bulita.net](https://bulita.net)|[chendong](https://www.paopao.info/#/user?username=chendong 'chendong(@paopao.info)')|布里塔 - 招聘求职转发|
|布里塔|[bulita.cn](https://bulita.cn)|[chendong](https://www.paopao.info/#/user?username=chendong 'chendong(@paopao.info)')|招聘求职等信息|

@ -13,7 +13,7 @@ services:
- ./scripts/paopao-mysql.sql:/docker-entrypoint-initdb.d/paopao.sql
- ./custom/data/mysql/data:/var/lib/mysql
ports:
- 127.0.0.1::3306
- 3306:3306
networks:
- paopao-network
@ -32,11 +32,22 @@ services:
# networks:
# - paopao-network
# redis:
# image: redis:7.0-alpine
# restart: always
# ports:
# - 6379:6379
# networks:
# - paopao-network
redis:
image: redis:7.0-alpine
image: redis/redis-stack:7.0.6-RC8
restart: always
ports:
- 6379:6379
- 8001:8001
environment:
REDISEARCH_ARGS: "MAXSEARCHRESULTS 5"
networks:
- paopao-network
@ -56,7 +67,7 @@ services:
- paopao-network
# meili:
# image: getmeili/meilisearch:v1.0
# image: getmeili/meilisearch:v1.1
# restart: always
# ports:
# - 7700:7700
@ -66,13 +77,14 @@ services:
# - MEILI_MASTER_KEY=paopao-meilisearch
# networks:
# - paopao-network
# # a ui for managing your meilisearch instances
# uirecord:
# image: bitriory/uirecord:latest
# pyroscope:
# image: pyroscope/pyroscope:latest
# restart: always
# ports:
# - 7701:3000
# - 4040:4040
# command:
# - 'server'
# networks:
# - paopao-network
@ -90,8 +102,7 @@ services:
- paopao-network
backend:
build:
context: .
image: bitbus/paopao-ce:nightly
restart: always
depends_on:
- db
@ -100,7 +111,7 @@ services:
# modify below to reflect your custom configure
volumes:
- ./config.yaml.sample:/app/paopao-ce/config.yaml
- ./custom/data/paopao-ce/data:/app/paopao-ce/data
- ./custom:/app/paopao-ce/custom
ports:
- 8008:8008
networks:

@ -94,6 +94,7 @@ release/paopao-ce --no-default-features --features sqlite3,localoss,loggerfile,r
|`Sms` | 短信验证 | 稳定 | 开启短信验证码功能,用于手机绑定验证手机是否注册者的;功能如果没有开启,手机绑定时任意短信验证码都可以绑定手机 |
|`Docs:OpenAPI` | 开发文档 | 稳定 | 开启openapi文档功能提供web api文档说明(visit http://127.0.0.1:8008/docs/openapi) |
|`PhoneBind` | 其他 | 稳定 | 手机绑定功能 |
|`Web:DisallowUserRegister` | 功能特性 | 稳定 | 不允许用户注册 |
> 功能项状态详情参考 [features-status](../../../features-status.md).

@ -0,0 +1,30 @@
Features:
Default: ["Sqlite3", "Zinc", "LocalOSS", "LoggerFile", "BigCacheIndex", "Friendship", "Frontend:EmbedWeb", "Web"]
BigCacheIndex: # 使用BigCache缓存泡泡广场消息流
MaxIndexPage: 1024 # 最大缓存页数必须是2^n, 代表最大同时缓存多少页数据
HardMaxCacheSize: 256 # 最大缓存大小(MB)0表示无限制
WebServer: # Web服务
HttpIp: 0.0.0.0
HttpPort: 8008
ReadTimeout: 60
WriteTimeout: 60
JWT: # 鉴权加密
Secret: 18a6413dc4fe394c66345ebe501b2f26
Issuer: paopao-api
Expire: 86400
Zinc: # Zinc搜索配置
Host: 127.0.0.1:4080
Index: paopao-data
User: admin
Password: admin
Secure: False
LocalOSS: # 本地文件OSS存储配置
SavePath: custom/data/oss
Secure: True
Bucket: paopao
Domain: api.alimy.me
Sqlite3: # Sqlite3数据库
Path: custom/data/sqlite3/paopao-ce.db
Redis:
InitAddress:
- 127.0.0.1:6379

@ -3,3 +3,6 @@
* [0000-讨论样式模版](./0000-讨论样式模版.md "讨论样式模版")
* [0001-FAQs](./0001-FAQs.md "FAQs")
### paopao.info
关于paopao-ce的交流、反馈也可以直接在我们官网[paopao.info](https://www.paopao.info)进行,发布动态时记得加上标签`#paopao-ce`或类似主题相关的标签,方便话题查找。欢迎大家在[paopao.info](https://www.paopao.info)畅快愉悦的交流一起让paopao-ce的用户体验更好、更便捷。

@ -1,6 +1,6 @@
| 编号 | 作者 | 发表时间 | 变更时间 | 版本 | 状态 |
| ----- | ----- | ----- | ----- | ----- | ----- |
| 000 | 北野 | 2022-11-04 |2022-11-04 | v1.0| 提议 |
| YYMMDDHH | 北野 | 2022-11-04 |2022-11-04 | v1.0| 提议 |
## <我是标题>
---- 这里写简要介绍 ----

@ -1,28 +0,0 @@
| 编号 | 作者 | 发表时间 | 变更时间 | 版本 | 状态 |
| ----- | ----- | ----- | ----- | ----- | ----- |
| 003| 北野 | 2022-11-04 | 2022-11-04 | v0.0 | 提议 |
### 关于Followship功能项的设计
---- 这里写简要介绍 ----
### 场景
---- 这里描述在什么使用场景下会需要本提按 ----
### 需求
实现类似于Twitter的追随者关注模式的推文传播模型使得广场页面可以浏览已关注用户的推文。
TODO-TL;DR...
### 方案
TODO
### 疑问
1. 如何开启这个功能?
TODO
### 更新记录
#### v0.0(2022-11-04) - 北野
* 初始文档, 先占个位置

@ -0,0 +1,60 @@
| 编号 | 作者 | 发表时间 | 变更时间 | 版本 | 状态 |
| ----- | ----- | ----- | ----- | ----- | ----- |
| 22110409| 北野 | 2022-11-04 | 2022-11-21 | v0.1 | 提议 |
### 关于Followship功能项的设计
Followship是实现类似Twitter Timeline模式**关注者模型**的时间线信息流广场推文列表的生成将主要与推文时间、用户的关注者相关。Twitter的推文消息流是非常智能的用户体验也非常好这得益于其背后的智能推荐算法以及完善的关注者模型体系当然还有很多其他机制共同作用下的结果。本提按作为一个总纲为paopao-ce引入类似的机制这将是一个持续完善的缓慢过程一切都是为了用户体验用户就是上帝用户需要什么paopao-ce就努力提供什么
### 场景
* 站点用户基数达到一定规模,推文总量很多、推文更新很快,需要一种机制更智能的生成广场推文列表改善用户体验;
* 单纯的想为站点引入关注者模型体系;
### 需求
#### 基本需求
* 实现类似于Twitter的**追随者关注模式**的推文传播模型,使得广场页面可以浏览已关注用户的推文;
* 提供 `关注/取消关注` 用户的机制;
* 浏览 `用户关注者` 列表的机制;
* 浏览 `用户追随者` 列表的机制;
* 用户主页显示 `关注者/追随者` 计数,并作为入口进入对应浏览 `关注者/追随者` 列表的页面;
* 广场推文页面 依据 **关注者模型**体系生成个性化推文列表;
#### 可选需求
* 建立 **发现机制(Discover)** 为用户 *智能推荐* 用户可能会**感兴趣、值得关注**的用户;
* 同上,广场推文页面可以附带一些**被推荐的推文**,这些推文不属于关注用户的推文,但是与关注用户相关,比如是用户关注者的关注者、热度用户等;
### 方案
#### 方案一 (仅使用后端关系数据库MySQL/Sqite3/PostgreSQL满足基本需求)
* 提供 `关注/取消关注` 用户的机制 - 前端/后端
* 浏览 `用户关注者` 列表的机制 - 前端/后端
* 浏览 `用户追随者` 列表的机制 - 前端/后端
* 用户主页显示 `关注者/追随者` 计数,并作为入口进入对应浏览 `关注者/追随者` 列表的页面 - 前端/后端
* 广场推文页面 依据 **关注者模型**体系生成个性化推文列表 - 后端
* 发送推文时,默认可见性为 `公开`, 公开的语义将是`关注者可见`, 因此用户如果没有登入或者没有关注任何用户,广场推文列表将只有自己的私密推文或者是空的 - 前端/后端
#### 方案二
在方案一的基础上通过引入 *图数据库* 实现可选需求。待研究、设计~
### 疑问
1. Friendship与Followship这两功能项是否可以共存还是互斥呢
理论上这两个功能项是可以共存的如果设置成共存就需要同时提供这两个机制的UI交互同时广场推文列表的生成机制也需要做相应适配可能会比较复杂。前期开发将把这两个功能设置为互斥的即**用户关系模型**只能设置为其中之一,待两个功能项都趋于稳定后,再测试这两个功能项共存后的用户体验,如果效果还不错就引入共存机制。
1. 如何开启这个功能?
在配置文件config.yaml中的`Features`中添加`Followship`功能项开启该功能:
```yaml
...
# features中加上 Followship
Features:
Default: ["Meili", "LoggerMeili", "Base", "Sqlite3", "BigCacheIndex", "MinIO", "Followship"]
Base: ["Redis", "PhoneBind"]
...
```
### 更新记录
#### v0.0(2022-11-04) - 北野
* 初始文档, 先占个位置;
#### v0.1(2022-11-21) - 北野
* 添加初始内容;

@ -1,6 +1,6 @@
| 编号 | 作者 | 发表时间 | 变更时间 | 版本 | 状态 |
| ----- | ----- | ----- | ----- | ----- | ----- |
| 002| 北野 | 2022-11-04 | 2023-01-04 | v1.0 | 提议 |
| 22110410 | 北野 | 2022-11-04 | 2023-01-04 | v1.0 | 提议 |
### Friendship功能项的设计概要
Friendship功能提供好友间分享推文信息的机制更好的帮助用户建立自己的推文分享小圈子。Friendship本质上想优化的是泡泡广场页面推文列表的生成机制开启功能后推文列表只能获取 `公开/私密/好友` 的推文,每个用户都有属于自己的个性化推文列表。在提供个性化推文列表生成机制的同时,好友体系的建立也顺便帮助用户建立自己的个性化有限范围内的灵魂社交小圈子,只有相互间拥有个性化认同感的用户才能互为好友。

@ -1,6 +1,6 @@
| 编号 | 作者 | 发表时间 | 变更时间 | 版本 | 状态 |
| ----- | ----- | ----- | ----- | ----- | ----- |
| 001| 北野 | 2022-11-04 | 2023-01-13 | v1.1 | 提议 |
| 22110411 | 北野 | 2022-11-04 | 2023-01-13 | v1.1 | 提议 |
## 概述
paopao-ce是一个清新文艺的微社区提供类似Twiter/微博的推文分享服务。paopao-ce的运营形态有点类似WordPress只不过WordPress是使用PHP语言开发的博客平台提供的是博客服务而paopao-ce提供的是类似Twitter的推文分享服务。paopao-ce 让 **个人或小组织** 可以快速、方便的部署一个提供**推文分享服务**的小站点,在有限范围内形成一个友善的社交小圈子微社区。

@ -1,6 +1,6 @@
| 编号 | 作者 | 发表时间 | 变更时间 | 版本 | 状态 |
| ----- | ----- | ----- | ----- | ----- | ----- |
| 005| 北野 | 2022-11-21 | 2023-01-04 | v1.1 | 提议 |
| 22112109 | 北野 | 2022-11-21 | 2023-01-04 | v1.1 | 提议 |
### 引入go-mir优化后端架构设计
引入[github.com/alimy/mir/v3](https://github.com/alimy/mir)优化后端的架构设计,使得后端代码更具扩展型。

@ -1,6 +1,6 @@
| 编号 | 作者 | 发表时间 | 变更时间 | 版本 | 状态 |
| ----- | ----- | ----- | ----- | ----- | ----- |
| 006| 北野 | 2022-11-23 | 2022-01-01 | v1.0 | 提议 |
| 22112309 | 北野 | 2022-11-23 | 2022-01-01 | v1.0 | 提议 |
### 关于paopao-ce的结构设计
本文档主要讨论paopao-ce目前的代码结构简要清晰的描述一个**API请求**从 **接受解析->逻辑处理->结果响应**的大概路径帮助开发人员快速了解paopao-ce代码基的基本面更好的融入paopao-ce的开发中做出PR贡献。

@ -1,6 +1,6 @@
| 编号 | 作者 | 发表时间 | 变更时间 | 版本 | 状态 |
| ----- | ----- | ----- | ----- | ----- | ----- |
| 011| 北野 | 2022-12-14 | 2022-01-09 | v1.1 | 提议 |
| 22121409 | 北野 | 2022-12-14 | 2022-01-09 | v1.1 | 提议 |
### 关于Lightship功能项的设计
Lightship(开放模式)功能提供完全公开的推文分享服务,有别于[Friendship](002-关于Friendship功能项的设计.md "关于Friendship功能项的设计")、[Followship](003-关于Followship功能项的设计.md "关于Followship功能项的设计")使用Lightship用户模式部署paopao-ce用户发布的所有推文都是公开可访问的广场推文列表展示的是全站所有公开推文的Timeline Tweets。
@ -26,7 +26,7 @@ Lightship(开放模式)功能提供完全公开的推文分享服务,有别于
### 疑问
1. 公开模式为什么命名为Lightship
1. 公开模式为什么命名为Lightship
Lightship有灯塔的意思灯塔是为航行船只指明方向的公共设施这里取其公有设施的属性指喻 开放模式下paopao-ce不需要授权公开访问推 文的意思。
1. 如何开启这个功能?
在配置文件config.yaml中的`Features`中添加`Lightship`功能项开启该功能:

@ -1,6 +1,6 @@
| 编号 | 作者 | 发表时间 | 变更时间 | 版本 | 状态 |
| ----- | ----- | ----- | ----- | ----- | ----- |
| 013| 北野 | 2023-01-13 | 2023-01-13 | v0.0 | 提议 |
| 23011309 | 北野 | 2023-01-13 | 2023-01-13 | v0.0 | 提议 |
### 概述
目前paopao-ce前端/后端 都使用中文提供业务服务暂时还没有提供国际化i18n机制提供国际化的业务服务。本提按提议提过在前端/后端中引入i8n机制以提供国际化业务服务。 本提按建立在[012-优化前端运行时配置获取机制的设计](012-优化前端运行时配置获取机制的设计.md)基础之上。

@ -1,6 +1,6 @@
| 编号 | 作者 | 发表时间 | 变更时间 | 版本 | 状态 |
| ----- | ----- | ----- | ----- | ----- | ----- |
| 012| 北野 | 2023-01-13 | 2023-01-13 | v0.0 | 提议 |
| 23011310 | 北野 | 2023-01-13 | 2023-01-13 | v0.0 | 提议 |
### 概述
目前的Web前端运行时配置是通过编译时配置[.env](../../web/.env)进行静态配置虽然能满足简单的功能需求但是非常不灵活。本提按提议一种由paopao-ce后端服务控制的前端运行时配置获取机制让前端更灵活的依据运行时配置提供产品服务。

@ -1,6 +1,6 @@
| 编号 | 作者 | 发表时间 | 变更时间 | 版本 | 状态 |
| ----- | ----- | ----- | ----- | ----- | ----- |
| 014| 北野 | 2023-02-09 | 2023-02-09 | v0.0 | 提议 |
| 23020910| 北野 | 2023-02-09 | 2023-02-09 | v0.0 | 提议 |
### 概述
TODO

@ -0,0 +1,25 @@
| 编号 | 作者 | 发表时间 | 变更时间 | 版本 | 状态 |
| ----- | ----- | ----- | ----- | ----- | ----- |
| 23021310| 北野 | 2023-02-13 | 2023-02-13 | v0.0 | 提议 |
### 概述
TODO
### 需求
TODO
### 方案
TODO
### 疑问
1. 为什么要引入bcrypt
TODO
### 参考文档
* [bcrypt](https://github.com/golang/crypto/blob/master/bcrypt/bcrypt.go)
* [bcrypt module](https://pkg.go.dev/golang.org/x/crypto@v0.6.0/bcrypt)
### 更新记录
#### v0.0(2023-02-13) - 北野
* 初始文档, 先占个位置

@ -0,0 +1,53 @@
| 编号 | 作者 | 发表时间 | 变更时间 | 版本 | 状态 |
| ----- | ----- | ----- | ----- | ----- | ----- |
| 23021510| 北野 | 2023-02-15 | 2023-02-16 | v1.1 | 提议 |
### 概述
Pyroscope 是一个开源的持续性能剖析平台。它能够帮你:
* 找出源代码中的性能问题和瓶颈
* 解决 CPU 利用率高的问题
* 理解应用程序的调用树call tree
* 追踪随一段时间内变化的情况
### 需求
* 开发环境下启用Pyroscope但是部署环境下禁用。
### 方案
#### 设计要点
* 使用//go:build pyroscope 可选编译app集成Pyroscope功能
* config.yaml中添加`Pyroscope` 功能来启用Pyroscope
#### 设计细节
* 参考实现(PR):
[add Pyroscope support #199](https://github.com/rocboss/paopao-ce/pull/199)
### 疑问
1. 为什么要引入Pyroscope
用于开发环境下对paopao-ce进行性能优化。
2. 如何开启这个功能?
* 构建时将 `pyroscope` 添加到TAGS中:
```sh
make run TAGS='pyroscope'
```
* 在配置文件config.yaml中的`Features`中添加`Pyroscope`功能项开启该功能:
```yaml
...
# features中加上 Friendship
Features:
Default: ["Meili", "LoggerMeili", "Base", "Sqlite3", "BigCacheIndex", "MinIO", "Pyroscope"]
Base: ["Redis", "PhoneBind"]
...
```
### 参考文档
* [pyroscope](https://github.com/pyroscope-io/pyroscope)
* [pyroscope client](https://github.com/pyroscope-io/client)
### 更新记录
#### v1.0(2023-02-15) - 北野
* 初始文档
#### v1.1(2023-02-16) - 北野
* 添加参考实现

@ -0,0 +1,49 @@
| 编号 | 作者 | 发表时间 | 变更时间 | 版本 | 状态 |
| ----- | ----- | ----- | ----- | ----- | ----- |
| 23040412| 北野 | 2023-04-04 | 2023-04-04 | v1.0 | 提议 |
### 概述
[Sentry](https://github.com/getsentry/sentry) Sentry is a developer-first error tracking and performance monitoring platform that helps developers see what actually matters, solve quicker, and learn continuously about their applications.
### 需求
* 通过配置文件开启Sentry功能
### 方案
#### 设计要点
* config.yaml中添加`Sentry` 功能来启用Sentry功能
#### 设计细节
* 参考实现(PR):
[add Sentry feature support #258](https://github.com/rocboss/paopao-ce/pull/258)
### 疑问
1. 为什么要引入Sentry
添加一种对paopao-ce的错误追踪与性能检测机制。
2. 如何开启这个功能?
* 在配置文件config.yaml中的`Features`中添加`Sentry`功能项开启该功能:
```yaml
...
# features中加上 Sentry
Features:
Default: ["Meili", "LoggerMeili", "Base", "Sqlite3", "BigCacheIndex", "MinIO", "Sentry"]
Base: ["Redis", "PhoneBind"]
Sentry:
Sentry: # Sentry配置
Dsn: "http://4ea0af5cd88d4512b7e52070506c80ec@localhost:9000/2"
Debug: True
AttachStacktrace: True
TracesSampleRate: 1.0
AttachLogrus: True # logrus是否附加到Sentry
AttachGin: True # gin是否附加到Sentry
...
```
### 参考文档
* [sentry](https://github.com/getsentry/sentry)
* [self-hosted](https://develop.sentry.dev/self-hosted/)
### 更新记录
#### v1.0(2023-04-04) - 北野
* 初始文档

@ -1,6 +1,6 @@
## Draft
* [001-关于paopao-ce的设计定位](001-关于paopao-ce的设计定位.md "关于paopao-ce的设计定位")
* [002-关于Friendship功能项的设计](002-关于Friendship功能项的设计.md "关于Friendship功能项的设计")
* [003-关于Followship功能项的设计](003-关于Followship功能项的设计.md "关于Followship功能项的设计")
* [005-引入go-mir优化后端架构设计](005-引入go-mir优化后端架构设计.md "引入go-mir优化后端架构设计")
* [006-关于paopao-ce的结构设计](006-关于paopao-ce的结构设计.md "关于paopao-ce的结构设计")
* [22110411-关于paopao-ce的设计定位](22110411-关于paopao-ce的设计定位.md "关于paopao-ce的设计定位")
* [22110410-关于Friendship功能项的设计](22110410-关于Friendship功能项的设计.md "关于Friendship功能项的设计")
* [22110409-关于Followship功能项的设计](22110409-关于Followship功能项的设计.md "关于Followship功能项的设计")
* [22112109-引入go-mir优化后端架构设计](22112109-引入go-mir优化后端架构设计.md "引入go-mir优化后端架构设计")
* [22112309-关于paopao-ce的结构设计](22112309-关于paopao-ce的结构设计.md "关于paopao-ce的结构设计")

@ -1,8 +1,8 @@
| 编号 | 作者 | 发表时间 | 变更时间 | 版本 | 状态 |
| ----- | ----- | ----- | ----- | ----- | ----- |
| <编号000> | <作者> | <发表时间> | <变更时间> | <版本号v1.0> | <提议/提案/决议/冻结> |
| <编号YYMMDDHH> | <作者> | <发表时间> | <变更时间> | <版本号v1.0> | <提议/提案/决议/冻结> |
* 编号: 填写提案编号,三位数比如001
* 编号: 填写提案编号,六位数格式为YYMMDDHH年(后两位)+月+日+时
* 作者: 填写发表者。
* 发表时间: 填写首次发表时间,之后保持不变。
* 变更时间: 填写变更时间,首次发表时,变更时间和发表时间一样。

@ -42,7 +42,7 @@
* `Frontend:EmbedWeb` 开启内嵌于后端Web API服务中的前端服务(目前状态: 内测)
* [ ] 提按文档
* [x] 服务初始化逻辑
* `Deprecated:OldWeb` 开启旧的Web服务(目前状态: 内测)
* `Deprecated:OldWeb` 开启旧的Web服务(目前状态: 已弃,不可用)
* [ ] 提按文档
* [x] 服务初始化逻辑
@ -105,6 +105,10 @@
* [ ] 提按文档
* [x] 接口定义
* [x] 业务逻辑实现
* `RedisCacheIndex` 使用Redis缓存 广场推文列表,缓存每个用户每一页,简单做到千人千面(目前状态: 内测阶段,推荐使用)
* [ ] 提按文档
* [x] 接口定义
* [x] 业务逻辑实现
#### 搜索:
* `Zinc` 基于[Zinc](https://github.com/zinclabs/zinc)搜索引擎提供推文搜索服务(目前状态: 稳定,推荐使用)
@ -119,6 +123,7 @@
* [ ] 提按文档
* [ ] 接口定义
* [ ] 业务逻辑实现
#### 日志:
* `LoggerFile` 使用文件写日志(目前状态: 稳定);
* [ ] 提按文档
@ -133,6 +138,12 @@
* [x] 接口定义
* [x] 业务逻辑实现
#### 监控:
* `Sentry` 使用Sentry进行错误跟踪与性能监控(目前状态: 内测);
* [x] [提按文档](docs/proposal/23040412-关于使用sentry用于错误追踪与性能检测的设计.md)
* [x] 接口定义
* [x] 业务逻辑实现
#### 关系模式:
* `Friendship` 弱关系好友模式,类似微信朋友圈(目前状态: 内测);
* [x] [提按文档](docs/proposal/002-关于Friendship功能项的设计.md)
@ -164,13 +175,24 @@
* [x] 业务逻辑实现
### 开发文档:
* `Docs:OpenAPI` 开启openapi文档功能提供web api文档说明(visit http://127.0.0.1:8008/docs/openapi)
* `Docs:OpenAPI` 开启openapi文档功能提供web api文档说明(visit http://127.0.0.1:8008/docs/openapi);
* [ ] 提按文档
* [x] 接口定义
* [x] 业务逻辑实现
* [x] 业务逻辑实现
### 性能优化
* [`Pyroscope`](docs/proposal/016-关于使用pyroscope用于性能调试的设计.md) 开启Pyroscope功能用于性能调试(目前状态: 内测);
* [x] 提按文档
* [x] 业务逻辑实现
### 其他:
* `PhoneBind` 手机绑定功能;
* [ ] 提按文档
* [x] 接口定义
* [x] 业务逻辑实现
* [x] 业务逻辑实现
### 功能特性:
* `Web:DisallowUserRegister` 不允许用户注册;
* [ ] 提按文档
* [x] 接口定义
* [x] 业务逻辑实现

168
go.mod

@ -1,133 +1,139 @@
module github.com/rocboss/paopao-ce
go 1.18
go 1.20
require (
github.com/Masterminds/semver/v3 v3.1.1
github.com/Masterminds/semver/v3 v3.2.1
github.com/afocus/captcha v0.0.0-20191010092841-4bd1f21c8868
github.com/alimy/cfg v0.3.0
github.com/alimy/mir/v3 v3.0.1
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible
github.com/alimy/mir/v3 v3.1.1
github.com/aliyun/aliyun-oss-go-sdk v2.2.7+incompatible
github.com/allegro/bigcache/v3 v3.0.2
github.com/bytedance/sonic v1.5.0
github.com/bytedance/sonic v1.8.8
github.com/cockroachdb/errors v1.9.1
github.com/disintegration/imaging v1.6.2
github.com/ethereum/go-ethereum v1.10.16
github.com/fatih/color v1.13.0
github.com/fbsobreira/gotron-sdk v0.0.0-20211102183839-58a64f4da5f4
github.com/gin-contrib/cors v1.3.1
github.com/gin-gonic/gin v1.8.1
github.com/go-redis/redis/v8 v8.11.4
github.com/fatih/color v1.15.0
github.com/getsentry/sentry-go v0.20.0
github.com/gin-contrib/cors v1.4.0
github.com/gin-gonic/gin v1.9.0
github.com/go-resty/resty/v2 v2.7.0
github.com/goccy/go-json v0.9.7
github.com/gofrs/uuid v4.0.0+incompatible
github.com/golang-jwt/jwt/v4 v4.4.3
github.com/goccy/go-json v0.10.2
github.com/gofrs/uuid v4.4.0+incompatible
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang-migrate/migrate/v4 v4.15.2
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.21.12+incompatible
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.23.3+incompatible
github.com/json-iterator/go v1.1.12
github.com/meilisearch/meilisearch-go v0.23.0
github.com/minio/minio-go/v7 v7.0.45
github.com/meilisearch/meilisearch-go v0.24.0
github.com/minio/minio-go/v7 v7.0.52
github.com/onsi/ginkgo/v2 v2.9.2
github.com/onsi/gomega v1.27.6
github.com/pyroscope-io/client v0.7.0
github.com/rueian/rueidis v0.0.100
github.com/sirupsen/logrus v1.9.0
github.com/smartwalle/alipay/v3 v3.1.7
github.com/spf13/viper v1.14.0
github.com/tencentyun/cos-go-sdk-v5 v0.7.35
github.com/smartwalle/alipay/v3 v3.2.1
github.com/sourcegraph/conc v0.3.0
github.com/spf13/viper v1.15.0
github.com/tencentyun/cos-go-sdk-v5 v0.7.41
github.com/yinheli/mahonia v0.0.0-20131226213531-0eef680515cc
google.golang.org/grpc v1.50.1
google.golang.org/protobuf v1.28.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0
go.uber.org/automaxprocs v1.5.2
google.golang.org/grpc v1.54.0
google.golang.org/protobuf v1.30.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/resty.v1 v1.12.0
gorm.io/driver/mysql v1.3.4
gorm.io/driver/postgres v1.3.7
gorm.io/driver/sqlite v1.3.4
gorm.io/gorm v1.23.4
gorm.io/plugin/dbresolver v1.1.0
gorm.io/plugin/soft_delete v1.1.0
modernc.org/sqlite v1.17.3
gorm.io/driver/mysql v1.5.0
gorm.io/driver/postgres v1.5.0
gorm.io/driver/sqlite v1.4.4
gorm.io/gorm v1.25.0
gorm.io/plugin/dbresolver v1.4.1
gorm.io/plugin/soft_delete v1.2.1
modernc.org/sqlite v1.22.1
)
require (
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
github.com/btcsuite/btcd v0.22.0-beta // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/clbanning/mxj v1.8.4 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f // indirect
github.com/cockroachdb/redact v1.1.3 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.1 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/go-logr/logr v1.2.3 // 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.11.2 // 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/gogo/protobuf v1.3.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // 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/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.12.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.11.0 // indirect
github.com/jackc/pgx/v4 v4.16.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.3.0 // 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.15.9 // indirect
github.com/klauspost/cpuid/v2 v2.1.0 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lib/pq v1.10.2 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.12 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mozillazg/go-httpheader v0.3.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pyroscope-io/godeltaprof v0.1.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/rs/xid v1.4.0 // indirect
github.com/smartwalle/crypto4go v1.0.2 // indirect
github.com/spf13/afero v1.9.2 // indirect
github.com/smartwalle/ncrypto v1.0.0 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.37.1-0.20220607072126-8a320890c08d // indirect
github.com/valyala/fasthttp v1.40.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b // indirect
golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.7.0 // indirect
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.1.1 // indirect
modernc.org/cc/v3 v3.36.0 // indirect
modernc.org/ccgo/v3 v3.16.6 // indirect
modernc.org/libc v1.16.7 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.1.1 // indirect
modernc.org/opt v0.1.1 // indirect
modernc.org/strutil v1.1.1 // indirect
modernc.org/token v1.0.0 // 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.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
)

654
go.sum

File diff suppressed because it is too large Load Diff

@ -6,60 +6,60 @@ package conf
import (
"log"
"sync"
"time"
"github.com/alimy/cfg"
)
var (
loggerSetting *LoggerSettingS
loggerFileSetting *LoggerFileSettingS
loggerZincSetting *LoggerZincSettingS
loggerMeiliSetting *LoggerMeiliSettingS
redisSetting *RedisSettingS
loggerSetting *loggerConf
loggerFileSetting *loggerFileConf
loggerZincSetting *loggerZincConf
loggerMeiliSetting *loggerMeiliConf
sentrySetting *sentryConf
redisSetting *redisConf
DatabaseSetting *DatabaseSetingS
MysqlSetting *MySQLSettingS
PostgresSetting *PostgresSettingS
Sqlite3Setting *Sqlite3SettingS
ServerSetting *HttpServerSettingS
WebServerSetting *HttpServerSettingS
AdminServerSetting *HttpServerSettingS
SpaceXServerSetting *HttpServerSettingS
BotServerSetting *HttpServerSettingS
LocalossServerSetting *HttpServerSettingS
FrontendWebSetting *HttpServerSettingS
DocsServerSetting *HttpServerSettingS
MobileServerSetting *GRPCServerSettingS
AppSetting *AppSettingS
CacheIndexSetting *CacheIndexSettingS
SimpleCacheIndexSetting *SimpleCacheIndexSettingS
BigCacheIndexSetting *BigCacheIndexSettingS
SmsJuheSetting *SmsJuheSettings
AlipaySetting *AlipaySettingS
TweetSearchSetting *TweetSearchS
ZincSetting *ZincSettingS
MeiliSetting *MeiliSettingS
ObjectStorage *ObjectStorageS
AliOSSSetting *AliOSSSettingS
COSSetting *COSSettingS
HuaweiOBSSetting *HuaweiOBSSettingS
MinIOSetting *MinIOSettingS
S3Setting *S3SettingS
LocalOSSSetting *LocalOSSSettingS
JWTSetting *JWTSettingS
Mutex *sync.Mutex
PyroscopeSetting *pyroscopeConf
DatabaseSetting *databaseConf
MysqlSetting *mysqlConf
PostgresSetting *postgresConf
Sqlite3Setting *sqlite3Conf
WebServerSetting *httpServerConf
AdminServerSetting *httpServerConf
SpaceXServerSetting *httpServerConf
BotServerSetting *httpServerConf
LocalossServerSetting *httpServerConf
FrontendWebSetting *httpServerConf
DocsServerSetting *httpServerConf
MobileServerSetting *grpcServerConf
AppSetting *appConf
CacheIndexSetting *cacheIndexConf
SimpleCacheIndexSetting *simpleCacheIndexConf
BigCacheIndexSetting *bigCacheIndexConf
RedisCacheIndexSetting *redisCacheIndexConf
SmsJuheSetting *smsJuheConf
AlipaySetting *alipayConf
TweetSearchSetting *tweetSearchConf
ZincSetting *zincConf
MeiliSetting *meiliConf
ObjectStorage *objectStorageConf
AliOSSSetting *aliOSSConf
COSSetting *cosConf
HuaweiOBSSetting *huaweiOBSConf
MinIOSetting *minioConf
S3Setting *s3Conf
LocalOSSSetting *localossConf
JWTSetting *jwtConf
)
func setupSetting(suite []string, noDefault bool) error {
setting, err := NewSetting()
vp, err := newViper()
if err != nil {
return err
}
// initialize features configure
ss, kv := setting.featuresInfoFrom("Features")
ss, kv := featuresInfoFrom(vp, "Features")
cfg.Initial(ss, kv)
if len(suite) > 0 {
cfg.Use(suite, noDefault)
@ -67,7 +67,6 @@ func setupSetting(suite []string, noDefault bool) error {
objects := map[string]any{
"App": &AppSetting,
"Server": &ServerSetting,
"WebServer": &WebServerSetting,
"AdminServer": &AdminServerSetting,
"SpaceXServer": &SpaceXServerSetting,
@ -79,8 +78,11 @@ func setupSetting(suite []string, noDefault bool) error {
"CacheIndex": &CacheIndexSetting,
"SimpleCacheIndex": &SimpleCacheIndexSetting,
"BigCacheIndex": &BigCacheIndexSetting,
"RedisCacheIndex": &RedisCacheIndexSetting,
"Alipay": &AlipaySetting,
"SmsJuhe": &SmsJuheSetting,
"Pyroscope": &PyroscopeSetting,
"Sentry": &sentrySetting,
"Logger": &loggerSetting,
"LoggerFile": &loggerFileSetting,
"LoggerZinc": &loggerZincSetting,
@ -102,27 +104,30 @@ func setupSetting(suite []string, noDefault bool) error {
"LocalOSS": &LocalOSSSetting,
"S3": &S3Setting,
}
if err = setting.Unmarshal(objects); err != nil {
return err
for k, v := range objects {
err := vp.UnmarshalKey(k, v)
if err != nil {
return err
}
}
JWTSetting.Expire *= time.Second
SimpleCacheIndexSetting.CheckTickDuration *= time.Second
SimpleCacheIndexSetting.ExpireTickDuration *= time.Second
BigCacheIndexSetting.ExpireInSecond *= time.Second
RedisCacheIndexSetting.ExpireInSecond *= time.Second
redisSetting.ConnWriteTimeout *= time.Second
Mutex = &sync.Mutex{}
return nil
}
func Initialize(suite []string, noDefault bool) {
func Initial(suite []string, noDefault bool) {
err := setupSetting(suite, noDefault)
if err != nil {
log.Fatalf("init.setupSetting err: %v", err)
}
setupLogger()
setupDBEngine()
initSentry()
}
func GetOssDomain() string {
@ -154,8 +159,9 @@ func GetOssDomain() string {
}
func RunMode() string {
if !cfg.If("Deprecated:OldWeb") {
return ServerSetting.RunMode
}
return AppSetting.RunMode
}
func UseSentryGin() bool {
return cfg.If("Sentry") && sentrySetting.AttachGin
}

@ -1,49 +1,50 @@
App: # APP基础设置项
RunMode: debug
AttachmentIncomeRate: 0.8
MaxCommentCount: 10
MaxCommentCount: 1000
DefaultContextTimeout: 60
DefaultPageSize: 10
MaxPageSize: 100
Server: # 服务设置
RunMode: debug
HttpIp: 0.0.0.0
HttpPort: 8008
ReadTimeout: 60
WriteTimeout: 60
Features:
Default: []
WebServer: # Web服务
RunMode: debug
HttpIp: 0.0.0.0
HttpPort: 8010
HttpPort: 8008
ReadTimeout: 60
WriteTimeout: 60
AdminServer: # Admin后台运维服务
RunMode: debug
HttpIp: 0.0.0.0
HttpPort: 8014
ReadTimeout: 60
WriteTimeout: 60
SpaceXServer: # SpaceX服务
RunMode: debug
HttpIp: 0.0.0.0
HttpPort: 8012
ReadTimeout: 60
WriteTimeout: 60
BotServer: # Bot服务
RunMode: debug
HttpIp: 0.0.0.0
HttpPort: 8016
ReadTimeout: 60
WriteTimeout: 60
LocalossServer: # Localoss服务
RunMode: debug
HttpIp: 0.0.0.0
HttpPort: 8018
ReadTimeout: 60
WriteTimeout: 60
FrontendWebServer: # Web前端静态资源服务
RunMode: debug
HttpIp: 0.0.0.0
HttpPort: 8006
ReadTimeout: 60
WriteTimeout: 60
DocsServer: # 开发文档服务
RunMode: debug
HttpIp: 0.0.0.0
HttpPort: 8011
ReadTimeout: 60
@ -70,12 +71,28 @@ SimpleCacheIndex: # 缓存泡泡广场消息流
ExpireTickDuration: 300 # 每多少秒后强制过期缓存, 设置为0禁止强制使缓存过期
BigCacheIndex: # 使用BigCache缓存泡泡广场消息流
MaxIndexPage: 1024 # 最大缓存页数必须是2^n, 代表最大同时缓存多少页数据
HardMaxCacheSize: 256 # 最大缓存大小(MB)0表示无限制
Verbose: False # 是否打印cache操作的log
ExpireInSecond: 300 # 多少秒(>0)后强制过期缓存
RedisCacheIndex: # 使用Redis缓存泡泡广场消息流
Verbose: False # 是否打印cache操作的log
ExpireInSecond: 300 # 多少秒(>0)后强制过期缓存
Pyroscope: # Pyroscope配置
AppName: "paopao-ce" # application name
Endpoint: "http://localhost:4040" # Pyroscope server address
AuthToken: # Pyroscope authentication token
Logger: none # Pyroscope logger (standard | logrus | none)
Sentry: # Sentry配置
Dsn: "http://4ea0af5cd88d4512b7e52070506c80ec@localhost:9000/2"
Debug: True
AttachStacktrace: True
TracesSampleRate: 1.0
AttachLogrus: True # logrus是否附加到Sentry
AttachGin: True # gin是否附加到Sentry
Logger: # 日志通用配置
Level: debug # 日志级别 panic|fatal|error|warn|info|debug|trace
LoggerFile: # 使用File写日志
SavePath: data/paopao-ce/logs
SavePath: custom/data/paopao-ce/logs
FileName: app
FileExt: .log
LoggerZinc: # 使用Zinc写日志
@ -145,7 +162,7 @@ S3: # Amazon S3 存储配置
Bucket: paopao
Domain:
LocalOSS: # 本地文件OSS存储配置
SavePath: data/paopao-ce/oss
SavePath: custom/data/paopao-ce/oss
Secure: False
Bucket: paopao
Domain: 127.0.0.1:8008
@ -165,14 +182,18 @@ Postgres: # PostgreSQL数据库
User: paopao
Password: paopao
DBName: paopao
Schema: public
Host: localhost
Port: 5432
SSLMode: disable
TimeZone: Asia/Shanghai
ApplicationName:
Sqlite3: # Sqlite3数据库
Path: data/sqlite3/paopao-ce.db
Path: custom/data/sqlite3/paopao-ce.db
Redis:
Host: 127.0.0.1:6379
Password:
DB:
InitAddress:
- redis:6379
Username:
Password:
SelectDB:
ConnWriteTimeout: 60 # 连接写超时时间 多少秒 默认 60秒

@ -9,7 +9,6 @@ import (
"time"
"github.com/alimy/cfg"
"github.com/go-redis/redis/v8"
"github.com/sirupsen/logrus"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
@ -20,22 +19,21 @@ import (
)
var (
db *gorm.DB
Redis *redis.Client
once sync.Once
_gormDB *gorm.DB
_onceGorm sync.Once
)
func MustGormDB() *gorm.DB {
once.Do(func() {
_onceGorm.Do(func() {
var err error
if db, err = newDBEngine(); err != nil {
if _gormDB, err = newGormDB(); err != nil {
logrus.Fatalf("new gorm db failed: %s", err)
}
})
return db
return _gormDB
}
func newDBEngine() (*gorm.DB, error) {
func newGormDB() (*gorm.DB, error) {
newLogger := logger.New(
logrus.StandardLogger(), // io writer日志输出的目标前缀和日志包含的内容
logger.Config{
@ -84,11 +82,3 @@ func newDBEngine() (*gorm.DB, error) {
return db, err
}
func setupDBEngine() {
Redis = redis.NewClient(&redis.Options{
Addr: redisSetting.Host,
Password: redisSetting.Password,
DB: redisSetting.DB,
})
}

@ -6,8 +6,11 @@ package conf
import (
"io"
"time"
"github.com/alimy/cfg"
"github.com/getsentry/sentry-go"
sentrylogrus "github.com/getsentry/sentry-go/logrus"
"github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
)
@ -42,3 +45,16 @@ func setupLogger() {
},
})
}
func setupSentryLogrus(opts sentry.ClientOptions) {
// Send only ERROR and higher level logs to Sentry
sentryLevels := []logrus.Level{logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel}
sentryHook, err := sentrylogrus.New(sentryLevels, opts)
if err != nil {
panic(err)
}
logrus.AddHook(sentryHook)
// Flushes before calling os.Exit(1) when using logger.Fatal
// (else all defers are not called, and Sentry does not have time to send the event)
logrus.RegisterExitHandler(func() { sentryHook.Flush(5 * time.Second) })
}

@ -0,0 +1,34 @@
// 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 conf
import (
"log"
"sync"
"github.com/rueian/rueidis"
)
var (
_redisClient rueidis.Client
_onceRedis sync.Once
)
func MustRedisClient() rueidis.Client {
_onceRedis.Do(func() {
client, err := rueidis.NewClient(rueidis.ClientOption{
InitAddress: redisSetting.InitAddress,
Username: redisSetting.Username,
Password: redisSetting.Password,
SelectDB: redisSetting.SelectDB,
ConnWriteTimeout: redisSetting.ConnWriteTimeout,
})
if err != nil {
log.Fatalf("create a redis client failed: %s", err)
}
_redisClient = client
})
return _redisClient
}

@ -0,0 +1,35 @@
// 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 conf
import (
"time"
"github.com/alimy/cfg"
"github.com/getsentry/sentry-go"
"github.com/rocboss/paopao-ce/pkg/version"
)
func initSentry() {
cfg.Be("Sentry", func() {
opts := sentry.ClientOptions{
Dsn: sentrySetting.Dsn,
Debug: sentrySetting.Debug,
AttachStacktrace: sentrySetting.AttachStacktrace,
TracesSampleRate: sentrySetting.TracesSampleRate,
}
_ = sentry.Init(opts)
if sentrySetting.AttachLogrus {
setupSentryLogrus(opts)
}
sentry.WithScope(func(scope *sentry.Scope) {
scope.SetExtras(map[string]any{
"version": version.VersionInfo(),
"time": time.Now().Local(),
})
sentry.CaptureMessage("paopao-ce sentry works!")
})
})
}

@ -1,38 +1,52 @@
// Copyright 2022 ROC. All rights reserved.
// 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 conf
import (
"embed"
"bytes"
_ "embed"
"fmt"
"strings"
"time"
"github.com/pyroscope-io/client/pyroscope"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"gorm.io/gorm/logger"
)
//go:embed config.yaml
var files embed.FS
var configBytes []byte
type Setting struct {
vp *viper.Viper
type pyroscopeConf struct {
AppName string
Endpoint string
AuthToken string
Logger string
}
type sentryConf struct {
Dsn string
Debug bool
AttachStacktrace bool
TracesSampleRate float64
AttachLogrus bool
AttachGin bool
}
type LoggerSettingS struct {
type loggerConf struct {
Level string
}
type LoggerFileSettingS struct {
type loggerFileConf struct {
SavePath string
FileName string
FileExt string
}
type LoggerZincSettingS struct {
type loggerZincConf struct {
Host string
Index string
User string
@ -40,7 +54,7 @@ type LoggerZincSettingS struct {
Secure bool
}
type LoggerMeiliSettingS struct {
type loggerMeiliConf struct {
Host string
Index string
ApiKey string
@ -49,7 +63,7 @@ type LoggerMeiliSettingS struct {
MinWorker int
}
type HttpServerSettingS struct {
type httpServerConf struct {
RunMode string
HttpIp string
HttpPort string
@ -57,12 +71,12 @@ type HttpServerSettingS struct {
WriteTimeout time.Duration
}
type GRPCServerSettingS struct {
type grpcServerConf struct {
Host string
Port string
}
type AppSettingS struct {
type appConf struct {
RunMode string
MaxCommentCount int64
AttachmentIncomeRate float64
@ -71,24 +85,30 @@ type AppSettingS struct {
MaxPageSize int
}
type CacheIndexSettingS struct {
type cacheIndexConf struct {
MaxUpdateQPS int
MinWorker int
}
type SimpleCacheIndexSettingS struct {
type simpleCacheIndexConf struct {
MaxIndexSize int
CheckTickDuration time.Duration
ExpireTickDuration time.Duration
}
type BigCacheIndexSettingS struct {
MaxIndexPage int
type bigCacheIndexConf struct {
MaxIndexPage int
HardMaxCacheSize int
ExpireInSecond time.Duration
Verbose bool
}
type redisCacheIndexConf struct {
ExpireInSecond time.Duration
Verbose bool
}
type AlipaySettingS struct {
type alipayConf struct {
AppID string
PrivateKey string
RootCertFile string
@ -97,19 +117,19 @@ type AlipaySettingS struct {
InProduction bool
}
type SmsJuheSettings struct {
type smsJuheConf struct {
Gateway string
Key string
TplID string
TplVal string
}
type TweetSearchS struct {
type tweetSearchConf struct {
MaxUpdateQPS int
MinWorker int
}
type ZincSettingS struct {
type zincConf struct {
Host string
Index string
User string
@ -117,19 +137,19 @@ type ZincSettingS struct {
Secure bool
}
type MeiliSettingS struct {
type meiliConf struct {
Host string
Index string
ApiKey string
Secure bool
}
type DatabaseSetingS struct {
type databaseConf struct {
TablePrefix string
LogLevel string
}
type MySQLSettingS struct {
type mysqlConf struct {
UserName string
Password string
Host string
@ -140,18 +160,18 @@ type MySQLSettingS struct {
MaxOpenConns int
}
type PostgresSettingS map[string]string
type postgresConf map[string]string
type Sqlite3SettingS struct {
type sqlite3Conf struct {
Path string
}
type ObjectStorageS struct {
type objectStorageConf struct {
RetainInDays int
TempDir string
}
type MinIOSettingS struct {
type minioConf struct {
AccessKey string
SecretKey string
Secure bool
@ -160,7 +180,7 @@ type MinIOSettingS struct {
Domain string
}
type S3SettingS struct {
type s3Conf struct {
AccessKey string
SecretKey string
Secure bool
@ -169,7 +189,7 @@ type S3SettingS struct {
Domain string
}
type AliOSSSettingS struct {
type aliOSSConf struct {
AccessKeyID string
AccessKeySecret string
Endpoint string
@ -177,7 +197,7 @@ type AliOSSSettingS struct {
Domain string
}
type COSSettingS struct {
type cosConf struct {
SecretID string
SecretKey string
Region string
@ -185,7 +205,7 @@ type COSSettingS struct {
Domain string
}
type HuaweiOBSSettingS struct {
type huaweiOBSConf struct {
AccessKey string
SecretKey string
Endpoint string
@ -193,92 +213,36 @@ type HuaweiOBSSettingS struct {
Domain string
}
type LocalOSSSettingS struct {
type localossConf struct {
SavePath string
Secure bool
Bucket string
Domain string
}
type RedisSettingS struct {
Host string
Password string
DB int
type redisConf struct {
InitAddress []string
Username string
Password string
SelectDB int
ConnWriteTimeout time.Duration
}
type JWTSettingS struct {
type jwtConf struct {
Secret string
Issuer string
Expire time.Duration
}
func NewSetting() (*Setting, error) {
cfgFile, err := files.Open("config.yaml")
if err != nil {
return nil, err
}
defer cfgFile.Close()
vp := viper.New()
vp.SetConfigName("config")
vp.AddConfigPath(".")
vp.AddConfigPath("custom/")
vp.SetConfigType("yaml")
if err = vp.ReadConfig(cfgFile); err != nil {
return nil, err
}
if err = vp.MergeInConfig(); err != nil {
return nil, err
}
return &Setting{vp}, nil
}
func (s *Setting) ReadSection(k string, v any) error {
err := s.vp.UnmarshalKey(k, v)
if err != nil {
return err
}
return nil
}
func (s *Setting) Unmarshal(objects map[string]any) error {
for k, v := range objects {
err := s.vp.UnmarshalKey(k, v)
if err != nil {
return err
}
}
return nil
}
func (s *Setting) featuresInfoFrom(k string) (map[string][]string, map[string]string) {
sub := s.vp.Sub(k)
keys := sub.AllKeys()
suites := make(map[string][]string)
kv := make(map[string]string, len(keys))
for _, key := range sub.AllKeys() {
val := sub.Get(key)
switch v := val.(type) {
case string:
kv[key] = v
case []any:
suites[key] = sub.GetStringSlice(key)
}
}
return suites, kv
}
func (s *HttpServerSettingS) GetReadTimeout() time.Duration {
func (s *httpServerConf) GetReadTimeout() time.Duration {
return s.ReadTimeout * time.Second
}
func (s *HttpServerSettingS) GetWriteTimeout() time.Duration {
func (s *httpServerConf) GetWriteTimeout() time.Duration {
return s.WriteTimeout * time.Second
}
func (s *MySQLSettingS) Dsn() string {
func (s *mysqlConf) Dsn() string {
return fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=%s&parseTime=%t&loc=Local",
s.UserName,
s.Password,
@ -289,17 +253,27 @@ func (s *MySQLSettingS) Dsn() string {
)
}
func (s PostgresSettingS) Dsn() string {
func (s postgresConf) Dsn() string {
var params []string
for k, v := range s {
if len(v) > 0 {
params = append(params, strings.ToLower(k)+"="+v)
if len(v) == 0 {
continue
}
lk := strings.ToLower(k)
tv := strings.Trim(v, " ")
switch lk {
case "schema":
params = append(params, "search_path="+tv)
case "applicationname":
params = append(params, "application_name="+tv)
default:
params = append(params, lk+"="+tv)
}
}
return strings.Join(params, " ")
}
func (s *Sqlite3SettingS) Dsn(driverName string) string {
func (s *sqlite3Conf) Dsn(driverName string) string {
pragmas := "_foreign_keys=1&_journal_mode=WAL&_synchronous=NORMAL&_busy_timeout=8000"
if driverName == "sqlite" {
pragmas = "_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=synchronous(NORMAL)&_pragma=busy_timeout(8000)&_pragma=journal_size_limit(100000000)"
@ -307,7 +281,7 @@ func (s *Sqlite3SettingS) Dsn(driverName string) string {
return fmt.Sprintf("file:%s?%s", s.Path, pragmas)
}
func (s *DatabaseSetingS) logLevel() logger.LogLevel {
func (s *databaseConf) logLevel() logger.LogLevel {
switch strings.ToLower(s.LogLevel) {
case "silent":
return logger.Silent
@ -322,7 +296,7 @@ func (s *DatabaseSetingS) logLevel() logger.LogLevel {
}
}
func (s *LoggerSettingS) logLevel() logrus.Level {
func (s *loggerConf) logLevel() logrus.Level {
switch strings.ToLower(s.Level) {
case "panic":
return logrus.PanicLevel
@ -343,15 +317,15 @@ func (s *LoggerSettingS) logLevel() logrus.Level {
}
}
func (s *LoggerZincSettingS) Endpoint() string {
func (s *loggerZincConf) Endpoint() string {
return endpoint(s.Host, s.Secure)
}
func (s *LoggerMeiliSettingS) Endpoint() string {
func (s *loggerMeiliConf) Endpoint() string {
return endpoint(s.Host, s.Secure)
}
func (s *LoggerMeiliSettingS) minWork() int {
func (s *loggerMeiliConf) minWork() int {
if s.MinWorker < 5 {
return 5
} else if s.MinWorker > 100 {
@ -360,7 +334,7 @@ func (s *LoggerMeiliSettingS) minWork() int {
return s.MinWorker
}
func (s *LoggerMeiliSettingS) maxLogBuffer() int {
func (s *loggerMeiliConf) maxLogBuffer() int {
if s.MaxLogBuffer < 10 {
return 10
} else if s.MaxLogBuffer > 1000 {
@ -369,18 +343,28 @@ func (s *LoggerMeiliSettingS) maxLogBuffer() int {
return s.MaxLogBuffer
}
func (s *ObjectStorageS) TempDirSlash() string {
func (s *objectStorageConf) TempDirSlash() string {
return strings.Trim(s.TempDir, " /") + "/"
}
func (s *ZincSettingS) Endpoint() string {
func (s *zincConf) Endpoint() string {
return endpoint(s.Host, s.Secure)
}
func (s *MeiliSettingS) Endpoint() string {
func (s *meiliConf) Endpoint() string {
return endpoint(s.Host, s.Secure)
}
func (s *pyroscopeConf) GetLogger() (logger pyroscope.Logger) {
switch strings.ToLower(s.Logger) {
case "standard":
logger = pyroscope.StandardLogger
case "logrus":
logger = logrus.StandardLogger()
}
return
}
func endpoint(host string, secure bool) string {
schema := "http"
if secure {
@ -388,3 +372,37 @@ func endpoint(host string, secure bool) string {
}
return schema + "://" + host
}
func newViper() (*viper.Viper, error) {
vp := viper.New()
vp.SetConfigName("config")
vp.AddConfigPath(".")
vp.AddConfigPath("custom/")
vp.SetConfigType("yaml")
err := vp.ReadConfig(bytes.NewReader(configBytes))
if err != nil {
return nil, err
}
if err = vp.MergeInConfig(); err != nil {
return nil, err
}
return vp, nil
}
func featuresInfoFrom(vp *viper.Viper, k string) (map[string][]string, map[string]string) {
sub := vp.Sub(k)
keys := sub.AllKeys()
suites := make(map[string][]string)
kv := make(map[string]string, len(keys))
for _, key := range sub.AllKeys() {
val := sub.Get(key)
switch v := val.(type) {
case string:
kv[key] = v
case []any:
suites[key] = sub.GetStringSlice(key)
}
}
return suites, kv
}

@ -5,6 +5,8 @@
package core
import (
"context"
"github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr"
)
@ -56,3 +58,21 @@ type CacheIndexService interface {
SendAction(act IdxAct, post *dbr.Post)
}
// RedisCache memory cache by Redis
type RedisCache interface {
SetPushToSearchJob(ctx context.Context) error
DelPushToSearchJob(ctx context.Context) error
SetImgCaptcha(ctx context.Context, id string, value string) error
GetImgCaptcha(ctx context.Context, id string) (string, error)
DelImgCaptcha(ctx context.Context, id string) error
GetCountSmsCaptcha(ctx context.Context, phone string) (int64, error)
IncrCountSmsCaptcha(ctx context.Context, phone string) error
GetCountLoginErr(ctx context.Context, id int64) (int64, error)
DelCountLoginErr(ctx context.Context, id int64) error
IncrCountLoginErr(ctx context.Context, id int64) error
GetCountWhisper(ctx context.Context, uid int64) (int64, error)
IncrCountWhisper(ctx context.Context, uid int64) error
SetRechargeStatus(ctx context.Context, tradeNo string) error
DelRechargeStatus(ctx context.Context, tradeNo string) error
}

@ -5,6 +5,7 @@
package core
import (
"github.com/rocboss/paopao-ce/internal/core/cs"
"github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr"
)
@ -24,6 +25,7 @@ type CommentService interface {
GetCommentReplyByID(id int64) (*CommentReply, error)
GetCommentContentsByIDs(ids []int64) ([]*CommentContent, error)
GetCommentRepliesByID(ids []int64) ([]*CommentReplyFormated, error)
GetCommentThumbsMap(userId int64, tweetId int64) (cs.CommentThumbsMap, cs.CommentThumbsMap, error)
}
// CommentManageService 评论管理服务
@ -33,4 +35,8 @@ type CommentManageService interface {
CreateCommentReply(reply *CommentReply) (*CommentReply, error)
DeleteCommentReply(reply *CommentReply) error
CreateCommentContent(content *CommentContent) (*CommentContent, error)
ThumbsUpComment(userId int64, tweetId, commentId int64) error
ThumbsDownComment(userId int64, tweetId, commentId int64) error
ThumbsUpReply(userId int64, tweetId, commentId, replyId int64) error
ThumbsDownReply(userId int64, tweetId, commentId, replyId int64) error
}

@ -0,0 +1,19 @@
// 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 cs
type CommentThumbs struct {
UserID int64 `json:"user_id"`
TweetID int64 `json:"tweet_id"`
CommentID int64 `json:"comment_id"`
ReplyID int64 `json:"reply_id"`
CommentType int8 `json:"comment_type"`
IsThumbsUp int8 `json:"is_thumbs_up"`
IsThumbsDown int8 `json:"is_thumbs_down"`
}
type CommentThumbsList []*CommentThumbs
type CommentThumbsMap map[int64]*CommentThumbs

@ -18,5 +18,11 @@ type TopicService interface {
CreateTag(tag *Tag) (*Tag, error)
DeleteTag(tag *Tag) error
GetTags(conditions *ConditionsT, offset, limit int) ([]*Tag, error)
GetHotTags(userId int64, limit int, offset int) ([]*TagFormated, error)
GetNewestTags(userId int64, limit int, offset int) ([]*TagFormated, error)
GetFollowTags(userId int64, limit int, offset int) ([]*TagFormated, error)
GetTagsByKeyword(keyword string) ([]*Tag, error)
FollowTopic(userId int64, topicId int64) error
UnfollowTopic(userId int64, topicId int64) error
StickTopic(userId int64, topicId int64) (int8, error)
}

@ -0,0 +1,229 @@
// 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 cache
import (
"bytes"
"encoding/gob"
"fmt"
"strconv"
"strings"
"time"
"github.com/Masterminds/semver/v3"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/pkg/types"
"github.com/sirupsen/logrus"
)
const (
_cacheIndexKey = "paopao_index"
)
var (
_ core.CacheIndexService = (*cacheIndexSrv)(nil)
_ core.VersionInfo = (*cacheIndexSrv)(nil)
)
type postsEntry struct {
key string
tweets *core.IndexTweetList
}
type tweetsCache interface {
core.VersionInfo
getTweetsBytes(key string) ([]byte, error)
setTweetsBytes(key string, bs []byte) error
delTweets(keys []string) error
allKeys() ([]string, error)
}
type cacheIndexSrv struct {
ips core.IndexPostsService
ams core.AuthorizationManageService
name string
version *semver.Version
indexActionCh chan *core.IndexAction
cachePostsCh chan *postsEntry
cache tweetsCache
lastCacheResetTime time.Time
preventDuration time.Duration
}
func (s *cacheIndexSrv) IndexPosts(user *core.User, offset int, limit int) (*core.IndexTweetList, error) {
key := s.keyFrom(user, offset, limit)
posts, err := s.getPosts(key)
if err == nil {
logrus.Debugf("cacheIndexSrv.IndexPosts get index posts from cache by key: %s", key)
return posts, nil
}
if posts, err = s.ips.IndexPosts(user, offset, limit); err != nil {
return nil, err
}
logrus.Debugf("cacheIndexSrv.IndexPosts get index posts from database by key: %s", key)
s.cachePosts(key, posts)
return posts, nil
}
func (s *cacheIndexSrv) getPosts(key string) (*core.IndexTweetList, error) {
data, err := s.cache.getTweetsBytes(key)
if err != nil {
logrus.Debugf("cacheIndexSrv.getPosts get posts by key: %s from cache err: %v", key, err)
return nil, err
}
buf := bytes.NewBuffer(data)
dec := gob.NewDecoder(buf)
var resp core.IndexTweetList
if err := dec.Decode(&resp); err != nil {
logrus.Debugf("cacheIndexSrv.getPosts get posts from cache in decode err: %v", err)
return nil, err
}
return &resp, nil
}
func (s *cacheIndexSrv) cachePosts(key string, tweets *core.IndexTweetList) {
entry := &postsEntry{key: key, tweets: tweets}
select {
case s.cachePostsCh <- entry:
logrus.Debugf("cacheIndexSrv.cachePosts cachePosts by chan of key: %s", key)
default:
go func(ch chan<- *postsEntry, entry *postsEntry) {
logrus.Debugf("cacheIndexSrv.cachePosts cachePosts indexAction by goroutine of key: %s", key)
ch <- entry
}(s.cachePostsCh, entry)
}
}
func (s *cacheIndexSrv) setPosts(entry *postsEntry) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
if err := enc.Encode(entry.tweets); err != nil {
logrus.Debugf("cacheIndexSrv.setPosts setPosts encode post entry err: %v", err)
return
}
if err := s.cache.setTweetsBytes(entry.key, buf.Bytes()); err != nil {
logrus.Debugf("cacheIndexSrv.setPosts setPosts set cache err: %v", err)
}
logrus.Debugf("cacheIndexSrv.setPosts setPosts set cache by key: %s", entry.key)
}
func (s *cacheIndexSrv) keyFrom(user *core.User, offset int, limit int) string {
var userId int64 = -1
if user != nil {
userId = user.ID
}
return fmt.Sprintf("%s:%d:%d:%d", _cacheIndexKey, userId, offset, limit)
}
func (s *cacheIndexSrv) SendAction(act core.IdxAct, post *core.Post) {
action := core.NewIndexAction(act, post)
select {
case s.indexActionCh <- action:
logrus.Debugf("cacheIndexSrv.SendAction send indexAction by chan: %s", act)
default:
go func(ch chan<- *core.IndexAction, act *core.IndexAction) {
logrus.Debugf("cacheIndexSrv.SendAction send indexAction by goroutine: %s", action.Act)
ch <- act
}(s.indexActionCh, action)
}
}
func (s *cacheIndexSrv) startIndexPosts() {
for {
select {
case entry := <-s.cachePostsCh:
s.setPosts(entry)
case action := <-s.indexActionCh:
s.handleIndexAction(action)
}
}
}
func (s *cacheIndexSrv) handleIndexAction(action *core.IndexAction) {
act, post := action.Act, action.Post
// 创建/删除 私密推文特殊处理
switch act {
case core.IdxActCreatePost, core.IdxActDeletePost:
if post.Visibility == core.PostVisitPrivate {
s.deleteCacheByUserId(post.UserID, true)
return
}
}
// 如果在s.preventDuration时间内就清除所有缓存否则只清除自个儿的缓存
// TODO: 需要优化只清除受影响的缓存,后续完善
if time.Since(s.lastCacheResetTime) > s.preventDuration {
s.deleteCacheByUserId(post.UserID, false)
} else {
s.deleteCacheByUserId(post.UserID, true)
}
}
func (s *cacheIndexSrv) deleteCacheByUserId(id int64, oneself bool) {
var keys []string
userId := strconv.FormatInt(id, 10)
friendSet := core.FriendSet{}
if !oneself {
friendSet = s.ams.MyFriendSet(id)
}
friendSet[userId] = types.Empty{}
// 获取需要删除缓存的key目前是仅删除自个儿的缓存
allKeys, err := s.cache.allKeys()
if err != nil {
logrus.Debugf("cacheIndexSrv.deleteCacheByUserId userId: %s err:%s", userId, err)
}
for _, key := range allKeys {
keyParts := strings.Split(key, ":")
if len(keyParts) > 2 && keyParts[0] == _cacheIndexKey {
if _, ok := friendSet[keyParts[1]]; ok {
keys = append(keys, key)
}
}
}
// 执行删缓存
s.cache.delTweets(keys)
s.lastCacheResetTime = time.Now()
logrus.Debugf("cacheIndexSrv.deleteCacheByUserId userId:%s oneself:%t keys:%d", userId, oneself, len(keys))
}
func (s *cacheIndexSrv) Name() string {
return s.name
}
func (s *cacheIndexSrv) Version() *semver.Version {
return s.version
}
func newCacheIndexSrv(ips core.IndexPostsService, ams core.AuthorizationManageService, tc tweetsCache) *cacheIndexSrv {
cacheIndex := &cacheIndexSrv{
ips: ips,
ams: ams,
cache: tc,
name: tc.Name(),
version: tc.Version(),
preventDuration: 10 * time.Second,
}
// indexActionCh capacity custom configure by conf.yaml need in [10, 10000]
// or re-compile source to adjust min/max capacity
capacity := conf.CacheIndexSetting.MaxUpdateQPS
if capacity < 10 {
capacity = 10
} else if capacity > 10000 {
capacity = 10000
}
cacheIndex.indexActionCh = make(chan *core.IndexAction, capacity)
cacheIndex.cachePostsCh = make(chan *postsEntry, capacity)
// 启动索引更新器
go cacheIndex.startIndexPosts()
return cacheIndex
}

@ -5,189 +5,51 @@
package cache
import (
"bytes"
"encoding/gob"
"fmt"
"strconv"
"strings"
"time"
"github.com/Masterminds/semver/v3"
"github.com/allegro/bigcache/v3"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/pkg/types"
"github.com/sirupsen/logrus"
)
var (
_ core.CacheIndexService = (*bigCacheIndexServant)(nil)
_ core.VersionInfo = (*bigCacheIndexServant)(nil)
_ tweetsCache = (*bigCacheTweetsCache)(nil)
)
type postsEntry struct {
key string
tweets *core.IndexTweetList
type bigCacheTweetsCache struct {
name string
version *semver.Version
bc *bigcache.BigCache
}
type bigCacheIndexServant struct {
ips core.IndexPostsService
ams core.AuthorizationManageService
indexActionCh chan *core.IndexAction
cachePostsCh chan *postsEntry
cache *bigcache.BigCache
lastCacheResetTime time.Time
preventDuration time.Duration
}
func (s *bigCacheIndexServant) IndexPosts(user *core.User, offset int, limit int) (*core.IndexTweetList, error) {
key := s.keyFrom(user, offset, limit)
posts, err := s.getPosts(key)
if err == nil {
logrus.Debugf("bigCacheIndexServant.IndexPosts get index posts from cache by key: %s", key)
return posts, nil
}
if posts, err = s.ips.IndexPosts(user, offset, limit); err != nil {
return nil, err
}
logrus.Debugf("bigCacheIndexServant.IndexPosts get index posts from database by key: %s", key)
s.cachePosts(key, posts)
return posts, nil
func (s *bigCacheTweetsCache) getTweetsBytes(key string) ([]byte, error) {
return s.bc.Get(key)
}
func (s *bigCacheIndexServant) getPosts(key string) (*core.IndexTweetList, error) {
data, err := s.cache.Get(key)
if err != nil {
logrus.Debugf("bigCacheIndexServant.getPosts get posts by key: %s from cache err: %v", key, err)
return nil, err
}
buf := bytes.NewBuffer(data)
dec := gob.NewDecoder(buf)
var resp core.IndexTweetList
if err := dec.Decode(&resp); err != nil {
logrus.Debugf("bigCacheIndexServant.getPosts get posts from cache in decode err: %v", err)
return nil, err
}
return &resp, nil
}
func (s *bigCacheIndexServant) cachePosts(key string, tweets *core.IndexTweetList) {
entry := &postsEntry{key: key, tweets: tweets}
select {
case s.cachePostsCh <- entry:
logrus.Debugf("bigCacheIndexServant.cachePosts cachePosts by chan of key: %s", key)
default:
go func(ch chan<- *postsEntry, entry *postsEntry) {
logrus.Debugf("bigCacheIndexServant.cachePosts cachePosts indexAction by goroutine of key: %s", key)
ch <- entry
}(s.cachePostsCh, entry)
}
}
func (s *bigCacheIndexServant) setPosts(entry *postsEntry) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
if err := enc.Encode(entry.tweets); err != nil {
logrus.Debugf("bigCacheIndexServant.setPosts setPosts encode post entry err: %v", err)
return
}
if err := s.cache.Set(entry.key, buf.Bytes()); err != nil {
logrus.Debugf("bigCacheIndexServant.setPosts setPosts set cache err: %v", err)
}
logrus.Debugf("bigCacheIndexServant.setPosts setPosts set cache by key: %s", entry.key)
}
func (s *bigCacheIndexServant) keyFrom(user *core.User, offset int, limit int) string {
var userId int64 = -1
if user != nil {
userId = user.ID
}
return fmt.Sprintf("index:%d:%d:%d", userId, offset, limit)
func (s *bigCacheTweetsCache) setTweetsBytes(key string, bs []byte) error {
return s.bc.Set(key, bs)
}
func (s *bigCacheIndexServant) SendAction(act core.IdxAct, post *core.Post) {
action := core.NewIndexAction(act, post)
select {
case s.indexActionCh <- action:
logrus.Debugf("bigCacheIndexServant.SendAction send indexAction by chan: %s", act)
default:
go func(ch chan<- *core.IndexAction, act *core.IndexAction) {
logrus.Debugf("bigCacheIndexServant.SendAction send indexAction by goroutine: %s", action.Act)
ch <- act
}(s.indexActionCh, action)
}
}
func (s *bigCacheIndexServant) startIndexPosts() {
for {
select {
case entry := <-s.cachePostsCh:
s.setPosts(entry)
case action := <-s.indexActionCh:
s.handleIndexAction(action)
}
}
}
func (s *bigCacheIndexServant) handleIndexAction(action *core.IndexAction) {
act, post := action.Act, action.Post
// 创建/删除 私密推文特殊处理
switch act {
case core.IdxActCreatePost, core.IdxActDeletePost:
if post.Visibility == core.PostVisitPrivate {
s.deleteCacheByUserId(post.UserID, true)
return
}
}
// 如果在s.preventDuration时间内就清除所有缓存否则只清除自个儿的缓存
// TODO: 需要优化只清除受影响的缓存,后续完善
if time.Since(s.lastCacheResetTime) > s.preventDuration {
s.deleteCacheByUserId(post.UserID, false)
} else {
s.deleteCacheByUserId(post.UserID, true)
func (s *bigCacheTweetsCache) delTweets(keys []string) error {
for _, k := range keys {
s.bc.Delete(k)
}
return nil
}
func (s *bigCacheIndexServant) deleteCacheByUserId(id int64, oneself bool) {
func (s *bigCacheTweetsCache) allKeys() ([]string, error) {
var keys []string
userId := strconv.FormatInt(id, 10)
friendSet := core.FriendSet{}
if !oneself {
friendSet = s.ams.MyFriendSet(id)
}
friendSet[userId] = types.Empty{}
// 获取需要删除缓存的key目前是仅删除自个儿的缓存
for it := s.cache.Iterator(); it.SetNext(); {
for it := s.bc.Iterator(); it.SetNext(); {
entry, err := it.Value()
if err != nil {
logrus.Debugf("bigCacheIndexServant.deleteCacheByUserId userId: %s err:%s", userId, err)
return
return nil, err
}
key := entry.Key()
keyParts := strings.Split(key, ":")
if len(keyParts) > 2 && keyParts[0] == "index" {
if _, ok := friendSet[keyParts[1]]; ok {
keys = append(keys, key)
}
}
}
// 执行删缓存
for _, k := range keys {
s.cache.Delete(k)
keys = append(keys, entry.Key())
}
s.lastCacheResetTime = time.Now()
logrus.Debugf("bigCacheIndexServant.deleteCacheByUserId userId:%s oneself:%t keys:%d", userId, oneself, len(keys))
return keys, nil
}
func (s *bigCacheIndexServant) Name() string {
func (s *bigCacheTweetsCache) Name() string {
return "BigCacheIndex"
}
func (s *bigCacheIndexServant) Version() *semver.Version {
func (s *bigCacheTweetsCache) Version() *semver.Version {
return semver.MustParse("v0.2.0")
}

@ -13,40 +13,37 @@ import (
"github.com/sirupsen/logrus"
)
func NewRedisCache() core.RedisCache {
return &redisCache{
c: conf.MustRedisClient(),
}
}
func NewBigCacheIndexService(ips core.IndexPostsService, ams core.AuthorizationManageService) (core.CacheIndexService, core.VersionInfo) {
s := conf.BigCacheIndexSetting
c := bigcache.DefaultConfig(s.ExpireInSecond)
c.Shards = s.MaxIndexPage
c.HardMaxCacheSize = s.HardMaxCacheSize
c.Verbose = s.Verbose
c.MaxEntrySize = 10000
c.Logger = logrus.StandardLogger()
config := bigcache.DefaultConfig(s.ExpireInSecond)
config.Shards = s.MaxIndexPage
config.Verbose = s.Verbose
config.MaxEntrySize = 10000
config.Logger = logrus.StandardLogger()
cache, err := bigcache.NewBigCache(config)
bc, err := bigcache.NewBigCache(c)
if err != nil {
logrus.Fatalf("initial bigCahceIndex failure by err: %v", err)
}
cacheIndex := newCacheIndexSrv(ips, ams, &bigCacheTweetsCache{
bc: bc,
})
return cacheIndex, cacheIndex
}
cacheIndex := &bigCacheIndexServant{
ips: ips,
ams: ams,
cache: cache,
preventDuration: 10 * time.Second,
}
// indexActionCh capacity custom configure by conf.yaml need in [10, 10000]
// or re-compile source to adjust min/max capacity
capacity := conf.CacheIndexSetting.MaxUpdateQPS
if capacity < 10 {
capacity = 10
} else if capacity > 10000 {
capacity = 10000
}
cacheIndex.indexActionCh = make(chan *core.IndexAction, capacity)
cacheIndex.cachePostsCh = make(chan *postsEntry, capacity)
// 启动索引更新器
go cacheIndex.startIndexPosts()
func NewRedisCacheIndexService(ips core.IndexPostsService, ams core.AuthorizationManageService) (core.CacheIndexService, core.VersionInfo) {
cacheIndex := newCacheIndexSrv(ips, ams, &redisCacheTweetsCache{
expireDuration: conf.RedisCacheIndexSetting.ExpireInSecond,
expireInSecond: int64(conf.RedisCacheIndexSetting.ExpireInSecond / time.Second),
c: conf.MustRedisClient(),
})
return cacheIndex, cacheIndex
}

@ -0,0 +1,153 @@
// 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 cache
import (
"context"
"fmt"
"time"
"unsafe"
"github.com/Masterminds/semver/v3"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rueian/rueidis"
)
var (
_ core.RedisCache = (*redisCache)(nil)
_ tweetsCache = (*redisCacheTweetsCache)(nil)
)
const (
_cacheIndexKeyPattern = _cacheIndexKey + "*"
_pushToSearchJobKey = "paopao_push_to_search_job"
_countLoginErrKey = "paopao_count_login_err"
_imgCaptchaKey = "paopao_img_captcha:"
_smsCaptchaKey = "paopao_sms_captcha"
_countWhisperKey = "paopao_whisper_key"
_rechargeStatusKey = "paopao_recharge_status:"
)
type redisCache struct {
c rueidis.Client
}
type redisCacheTweetsCache struct {
expireDuration time.Duration
expireInSecond int64
c rueidis.Client
}
func (s *redisCacheTweetsCache) getTweetsBytes(key string) ([]byte, error) {
res, err := rueidis.MGetCache(s.c, context.Background(), s.expireDuration, []string{key})
if err != nil {
return nil, err
}
message := res[key]
return message.AsBytes()
}
func (s *redisCacheTweetsCache) setTweetsBytes(key string, bs []byte) error {
cmd := s.c.B().Set().Key(key).Value(rueidis.BinaryString(bs)).ExSeconds(s.expireInSecond).Build()
return s.c.Do(context.Background(), cmd).Error()
}
func (s *redisCacheTweetsCache) delTweets(keys []string) error {
cmd := s.c.B().Del().Key(keys...).Build()
return s.c.Do(context.Background(), cmd).Error()
}
func (s *redisCacheTweetsCache) allKeys() ([]string, error) {
cmd := s.c.B().Keys().Pattern(_cacheIndexKeyPattern).Build()
return s.c.Do(context.Background(), cmd).AsStrSlice()
}
func (s *redisCacheTweetsCache) Name() string {
return "RedisCacheIndex"
}
func (s *redisCacheTweetsCache) Version() *semver.Version {
return semver.MustParse("v0.1.0")
}
func (r *redisCache) SetPushToSearchJob(ctx context.Context) error {
return r.c.Do(ctx, r.c.B().Set().
Key(_pushToSearchJobKey).Value("1").
Nx().ExSeconds(3600).
Build()).Error()
}
func (r *redisCache) DelPushToSearchJob(ctx context.Context) error {
return r.c.Do(ctx, r.c.B().Del().Key(_pushToSearchJobKey).Build()).Error()
}
func (r *redisCache) SetImgCaptcha(ctx context.Context, id string, value string) error {
return r.c.Do(ctx, r.c.B().Set().
Key(_imgCaptchaKey+id).Value(value).
ExSeconds(300).
Build()).Error()
}
func (r *redisCache) GetImgCaptcha(ctx context.Context, id string) (string, error) {
res, err := r.c.Do(ctx, r.c.B().Get().Key(_imgCaptchaKey+id).Build()).AsBytes()
return unsafe.String(&res[0], len(res)), err
}
func (r *redisCache) DelImgCaptcha(ctx context.Context, id string) error {
return r.c.Do(ctx, r.c.B().Del().Key(_imgCaptchaKey+id).Build()).Error()
}
func (r *redisCache) GetCountSmsCaptcha(ctx context.Context, phone string) (int64, error) {
return r.c.Do(ctx, r.c.B().Get().Key(_smsCaptchaKey+phone).Build()).AsInt64()
}
func (r *redisCache) IncrCountSmsCaptcha(ctx context.Context, phone string) (err error) {
if err = r.c.Do(ctx, r.c.B().Incr().Key(_smsCaptchaKey+phone).Build()).Error(); err == nil {
currentTime := time.Now()
endTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), 23, 59, 59, 0, currentTime.Location())
err = r.c.Do(ctx, r.c.B().Expire().Key(_smsCaptchaKey+phone).Seconds(int64(endTime.Sub(currentTime)/time.Second)).Build()).Error()
}
return
}
func (r *redisCache) GetCountLoginErr(ctx context.Context, id int64) (int64, error) {
return r.c.Do(ctx, r.c.B().Get().Key(fmt.Sprintf("%s:%d", _countLoginErrKey, id)).Build()).AsInt64()
}
func (r *redisCache) DelCountLoginErr(ctx context.Context, id int64) error {
return r.c.Do(ctx, r.c.B().Del().Key(fmt.Sprintf("%s:%d", _countLoginErrKey, id)).Build()).Error()
}
func (r *redisCache) IncrCountLoginErr(ctx context.Context, id int64) error {
err := r.c.Do(ctx, r.c.B().Incr().Key(fmt.Sprintf("%s:%d", _countLoginErrKey, id)).Build()).Error()
if err == nil {
err = r.c.Do(ctx, r.c.B().Expire().Key(fmt.Sprintf("%s:%d", _countLoginErrKey, id)).Seconds(3600).Build()).Error()
}
return err
}
func (r *redisCache) GetCountWhisper(ctx context.Context, uid int64) (int64, error) {
return r.c.Do(ctx, r.c.B().Get().Key(fmt.Sprintf("%s:%d", _countWhisperKey, uid)).Build()).AsInt64()
}
func (r *redisCache) IncrCountWhisper(ctx context.Context, uid int64) (err error) {
key := fmt.Sprintf("%s:%d", _countWhisperKey, uid)
if err = r.c.Do(ctx, r.c.B().Incr().Key(key).Build()).Error(); err == nil {
currentTime := time.Now()
endTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), 23, 59, 59, 0, currentTime.Location())
err = r.c.Do(ctx, r.c.B().Expire().Key(key).Seconds(int64(endTime.Sub(currentTime)/time.Second)).Build()).Error()
}
return
}
func (r *redisCache) SetRechargeStatus(ctx context.Context, tradeNo string) error {
return r.c.Do(ctx, r.c.B().Set().
Key(_rechargeStatusKey+tradeNo).Value("1").
Nx().ExSeconds(5).Build()).Error()
}
func (r *redisCache) DelRechargeStatus(ctx context.Context, tradeNo string) error {
return r.c.Do(ctx, r.c.B().Del().Key(_rechargeStatusKey+tradeNo).Build()).Error()
}

@ -5,8 +5,12 @@
package jinzhu
import (
"time"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/core/cs"
"github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr"
"github.com/rocboss/paopao-ce/pkg/types"
"gorm.io/gorm"
)
@ -35,6 +39,26 @@ func newCommentManageService(db *gorm.DB) core.CommentManageService {
}
}
func (s *commentServant) GetCommentThumbsMap(userId int64, tweetId int64) (cs.CommentThumbsMap, cs.CommentThumbsMap, error) {
if userId < 0 {
return nil, nil, nil
}
commentThumbsList := cs.CommentThumbsList{}
err := s.db.Model(&dbr.TweetCommentThumbs{}).Where("user_id=? AND tweet_id=?", userId, tweetId).Find(&commentThumbsList).Error
if err != nil {
return nil, nil, err
}
commentThumbs, replyThumbs := make(cs.CommentThumbsMap), make(cs.CommentThumbsMap)
for _, thumbs := range commentThumbsList {
if thumbs.CommentType == 0 {
commentThumbs[thumbs.CommentID] = thumbs
} else {
replyThumbs[thumbs.ReplyID] = thumbs
}
}
return commentThumbs, replyThumbs, nil
}
func (s *commentServant) GetComments(conditions *core.ConditionsT, offset, limit int) ([]*core.Comment, error) {
return (&dbr.Comment{}).List(s.db, conditions, offset, limit)
}
@ -72,6 +96,7 @@ func (s *commentServant) GetCommentRepliesByID(ids []int64) ([]*core.CommentRepl
CommentReply := &dbr.CommentReply{}
replies, err := CommentReply.List(s.db, &dbr.ConditionsT{
"comment_id IN ?": ids,
"ORDER": "id ASC",
}, 0, 0)
if err != nil {
@ -106,7 +131,22 @@ func (s *commentServant) GetCommentRepliesByID(ids []int64) ([]*core.CommentRepl
}
func (s *commentManageServant) DeleteComment(comment *core.Comment) error {
return comment.Delete(s.db)
db := s.db.Begin()
defer db.Rollback()
err := comment.Delete(s.db)
if err != nil {
return err
}
err = db.Model(&dbr.TweetCommentThumbs{}).Where("user_id=? AND tweet_id=? AND comment_id=?", comment.UserID, comment.PostID, comment.ID).Updates(map[string]any{
"deleted_on": time.Now().Unix(),
"is_del": 1,
}).Error
if err != nil {
return err
}
db.Commit()
return nil
}
func (s *commentManageServant) CreateComment(comment *core.Comment) (*core.Comment, error) {
@ -117,10 +157,244 @@ func (s *commentManageServant) CreateCommentReply(reply *core.CommentReply) (*co
return reply.Create(s.db)
}
func (s *commentManageServant) DeleteCommentReply(reply *core.CommentReply) error {
return reply.Delete(s.db)
func (s *commentManageServant) DeleteCommentReply(reply *core.CommentReply) (err error) {
db := s.db.Begin()
defer db.Rollback()
err = reply.Delete(s.db)
if err != nil {
return
}
err = db.Model(&dbr.TweetCommentThumbs{}).
Where("user_id=? AND comment_id=? AND reply_id=?", reply.UserID, reply.CommentID, reply.ID).Updates(map[string]any{
"deleted_on": time.Now().Unix(),
"is_del": 1,
}).Error
if err != nil {
return
}
db.Commit()
return
}
func (s *commentManageServant) CreateCommentContent(content *core.CommentContent) (*core.CommentContent, error) {
return content.Create(s.db)
}
func (s *commentManageServant) ThumbsUpComment(userId int64, tweetId, commentId int64) error {
db := s.db.Begin()
defer db.Rollback()
var (
thumbsUpCount int32 = 0
thumbsDownCount int32 = 0
)
commentThumbs := &dbr.TweetCommentThumbs{}
// 检查thumbs状态
err := s.db.Where("user_id=? AND tweet_id=? AND comment_id=? AND comment_type=0", userId, tweetId, commentId).Take(commentThumbs).Error
if err == nil {
switch {
case commentThumbs.IsThumbsUp == types.Yes && commentThumbs.IsThumbsDown == types.No:
thumbsUpCount, thumbsDownCount = -1, 0
case commentThumbs.IsThumbsUp == types.No && commentThumbs.IsThumbsDown == types.No:
thumbsUpCount, thumbsDownCount = 1, 0
default:
thumbsUpCount, thumbsDownCount = 1, -1
commentThumbs.IsThumbsDown = types.No
}
commentThumbs.IsThumbsUp = 1 - commentThumbs.IsThumbsUp
commentThumbs.ModifiedOn = time.Now().Unix()
} else {
commentThumbs = &dbr.TweetCommentThumbs{
UserID: userId,
TweetID: tweetId,
CommentID: commentId,
IsThumbsUp: types.Yes,
IsThumbsDown: types.No,
CommentType: 0,
Model: &dbr.Model{
CreatedOn: time.Now().Unix(),
},
}
thumbsUpCount, thumbsDownCount = 1, 0
}
// 更新thumbs状态
if err = s.db.Save(commentThumbs).Error; err != nil {
return err
}
// 更新thumbsUpCount
if err = s.updateCommentThumbsUpCount(&dbr.Comment{}, commentId, thumbsUpCount, thumbsDownCount); err != nil {
return err
}
db.Commit()
return nil
}
func (s *commentManageServant) ThumbsDownComment(userId int64, tweetId, commentId int64) error {
db := s.db.Begin()
defer db.Rollback()
var (
thumbsUpCount int32 = 0
thumbsDownCount int32 = 0
)
commentThumbs := &dbr.TweetCommentThumbs{}
// 检查thumbs状态
err := s.db.Where("user_id=? AND tweet_id=? AND comment_id=? AND comment_type=0", userId, tweetId, commentId).Take(commentThumbs).Error
if err == nil {
switch {
case commentThumbs.IsThumbsDown == types.Yes:
thumbsUpCount, thumbsDownCount = 0, -1
case commentThumbs.IsThumbsDown == types.No && commentThumbs.IsThumbsUp == types.No:
thumbsUpCount, thumbsDownCount = 0, 1
default:
thumbsUpCount, thumbsDownCount = -1, 1
commentThumbs.IsThumbsUp = types.No
}
commentThumbs.IsThumbsDown = 1 - commentThumbs.IsThumbsDown
commentThumbs.ModifiedOn = time.Now().Unix()
} else {
commentThumbs = &dbr.TweetCommentThumbs{
UserID: userId,
TweetID: tweetId,
CommentID: commentId,
IsThumbsUp: types.No,
IsThumbsDown: types.Yes,
CommentType: 0,
Model: &dbr.Model{
CreatedOn: time.Now().Unix(),
},
}
thumbsUpCount, thumbsDownCount = 0, 1
}
// 更新thumbs状态
if err = s.db.Save(commentThumbs).Error; err != nil {
return err
}
// 更新thumbsUpCount
if err = s.updateCommentThumbsUpCount(&dbr.Comment{}, commentId, thumbsUpCount, thumbsDownCount); err != nil {
return err
}
db.Commit()
return nil
}
func (s *commentManageServant) ThumbsUpReply(userId int64, tweetId, commentId, replyId int64) error {
db := s.db.Begin()
defer db.Rollback()
var (
thumbsUpCount int32 = 0
thumbsDownCount int32 = 0
)
commentThumbs := &dbr.TweetCommentThumbs{}
// 检查thumbs状态
err := s.db.Where("user_id=? AND tweet_id=? AND comment_id=? AND reply_id=? AND comment_type=1", userId, tweetId, commentId, replyId).Take(commentThumbs).Error
if err == nil {
switch {
case commentThumbs.IsThumbsUp == types.Yes:
thumbsUpCount, thumbsDownCount = -1, 0
case commentThumbs.IsThumbsUp == types.No && commentThumbs.IsThumbsDown == types.No:
thumbsUpCount, thumbsDownCount = 1, 0
default:
thumbsUpCount, thumbsDownCount = 1, -1
commentThumbs.IsThumbsDown = types.No
}
commentThumbs.IsThumbsUp = 1 - commentThumbs.IsThumbsUp
commentThumbs.ModifiedOn = time.Now().Unix()
} else {
commentThumbs = &dbr.TweetCommentThumbs{
UserID: userId,
TweetID: tweetId,
CommentID: commentId,
ReplyID: replyId,
IsThumbsUp: types.Yes,
IsThumbsDown: types.No,
CommentType: 1,
Model: &dbr.Model{
CreatedOn: time.Now().Unix(),
},
}
thumbsUpCount, thumbsDownCount = 1, 0
}
// 更新thumbs状态
if err = s.db.Save(commentThumbs).Error; err != nil {
return err
}
// 更新thumbsUpCount
if err = s.updateCommentThumbsUpCount(&dbr.CommentReply{}, replyId, thumbsUpCount, thumbsDownCount); err != nil {
return err
}
db.Commit()
return nil
}
func (s *commentManageServant) ThumbsDownReply(userId int64, tweetId, commentId, replyId int64) error {
db := s.db.Begin()
defer db.Rollback()
var (
thumbsUpCount int32 = 0
thumbsDownCount int32 = 0
)
commentThumbs := &dbr.TweetCommentThumbs{}
// 检查thumbs状态
err := s.db.Where("user_id=? AND tweet_id=? AND comment_id=? AND reply_id=? AND comment_type=1", userId, tweetId, commentId, replyId).Take(commentThumbs).Error
if err == nil {
switch {
case commentThumbs.IsThumbsDown == types.Yes:
thumbsUpCount, thumbsDownCount = 0, -1
case commentThumbs.IsThumbsUp == types.No && commentThumbs.IsThumbsDown == types.No:
thumbsUpCount, thumbsDownCount = 0, 1
default:
thumbsUpCount, thumbsDownCount = -1, 1
commentThumbs.IsThumbsUp = types.No
}
commentThumbs.IsThumbsDown = 1 - commentThumbs.IsThumbsDown
commentThumbs.ModifiedOn = time.Now().Unix()
} else {
commentThumbs = &dbr.TweetCommentThumbs{
UserID: userId,
TweetID: tweetId,
CommentID: commentId,
ReplyID: replyId,
IsThumbsUp: types.No,
IsThumbsDown: types.Yes,
CommentType: 1,
Model: &dbr.Model{
CreatedOn: time.Now().Unix(),
},
}
thumbsUpCount, thumbsDownCount = 0, 1
}
// 更新thumbs状态
if err = s.db.Save(commentThumbs).Error; err != nil {
return err
}
// 更新thumbsUpCount
if err = s.updateCommentThumbsUpCount(&dbr.CommentReply{}, replyId, thumbsUpCount, thumbsDownCount); err != nil {
return err
}
db.Commit()
return nil
}
func (s *commentManageServant) updateCommentThumbsUpCount(obj any, id int64, thumbsUpCount, thumbsDownCount int32) error {
updateColumns := make(map[string]any, 2)
if thumbsUpCount == 1 {
updateColumns["thumbs_up_count"] = gorm.Expr("thumbs_up_count + 1")
} else if thumbsUpCount == -1 {
updateColumns["thumbs_up_count"] = gorm.Expr("thumbs_up_count - 1")
}
if thumbsDownCount == 1 {
updateColumns["thumbs_down_count"] = gorm.Expr("thumbs_down_count + 1")
} else if thumbsDownCount == -1 {
updateColumns["thumbs_down_count"] = gorm.Expr("thumbs_down_count - 1")
}
if len(updateColumns) > 0 {
updateColumns["modified_on"] = time.Now().Unix()
return s.db.Model(obj).Where("id=?", id).UpdateColumns(updateColumns).Error
}
return nil
}

@ -7,27 +7,33 @@ package dbr
import (
"time"
"github.com/rocboss/paopao-ce/pkg/types"
"gorm.io/gorm"
)
type Comment struct {
*Model
PostID int64 `json:"post_id"`
UserID int64 `json:"user_id"`
IP string `json:"ip"`
IPLoc string `json:"ip_loc"`
PostID int64 `json:"post_id"`
UserID int64 `json:"user_id"`
IP string `json:"ip"`
IPLoc string `json:"ip_loc"`
ThumbsUpCount int32 `json:"thumbs_up_count"`
ThumbsDownCount int32 `json:"-"`
}
type CommentFormated struct {
ID int64 `json:"id"`
PostID int64 `json:"post_id"`
UserID int64 `json:"user_id"`
User *UserFormated `json:"user"`
Contents []*CommentContent `json:"contents"`
Replies []*CommentReplyFormated `json:"replies"`
IPLoc string `json:"ip_loc"`
CreatedOn int64 `json:"created_on"`
ModifiedOn int64 `json:"modified_on"`
ID int64 `json:"id"`
PostID int64 `json:"post_id"`
UserID int64 `json:"user_id"`
User *UserFormated `json:"user"`
Contents []*CommentContent `json:"contents"`
Replies []*CommentReplyFormated `json:"replies"`
IPLoc string `json:"ip_loc"`
ThumbsUpCount int32 `json:"thumbs_up_count"`
IsThumbsUp int8 `json:"is_thumbs_up"`
IsThumbsDown int8 `json:"is_thumbs_down"`
CreatedOn int64 `json:"created_on"`
ModifiedOn int64 `json:"modified_on"`
}
func (c *Comment) Format() *CommentFormated {
@ -35,15 +41,18 @@ func (c *Comment) Format() *CommentFormated {
return &CommentFormated{}
}
return &CommentFormated{
ID: c.Model.ID,
PostID: c.PostID,
UserID: c.UserID,
User: &UserFormated{},
Contents: []*CommentContent{},
Replies: []*CommentReplyFormated{},
IPLoc: c.IPLoc,
CreatedOn: c.CreatedOn,
ModifiedOn: c.ModifiedOn,
ID: c.Model.ID,
PostID: c.PostID,
UserID: c.UserID,
User: &UserFormated{},
Contents: []*CommentContent{},
Replies: []*CommentReplyFormated{},
IPLoc: c.IPLoc,
ThumbsUpCount: c.ThumbsUpCount,
IsThumbsUp: types.No,
IsThumbsDown: types.No,
CreatedOn: c.CreatedOn,
ModifiedOn: c.ModifiedOn,
}
}

@ -7,30 +7,36 @@ package dbr
import (
"time"
"github.com/rocboss/paopao-ce/pkg/types"
"gorm.io/gorm"
)
type CommentReply struct {
*Model
CommentID int64 `json:"comment_id"`
UserID int64 `json:"user_id"`
AtUserID int64 `json:"at_user_id"`
Content string `json:"content"`
IP string `json:"ip"`
IPLoc string `json:"ip_loc"`
CommentID int64 `json:"comment_id"`
UserID int64 `json:"user_id"`
AtUserID int64 `json:"at_user_id"`
Content string `json:"content"`
IP string `json:"ip"`
IPLoc string `json:"ip_loc"`
ThumbsUpCount int32 `json:"thumbs_up_count"`
ThumbsDownCount int32 `json:"-"`
}
type CommentReplyFormated struct {
ID int64 `json:"id"`
CommentID int64 `json:"comment_id"`
UserID int64 `json:"user_id"`
User *UserFormated `json:"user"`
AtUserID int64 `json:"at_user_id"`
AtUser *UserFormated `json:"at_user"`
Content string `json:"content"`
IPLoc string `json:"ip_loc"`
CreatedOn int64 `json:"created_on"`
ModifiedOn int64 `json:"modified_on"`
ID int64 `json:"id"`
CommentID int64 `json:"comment_id"`
UserID int64 `json:"user_id"`
User *UserFormated `json:"user"`
AtUserID int64 `json:"at_user_id"`
AtUser *UserFormated `json:"at_user"`
Content string `json:"content"`
IPLoc string `json:"ip_loc"`
ThumbsUpCount int32 `json:"thumbs_up_count"`
IsThumbsUp int8 `json:"is_thumbs_up"`
IsThumbsDown int8 `json:"is_thumbs_down"`
CreatedOn int64 `json:"created_on"`
ModifiedOn int64 `json:"modified_on"`
}
func (c *CommentReply) Format() *CommentReplyFormated {
@ -39,16 +45,19 @@ func (c *CommentReply) Format() *CommentReplyFormated {
}
return &CommentReplyFormated{
ID: c.ID,
CommentID: c.CommentID,
UserID: c.UserID,
User: &UserFormated{},
AtUserID: c.AtUserID,
AtUser: &UserFormated{},
Content: c.Content,
IPLoc: c.IPLoc,
CreatedOn: c.CreatedOn,
ModifiedOn: c.ModifiedOn,
ID: c.ID,
CommentID: c.CommentID,
UserID: c.UserID,
User: &UserFormated{},
AtUserID: c.AtUserID,
AtUser: &UserFormated{},
Content: c.Content,
IPLoc: c.IPLoc,
ThumbsUpCount: c.ThumbsUpCount,
IsThumbsUp: types.No,
IsThumbsDown: types.No,
CreatedOn: c.CreatedOn,
ModifiedOn: c.ModifiedOn,
}
}

@ -0,0 +1,16 @@
// 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 dbr
type TweetCommentThumbs struct {
*Model
UserID int64 `json:"user_id"`
TweetID int64 `json:"tweet_id"`
CommentID int64 `json:"comment_id"`
ReplyID int64 `json:"reply_id"`
CommentType int8 `json:"comment_type"`
IsThumbsUp int8 `json:"is_thumbs_up"`
IsThumbsDown int8 `json:"is_thumbs_down"`
}

@ -7,6 +7,7 @@ package dbr
import (
"github.com/sirupsen/logrus"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const (
@ -73,7 +74,7 @@ func (c *Contact) List(db *gorm.DB, conditions ConditionsT, offset, limit int) (
}
}
db.Joins("User").Order("`User`.`nickname` ASC")
db.Joins("User").Order(clause.OrderByColumn{Column: clause.Column{Name: "nickname"}, Desc: false})
if err = db.Find(&contacts).Error; err != nil {
return nil, err
}

@ -26,6 +26,7 @@ type Post struct {
UserID int64 `json:"user_id"`
CommentCount int64 `json:"comment_count"`
CollectionCount int64 `json:"collection_count"`
ShareCount int64 `json:"share_count"`
UpvoteCount int64 `json:"upvote_count"`
Visibility PostVisibleT `json:"visibility"`
IsTop int `json:"is_top"`
@ -45,6 +46,7 @@ type PostFormated struct {
Contents []*PostContentFormated `json:"contents"`
CommentCount int64 `json:"comment_count"`
CollectionCount int64 `json:"collection_count"`
ShareCount int64 `json:"share_count"`
UpvoteCount int64 `json:"upvote_count"`
Visibility PostVisibleT `json:"visibility"`
IsTop int `json:"is_top"`
@ -71,6 +73,7 @@ func (p *Post) Format() *PostFormated {
Contents: []*PostContentFormated{},
CommentCount: p.CommentCount,
CollectionCount: p.CollectionCount,
ShareCount: p.ShareCount,
UpvoteCount: p.UpvoteCount,
Visibility: p.Visibility,
IsTop: p.IsTop,

@ -8,6 +8,7 @@ import (
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type PostCollection struct {
@ -31,7 +32,7 @@ func (p *PostCollection) Get(db *gorm.DB) (*PostCollection, error) {
db = db.Where(tn+"user_id = ?", p.UserID)
}
db = db.Joins("Post").Where("Post.visibility <> ?", PostVisitPrivate).Order("Post.id DESC")
db = db.Joins("Post").Where("visibility <> ? OR (visibility = ? AND ? = ?)", PostVisitPrivate, PostVisitPrivate, clause.Column{Table: "Post", Name: "user_id"}, p.UserID).Order(clause.OrderByColumn{Column: clause.Column{Table: "Post", Name: "id"}, Desc: true})
err := db.First(&star).Error
if err != nil {
return &star, err
@ -73,7 +74,7 @@ func (p *PostCollection) List(db *gorm.DB, conditions *ConditionsT, offset, limi
}
}
db = db.Joins("Post").Where("Post.visibility <> ?", PostVisitPrivate).Order("Post.id DESC")
db = db.Joins("Post").Where(`visibility <> ? OR (visibility = ? AND ? = ?)`, PostVisitPrivate, PostVisitPrivate, clause.Column{Table: "Post", Name: "user_id"}, p.UserID).Order(clause.OrderByColumn{Column: clause.Column{Table: "Post", Name: "id"}, Desc: true})
if err = db.Where(tn+"is_del = ?", 0).Find(&collections).Error; err != nil {
return nil, err
}
@ -97,7 +98,7 @@ func (p *PostCollection) Count(db *gorm.DB, conditions *ConditionsT) (int64, err
}
}
db = db.Joins("Post").Where("Post.visibility <> ?", PostVisitPrivate)
db = db.Joins("Post").Where(`visibility <> ? OR (visibility = ? AND ? = ?)`, PostVisitPrivate, PostVisitPrivate, clause.Column{Table: "Post", Name: "user_id"}, p.UserID)
if err := db.Model(p).Count(&count).Error; err != nil {
return 0, err
}

@ -8,6 +8,7 @@ import (
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type PostStar struct {
@ -31,7 +32,7 @@ func (p *PostStar) Get(db *gorm.DB) (*PostStar, error) {
db = db.Where(tn+"user_id = ?", p.UserID)
}
db = db.Joins("Post").Where("Post.visibility <> ?", PostVisitPrivate).Order("Post.id DESC")
db = db.Joins("Post").Where("visibility <> ? OR (visibility = ? AND ? = ?)", PostVisitPrivate, PostVisitPrivate, clause.Column{Table: "Post", Name: "user_id"}, p.UserID).Order(clause.OrderByColumn{Column: clause.Column{Table: "Post", Name: "id"}, Desc: true})
if err := db.First(&star).Error; err != nil {
return nil, err
}
@ -71,7 +72,7 @@ func (p *PostStar) List(db *gorm.DB, conditions *ConditionsT, offset, limit int)
}
}
db = db.Joins("Post").Where("Post.visibility <> ?", PostVisitPrivate).Order("Post.id DESC")
db = db.Joins("Post").Where("visibility <> ? OR (visibility = ? AND ? = ?)", PostVisitPrivate, PostVisitPrivate, clause.Column{Table: "Post", Name: "user_id"}, p.UserID).Order(clause.OrderByColumn{Column: clause.Column{Table: "Post", Name: "id"}, Desc: true})
if err = db.Find(&stars).Error; err != nil {
return nil, err
}
@ -94,7 +95,7 @@ func (p *PostStar) Count(db *gorm.DB, conditions *ConditionsT) (int64, error) {
}
}
db = db.Joins("Post").Where("Post.visibility <> ?", PostVisitPrivate)
db = db.Joins("Post").Where("visibility <> ? OR (visibility = ? AND ? = ?)", PostVisitPrivate, PostVisitPrivate, clause.Column{Table: "Post", Name: "user_id"}, p.UserID)
if err := db.Model(p).Count(&count).Error; err != nil {
return 0, err
}

@ -16,12 +16,27 @@ type Tag struct {
Tag string `json:"tag"`
QuoteNum int64 `json:"quote_num"`
}
type TopicUser struct {
*Model
UserID int64 `json:"user_id"`
TopicID int64 `json:"topic_id"`
AliasName string `json:"-"`
Remark string `json:"-"`
QuoteNum int64 `json:"quote_num"`
IsTop int8 `json:"is_top"`
ReserveA string `json:"-"`
ReserveB string `json:"-"`
}
type TagFormated struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
User *UserFormated `json:"user"`
Tag string `json:"tag"`
QuoteNum int64 `json:"quote_num"`
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
User *UserFormated `json:"user"`
Tag string `json:"tag"`
QuoteNum int64 `json:"quote_num"`
IsFollowing int8 `json:"is_following"`
IsTop int8 `json:"is_top"`
}
func (t *Tag) Format() *TagFormated {
@ -30,11 +45,13 @@ func (t *Tag) Format() *TagFormated {
}
return &TagFormated{
ID: t.ID,
UserID: t.UserID,
User: &UserFormated{},
Tag: t.Tag,
QuoteNum: t.QuoteNum,
ID: t.ID,
UserID: t.UserID,
User: &UserFormated{},
Tag: t.Tag,
QuoteNum: t.QuoteNum,
IsFollowing: 0,
IsTop: 0,
}
}

@ -49,6 +49,7 @@ func NewDataService() (core.DataService, core.VersionInfo) {
pvs := security.NewPhoneVerifyService()
ams := NewAuthorizationManageService()
ths := newTweetHelpService(db)
ums := newUserManageService(db)
// initialize core.IndexPostsService
if cfg.If("Friendship") {
@ -63,23 +64,30 @@ func NewDataService() (core.DataService, core.VersionInfo) {
}
// initialize core.CacheIndexService
if cfg.If("SimpleCacheIndex") {
// simpleCache use special post index service
ips = newSimpleIndexPostsService(db, ths)
cis, v = cache.NewSimpleCacheIndexService(ips)
} else if cfg.If("BigCacheIndex") {
// TODO: make cache index post in different scence like friendship/followship/lightship
cis, v = cache.NewBigCacheIndexService(ips, ams)
} else {
cfg.On(cfg.Actions{
"SimpleCacheIndex": func() {
// simpleCache use special post index service
ips = newSimpleIndexPostsService(db, ths)
cis, v = cache.NewSimpleCacheIndexService(ips)
},
"BigCacheIndex": func() {
// TODO: make cache index post in different scence like friendship/followship/lightship
cis, v = cache.NewBigCacheIndexService(ips, ams)
},
"RedisCacheIndex": func() {
cis, v = cache.NewRedisCacheIndexService(ips, ams)
},
}, func() {
// defualt no cache
cis, v = cache.NewNoneCacheIndexService(ips)
}
})
logrus.Infof("use %s as cache index service by version: %s", v.Name(), v.Version())
ds := &dataServant{
IndexPostsService: cis,
WalletService: newWalletService(db),
MessageService: newMessageService(db),
TopicService: newTopicService(db),
TopicService: newTopicService(db, ums),
TweetService: newTweetService(db),
TweetManageService: newTweetManageService(db, cis),
TweetHelpService: newTweetHelpService(db),

@ -5,6 +5,7 @@
package jinzhu
import (
"errors"
"strings"
"github.com/rocboss/paopao-ce/internal/core"
@ -17,12 +18,23 @@ var (
)
type topicServant struct {
db *gorm.DB
db *gorm.DB
ums core.UserManageService
tnTopicUser string
tnDotTopicUser string
}
func newTopicService(db *gorm.DB) core.TopicService {
type topicInfo struct {
TopicId int64
IsTop int8
}
func newTopicService(db *gorm.DB, ums core.UserManageService) core.TopicService {
return &topicServant{
db: db,
db: db,
ums: ums,
tnTopicUser: db.NamingStrategy.TableName("TopicUser"),
tnDotTopicUser: db.NamingStrategy.TableName("TopicUser") + ".",
}
}
@ -38,6 +50,110 @@ func (s *topicServant) GetTags(conditions *core.ConditionsT, offset, limit int)
return (&dbr.Tag{}).List(s.db, conditions, offset, limit)
}
func (s *topicServant) GetHotTags(userId int64, limit int, offset int) ([]*core.TagFormated, error) {
tags, err := (&dbr.Tag{}).List(s.db, &core.ConditionsT{
"ORDER": "quote_num DESC",
}, offset, limit)
if err != nil {
return nil, err
}
return s.tagsFormat(userId, nil, tags)
}
func (s *topicServant) GetNewestTags(userId int64, limit int, offset int) ([]*core.TagFormated, error) {
tags, err := (&dbr.Tag{}).List(s.db, &core.ConditionsT{
"ORDER": "id DESC",
}, offset, limit)
if err != nil {
return nil, err
}
return s.tagsFormat(userId, nil, tags)
}
func (s *topicServant) GetFollowTags(userId int64, limit int, offset int) ([]*core.TagFormated, 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
if err != nil {
return nil, err
}
userTopicsMap := make(map[int64]*topicInfo, len(userTopics))
topicIds := make([]int64, 0, len(userTopics))
topicIdsMap := make(map[int64]int, len(userTopics))
for idx, info := range userTopics {
userTopicsMap[info.TopicId] = info
topicIds = append(topicIds, info.TopicId)
topicIdsMap[info.TopicId] = idx
}
var tags []*core.Tag
err = s.db.Model(&dbr.Tag{}).Where("quote_num > 0 and id in ?", topicIds).Order("quote_num DESC").Find(&tags).Error
if err != nil {
return nil, err
}
formtedTags, err := s.tagsFormat(-1, userTopicsMap, tags)
if err != nil {
return nil, err
}
// 置顶排序后处理
// TODO: 垃圾办法最好是topic_user join tag 一次查询但是gorm的join真他喵的别扭F*K
res := make([]*core.TagFormated, len(topicIds), len(topicIds))
for _, tag := range formtedTags {
res[topicIdsMap[tag.ID]] = tag
}
return res, nil
}
func (s *topicServant) tagsFormat(userId int64, userTopicsMap map[int64]*topicInfo, tags []*core.Tag) ([]*core.TagFormated, error) {
// 获取创建者User IDs
userIds := []int64{}
tagIds := []int64{}
for _, tag := range tags {
userIds = append(userIds, tag.UserID)
tagIds = append(tagIds, tag.ID)
}
users, err := s.ums.GetUsersByIDs(userIds)
if err != nil {
return nil, err
}
tagsFormated := []*core.TagFormated{}
for _, tag := range tags {
tagFormated := tag.Format()
for _, user := range users {
if user.ID == tagFormated.UserID {
tagFormated.User = user.Format()
}
}
tagsFormated = append(tagsFormated, tagFormated)
}
// 填充话题follow信息
if userId > -1 && len(userTopicsMap) <= 0 {
userTopics := []*topicInfo{}
err = s.db.Model(&dbr.TopicUser{}).Where("is_del=0 and user_id=? and topic_id in ?", userId, tagIds).Find(&userTopics).Error
if err != nil {
return nil, err
}
userTopicsMap = make(map[int64]*topicInfo, len(userTopics))
for _, info := range userTopics {
userTopicsMap[info.TopicId] = info
}
}
if len(userTopicsMap) > 0 {
for _, tag := range tagsFormated {
if info, exist := userTopicsMap[tag.ID]; exist {
tag.IsFollowing, tag.IsTop = 1, info.IsTop
}
}
}
return tagsFormated, nil
}
func (s *topicServant) GetTagsByKeyword(keyword string) ([]*core.Tag, error) {
tag := &dbr.Tag{}
@ -53,3 +169,39 @@ func (s *topicServant) GetTagsByKeyword(keyword string) ([]*core.Tag, error) {
}, 0, 6)
}
}
func (s *topicServant) FollowTopic(userId int64, topicId int64) (err error) {
return s.db.Create(&dbr.TopicUser{
UserID: userId,
TopicID: topicId,
IsTop: 0,
}).Error
}
func (s *topicServant) UnfollowTopic(userId int64, topicId int64) error {
return s.db.Exec("DELETE FROM "+s.tnTopicUser+" WHERE user_id=? AND topic_id=?", userId, topicId).Error
}
func (s *topicServant) StickTopic(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_top", gorm.Expr("1-is_top")).Error
if err != nil {
return
}
status = -1
err = db.Model(m).Where("user_id=? and topic_id=?", userId, topicId).Select("is_top").Scan(&status).Error
if err != nil {
return
}
if status < 0 {
return -1, errors.New("topic not exist")
}
db.Commit()
return
}

@ -293,7 +293,8 @@ func (s *tweetManageServant) VisiblePost(post *core.Post, visibility core.PostVi
tags := strings.Split(post.Tags, ",")
for _, t := range tags {
tag := &dbr.Tag{
Tag: t,
UserID: post.UserID,
Tag: t,
}
// TODO: 暂时宽松不处理错误,这里或许可以有优化,后续完善
if oldVisibility == dbr.PostVisitPrivate {

@ -57,7 +57,7 @@ func NewZincTweetSearchService(ams core.AuthorizationManageService) (core.TweetS
ams: ams,
},
indexName: s.Index,
client: zinc.NewClient(s),
client: zinc.NewClient(s.Endpoint(), s.User, s.Password),
publicFilter: fmt.Sprintf("visibility:%d", core.PostVisitPublic),
privateFilter: fmt.Sprintf("visibility:%d AND user_id:%%d", core.PostVisitPrivate),
friendFilter: fmt.Sprintf("visibility:%d", core.PostVisitFriend),

@ -1,11 +1,11 @@
package security
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/cockroachdb/errors"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/pkg/json"

@ -5,7 +5,6 @@
package storage
import (
"errors"
"fmt"
"io"
"os"
@ -14,6 +13,7 @@ import (
"time"
"github.com/Masterminds/semver/v3"
"github.com/cockroachdb/errors"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/sirupsen/logrus"
)

@ -6,15 +6,9 @@ package internal
import (
"github.com/rocboss/paopao-ce/internal/migration"
"github.com/rocboss/paopao-ce/internal/servants/web/broker"
"github.com/rocboss/paopao-ce/internal/servants/web/routers/api"
)
func Initialize() {
func Initial() {
// migrate database if needed
migration.Run()
// initialize service
broker.Initialize()
api.Initialize()
}

@ -9,6 +9,25 @@ import (
"github.com/rocboss/paopao-ce/internal/servants/base"
)
const (
TagTypeHot TagType = "hot"
TagTypeNew TagType = "new"
TagTypeFollow TagType = "follow"
TagTypeHotExtral TagType = "hot_extral"
)
type TweetCommentsReq struct {
SimpleInfo `form:"-" binding:"-"`
TweetId int64 `form:"id" binding:"required"`
SortStrategy string `form:"sort_strategy"`
Page int `form:"-" binding:"-"`
PageSize int `form:"-" binding:"-"`
}
type TweetCommentsResp base.PageResp
type TagType string
type TimelineReq struct {
BaseInfo `form:"-" binding:"-"`
Query string `form:"query"`
@ -44,6 +63,24 @@ type GetUserProfileResp struct {
IsFriend bool `json:"is_friend"`
}
type TopicListReq struct {
SimpleInfo `form:"-" binding:"-"`
Type TagType `json:"type" form:"type" binding:"required"`
Num int `json:"num" form:"num" binding:"required"`
ExtralNum int `json:"extral_num" form:"extral_num"`
}
// TopicListResp 主题返回值
// TODO: 优化内容定义
type TopicListResp struct {
Topics []*core.TagFormated `json:"topics"`
ExtralTopics []*core.TagFormated `json:"extral_topics,omitempty"`
}
func (r *GetUserTweetsReq) SetPageInfo(page int, pageSize int) {
r.Page, r.PageSize = page, pageSize
}
func (r *TweetCommentsReq) SetPageInfo(page int, pageSize int) {
r.Page, r.PageSize = page, pageSize
}

@ -12,6 +12,19 @@ import (
"github.com/rocboss/paopao-ce/internal/core"
)
type TweetCommentThumbsReq struct {
SimpleInfo `json:"-" binding:"-"`
TweetId int64 `json:"tweet_id" binding:"required"`
CommentId int64 `json:"comment_id" binding:"required"`
}
type TweetReplyThumbsReq struct {
SimpleInfo `json:"-" binding:"-"`
TweetId int64 `json:"tweet_id" binding:"required"`
CommentId int64 `json:"comment_id" binding:"required"`
ReplyId int64 `json:"reply_id" binding:"required"`
}
type PostContentItem struct {
Content string `json:"content" binding:"required"`
Type core.PostContentT `json:"type" binding:"required"`
@ -146,6 +159,25 @@ type DownloadAttachmentResp struct {
SignedURL string `json:"signed_url"`
}
type StickTopicReq struct {
SimpleInfo `json:"-" binding:"-"`
TopicId int64 `json:"topic_id" binding:"required"`
}
type StickTopicResp struct {
StickStatus int8 `json:"top_status"`
}
type FollowTopicReq struct {
SimpleInfo `json:"-" binding:"-"`
TopicId int64 `json:"topic_id" binding:"required"`
}
type UnfollowTopicReq struct {
SimpleInfo `json:"-" binding:"-"`
TopicId int64 `json:"topic_id" binding:"required"`
}
// Check 检查PostContentItem属性
func (p *PostContentItem) Check(acs core.AttachmentCheckService) error {
// 检查附件是否是本站资源

@ -6,42 +6,15 @@ package web
import (
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/servants/base"
"github.com/rocboss/paopao-ce/pkg/debug"
"github.com/rocboss/paopao-ce/pkg/version"
)
const (
TagTypeHot TagType = "hot"
TagTypeNew TagType = "new"
)
type TagType string
type TweetDetailReq struct {
TweetId int64 `form:"id"`
}
type TweetDetailResp core.PostFormated
type TweetCommentsReq struct {
TweetId int64 `form:"id"`
Page int `form:"-"`
PageSize int `form:"-"`
}
type TweetCommentsResp base.PageResp
type TopicListReq struct {
Type TagType `json:"type" form:"type" binding:"required"`
Num int `json:"num" form:"num" binding:"required"`
}
// TopicListResp 主题返回值
// TODO: 优化内容定义
type TopicListResp struct {
Topics []*core.TagFormated `json:"topics"`
}
type GetCaptchaResp struct {
Id string `json:"id"`
Content string `json:"b64s"`
@ -54,7 +27,7 @@ type SendCaptchaReq struct {
}
type VersionResp struct {
BuildInfo *debug.BuildInfo `json:"build_info"`
BuildInfo *version.BuildInfo `json:"build_info"`
}
type LoginReq struct {

@ -37,7 +37,7 @@ func newUserSrv() api.User {
func newUserBinding() api.UserBinding {
return &userBinding{
UnimplementedUserBinding: &api.UnimplementedUserBinding{
BindAny: base.BindAny,
BindAny: base.NewBindAnyFn(),
},
}
}

@ -9,12 +9,16 @@ import (
"fmt"
"math"
"net/http"
"time"
"github.com/alimy/mir/v3"
"github.com/cockroachdb/errors"
"github.com/getsentry/sentry-go"
sentrygin "github.com/getsentry/sentry-go/gin"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/dao"
"github.com/rocboss/paopao-ce/internal/dao/cache"
"github.com/rocboss/paopao-ce/pkg/app"
"github.com/rocboss/paopao-ce/pkg/types"
"github.com/rocboss/paopao-ce/pkg/xerror"
@ -24,9 +28,9 @@ import (
type BaseServant types.Empty
type DaoServant struct {
Redis *redis.Client
Ds core.DataService
Ts core.TweetSearchService
Redis core.RedisCache
}
type BaseBinding types.Empty
@ -39,6 +43,10 @@ type JsonResp struct {
Data any `json:"data,omitempty"`
}
type SentryHubSetter interface {
SetSentryHub(hub *sentry.Hub)
}
type UserSetter interface {
SetUser(*core.User)
}
@ -75,7 +83,7 @@ func UserNameFrom(c *gin.Context) (string, bool) {
return "", false
}
func BindAny(c *gin.Context, obj any) mir.Error {
func bindAny(c *gin.Context, obj any) mir.Error {
var errs xerror.ValidErrors
err := c.ShouldBind(obj)
if err != nil {
@ -99,6 +107,40 @@ func BindAny(c *gin.Context, obj any) mir.Error {
return nil
}
func bindAnySentry(c *gin.Context, obj any) mir.Error {
hub := sentrygin.GetHubFromContext(c)
var errs xerror.ValidErrors
err := c.ShouldBind(obj)
if err != nil {
xerr := mir.NewError(xerror.InvalidParams.StatusCode(), xerror.InvalidParams.WithDetails(errs.Error()))
if hub != nil {
hub.CaptureException(errors.Wrap(xerr, "bind object"))
}
return xerr
}
// setup sentry hub if needed
if setter, ok := obj.(SentryHubSetter); ok && hub != nil {
setter.SetSentryHub(hub)
}
// setup *core.User if needed
if setter, ok := obj.(UserSetter); ok {
user, _ := UserFrom(c)
setter.SetUser(user)
}
// setup UserId if needed
if setter, ok := obj.(UserIdSetter); ok {
uid, _ := UserIdFrom(c)
setter.SetUserId(uid)
}
// setup PageInfo if needed
if setter, ok := obj.(PageInfoSetter); ok {
page, pageSize := app.GetPageInfo(c)
setter.SetPageInfo(page, pageSize)
}
return nil
}
func RenderAny(c *gin.Context, data any, err mir.Error) {
if err == nil {
c.JSON(http.StatusOK, &JsonResp{
@ -141,8 +183,8 @@ func (s *DaoServant) GetTweetBy(id int64) (*core.PostFormated, error) {
}
func (s *DaoServant) PushPostsToSearch(c context.Context) {
if ok, err := s.Redis.SetNX(c, "JOB_PUSH_TO_SEARCH", 1, time.Hour).Result(); ok {
defer s.Redis.Del(c, "JOB_PUSH_TO_SEARCH")
if err := s.Redis.SetPushToSearchJob(c); err == nil {
defer s.Redis.DelPushToSearchJob(c)
splitNum := 1000
totalRows, _ := s.Ds.GetPostCount(&core.ConditionsT{
@ -170,7 +212,7 @@ func (s *DaoServant) PushPostsToSearch(c context.Context) {
}
}
} else {
logrus.Warnf("set cache by key JOB_PUSH_TO_SEARCH failed: %s", err)
logrus.Errorf("redis: set JOB_PUSH_TO_SEARCH error: %s", err)
}
}
@ -210,3 +252,18 @@ func (s *DaoServant) GetTweetList(conditions *core.ConditionsT, offset, limit in
postFormated, err := s.Ds.MergePosts(posts)
return posts, postFormated, err
}
func NewDaoServant() *DaoServant {
return &DaoServant{
Redis: cache.NewRedisCache(),
Ds: dao.DataService(),
Ts: dao.TweetSearchService(),
}
}
func NewBindAnyFn() func(c *gin.Context, obj any) mir.Error {
if conf.UseSentryGin() {
return bindAnySentry
}
return bindAny
}

@ -37,7 +37,7 @@ func newUserSrv() api.User {
func newUserBinding() api.UserBinding {
return &userBinding{
UnimplementedUserBinding: &api.UnimplementedUserBinding{
BindAny: base.BindAny,
BindAny: base.NewBindAnyFn(),
},
}
}

@ -8,7 +8,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/pkg/app"
"github.com/rocboss/paopao-ce/pkg/errcode"
)
func Admin() gin.HandlerFunc {
@ -23,7 +22,7 @@ func Admin() gin.HandlerFunc {
}
response := app.NewResponse(c)
response.ToErrorResponse(errcode.NoAdminPermission)
response.ToErrorResponse(_errNoAdminPermission)
c.Abort()
}
}

@ -11,7 +11,7 @@ import (
"github.com/golang-jwt/jwt/v4"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/pkg/app"
"github.com/rocboss/paopao-ce/pkg/errcode"
"github.com/rocboss/paopao-ce/pkg/xerror"
)
func JWT() gin.HandlerFunc {
@ -19,7 +19,7 @@ func JWT() gin.HandlerFunc {
return func(c *gin.Context) {
var (
token string
ecode = errcode.Success
ecode = xerror.Success
)
if s, exist := c.GetQuery("token"); exist {
token = s
@ -29,7 +29,7 @@ func JWT() gin.HandlerFunc {
// 验证前端传过来的token格式不为空开头为Bearer
if token == "" || !strings.HasPrefix(token, "Bearer ") {
response := app.NewResponse(c)
response.ToErrorResponse(errcode.UnauthorizedTokenError)
response.ToErrorResponse(xerror.UnauthorizedTokenError)
c.Abort()
return
}
@ -38,15 +38,15 @@ func JWT() gin.HandlerFunc {
token = token[7:]
}
if token == "" {
ecode = errcode.InvalidParams
ecode = xerror.InvalidParams
} else {
claims, err := app.ParseToken(token)
if err != nil {
switch err.(*jwt.ValidationError).Errors {
case jwt.ValidationErrorExpired:
ecode = errcode.UnauthorizedTokenTimeout
ecode = xerror.UnauthorizedTokenTimeout
default:
ecode = errcode.UnauthorizedTokenError
ecode = xerror.UnauthorizedTokenError
}
} else {
c.Set("UID", claims.UID)
@ -57,17 +57,17 @@ func JWT() gin.HandlerFunc {
if err == nil {
c.Set("USER", user)
} else {
ecode = errcode.UnauthorizedAuthNotExist
ecode = xerror.UnauthorizedAuthNotExist
}
// 强制下线机制
if (conf.JWTSetting.Issuer + ":" + user.Salt) != claims.Issuer {
ecode = errcode.UnauthorizedTokenTimeout
ecode = xerror.UnauthorizedTokenTimeout
}
}
}
if ecode != errcode.Success {
if ecode != xerror.Success {
response := app.NewResponse(c)
response.ToErrorResponse(ecode)
c.Abort()

@ -9,7 +9,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/pkg/app"
"github.com/rocboss/paopao-ce/pkg/errcode"
)
func Priv() gin.HandlerFunc {
@ -20,7 +19,7 @@ func Priv() gin.HandlerFunc {
if user.Status == core.UserStatusNormal {
if user.Phone == "" {
response := app.NewResponse(c)
response.ToErrorResponse(errcode.AccountNoPhoneBind)
response.ToErrorResponse(_errAccountNoPhoneBind)
c.Abort()
return
}
@ -30,7 +29,7 @@ func Priv() gin.HandlerFunc {
}
}
response := app.NewResponse(c)
response.ToErrorResponse(errcode.UserHasBeenBanned)
response.ToErrorResponse(_errUserHasBeenBanned)
c.Abort()
}
} else {
@ -42,7 +41,7 @@ func Priv() gin.HandlerFunc {
}
}
response := app.NewResponse(c)
response.ToErrorResponse(errcode.UserHasBeenBanned)
response.ToErrorResponse(_errUserHasBeenBanned)
c.Abort()
}
}

@ -0,0 +1,16 @@
// 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 chain
import (
"github.com/rocboss/paopao-ce/pkg/xerror"
)
// nolint
var (
_errUserHasBeenBanned = xerror.NewError(20006, "该账户已被封停")
_errAccountNoPhoneBind = xerror.NewError(20013, "拒绝操作: 账户未绑定手机号")
_errNoAdminPermission = xerror.NewError(20022, "无管理权限")
)

@ -37,7 +37,7 @@ func newUserSrv() api.User {
func newUserBinding() api.UserBinding {
return &userBinding{
UnimplementedUserBinding: &api.UnimplementedUserBinding{
BindAny: base.BindAny,
BindAny: base.NewBindAnyFn(),
},
}
}

@ -37,7 +37,7 @@ func newUserSrv() api.User {
func newUserBinding() api.UserBinding {
return &userBinding{
UnimplementedUserBinding: &api.UnimplementedUserBinding{
BindAny: base.BindAny,
BindAny: base.NewBindAnyFn(),
},
}
}

@ -16,7 +16,7 @@ import (
// RegisterWebStatick register web static assets route
func RegisterWebStatick(e *gin.Engine) {
routeWebStatic(e, "/", "/index.html", "/favicon.ico", "/assets/*filepath")
routeWebStatic(e, "/", "/index.html", "/favicon.ico", "/logo.png", "/sw.js", "/manifest.json", "/assets/*filepath")
}
func routeWebStatic(e *gin.Engine, paths ...string) {

@ -59,7 +59,7 @@ func newAdminSrv(s *base.DaoServant) api.Admin {
func newAdminBinding() api.AdminBinding {
return &adminBinding{
UnimplementedAdminBinding: &api.UnimplementedAdminBinding{
BindAny: base.BindAny,
BindAny: base.NewBindAnyFn(),
},
}
}

@ -6,7 +6,6 @@ package web
import (
"fmt"
"time"
"github.com/alimy/mir/v3"
"github.com/gin-gonic/gin"
@ -80,7 +79,7 @@ func (b *alipayPubBinding) BindAlipayNotify(c *gin.Context) (*web.AlipayNotifyRe
func (s *alipayPubSrv) AlipayNotify(req *web.AlipayNotifyReq) mir.Error {
if req.TradeStatus == alipay.TradeStatusSuccess {
if ok, _ := s.Redis.SetNX(req.Ctx, "PaoPaoRecharge:"+req.TradeNo, 1, time.Second*5).Result(); ok {
if err := s.Redis.SetRechargeStatus(req.Ctx, req.TradeNo); err == nil {
recharge, err := s.Ds.GetRechargeByID(req.ID)
if err != nil {
logrus.Errorf("GetRechargeByID id:%d err: %s", req.ID, err)
@ -89,7 +88,7 @@ func (s *alipayPubSrv) AlipayNotify(req *web.AlipayNotifyReq) mir.Error {
if recharge.TradeStatus != "TRADE_SUCCESS" {
// 标记为已付款
err := s.Ds.HandleRechargeSuccess(recharge, req.TradeNo)
defer s.Redis.Del(req.Ctx, "PaoPaoRecharge:"+req.TradeNo)
defer s.Redis.DelRechargeStatus(req.Ctx, req.TradeNo)
if err != nil {
logrus.Errorf("HandleRechargeSuccess id:%d err: %s", req.ID, err)
return _errRechargeNotifyError
@ -201,7 +200,7 @@ func newAlipayPubSrv(s *base.DaoServant) api.AlipayPub {
func newAlipayPubBinding(alipayClient *alipay.Client) api.AlipayPubBinding {
return &alipayPubBinding{
UnimplementedAlipayPubBinding: &api.UnimplementedAlipayPubBinding{
BindAny: base.BindAny,
BindAny: base.NewBindAnyFn(),
},
alipayClient: alipayClient,
}
@ -225,7 +224,7 @@ func newAlipayPrivSrv(s *base.DaoServant, client *alipay.Client) api.AlipayPriv
func newAlipayPrivBinding() api.AlipayPrivBinding {
return &alipayPrivBinding{
UnimplementedAlipayPrivBinding: &api.UnimplementedAlipayPrivBinding{
BindAny: base.BindAny,
BindAny: base.NewBindAnyFn(),
},
}
}

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

Loading…
Cancel
Save