Merge branch 'dev' into dev

pull/128/head
faddddeout 2 years ago committed by GitHub
commit 91f56c26c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -25,8 +25,8 @@ WORKDIR /paopao-ce
COPY . .
COPY --from=frontend /web/dist ./web/dist
ENV GOPROXY=https://goproxy.cn
RUN [ $EMBED_UI != yes ] || make build TAGS='embed'
RUN [ $EMBED_UI = yes ] || make build
RUN [ $EMBED_UI != yes ] || make build TAGS='embed jsoniter'
RUN [ $EMBED_UI = yes ] || make build TAGS='jsoniter'
FROM alpine:3.16
ARG API_HOST

@ -8,12 +8,15 @@ 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 cat version)
BUILD_VERSION := $(shell git describe --tags | cut -f 1 -d "-")
BUILD_DATE := $(shell date +'%Y-%m-%d %H:%M:%S')
SHA_SHORT := $(shell git rev-parse --short HEAD)
TAGS = ""
LDFLAGS = -X "main.version=${BUILD_VERSION}" -X "main.buildDate=${BUILD_DATE}" -X "main.commitID=${SHA_SHORT}" -w -s
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
all: fmt build

@ -70,11 +70,12 @@ PaoPao主要由以下优秀的开源项目/工具构建
### 安装说明
***宝塔安装***
我们为宝塔用户提供了超详细安装教程 [点此查看](https://www.rocs.me/archives/paopao_bt_install.html)
### 方式一. 宝塔安装
***普通安装***
我们为宝塔用户提供了超详细安装教程(v0.1.0版本),仅供参考,[点此查看](https://www.rocs.me/archives/paopao_bt_install.html)
### 方式二. 手动安装
克隆代码库
@ -111,23 +112,6 @@ PaoPao主要由以下优秀的开源项目/工具构建
```
提示: 如果需要内嵌web前端ui请先构建web前端(建议设置web/.env为VITE_HOST="")。
5. docker构建
```sh
# 默认参数构建, 默认内嵌web ui并设置api host为空
%> docker build -t your/paopao-ce:tag .
# 内嵌web ui并且自定义API host参数
%> docker build -t your/paopao-ce:tag --build-arg API_HOST=http://paopao.info .
# 内嵌web ui并且使用本地web/.env中的API host
%> docker build -t your/paopao-ce:tag --build-arg USE_API_HOST=no .
# 内嵌web ui并且使用本地编译的web/dist构建
%> docker build -t your/paopao-ce:tag --build-arg USE_DIST=yes .
# 只编译api server
%> docker build -t your/paopao-ce:tag --build-arg EMBED_UI=no .
```
#### 前端
@ -147,20 +131,6 @@ PaoPao主要由以下优秀的开源项目/工具构建
build完成后可以在dist目录获取编译产出配置nginx指向至该目录即可
3. 使用Docker构建
```sh
%> cd web
# 默认参数构建
%> docker build -t your/paopao-ce:web .
# 自定义API host 参数构建
%> docker build -t your/paopao-ce:web --build-arg API_HOST=http://paopao.info .
# 使用本地编译的dist构建
%> docker build -t your/paopao-ce:web --build-arg USE_DIST=yes .
```
#### 桌面端
1. 进入前端目录 `web`,编辑 `.env` 文件中后端服务地址,下载依赖包
@ -184,13 +154,52 @@ PaoPao主要由以下优秀的开源项目/工具构建
桌面端是使用[Rust](https://www.rust-lang.org/) + [tauri](https://github.com/tauri-apps/tauri)编写
需要安装tauri的依赖具体参考[https://tauri.studio/v1/guides/getting-started/prerequisites](https://tauri.studio/v1/guides/getting-started/prerequisites).
### docker-compose 运行
### 方式三. 使用Docker构建、运行
* 后端:
```sh
# 默认参数构建, 默认内嵌web ui并设置api host为空
docker build -t your/paopao-ce:tag .
# 内嵌web ui并且自定义API host参数
docker build -t your/paopao-ce:tag --build-arg API_HOST=http://paopao.info .
# 内嵌web ui并且使用本地web/.env中的API host
docker build -t your/paopao-ce:tag --build-arg USE_API_HOST=no .
# 内嵌web ui并且使用本地编译的web/dist构建
docker build -t your/paopao-ce:tag --build-arg USE_DIST=yes .
# 只编译api server
docker build -t your/paopao-ce:tag --build-arg EMBED_UI=no .
# 运行
docker run -p 8008:8008 -v ${PWD}/config.yaml.sample:/app/paopao-ce/config.yaml your/paopao-ce:tag
```
* 前端:
```sh
cd web
# 默认参数构建
docker build -t your/paopao-ce:web .
# 自定义API host 参数构建
docker build -t your/paopao-ce:web --build-arg API_HOST=http://paopao.info .
# 使用本地编译的dist构建
docker build -t your/paopao-ce:web --build-arg USE_DIST=yes .
```
### 方式四. 使用 docker-compose 运行
```sh
%> git clone https://github.com/rocboss/paopao-ce.git
%> docker compose up --build
# visit http://localhost:8008
git clone https://github.com/rocboss/paopao-ce.git
docker compose up --build
# visit paopao-ce(http://127.0.0.1:8008) and phpMysqlAdmin(http://127.0.0.1:8080)
```
默认是使用config.yaml.sample的配置如果需要自定义配置请拷贝默认配置文件(比如config.yaml)修改后再同步配置到docker-compose.yaml如下
```
# file: docker-compose.yaml
...
@ -212,6 +221,91 @@ PaoPao主要由以下优秀的开源项目/工具构建
....
```
> 注意:默认提供的 docker-compose.yaml 仅用于搭建本机开发调试环境paopao-ce/phpMysqlAdmin 默认只能本机访问,如果需要产品部署供外网访问,请自行修改配置参数或使用其他方式部署。
### API 文档
构建时将 `docs` 添加到TAGS中:
```sh
make run TAGS='docs'
# visit http://127.0.0.1:8008/docs
```
### 配置说明
`config.yaml.sample` 是一份完整的配置文件模版paopao-ce启动时会读取`./configs/config.yaml`、`./config.yaml`任意一份配置文件(优先读取最先找到的文件)。
```sh
cp config.yaml.sample config.yaml
vim config.yaml # 修改参数
paopao-ce
```
配置文件中的 `Features` 小节是声明paopao-ce运行时开启哪些功能项:
```yaml
...
Features:
Default: ["Base", "MySQL", "Option", "LocalOSS", "LoggerFile"]
Develop: ["Base", "MySQL", "Option", "Sms", "AliOSS", "LoggerZinc"]
Demo: ["Base", "MySQL", "Option", "Sms", "MinIO", "LoggerZinc"]
Slim: ["Base", "Sqlite3", "LocalOSS", "LoggerFile"]
Base: ["Zinc", "Redis", "Alipay",]
Option: ["SimpleCacheIndex"]
Sms: "SmsJuhe"
...
```
如上:
Default/Develop/Demo/Slim 是不同 功能集套件(Features Suite) Base/Option 是子功能套件, Sms是关于短信验证码功能的参数选项。
这里 `Default`套件 代表的意思是: 使用`Base/Option` 中的功能,外加 `MySQL/LocalOSS/LoggerFile`功能,也就是说开启了`Zinc/Redis/Alipay/SimpleCacheIndex/MySQL/LocalOSS/LoggerFile` 7项功能
`Develop`套件依例类推。
使用Feautures:
```sh
release/paopao-ce --help
Usage of release/paopao-ce:
-features value
use special features
-no-default-features
whether use default features
# 默认使用 Default 功能套件
release/paopao-ce
# 不包含 default 中的功能集,仅仅使用 develop 中声明的功能集
release/paopao-ce --no-default-features --features develop
# 使用 default 中的功能集,外加 sms 功能
release/paopao-ce --features sms
# 手动指定需要开启的功能集
release/paopao-ce --no-default-features --features sqlite3,localoss,loggerfile,redis
```
目前支持的功能集合:
* 数据库: MySQL/Sqlite3/PostgreSQL
* 对象存储: AliOSS/MinIO/LocalOSS
`AliOSS` 阿里云对象存储服务;
`MinIO` [MinIO](https://github.com/minio/minio)对象存储服务;
`LocalOSS` 提供使用本地目录文件作为对象存储的功能,仅用于开发调试环境;
* 缓存: Redis/SimpleCacheIndex/BigCacheIndex
`SimpleCacheIndex` 提供简单的 广场推文列表 的缓存功能;
`BigCacheIndex` 使用[BigCache](https://github.com/allegro/bigcache)缓存 广场推文列表,缓存每个用户每一页,简单做到千人千面;
* 搜索: Zinc/Meili
`Zinc` 基于[Zinc](https://github.com/zinclabs/zinc)搜索引擎提供推文搜索服务(目前状态: 稳定,推荐使用)
`Meili` 基于[Meilisearch](https://github.com/meilisearch/meilisearch)搜索引擎提供推文搜索服务(目前状态: 内测阶段);
* 日志: LoggerFile/LoggerZinc/LoggerMeili
`LoggerFile` 使用文件写日志(目前状态: 稳定);
`LoggerZinc` 使用[Zinc](https://github.com/zinclabs/zinc)写日志(目前状态: 稳定,推荐使用);
`LoggerMeili` 使用[Meilisearch](https://github.com/meilisearch/meilisearch)写日志(目前状态: 内测阶段);
* 支付: Alipay
* 短信验证码: SmsJuhe(需要开启sms)
`Sms`功能如果没有开启,任意短信验证码都可以绑定手机;
### 其他说明

@ -12,10 +12,11 @@ Server: # 服务设置
ReadTimeout: 60
WriteTimeout: 60
Features:
Default: ["Base", "MySQL", "Option", "MinIO", "LoggerFile"]
Develop: ["Base", "MySQL", "Option", "Sms", "AliOSS", "LoggerZinc"]
Default: ["Base", "MySQL", "Option", "Zinc", "LocalOSS", "LoggerFile"]
Develop: ["Base", "MySQL", "BigCacheIndex", "Meili", "Sms", "AliOSS", "LoggerMeili"]
Demo: ["Base", "MySQL", "Option", "Zinc", "Sms", "MinIO", "LoggerZinc"]
Slim: ["Base", "Sqlite3", "LocalOSS", "LoggerFile"]
Base: ["Zinc", "Redis", "Alipay",]
Base: ["Redis", "Alipay"]
Option: ["SimpleCacheIndex"]
Sms: "SmsJuhe"
SmsJuhe:
@ -25,31 +26,53 @@ SmsJuhe:
Alipay:
AppID:
PrivateKey:
CacheIndex:
MaxUpdateQPS: 100 # 最大添加/删除/更新Post的QPS, 设置范围[10, 10000], 默认100
SimpleCacheIndex: # 缓存泡泡广场消息流
MaxIndexSize: 200 # 最大缓存条数
CheckTickDuration: 60 # 循环自检查每多少秒一次
ExpireTickDuration: 300 # 每多少秒后强制过期缓存, 设置为0禁止强制使缓存过期
ActionQPS: 100 # 添加/删除/更新Post的QPS, 默认100范围设置[10, 10000]
BigCacheIndex: # 使用BigCache缓存泡泡广场消息流
MaxIndexPage: 1024 # 最大缓存页数必须是2^n, 代表最大同时缓存多少页数据
Verbose: False # 是否打印cache操作的log
ExpireInSecond: 300 # 多少秒(>0)后强制过期缓存
Logger: # 日志通用配置
Level: debug # 日志级别 panic|fatal|error|warn|info|debug|trace
LoggerFile: # 使用File写日志
SavePath: data/paopao-ce/logs
FileName: app
FileExt: .log
Level: info
LoggerZinc: # 使用Zinc写日志
Host: http://zinc:4080/es/_bulk
Host: zinc:4080
Index: paopao-log
User: admin
Password: admin
Level: info
Secure: False
LoggerMeili: # 使用Meili写日志
Host: meili:7700
Index: paopao-log
ApiKey: paopao-meilisearch
Secure: False
MinWorker: 5 # 最小后台工作者, 设置范围[5, 100], 默认5
MaxLogBuffer: 100 # 最大log缓存条数, 设置范围[10, 10000], 默认100
JWT: # 鉴权加密
Secret: 18a6413dc4fe394c66345ebe501b2f26
Issuer: paopao-api
Expire: 86400
TweetSearch: # 推文关键字搜索相关配置
MaxUpdateQPS: 100 # 最大添加/删除/更新Post的QPS设置范围[10, 10000], 默认100
MinWorker: 10 # 最小后台更新工作者, 设置范围[5, 1000], 默认10
Zinc: # Zinc搜索配置
Host: http://zinc:4080
Host: zinc:4080
Index: paopao-data
User: admin
Password: admin
Secure: False
Meili: # Meili搜索配置
Host: meili:7700
Index: paopao-data
ApiKey: paopao-meilisearch
Secure: False
AliOSS: # 阿里云OSS存储配置
Endpoint:
AccessKeyID:
@ -76,8 +99,8 @@ LocalOSS: # 本地文件OSS存储配置
Bucket: paopao
Domain: 127.0.0.1:8008
Database: # Database通用配置
LogLevel: 2
TablePrefix: p_
LogLevel: error # 日志级别 silent|error|warn|info
TablePrefix: p_ # 表名前缀
MySQL: # MySQL数据库
Username: paopao
Password: paopao
@ -87,7 +110,7 @@ MySQL: # MySQL数据库
ParseTime: True
MaxIdleConns: 10
MaxOpenConns: 30
Postgres:
Postgres: # PostgreSQL数据库
User: paopao
Password: paopao
DBName: paopao

@ -8,7 +8,7 @@ services:
MYSQL_DATABASE: paopao
MYSQL_USER: paopao
MYSQL_PASSWORD: paopao
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
MYSQL_RANDOM_ROOT_PASSWORD: yes
volumes:
- ./scripts/paopao-mysql.sql:/docker-entrypoint-initdb.d/paopao.sql
- ./data/mysql/data:/var/lib/mysql
@ -25,23 +25,9 @@ services:
networks:
- paopao-network
minio:
image: bitnami/minio:latest
restart: always
environment:
MINIO_ROOT_USER: minio-root-user
MINIO_ROOT_PASSWORD: minio-root-password
MINIO_DEFAULT_BUCKETS: paopao:public
ports:
- 127.0.0.1:9000:9000
- 127.0.0.1:9001:9001
volumes:
- ./data/minio/data:/data
networks:
- paopao-network
zinc:
image: public.ecr.aws/prabhat/zinc:latest
user: root
restart: always
ports:
- 127.0.0.1::4080
@ -54,6 +40,31 @@ services:
networks:
- paopao-network
# meili:
# image: getmeili/meilisearch:v0.27.0
# restart: always
# ports:
# - 7700:7700
# volumes:
# - ./data/meili/data:/meili_data
# environment:
# - MEILI_MASTER_KEY=paopao-meilisearch
# networks:
# - paopao-network
phpmyadmin:
image: phpmyadmin:5.2
depends_on:
- db
ports:
- 127.0.0.1:8080:80
environment:
- PMA_HOST=db
- PMA_USER=paopao
- PMA_PASSWORD=paopao
networks:
- paopao-network
backend:
build:
context: .
@ -62,7 +73,6 @@ services:
- db
- redis
- zinc
- minio
# modify below to reflect your custom configure
volumes:
- ./config.yaml.sample:/app/paopao-ce/config.yaml

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,17 @@
//go:build docs
// +build docs
package docs
import (
"embed"
"net/http"
)
//go:embed index.html openapi.json **/*
var files embed.FS
// NewFileSystem get an embed static assets http.FileSystem instance.
func NewFileSystem() http.FileSystem {
return http.FS(files)
}

@ -0,0 +1,21 @@
<!DOCTYPE html>
<!-- Important: must specify -->
<html>
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<title>paopao-ce develop documents</title>
<!-- Important: rapi-doc uses utf8 charecters -->
<script
type="module"
src="/docs/assets/rapidoc-min.js"
></script>
</head>
<body>
<rapi-doc
spec-url="/docs/openapi.json"
render-style="read"
>
</rapi-doc>
</body>
</html>

@ -0,0 +1,334 @@
{
"openapi": "3.0.0",
"info": {
"title": "paopao-ce API",
"description": "# The paopao-ce API documentation\n\nWelcome to the paopao-ce API documentation!\n\n",
"version": "0.1.0"
},
"servers": [
{
"url": "https://api.paopao.info"
}
],
"tags": [],
"paths": {
"/{bucket}": {
"post": {
"summary": "Upload Image",
"description": "Upload an image to the given bucket.\nThe `content-type` header must be provided as well\nas the `content-length` header otherwise the request will be rejected.\n\nThe uploaded file must also not exceed the given `content-length`.",
"parameters": [
{
"name": "bucket",
"schema": {
"type": "string"
},
"in": "path",
"description": "The bucket that the image should be uploaded.",
"required": true,
"deprecated": false
},
{
"name": "content-length",
"schema": {
"type": "integer",
"format": "uint64"
},
"in": "header",
"description": "The total size of the image in bytes.",
"required": true,
"deprecated": false
},
{
"name": "format",
"schema": {
"$ref": "#/components/schemas/ImageKind"
},
"in": "query",
"description": "The format that the uploaded image is encoded in.\n\nIf not provided, lust will guess the encoding.",
"required": false,
"deprecated": false
}
],
"requestBody": {
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UploadInfo"
}
}
}
},
"404": {
"description": "Bucket not found"
},
"400": {
"description": "The image format was incorrect or the system was\nunable to guess the format of the image."
},
"413": {
"description": "The upload exceeds the configured maximum file size."
},
"401": {
"description": "You are not authorized to complete this action.\n\nThis normally means the `Authorization` bearer has been left out\nof the request or is invalid."
}
}
}
},
"/{bucket}/{image_id}": {
"get": {
"summary": "Fetch Image",
"description": "Fetch the image from the storage backend and apply and additional affects\nif required.",
"parameters": [
{
"name": "bucket",
"schema": {
"type": "string"
},
"in": "path",
"description": "The bucket to try fetch the image from.",
"required": true,
"deprecated": false
},
{
"name": "image_id",
"schema": {
"type": "string",
"format": "uuid"
},
"in": "path",
"description": "The id of the image.",
"required": true,
"deprecated": false
},
{
"name": "format",
"schema": {
"$ref": "#/components/schemas/ImageKind"
},
"in": "query",
"description": "The encoding format that the image should be returned as.",
"required": false,
"deprecated": false
},
{
"name": "size",
"schema": {
"type": "string"
},
"in": "query",
"description": "The size preset that should be used when returning the image.",
"required": false,
"deprecated": false
},
{
"name": "width",
"schema": {
"type": "integer",
"format": "uint32"
},
"in": "query",
"description": "A custom width to resize the returned image to.",
"required": false,
"deprecated": false
},
{
"name": "height",
"schema": {
"type": "integer",
"format": "uint32"
},
"in": "query",
"description": "A custom height to resize the returned image to.",
"required": false,
"deprecated": false
},
{
"name": "accept",
"schema": {
"type": "string"
},
"in": "header",
"description": "A set of `,` seperated content-types that could be sent as a response.\nE.g. `image/png,image/webp,image/gif`",
"required": false,
"deprecated": false
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
},
"headers": {
"CONTENT-TYPE": {
"required": true,
"deprecated": false,
"schema": {
"type": "string"
}
}
}
},
"400": {
"description": "The request is invalid with the current configuration.\n\nSee the detail section for more info.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Detail"
}
}
}
},
"404": {
"description": "Bucket does not exist or image does not exist.\n\nSee the detail section for more info.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Detail"
}
}
}
}
}
},
"delete": {
"summary": "Delete Image",
"description": "Delete the given image.\nThis will purge all variants of the image including sizing presets and formats.\n\nImages that do not exist already will be ignored and will not return a 404.",
"parameters": [
{
"name": "bucket",
"schema": {
"type": "string"
},
"in": "path",
"description": "The bucket to try delete the image from.",
"required": true,
"deprecated": false
},
{
"name": "image_id",
"schema": {
"type": "string",
"format": "uuid"
},
"in": "path",
"description": "The image to delete try delete.",
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": ""
},
"401": {
"description": "You are not authorized to complete this action.\n\nThis normally means the `Authorization` bearer has been left out\nof the request or is invalid."
},
"404": {
"description": "Bucket does not exist."
}
}
}
}
},
"components": {
"schemas": {
"Detail": {
"type": "object",
"required": [
"detail"
],
"properties": {
"detail": {
"type": "string",
"description": "Additional information regarding the response."
}
}
},
"ImageKind": {
"type": "string",
"enum": [
"png",
"jpeg",
"webp",
"gif"
]
},
"ImageUploadInfo": {
"type": "object",
"required": [
"sizing_id"
],
"properties": {
"sizing_id": {
"type": "integer",
"format": "uint32",
"description": "The computed image sizing id.\n\nThis is useful for tracking files outside of lust as this is\ngenerally used for filtering within the storage systems."
}
}
},
"UploadInfo": {
"type": "object",
"required": [
"image_id",
"processing_time",
"io_time",
"checksum",
"images",
"bucket_id"
],
"properties": {
"image_id": {
"type": "string",
"format": "uuid",
"description": "The generated ID for the file.\n\nThis can be used to access the file for the given bucket."
},
"processing_time": {
"type": "number",
"format": "float",
"description": "The time spent processing the image in seconds."
},
"io_time": {
"type": "number",
"format": "float",
"description": "The time spent uploading the image to the persistent store."
},
"checksum": {
"type": "integer",
"format": "uint32",
"description": "The crc32 checksum of the uploaded image."
},
"images": {
"type": "array",
"description": "The information that is specific to the image.",
"items": {
"$ref": "#/components/schemas/ImageUploadInfo"
}
},
"bucket_id": {
"type": "integer",
"format": "uint32",
"description": "The id of the bucket the image was stored in.\n\nThis is useful for tracking files outside of lust as this is\ngenerally used for filtering within the storage systems."
}
}
}
}
}
}

@ -3,8 +3,10 @@ module github.com/rocboss/paopao-ce
go 1.16
require (
github.com/Masterminds/semver/v3 v3.1.1
github.com/afocus/captcha v0.0.0-20191010092841-4bd1f21c8868
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible
github.com/allegro/bigcache/v3 v3.0.2
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/disintegration/imaging v1.6.2
@ -16,8 +18,11 @@ require (
github.com/go-playground/validator/v10 v10.10.1 // indirect
github.com/go-redis/redis/v8 v8.11.4
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/google/go-cmp v0.5.7 // indirect
github.com/json-iterator/go v1.1.12
github.com/meilisearch/meilisearch-go v0.19.1
github.com/minio/minio-go/v7 v7.0.27
github.com/sirupsen/logrus v1.8.1
github.com/smartwalle/alipay/v3 v3.1.7
@ -26,7 +31,6 @@ require (
github.com/yinheli/mahonia v0.0.0-20131226213531-0eef680515cc
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 // indirect
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 // indirect
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
google.golang.org/protobuf v1.27.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0

@ -82,8 +82,13 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible h1:9gWa46nstkJ9miBReJcN8Gq34cBFbzSpQZVVT9N09TM=
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/allegro/bigcache/v3 v3.0.2 h1:AKZCw+5eAaVyNTBmI2fgyPVJhHkdWder3O9IrprcQfI=
github.com/allegro/bigcache/v3 v3.0.2/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/arrow/go/arrow v0.0.0-20191024131854-af6fa24be0db/go.mod h1:VTxUBvSJ3s3eHAg65PNgrsn5BtqCRPdmyXh6rAfdxN0=
github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
@ -270,6 +275,8 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
@ -278,6 +285,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
@ -488,6 +497,8 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -511,8 +522,9 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.13.5 h1:9O69jUPDcsT9fEm74W92rZL9FQY7rCdaXVneq+yyzl4=
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s=
@ -552,6 +564,8 @@ github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaW
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
@ -584,6 +598,8 @@ github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/meilisearch/meilisearch-go v0.19.1 h1:CDi7p5Ev18h0hMXaJZ/1GzSKu3lvPCsaJfrLco3bMEM=
github.com/meilisearch/meilisearch-go v0.19.1/go.mod h1:PnFFq9tELcH5mLVKCoTHRS58B3HEA8vKdBSoG6g/FCE=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4=
@ -743,8 +759,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
@ -763,9 +780,13 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.36.0 h1:NhqfO/cB7Ajn1czkKnWkMHyPYr5nyND14ZGPk23g0/c=
github.com/valyala/fasthttp v1.36.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
@ -829,6 +850,7 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 h1:syTAU9FwmvzEoIYMqcPHOcVm4H3U5u90WsvuYgwpETU=
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -926,8 +948,8 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1040,6 +1062,7 @@ golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=

@ -7,21 +7,27 @@ import (
)
var (
loggerFileSetting *LoggerFileSettingS
loggerZincSetting *LoggerZincSettingS
databaseSetting *DatabaseSetingS
mysqlSetting *MySQLSettingS
postgresSetting *PostgresSettingS
sqlite3Setting *Sqlite3SettingS
redisSetting *RedisSettingS
features *FeaturesSettingS
LoggerSetting *LoggerSettingS
loggerFileSetting *LoggerFileSettingS
loggerZincSetting *LoggerZincSettingS
loggerMeiliSetting *LoggerMeiliSettingS
databaseSetting *DatabaseSetingS
mysqlSetting *MySQLSettingS
postgresSetting *PostgresSettingS
sqlite3Setting *Sqlite3SettingS
redisSetting *RedisSettingS
features *FeaturesSettingS
ServerSetting *ServerSettingS
AppSetting *AppSettingS
CacheIndexSetting *CacheIndexSettingS
SimpleCacheIndexSetting *SimpleCacheIndexSettingS
BigCacheIndexSetting *BigCacheIndexSettingS
SmsJuheSetting *SmsJuheSettings
AlipaySetting *AlipaySettingS
TweetSearchSetting *TweetSearchS
ZincSetting *ZincSettingS
MeiliSetting *MeiliSettingS
AliOSSSetting *AliOSSSettingS
MinIOSetting *MinIOSettingS
S3Setting *S3SettingS
@ -46,16 +52,22 @@ func setupSetting(suite []string, noDefault bool) error {
objects := map[string]interface{}{
"App": &AppSetting,
"Server": &ServerSetting,
"CacheIndex": &CacheIndexSetting,
"SimpleCacheIndex": &SimpleCacheIndexSetting,
"BigCacheIndex": &BigCacheIndexSetting,
"Alipay": &AlipaySetting,
"SmsJuhe": &SmsJuheSetting,
"Logger": &LoggerSetting,
"LoggerFile": &loggerFileSetting,
"LoggerZinc": &loggerZincSetting,
"LoggerMeili": &loggerMeiliSetting,
"Database": &databaseSetting,
"MySQL": &mysqlSetting,
"Postgres": &postgresSetting,
"Sqlite3": &sqlite3Setting,
"TweetSearch": &TweetSearchSetting,
"Zinc": &ZincSetting,
"Meili": &MeiliSetting,
"Redis": &redisSetting,
"JWT": &JWTSetting,
"AliOSS": &AliOSSSetting,
@ -70,6 +82,10 @@ func setupSetting(suite []string, noDefault bool) error {
JWTSetting.Expire *= time.Second
ServerSetting.ReadTimeout *= time.Second
ServerSetting.WriteTimeout *= time.Second
SimpleCacheIndexSetting.CheckTickDuration *= time.Second
SimpleCacheIndexSetting.ExpireTickDuration *= time.Second
BigCacheIndexSetting.ExpireInSecond *= time.Second
Mutex = &sync.Mutex{}
return nil
}

@ -23,10 +23,10 @@ func newDBEngine() (*gorm.DB, error) {
newLogger := logger.New(
logrus.StandardLogger(), // io writer日志输出的目标前缀和日志包含的内容
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: databaseSetting.LogLevel, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound记录未找到错误
Colorful: false, // 禁用彩色打印
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: databaseSetting.logLevel(), // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound记录未找到错误
Colorful: false, // 禁用彩色打印
},
)

@ -1,92 +1,34 @@
package conf
import (
"encoding/json"
"fmt"
"io"
"time"
"github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
"gopkg.in/resty.v1"
)
type zincLogIndex struct {
Index map[string]string `json:"index"`
}
type zincLogData struct {
Time time.Time `json:"time"`
Level logrus.Level `json:"level"`
Message string `json:"message"`
Data logrus.Fields `json:"data"`
}
type zincLogHook struct {
host string
index string
user string
password string
}
func (h *zincLogHook) Fire(entry *logrus.Entry) error {
index := &zincLogIndex{
Index: map[string]string{
"_index": h.index,
},
}
indexBytes, _ := json.Marshal(index)
data := &zincLogData{
Time: entry.Time,
Level: entry.Level,
Message: entry.Message,
Data: entry.Data,
func newFileLogger() io.Writer {
return &lumberjack.Logger{
Filename: loggerFileSetting.SavePath + "/" + loggerFileSetting.FileName + loggerFileSetting.FileExt,
MaxSize: 600,
MaxAge: 10,
LocalTime: true,
}
dataBytes, _ := json.Marshal(data)
logStr := string(indexBytes) + "\n" + string(dataBytes) + "\n"
client := resty.New()
if _, err := client.SetDisableWarn(true).R().
SetHeader("Content-Type", "application/json").
SetBasicAuth(h.user, h.password).
SetBody(logStr).
Post(h.host); err != nil {
fmt.Println(err.Error())
}
return nil
}
func (h *zincLogHook) Levels() []logrus.Level {
return logrus.AllLevels
}
func setupLogger() {
logrus.SetFormatter(&logrus.JSONFormatter{})
logrus.SetLevel(LoggerSetting.logLevel())
if CfgIf("LoggerFile") {
out := &lumberjack.Logger{
Filename: loggerFileSetting.SavePath + "/" + loggerFileSetting.FileName + loggerFileSetting.FileExt,
MaxSize: 600,
MaxAge: 10,
LocalTime: true,
}
out := newFileLogger()
logrus.SetOutput(out)
if level, err := logrus.ParseLevel(loggerFileSetting.Level); err == nil {
logrus.SetLevel(level)
}
} else if CfgIf("LoggerZinc") {
hook := &zincLogHook{
host: loggerZincSetting.Host,
index: loggerZincSetting.Index,
user: loggerZincSetting.User,
password: loggerZincSetting.Password,
}
if level, err := logrus.ParseLevel(loggerZincSetting.Level); err == nil {
logrus.SetLevel(level)
}
hook := newZincLogHook()
logrus.SetOutput(io.Discard)
logrus.AddHook(hook)
} else if CfgIf("LoggerMeili") {
hook := newMeiliLogHook()
logrus.SetOutput(io.Discard)
logrus.AddHook(hook)
}

@ -0,0 +1,77 @@
package conf
import (
"github.com/meilisearch/meilisearch-go"
"github.com/sirupsen/logrus"
)
type meiliLogData []map[string]interface{}
type meiliLogHook struct {
config meilisearch.ClientConfig
idxName string
addDocsCh chan *meiliLogData
}
func (h *meiliLogHook) Fire(entry *logrus.Entry) error {
data := meiliLogData{{
"id": entry.Time.Unix(),
"time": entry.Time,
"level": entry.Level,
"message": entry.Message,
"data": entry.Data,
}}
// 先尝试进log缓存否则直接加文档
select {
case h.addDocsCh <- &data:
default:
h.index().AddDocuments(data)
}
return nil
}
func (h *meiliLogHook) handleAddDocs() {
index := h.index()
for item := range h.addDocsCh {
index.AddDocuments(item)
}
}
func (h *meiliLogHook) Levels() []logrus.Level {
return logrus.AllLevels
}
func (h *meiliLogHook) index() *meilisearch.Index {
return meilisearch.NewClient(h.config).Index(h.idxName)
}
func newMeiliLogHook() *meiliLogHook {
hook := &meiliLogHook{
config: meilisearch.ClientConfig{
Host: loggerMeiliSetting.Endpoint(),
APIKey: loggerMeiliSetting.ApiKey,
},
idxName: loggerMeiliSetting.Index,
}
client := meilisearch.NewClient(hook.config)
index := client.Index(hook.idxName)
if _, err := index.FetchInfo(); err != nil {
client.CreateIndex(&meilisearch.IndexConfig{
Uid: hook.idxName,
PrimaryKey: "id",
})
}
// 初始化addDocsCh
hook.addDocsCh = make(chan *meiliLogData, loggerMeiliSetting.maxLogBuffer())
// 启动后台log工作者
for minWork := loggerMeiliSetting.minWork(); minWork > 0; minWork-- {
go hook.handleAddDocs()
}
return hook
}

@ -0,0 +1,71 @@
package conf
import (
"fmt"
"time"
"github.com/rocboss/paopao-ce/pkg/json"
"github.com/sirupsen/logrus"
"gopkg.in/resty.v1"
)
type zincLogData struct {
Time time.Time `json:"time"`
Level logrus.Level `json:"level"`
Message string `json:"message"`
Data logrus.Fields `json:"data"`
}
type zincLogIndex struct {
Index map[string]string `json:"index"`
}
type zincLogHook struct {
host string
index string
user string
password string
}
func (h *zincLogHook) Fire(entry *logrus.Entry) error {
index := &zincLogIndex{
Index: map[string]string{
"_index": h.index,
},
}
indexBytes, _ := json.Marshal(index)
data := &zincLogData{
Time: entry.Time,
Level: entry.Level,
Message: entry.Message,
Data: entry.Data,
}
dataBytes, _ := json.Marshal(data)
logStr := string(indexBytes) + "\n" + string(dataBytes) + "\n"
client := resty.New()
if _, err := client.SetDisableWarn(true).R().
SetHeader("Content-Type", "application/json").
SetBasicAuth(h.user, h.password).
SetBody(logStr).
Post(h.host); err != nil {
fmt.Println(err.Error())
}
return nil
}
func (h *zincLogHook) Levels() []logrus.Level {
return logrus.AllLevels
}
func newZincLogHook() *zincLogHook {
return &zincLogHook{
host: loggerZincSetting.Endpoint() + "/es/_bulk",
index: loggerZincSetting.Index,
user: loggerZincSetting.User,
password: loggerZincSetting.Password,
}
}

@ -5,6 +5,7 @@ import (
"strings"
"time"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"gorm.io/gorm/logger"
)
@ -13,11 +14,14 @@ type Setting struct {
vp *viper.Viper
}
type LoggerSettingS struct {
Level string
}
type LoggerFileSettingS struct {
SavePath string
FileName string
FileExt string
Level string
}
type LoggerZincSettingS struct {
@ -25,7 +29,16 @@ type LoggerZincSettingS struct {
Index string
User string
Password string
Level string
Secure bool
}
type LoggerMeiliSettingS struct {
Host string
Index string
ApiKey string
Secure bool
MaxLogBuffer int
MinWorker int
}
type ServerSettingS struct {
@ -47,11 +60,21 @@ type AppSettingS struct {
TronApiKeys []string
}
type CacheIndexSettingS struct {
MaxUpdateQPS int
MinWorker int
}
type SimpleCacheIndexSettingS struct {
MaxIndexSize int
CheckTickDuration int
ExpireTickDuration int
ActionQPS int
CheckTickDuration time.Duration
ExpireTickDuration time.Duration
}
type BigCacheIndexSettingS struct {
MaxIndexPage int
ExpireInSecond time.Duration
Verbose bool
}
type AlipaySettingS struct {
@ -71,16 +94,29 @@ type FeaturesSettingS struct {
features map[string]string
}
type TweetSearchS struct {
MaxUpdateQPS int
MinWorker int
}
type ZincSettingS struct {
Host string
Index string
User string
Password string
Secure bool
}
type MeiliSettingS struct {
Host string
Index string
ApiKey string
Secure bool
}
type DatabaseSetingS struct {
TablePrefix string
LogLevel logger.LogLevel
LogLevel string
}
type MySQLSettingS struct {
@ -282,3 +318,81 @@ func (s PostgresSettingS) Dsn() string {
}
return strings.Join(params, " ")
}
func (s *DatabaseSetingS) logLevel() logger.LogLevel {
switch strings.ToLower(s.LogLevel) {
case "silent":
return logger.Silent
case "error":
return logger.Error
case "warn":
return logger.Warn
case "info":
return logger.Info
default:
return logger.Error
}
}
func (s *LoggerSettingS) logLevel() logrus.Level {
switch strings.ToLower(s.Level) {
case "panic":
return logrus.PanicLevel
case "fatal":
return logrus.FatalLevel
case "error":
return logrus.ErrorLevel
case "warn", "warning":
return logrus.WarnLevel
case "info":
return logrus.InfoLevel
case "debug":
return logrus.DebugLevel
case "trace":
return logrus.TraceLevel
default:
return logrus.ErrorLevel
}
}
func (s *LoggerZincSettingS) Endpoint() string {
return endpoint(s.Host, s.Secure)
}
func (s *LoggerMeiliSettingS) Endpoint() string {
return endpoint(s.Host, s.Secure)
}
func (s *LoggerMeiliSettingS) minWork() int {
if s.MinWorker < 5 {
return 5
} else if s.MinWorker > 100 {
return 100
}
return s.MinWorker
}
func (s *LoggerMeiliSettingS) maxLogBuffer() int {
if s.MaxLogBuffer < 10 {
return 10
} else if s.MaxLogBuffer > 1000 {
return 1000
}
return s.MaxLogBuffer
}
func (s *ZincSettingS) Endpoint() string {
return endpoint(s.Host, s.Secure)
}
func (s *MeiliSettingS) Endpoint() string {
return endpoint(s.Host, s.Secure)
}
func endpoint(host string, secure bool) string {
schema := "http"
if secure {
schema = "https"
}
return schema + "://" + host
}

@ -6,6 +6,7 @@ const (
IdxActUpdatePost
IdxActDeletePost
IdxActStickPost
IdxActVisiblePost
)
type IndexActionT uint8
@ -22,6 +23,8 @@ func (a IndexActionT) String() string {
return "delete post"
case IdxActStickPost:
return "stick post"
case IdxActVisiblePost:
return "visible post"
default:
return "unknow action"
}
@ -29,6 +32,8 @@ func (a IndexActionT) String() string {
// CacheIndexService cache index service interface
type CacheIndexService interface {
VersionInfo
IndexPostsService
SendAction(active IndexActionT)
}

@ -7,7 +7,6 @@ import (
// DataService data service interface that process data related logic on database
type DataService interface {
WalletService
SearchService
IndexPostsService
GetComments(conditions *model.ConditionsT, offset, limit int) ([]*model.Comment, error)
@ -34,8 +33,11 @@ type DataService interface {
DeletePost(post *model.Post) error
LockPost(post *model.Post) error
StickPost(post *model.Post) error
VisiblePost(post *model.Post, visibility model.PostVisibleT) error
GetPostByID(id int64) (*model.Post, error)
GetPosts(conditions *model.ConditionsT, offset, limit int) ([]*model.Post, error)
MergePosts(posts []*model.Post) ([]*model.PostFormated, error)
RevampPosts(posts []*model.PostFormated) ([]*model.PostFormated, error)
GetPostCount(conditions *model.ConditionsT) (int64, error)
UpdatePost(post *model.Post) error
GetUserPostStar(postID, userID int64) (*model.PostStar, error)
@ -70,4 +72,6 @@ type DataService interface {
GetLatestPhoneCaptcha(phone string) (*model.Captcha, error)
UsePhoneCaptcha(captcha *model.Captcha) error
SendPhoneCaptcha(phone string) error
IsFriend(userID int64, friendID int64) bool
}

@ -5,5 +5,5 @@ import (
)
type IndexPostsService interface {
IndexPosts(offset int, limit int) ([]*model.PostFormated, error)
IndexPosts(userId int64, offset int, limit int) ([]*model.PostFormated, error)
}

@ -1,7 +1,7 @@
package core
import (
"github.com/rocboss/paopao-ce/pkg/zinc"
"github.com/rocboss/paopao-ce/internal/model"
)
const (
@ -11,17 +11,25 @@ const (
type SearchType string
type QueryT struct {
Query string
Type SearchType
type QueryReq struct {
Query string
Visibility []model.PostVisibleT
Type SearchType
}
// SearchService search service interface that implement base zinc
type SearchService interface {
CreateSearchIndex(indexName string)
BulkPushDoc(data []map[string]interface{}) (bool, error)
DelDoc(indexName, id string) error
QueryAll(q *QueryT, indexName string, offset, limit int) (*zinc.QueryResultT, error)
QuerySearch(indexName, query string, offset, limit int) (*zinc.QueryResultT, error)
QueryTagSearch(indexName, query string, offset, limit int) (*zinc.QueryResultT, error)
type QueryResp struct {
Items []*model.PostFormated
Total int64
}
type DocItems []map[string]interface{}
// TweetSearchService tweet search service interface
type TweetSearchService interface {
VersionInfo
IndexName() string
AddDocuments(documents DocItems, primaryKey ...string) (bool, error)
DeleteDocuments(identifiers []string) error
Search(q *QueryReq, offset, limit int) (*QueryResp, error)
}

@ -6,6 +6,8 @@ import (
// ObjectStorageService storage service interface that implement base AliOSS、MINIO or other
type ObjectStorageService interface {
VersionInfo
PutObject(objectKey string, reader io.Reader, objectSize int64, contentType string) (string, error)
SignURL(objectKey string, expiredInSec int64) (string, error)
ObjectURL(objetKey string) string

@ -0,0 +1,10 @@
package core
import (
"github.com/Masterminds/semver/v3"
)
type VersionInfo interface {
Name() string
Version() *semver.Version
}

@ -0,0 +1,40 @@
package dao
import (
"sync/atomic"
"time"
"github.com/allegro/bigcache/v3"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/model"
)
var (
_ core.CacheIndexService = (*simpleCacheIndexServant)(nil)
_ core.CacheIndexService = (*bigCacheIndexServant)(nil)
)
type postsEntry struct {
key string
posts []*model.PostFormated
}
type indexPostsFunc func(int64, int, int) ([]*model.PostFormated, error)
type bigCacheIndexServant struct {
getIndexPosts indexPostsFunc
indexActionCh chan core.IndexActionT
cachePostsCh chan *postsEntry
cache *bigcache.BigCache
lastCacheResetTime time.Time
}
type simpleCacheIndexServant struct {
getIndexPosts indexPostsFunc
indexActionCh chan core.IndexActionT
indexPosts []*model.PostFormated
atomicIndex atomic.Value
maxIndexSize int
checkTick *time.Ticker
expireIndexTick *time.Ticker
}

@ -0,0 +1,158 @@
package dao
import (
"bytes"
"encoding/gob"
"fmt"
"time"
"github.com/Masterminds/semver/v3"
"github.com/allegro/bigcache/v3"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/model"
"github.com/sirupsen/logrus"
)
func newBigCacheIndexServant(getIndexPosts indexPostsFunc) *bigCacheIndexServant {
s := conf.BigCacheIndexSetting
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)
if err != nil {
logrus.Fatalf("initial bigCahceIndex failure by err: %v", err)
}
cacheIndex := &bigCacheIndexServant{
getIndexPosts: getIndexPosts,
cache: cache,
}
// 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.IndexActionT, capacity)
cacheIndex.cachePostsCh = make(chan *postsEntry, capacity)
// 启动索引更新器
go cacheIndex.startIndexPosts()
return cacheIndex
}
func (s *bigCacheIndexServant) IndexPosts(userId int64, offset int, limit int) ([]*model.PostFormated, error) {
key := s.keyFrom(userId, offset, limit)
posts, err := s.getPosts(key)
if err == nil {
logrus.Debugf("get index posts from cache by key: %s userId: %d offset:%d limit:%d", key, userId, offset, limit)
return posts, nil
}
if posts, err = s.getIndexPosts(userId, offset, limit); err != nil {
return nil, err
}
logrus.Debugf("get index posts from database by userId: %d offset:%d limit:%d", userId, offset, limit)
s.cachePosts(key, posts)
return posts, nil
}
func (s *bigCacheIndexServant) getPosts(key string) ([]*model.PostFormated, error) {
data, err := s.cache.Get(key)
if err != nil {
logrus.Debugf("get posts by key: %s from cache err: %v", key, err)
return nil, err
}
buf := bytes.NewBuffer(data)
dec := gob.NewDecoder(buf)
var posts []*model.PostFormated
if err := dec.Decode(&posts); err != nil {
logrus.Debugf("get posts from cache in decode err: %v", err)
return nil, err
}
return posts, nil
}
func (s *bigCacheIndexServant) cachePosts(key string, posts []*model.PostFormated) {
entry := &postsEntry{key: key, posts: posts}
select {
case s.cachePostsCh <- entry:
logrus.Debugf("cachePosts by chan of key: %s", key)
default:
go func(ch chan<- *postsEntry, entry *postsEntry) {
logrus.Debugf("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.posts); err != nil {
logrus.Debugf("setPosts encode post entry err: %v", err)
return
}
if err := s.cache.Set(entry.key, buf.Bytes()); err != nil {
logrus.Debugf("setPosts set cache err: %v", err)
}
logrus.Debugf("setPosts set cache by key: %s", entry.key)
}
func (s *bigCacheIndexServant) keyFrom(userId int64, offset int, limit int) string {
return fmt.Sprintf("index:%d:%d:%d", userId, offset, limit)
}
func (s *bigCacheIndexServant) SendAction(act core.IndexActionT) {
select {
case s.indexActionCh <- act:
logrus.Debugf("send indexAction by chan: %s", act)
default:
go func(ch chan<- core.IndexActionT, act core.IndexActionT) {
logrus.Debugf("send indexAction by goroutine: %s", act)
ch <- act
}(s.indexActionCh, act)
}
}
func (s *bigCacheIndexServant) startIndexPosts() {
for {
select {
case entry := <-s.cachePostsCh:
s.setPosts(entry)
case action := <-s.indexActionCh:
switch action {
// TODO: 这里列出来是因为后续可能会精细化处理每种情况
case core.IdxActCreatePost,
core.IdxActUpdatePost,
core.IdxActDeletePost,
core.IdxActStickPost,
core.IdxActVisiblePost:
// TODO: 粗糙处理cache后续需要针对每一种情况精细化处理
if time.Since(s.lastCacheResetTime) > time.Minute {
s.cache.Reset()
s.lastCacheResetTime = time.Now()
logrus.Debugf("reset cache by %s", action)
}
default:
// nop
}
}
}
}
func (s *bigCacheIndexServant) Name() string {
return "BigCacheIndex"
}
func (s *bigCacheIndexServant) Version() *semver.Version {
return semver.MustParse("v0.1.0")
}

@ -4,6 +4,7 @@ import (
"errors"
"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/internal/model"
@ -14,26 +15,26 @@ var (
errNotExist = errors.New("index posts cache not exist")
)
func newSimpleCacheIndexServant(getIndexPosts func(offset, limit int) ([]*model.PostFormated, error)) *simpleCacheIndexServant {
func newSimpleCacheIndexServant(getIndexPosts indexPostsFunc) *simpleCacheIndexServant {
s := conf.SimpleCacheIndexSetting
cacheIndex := &simpleCacheIndexServant{
getIndexPosts: getIndexPosts,
maxIndexSize: s.MaxIndexSize,
indexPosts: make([]*model.PostFormated, 0),
checkTick: time.NewTicker(time.Duration(s.CheckTickDuration) * time.Second), // check whether need update index every 1 minute
checkTick: time.NewTicker(s.CheckTickDuration), // check whether need update index every 1 minute
expireIndexTick: time.NewTicker(time.Second),
}
// force expire index every ExpireTickDuration second
if s.ExpireTickDuration != 0 {
cacheIndex.expireIndexTick.Reset(time.Duration(s.CheckTickDuration) * time.Second)
cacheIndex.expireIndexTick.Reset(s.CheckTickDuration)
} else {
cacheIndex.expireIndexTick.Stop()
}
// indexActionCh capacity custom configure by conf.yaml need in [10, 10000]
// or re-compile source to adjust min/max capacity
capacity := s.ActionQPS
capacity := conf.CacheIndexSetting.MaxUpdateQPS
if capacity < 10 {
capacity = 10
} else if capacity > 10000 {
@ -48,7 +49,7 @@ func newSimpleCacheIndexServant(getIndexPosts func(offset, limit int) ([]*model.
return cacheIndex
}
func (s *simpleCacheIndexServant) IndexPosts(offset int, limit int) ([]*model.PostFormated, error) {
func (s *simpleCacheIndexServant) IndexPosts(_userId int64, offset int, limit int) ([]*model.PostFormated, error) {
posts := s.atomicIndex.Load().([]*model.PostFormated)
end := offset + limit
size := len(posts)
@ -78,7 +79,7 @@ func (s *simpleCacheIndexServant) startIndexPosts() {
case <-s.checkTick.C:
if len(s.indexPosts) == 0 {
logrus.Debugf("index posts by checkTick")
if s.indexPosts, err = s.getIndexPosts(0, s.maxIndexSize); err == nil {
if s.indexPosts, err = s.getIndexPosts(0, 0, s.maxIndexSize); err == nil {
s.atomicIndex.Store(s.indexPosts)
} else {
logrus.Errorf("get index posts err: %v", err)
@ -92,7 +93,12 @@ func (s *simpleCacheIndexServant) startIndexPosts() {
}
case action := <-s.indexActionCh:
switch action {
case core.IdxActCreatePost, core.IdxActUpdatePost, core.IdxActDeletePost, core.IdxActStickPost:
// TODO: 这里列出来是因为后续可能会精细化处理每种情况
case core.IdxActCreatePost,
core.IdxActUpdatePost,
core.IdxActDeletePost,
core.IdxActStickPost,
core.IdxActVisiblePost:
// prevent many update post in least time
if len(s.indexPosts) != 0 {
logrus.Debugf("remove index posts by action %s", action)
@ -105,3 +111,11 @@ func (s *simpleCacheIndexServant) startIndexPosts() {
}
}
}
func (s *simpleCacheIndexServant) Name() string {
return "SimpleCacheIndex"
}
func (s *simpleCacheIndexServant) Version() *semver.Version {
return semver.MustParse("v0.1.0")
}

@ -1,14 +1,8 @@
package dao
import (
"sync/atomic"
"time"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/minio/minio-go/v7"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/model"
"github.com/rocboss/paopao-ce/pkg/zinc"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
@ -16,12 +10,8 @@ import (
var (
_ core.DataService = (*dataServant)(nil)
_ core.ObjectStorageService = (*aliossServant)(nil)
_ core.ObjectStorageService = (*minioServant)(nil)
_ core.ObjectStorageService = (*s3Servant)(nil)
_ core.ObjectStorageService = (*localossServant)(nil)
_ core.AttachmentCheckService = (*attachmentCheckServant)(nil)
_ core.CacheIndexService = (*simpleCacheIndexServant)(nil)
_ core.TweetSearchService = (*zincTweetSearchServant)(nil)
)
type dataServant struct {
@ -32,77 +22,35 @@ type dataServant struct {
zinc *zinc.ZincClient
}
type simpleCacheIndexServant struct {
getIndexPosts func(offset, limit int) ([]*model.PostFormated, error)
indexActionCh chan core.IndexActionT
indexPosts []*model.PostFormated
atomicIndex atomic.Value
maxIndexSize int
checkTick *time.Ticker
expireIndexTick *time.Ticker
}
type localossServant struct {
savePath string
domain string
}
type aliossServant struct {
bucket *oss.Bucket
domain string
}
type minioServant struct {
client *minio.Client
bucket string
domain string
}
type s3Servant = minioServant
type attachmentCheckServant struct {
domain string
}
func NewDataService(engine *gorm.DB, zinc *zinc.ZincClient) core.DataService {
func NewDataService() core.DataService {
client := zinc.NewClient(conf.ZincSetting)
ds := &dataServant{
engine: engine,
zinc: zinc,
engine: conf.DBEngine,
zinc: client,
}
// initialize CacheIndex if needed
ds.useCacheIndex = true
if conf.CfgIf("SimpleCacheIndex") {
ds.useCacheIndex = true
ds.cacheIndex = newSimpleCacheIndexServant(ds.getIndexPosts)
} else if conf.CfgIf("BigCacheIndex") {
ds.cacheIndex = newBigCacheIndexServant(ds.getIndexPosts)
} else {
ds.useCacheIndex = false
}
return ds
}
func NewObjectStorageService() (oss core.ObjectStorageService) {
if conf.CfgIf("AliOSS") {
oss = newAliossServent()
logrus.Infoln("use AliOSS as object storage")
} else if conf.CfgIf("MinIO") {
oss = newMinioServeant()
logrus.Infoln("use MinIO as object storage")
} else if conf.CfgIf("S3") {
oss = newS3Servent()
logrus.Infoln("use S3 as object storage")
} else if conf.CfgIf("LocalOSS") {
oss = newLocalossServent()
logrus.Infoln("use LocalOSS as object storage")
} else {
// default use AliOSS
oss = newAliossServent()
logrus.Infoln("use default AliOSS as object storage")
if ds.useCacheIndex {
logrus.Infof("use %s as cache index service by version: %s", ds.cacheIndex.Name(), ds.cacheIndex.Version())
}
return
return ds
}
func NewAttachmentCheckerService() core.AttachmentCheckService {
func NewAttachmentCheckService() core.AttachmentCheckService {
return &attachmentCheckServant{
domain: getOssDomain(),
}

@ -0,0 +1,55 @@
package dao
import (
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/minio/minio-go/v7"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/sirupsen/logrus"
)
var (
_ core.ObjectStorageService = (*aliossServant)(nil)
_ core.ObjectStorageService = (*minioServant)(nil)
_ core.ObjectStorageService = (*s3Servant)(nil)
_ core.ObjectStorageService = (*localossServant)(nil)
)
type localossServant struct {
savePath string
domain string
}
type aliossServant struct {
bucket *oss.Bucket
domain string
}
type minioServant struct {
client *minio.Client
bucket string
domain string
}
type s3Servant = minioServant
func NewObjectStorageService() (oss core.ObjectStorageService) {
if conf.CfgIf("AliOSS") {
oss = newAliossServent()
} else if conf.CfgIf("MinIO") {
oss = newMinioServeant()
} else if conf.CfgIf("S3") {
oss = newS3Servent()
logrus.Infof("use S3 as object storage by version %s", oss.Version())
return
} else if conf.CfgIf("LocalOSS") {
oss = newLocalossServent()
} else {
// default use AliOSS as object storage service
oss = newAliossServent()
logrus.Infof("use default AliOSS as object storage by version %s", oss.Version())
return
}
logrus.Infof("use %s as object storage by version %s", oss.Name(), oss.Version())
return
}

@ -5,6 +5,7 @@ import (
"net/url"
"strings"
"github.com/Masterminds/semver/v3"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/sirupsen/logrus"
@ -27,6 +28,14 @@ func newAliossServent() *aliossServant {
}
}
func (s *aliossServant) Name() string {
return "AliOSS"
}
func (s *aliossServant) Version() *semver.Version {
return semver.MustParse("v0.1.0")
}
func (s *aliossServant) PutObject(objectKey string, reader io.Reader, objectSize int64, contentType string) (string, error) {
err := s.bucket.PutObject(objectKey, reader, oss.ContentLength(objectSize), oss.ContentType(contentType))
if err != nil {

@ -9,6 +9,7 @@ import (
"strings"
"time"
"github.com/Masterminds/semver/v3"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/sirupsen/logrus"
)
@ -25,6 +26,14 @@ func newLocalossServent() *localossServant {
}
}
func (s *localossServant) Name() string {
return "LocalOSS"
}
func (s *localossServant) Version() *semver.Version {
return semver.MustParse("v0.1.0")
}
func (s *localossServant) PutObject(objectKey string, reader io.Reader, objectSize int64, contentType string) (string, error) {
saveDir := s.savePath + filepath.Dir(objectKey)
err := os.MkdirAll(saveDir, 0750)

@ -7,6 +7,7 @@ import (
"strings"
"time"
"github.com/Masterminds/semver/v3"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/rocboss/paopao-ce/internal/conf"
@ -29,6 +30,14 @@ func newMinioServeant() *minioServant {
}
}
func (s *minioServant) Name() string {
return "MinIO"
}
func (s *minioServant) Version() *semver.Version {
return semver.MustParse("v0.1.0")
}
func (s *minioServant) PutObject(objectKey string, reader io.Reader, objectSize int64, contentType string) (string, error) {
uploadInfo, err := s.client.PutObject(context.Background(), s.bucket, objectKey, reader, objectSize, minio.PutObjectOptions{ContentType: contentType})
if err != nil {
@ -37,6 +46,7 @@ func (s *minioServant) PutObject(objectKey string, reader io.Reader, objectSize
logrus.Infoln("Successfully uploaded bytes: ", uploadInfo)
return s.domain + objectKey, nil
}
func (s *minioServant) SignURL(objectKey string, expiredInSec int64) (string, error) {
// TODO: Set request parameters for content-disposition.
reqParams := make(url.Values)

@ -1,6 +1,7 @@
package dao
import (
"strings"
"time"
"github.com/rocboss/paopao-ce/internal/core"
@ -39,6 +40,46 @@ func (d *dataServant) StickPost(post *model.Post) error {
return nil
}
func (d *dataServant) VisiblePost(post *model.Post, visibility model.PostVisibleT) error {
oldVisibility := post.Visibility
post.Visibility = visibility
// TODO: 这个判断是否可以不要呢
if oldVisibility == visibility {
return nil
}
// 私密推文 特殊处理
if visibility == model.PostVisitPrivate {
// 强制取消置顶
// TODO: 置顶推文用户是否有权设置成私密? 后续完善
post.IsTop = 0
}
db := d.engine.Begin()
err := post.Update(db)
if err != nil {
db.Rollback()
return err
}
// tag处理
tags := strings.Split(post.Tags, ",")
for _, t := range tags {
tag := &model.Tag{
Tag: t,
}
// TODO: 暂时宽松不处理错误,这里或许可以有优化,后续完善
if oldVisibility == model.PostVisitPrivate {
// 从私密转为非私密才需要重新创建tag
d.createTag(db, tag)
} else if visibility == model.PostVisitPrivate {
// 从非私密转为私密才需要删除tag
d.deleteTag(db, tag)
}
}
db.Commit()
d.indexActive(core.IdxActVisiblePost)
return nil
}
func (d *dataServant) GetPostByID(id int64) (*model.Post, error) {
post := &model.Post{
Model: &model.Model{
@ -79,7 +120,7 @@ func (d *dataServant) GetUserPostStars(userID int64, offset, limit int) ([]*mode
}
return star.List(d.engine, &model.ConditionsT{
"ORDER": "id DESC",
"ORDER": d.engine.NamingStrategy.TableName("PostStar") + ".id DESC",
}, offset, limit)
}
@ -118,7 +159,7 @@ func (d *dataServant) GetUserPostCollections(userID int64, offset, limit int) ([
}
return collection.List(d.engine, &model.ConditionsT{
"ORDER": "id DESC",
"ORDER": d.engine.NamingStrategy.TableName("PostCollection") + ".id DESC",
}, offset, limit)
}
@ -166,3 +207,80 @@ func (d *dataServant) GetPostAttatchmentBill(postID, userID int64) (*model.PostA
return bill.Get(d.engine)
}
// MergePosts post数据整合
func (d *dataServant) MergePosts(posts []*model.Post) ([]*model.PostFormated, error) {
postIds := make([]int64, 0, len(posts))
userIds := make([]int64, 0, len(posts))
for _, post := range posts {
postIds = append(postIds, post.ID)
userIds = append(userIds, post.UserID)
}
postContents, err := d.GetPostContentsByIDs(postIds)
if err != nil {
return nil, err
}
users, err := d.GetUsersByIDs(userIds)
if err != nil {
return nil, err
}
userMap := make(map[int64]*model.UserFormated, len(users))
for _, user := range users {
userMap[user.ID] = user.Format()
}
contentMap := make(map[int64][]*model.PostContentFormated, len(postContents))
for _, content := range postContents {
contentMap[content.PostID] = append(contentMap[content.PostID], content.Format())
}
// 数据整合
postsFormated := make([]*model.PostFormated, 0, len(posts))
for _, post := range posts {
postFormated := post.Format()
postFormated.User = userMap[post.UserID]
postFormated.Contents = contentMap[post.ID]
postsFormated = append(postsFormated, postFormated)
}
return postsFormated, nil
}
// RevampPosts post数据整形修复
func (d *dataServant) RevampPosts(posts []*model.PostFormated) ([]*model.PostFormated, error) {
postIds := make([]int64, 0, len(posts))
userIds := make([]int64, 0, len(posts))
for _, post := range posts {
postIds = append(postIds, post.ID)
userIds = append(userIds, post.UserID)
}
postContents, err := d.GetPostContentsByIDs(postIds)
if err != nil {
return nil, err
}
users, err := d.GetUsersByIDs(userIds)
if err != nil {
return nil, err
}
userMap := make(map[int64]*model.UserFormated, len(users))
for _, user := range users {
userMap[user.ID] = user.Format()
}
contentMap := make(map[int64][]*model.PostContentFormated, len(postContents))
for _, content := range postContents {
contentMap[content.PostID] = append(contentMap[content.PostID], content.Format())
}
// 数据整合
for _, post := range posts {
post.User = userMap[post.UserID]
post.Contents = contentMap[post.ID]
}
return posts, nil
}

@ -6,59 +6,23 @@ import (
"github.com/sirupsen/logrus"
)
func (d *dataServant) IndexPosts(offset int, limit int) ([]*model.PostFormated, error) {
func (d *dataServant) IndexPosts(userId int64, offset int, limit int) ([]*model.PostFormated, error) {
if d.useCacheIndex {
if posts, err := d.cacheIndex.IndexPosts(offset, limit); err == nil {
logrus.Debugln("get index posts from cached")
if posts, err := d.cacheIndex.IndexPosts(userId, offset, limit); err == nil {
logrus.Debugf("get index posts from cached by userId: %d", userId)
return posts, nil
}
}
logrus.Debugf("get index posts from database but useCacheIndex: %t", d.useCacheIndex)
return d.getIndexPosts(offset, limit)
return d.getIndexPosts(userId, offset, limit)
}
func (d *dataServant) MergePosts(posts []*model.Post) ([]*model.PostFormated, error) {
postIds := make([]int64, 0, len(posts))
userIds := make([]int64, 0, len(posts))
for _, post := range posts {
postIds = append(postIds, post.ID)
userIds = append(userIds, post.UserID)
}
postContents, err := d.GetPostContentsByIDs(postIds)
if err != nil {
return nil, err
}
users, err := d.GetUsersByIDs(userIds)
if err != nil {
return nil, err
}
userMap := make(map[int64]*model.UserFormated, len(users))
for _, user := range users {
userMap[user.ID] = user.Format()
}
contentMap := make(map[int64][]*model.PostContentFormated, len(postContents))
for _, content := range postContents {
contentMap[content.PostID] = append(contentMap[content.PostID], content.Format())
}
// 数据整合
postsFormated := make([]*model.PostFormated, 0, len(posts))
for _, post := range posts {
postFormated := post.Format()
postFormated.User = userMap[post.UserID]
postFormated.Contents = contentMap[post.ID]
postsFormated = append(postsFormated, postFormated)
}
return postsFormated, nil
}
func (d *dataServant) getIndexPosts(offset int, limit int) ([]*model.PostFormated, error) {
// getIndexPosts _userId保留未来使用
// TODO: 未来可能根据userId查询广场推文列表简单做到不同用户的主页都是不同的
func (d *dataServant) getIndexPosts(_userId int64, offset int, limit int) ([]*model.PostFormated, error) {
posts, err := (&model.Post{}).List(d.engine, &model.ConditionsT{
"ORDER": "is_top DESC, latest_replied_on DESC",
"visibility IN ?": []model.PostVisibleT{model.PostVisitPublic, model.PostVisitFriend},
"ORDER": "is_top DESC, latest_replied_on DESC",
}, offset, limit)
if err != nil {
logrus.Debugf("getIndexPosts err: %v", err)

@ -1,165 +1,71 @@
package dao
import (
"github.com/meilisearch/meilisearch-go"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/pkg/zinc"
"github.com/sirupsen/logrus"
)
func (d *dataServant) CreateSearchIndex(indexName string) {
// 不存在则创建索引
d.zinc.CreateIndex(indexName, &zinc.ZincIndexProperty{
"id": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Store: true,
Sortable: true,
},
"user_id": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Store: true,
},
"comment_count": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
"collection_count": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
"upvote_count": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
"is_top": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
"is_essence": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
"content": &zinc.ZincIndexPropertyT{
Type: "text",
Index: true,
Store: true,
Aggregatable: true,
Highlightable: true,
Analyzer: "gse_search",
SearchAnalyzer: "gse_standard",
},
"tags": &zinc.ZincIndexPropertyT{
Type: "keyword",
Index: true,
Store: true,
},
"ip_loc": &zinc.ZincIndexPropertyT{
Type: "keyword",
Index: true,
Store: true,
},
"latest_replied_on": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
"attachment_price": &zinc.ZincIndexPropertyT{
Type: "numeric",
Sortable: true,
Store: true,
},
"created_on": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
"modified_on": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
})
var (
_ core.TweetSearchService = (*zincTweetSearchServant)(nil)
_ core.TweetSearchService = (*bridgeTweetSearchServant)(nil)
)
type documents struct {
primaryKey []string
docItems core.DocItems
identifiers []string
}
func (d *dataServant) BulkPushDoc(data []map[string]interface{}) (bool, error) {
return d.zinc.BulkPushDoc(data)
type bridgeTweetSearchServant struct {
ts core.TweetSearchService
updateDocsCh chan *documents
}
func (d *dataServant) DelDoc(indexName, id string) error {
return d.zinc.DelDoc(indexName, id)
type zincTweetSearchServant struct {
indexName string
client *zinc.ZincClient
}
func (d *dataServant) QueryAll(q *core.QueryT, indexName string, offset, limit int) (*zinc.QueryResultT, error) {
// 普通搜索
if q.Type == core.SearchTypeDefault && q.Query != "" {
return d.QuerySearch(indexName, q.Query, offset, limit)
}
// Tag分类
if q.Type == core.SearchTypeTag && q.Query != "" {
return d.QueryTagSearch(indexName, q.Query, offset, limit)
}
queryMap := map[string]interface{}{
"query": map[string]interface{}{
"match_all": map[string]string{},
},
"sort": []string{"-is_top", "-latest_replied_on"},
"from": offset,
"size": limit,
}
rsp, err := d.zinc.EsQuery(indexName, queryMap)
if err != nil {
return nil, err
}
return rsp, err
type meiliTweetSearchServant struct {
client *meilisearch.Client
index *meilisearch.Index
}
func (d *dataServant) QuerySearch(indexName, query string, offset, limit int) (*zinc.QueryResultT, error) {
rsp, err := d.zinc.EsQuery(indexName, map[string]interface{}{
"query": map[string]interface{}{
"match_phrase": map[string]interface{}{
"content": query,
},
},
"sort": []string{"-is_top", "-latest_replied_on"},
"from": offset,
"size": limit,
})
if err != nil {
return nil, err
func NewTweetSearchService() core.TweetSearchService {
bts := &bridgeTweetSearchServant{}
capacity := conf.TweetSearchSetting.MaxUpdateQPS
if capacity < 10 {
capacity = 10
} else if capacity > 10000 {
capacity = 10000
}
bts.updateDocsCh = make(chan *documents, capacity)
return rsp, err
}
if conf.CfgIf("Zinc") {
bts.ts = newZincTweetSearchServant()
} else if conf.CfgIf("Meili") {
bts.ts = newMeiliTweetSearchServant()
} else {
// default use Zinc as tweet search service
bts.ts = newZincTweetSearchServant()
}
logrus.Infof("use %s as tweet search serice by version %s", bts.ts.Name(), bts.ts.Version())
func (d *dataServant) QueryTagSearch(indexName, query string, offset, limit int) (*zinc.QueryResultT, error) {
rsp, err := d.zinc.ApiQuery(indexName, map[string]interface{}{
"search_type": "querystring",
"query": map[string]interface{}{
"term": "tags." + query + ":1",
},
"sort_fields": []string{"-is_top", "-latest_replied_on"},
"from": offset,
"max_results": limit,
})
if err != nil {
return nil, err
numWorker := conf.TweetSearchSetting.MinWorker
if numWorker < 5 {
numWorker = 5
} else if numWorker > 1000 {
numWorker = 1000
}
logrus.Debugf("use %d backend worker to update documents to search engine", numWorker)
// 启动文档更新器
for ; numWorker > 0; numWorker-- {
go bts.startUpdateDocs()
}
return rsp, err
return bts
}

@ -0,0 +1,71 @@
package dao
import (
"github.com/Masterminds/semver/v3"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/sirupsen/logrus"
)
func (s *bridgeTweetSearchServant) Name() string {
return "BridgeTweetSearch"
}
func (s *bridgeTweetSearchServant) Version() *semver.Version {
return semver.MustParse("v0.1.0")
}
func (s *bridgeTweetSearchServant) IndexName() string {
return s.ts.IndexName()
}
func (s *bridgeTweetSearchServant) AddDocuments(data core.DocItems, primaryKey ...string) (bool, error) {
s.updateDocs(&documents{
primaryKey: primaryKey,
docItems: data,
})
return true, nil
}
func (s *bridgeTweetSearchServant) DeleteDocuments(identifiers []string) error {
s.updateDocs(&documents{
identifiers: identifiers,
})
return nil
}
func (s *bridgeTweetSearchServant) Search(q *core.QueryReq, offset, limit int) (*core.QueryResp, error) {
return s.ts.Search(q, offset, limit)
}
func (s *bridgeTweetSearchServant) updateDocs(doc *documents) {
select {
case s.updateDocsCh <- doc:
logrus.Debugln("addDocuments send documents by chan")
default:
go func(item *documents) {
if len(item.docItems) > 0 {
if _, err := s.ts.AddDocuments(item.docItems, item.primaryKey...); err != nil {
logrus.Errorf("addDocuments in gorotine occurs error: %v", err)
}
} else if len(item.identifiers) > 0 {
if err := s.ts.DeleteDocuments(item.identifiers); err != nil {
logrus.Errorf("deleteDocuments in gorotine occurs error: %s", err)
}
}
}(doc)
}
}
func (s *bridgeTweetSearchServant) startUpdateDocs() {
for doc := range s.updateDocsCh {
if len(doc.docItems) > 0 {
if _, err := s.ts.AddDocuments(doc.docItems, doc.primaryKey...); err != nil {
logrus.Errorf("addDocuments occurs error: %v", err)
}
} else if len(doc.identifiers) > 0 {
if err := s.ts.DeleteDocuments(doc.identifiers); err != nil {
logrus.Errorf("deleteDocuments occurs error: %s", err)
}
}
}
}

@ -0,0 +1,139 @@
package dao
import (
"github.com/Masterminds/semver/v3"
"github.com/meilisearch/meilisearch-go"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/model"
"github.com/rocboss/paopao-ce/pkg/json"
"github.com/sirupsen/logrus"
)
func newMeiliTweetSearchServant() *meiliTweetSearchServant {
s := conf.MeiliSetting
client := meilisearch.NewClient(meilisearch.ClientConfig{
Host: s.Endpoint(),
APIKey: s.ApiKey,
})
if _, err := client.Index(s.Index).FetchInfo(); err != nil {
logrus.Debugf("create index because fetch index info error: %v", err)
client.CreateIndex(&meilisearch.IndexConfig{
Uid: s.Index,
PrimaryKey: "id",
})
searchableAttributes := []string{"content", "tags"}
sortableAttributes := []string{"is_top", "latest_replied_on"}
index := client.Index(s.Index)
index.UpdateSearchableAttributes(&searchableAttributes)
index.UpdateSortableAttributes(&sortableAttributes)
}
return &meiliTweetSearchServant{
client: client,
index: client.Index(s.Index),
}
}
func (s *meiliTweetSearchServant) Name() string {
return "Meili"
}
func (s *meiliTweetSearchServant) Version() *semver.Version {
return semver.MustParse("v0.1.0")
}
func (s *meiliTweetSearchServant) IndexName() string {
return s.index.UID
}
func (s *meiliTweetSearchServant) AddDocuments(data core.DocItems, primaryKey ...string) (bool, error) {
task, err := s.index.AddDocuments(data, primaryKey...)
if err != nil {
logrus.Errorf("meiliTweetSearchServant.AddDocuments error: %v", err)
return false, err
}
logrus.Debugf("meiliTweetSearchServant.AddDocuments task: %+v", task.Details)
return true, nil
}
func (s *meiliTweetSearchServant) DeleteDocuments(identifiers []string) error {
task, err := s.index.DeleteDocuments(identifiers)
if err != nil {
logrus.Errorf("meiliTweetSearchServant.DeleteDocuments error: %v", err)
return err
}
logrus.Debugf("meiliTweetSearchServant.DeleteDocuments task: %+v", task.Details)
return nil
}
func (s *meiliTweetSearchServant) Search(q *core.QueryReq, offset, limit int) (*core.QueryResp, error) {
if q.Type == core.SearchTypeDefault && q.Query != "" {
return s.queryByContent(q, offset, limit)
} else if q.Type == core.SearchTypeTag && q.Query != "" {
return s.queryByTag(q, offset, limit)
}
return s.queryAny(offset, limit)
}
func (s *meiliTweetSearchServant) queryByContent(q *core.QueryReq, offset, limit int) (*core.QueryResp, error) {
resp, err := s.index.Search(q.Query, &meilisearch.SearchRequest{
Offset: int64(offset),
Limit: int64(limit),
AttributesToRetrieve: []string{"*"},
Sort: []string{"is_top:desc", "latest_replied_on:desc"},
})
if err != nil {
return nil, err
}
return s.postsFrom(resp)
}
func (s *meiliTweetSearchServant) queryByTag(q *core.QueryReq, offset, limit int) (*core.QueryResp, error) {
resp, err := s.index.Search("#"+q.Query, &meilisearch.SearchRequest{
Offset: int64(offset),
Limit: int64(limit),
AttributesToRetrieve: []string{"*"},
Sort: []string{"is_top:desc", "latest_replied_on:desc"},
})
if err != nil {
return nil, err
}
return s.postsFrom(resp)
}
func (s *meiliTweetSearchServant) queryAny(offset, limit int) (*core.QueryResp, error) {
resp, err := s.index.Search("", &meilisearch.SearchRequest{
Offset: int64(offset),
Limit: int64(limit),
Matches: true,
Sort: []string{"is_top:desc", "latest_replied_on:desc"},
})
if err != nil {
return nil, err
}
return s.postsFrom(resp)
}
func (s *meiliTweetSearchServant) postsFrom(resp *meilisearch.SearchResponse) (*core.QueryResp, error) {
logrus.Debugf("resp Hits:%d NbHits:%d offset: %d limit:%d ", len(resp.Hits), resp.NbHits, resp.Offset, resp.Limit)
posts := make([]*model.PostFormated, 0, len(resp.Hits))
for _, hit := range resp.Hits {
item := &model.PostFormated{}
raw, err := json.Marshal(hit)
if err != nil {
return nil, err
}
if err = json.Unmarshal(raw, item); err != nil {
return nil, err
}
posts = append(posts, item)
}
return &core.QueryResp{
Items: posts,
Total: resp.NbHits,
}, nil
}

@ -0,0 +1,235 @@
package dao
import (
"github.com/Masterminds/semver/v3"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/model"
"github.com/rocboss/paopao-ce/pkg/json"
"github.com/rocboss/paopao-ce/pkg/zinc"
)
func newZincTweetSearchServant() *zincTweetSearchServant {
s := conf.ZincSetting
zts := &zincTweetSearchServant{
indexName: s.Index,
client: zinc.NewClient(s),
}
zts.createIndex()
return zts
}
func (s *zincTweetSearchServant) Name() string {
return "Zinc"
}
func (s *zincTweetSearchServant) Version() *semver.Version {
return semver.MustParse("v0.1.0")
}
func (s *zincTweetSearchServant) IndexName() string {
return s.indexName
}
func (s *zincTweetSearchServant) AddDocuments(data core.DocItems, primaryKey ...string) (bool, error) {
buf := make(core.DocItems, 0, len(data)+1)
if len(primaryKey) > 0 {
buf = append(buf, map[string]interface{}{
"index": map[string]interface{}{
"_index": s.indexName,
"_id": primaryKey[0],
},
})
} else {
buf = append(buf, map[string]interface{}{
"index": map[string]interface{}{
"_index": s.indexName,
},
})
}
buf = append(buf, data...)
return s.client.BulkPushDoc(buf)
}
func (s *zincTweetSearchServant) DeleteDocuments(identifiers []string) error {
for _, id := range identifiers {
if err := s.client.DelDoc(s.indexName, id); err != nil {
return err
}
}
return nil
}
func (s *zincTweetSearchServant) Search(q *core.QueryReq, offset, limit int) (*core.QueryResp, error) {
if q.Type == core.SearchTypeDefault && q.Query != "" {
return s.queryByContent(q, offset, limit)
} else if q.Type == core.SearchTypeTag && q.Query != "" {
return s.queryByTag(q, offset, limit)
}
return s.queryAny(offset, limit)
}
func (s *zincTweetSearchServant) queryByContent(q *core.QueryReq, offset, limit int) (*core.QueryResp, error) {
resp, err := s.client.EsQuery(s.indexName, map[string]interface{}{
"query": map[string]interface{}{
"match_phrase": map[string]interface{}{
"content": q.Query,
},
},
"sort": []string{"-is_top", "-latest_replied_on"},
"from": offset,
"size": limit,
})
if err != nil {
return nil, err
}
return s.postsFrom(resp)
}
func (s *zincTweetSearchServant) queryByTag(q *core.QueryReq, offset, limit int) (*core.QueryResp, error) {
resp, err := s.client.ApiQuery(s.indexName, map[string]interface{}{
"search_type": "querystring",
"query": map[string]interface{}{
"term": "tags." + q.Query + ":1",
},
"sort_fields": []string{"-is_top", "-latest_replied_on"},
"from": offset,
"max_results": limit,
})
if err != nil {
return nil, err
}
return s.postsFrom(resp)
}
func (s *zincTweetSearchServant) queryAny(offset, limit int) (*core.QueryResp, error) {
queryMap := map[string]interface{}{
"query": map[string]interface{}{
"match_all": map[string]string{},
},
"sort": []string{"-is_top", "-latest_replied_on"},
"from": offset,
"size": limit,
}
resp, err := s.client.EsQuery(s.indexName, queryMap)
if err != nil {
return nil, err
}
return s.postsFrom(resp)
}
func (s *zincTweetSearchServant) postsFrom(resp *zinc.QueryResultT) (*core.QueryResp, error) {
posts := make([]*model.PostFormated, 0, len(resp.Hits.Hits))
for _, hit := range resp.Hits.Hits {
item := &model.PostFormated{}
raw, err := json.Marshal(hit.Source)
if err != nil {
return nil, err
}
if err = json.Unmarshal(raw, item); err != nil {
return nil, err
}
posts = append(posts, item)
}
return &core.QueryResp{
Items: posts,
Total: resp.Hits.Total.Value,
}, nil
}
func (s *zincTweetSearchServant) createIndex() {
// 不存在则创建索引
s.client.CreateIndex(s.indexName, &zinc.ZincIndexProperty{
"id": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Store: true,
Sortable: true,
},
"user_id": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Store: true,
},
"comment_count": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
"collection_count": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
"upvote_count": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
"visibility": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
"is_top": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
"is_essence": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
"content": &zinc.ZincIndexPropertyT{
Type: "text",
Index: true,
Store: true,
Aggregatable: true,
Highlightable: true,
Analyzer: "gse_search",
SearchAnalyzer: "gse_standard",
},
"tags": &zinc.ZincIndexPropertyT{
Type: "keyword",
Index: true,
Store: true,
},
"ip_loc": &zinc.ZincIndexPropertyT{
Type: "keyword",
Index: true,
Store: true,
},
"latest_replied_on": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
"attachment_price": &zinc.ZincIndexPropertyT{
Type: "numeric",
Sortable: true,
Store: true,
},
"created_on": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
"modified_on": &zinc.ZincIndexPropertyT{
Type: "numeric",
Index: true,
Sortable: true,
Store: true,
},
})
}

@ -1,17 +1,32 @@
package dao
import "github.com/rocboss/paopao-ce/internal/model"
import (
"github.com/rocboss/paopao-ce/internal/model"
"gorm.io/gorm"
)
func (d *dataServant) CreateTag(tag *model.Tag) (*model.Tag, error) {
return d.createTag(d.engine, tag)
}
func (d *dataServant) DeleteTag(tag *model.Tag) error {
return d.deleteTag(d.engine, tag)
}
func (d *dataServant) GetTags(conditions *model.ConditionsT, offset, limit int) ([]*model.Tag, error) {
return (&model.Tag{}).List(d.engine, conditions, offset, limit)
}
func (d *dataServant) createTag(db *gorm.DB, tag *model.Tag) (*model.Tag, error) {
t, err := tag.Get(d.engine)
if err != nil {
tag.QuoteNum = 1
return tag.Create(d.engine)
return tag.Create(db)
}
// 更新
t.QuoteNum++
err = t.Update(d.engine)
err = t.Update(db)
if err != nil {
return nil, err
@ -20,16 +35,11 @@ func (d *dataServant) CreateTag(tag *model.Tag) (*model.Tag, error) {
return t, nil
}
func (d *dataServant) DeleteTag(tag *model.Tag) error {
tag, err := tag.Get(d.engine)
func (d *dataServant) deleteTag(db *gorm.DB, tag *model.Tag) error {
tag, err := tag.Get(db)
if err != nil {
return err
}
tag.QuoteNum--
return tag.Update(d.engine)
}
func (d *dataServant) GetTags(conditions *model.ConditionsT, offset, limit int) ([]*model.Tag, error) {
return (&model.Tag{}).List(d.engine, conditions, offset, limit)
return tag.Update(db)
}

@ -1,7 +1,6 @@
package dao
import (
"encoding/json"
"errors"
"fmt"
"math/rand"
@ -12,6 +11,7 @@ import (
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/internal/model"
"github.com/rocboss/paopao-ce/pkg/json"
"gopkg.in/resty.v1"
)
@ -158,3 +158,8 @@ func (d *dataServant) SendPhoneCaptcha(phone string) error {
captchaModel.Create(d.engine)
return nil
}
func (d *dataServant) IsFriend(_userID int64, _friendID int64) bool {
// TODO: you are friend in all now
return true
}

@ -74,3 +74,36 @@ func JWT() gin.HandlerFunc {
c.Next()
}
}
func JwtLoose() gin.HandlerFunc {
return func(c *gin.Context) {
token, exist := c.GetQuery("token")
if !exist {
token = c.GetHeader("Authorization")
// 验证前端传过来的token格式不为空开头为Bearer
if strings.HasPrefix(token, "Bearer ") {
// 验证通过提取有效部分除去Bearer)
token = token[7:]
} else {
c.Next()
}
}
if len(token) > 0 {
if claims, err := app.ParseToken(token); err == nil {
c.Set("UID", claims.UID)
c.Set("USERNAME", claims.Username)
// 加载用户信息
user := &model.User{
Model: &model.Model{
ID: claims.UID,
},
}
user, err := user.Get(conf.DBEngine)
if err == nil && (conf.JWTSetting.Issuer+":"+user.Salt) == claims.Issuer {
c.Set("USER", user)
}
}
}
c.Next()
}
}

@ -7,20 +7,31 @@ import (
"gorm.io/gorm"
)
// PostVisibleT 可访问类型0公开1私密2好友
type PostVisibleT uint8
const (
PostVisitPublic PostVisibleT = iota
PostVisitPrivate
PostVisitFriend
PostVisitInvalid
)
type Post struct {
*Model
UserID int64 `json:"user_id"`
CommentCount int64 `json:"comment_count"`
CollectionCount int64 `json:"collection_count"`
UpvoteCount int64 `json:"upvote_count"`
IsTop int `json:"is_top"`
IsEssence int `json:"is_essence"`
IsLock int `json:"is_lock"`
LatestRepliedOn int64 `json:"latest_replied_on"`
Tags string `json:"tags"`
AttachmentPrice int64 `json:"attachment_price"`
IP string `json:"ip"`
IPLoc string `json:"ip_loc"`
UserID int64 `json:"user_id"`
CommentCount int64 `json:"comment_count"`
CollectionCount int64 `json:"collection_count"`
UpvoteCount int64 `json:"upvote_count"`
Visibility PostVisibleT `json:"visibility"`
IsTop int `json:"is_top"`
IsEssence int `json:"is_essence"`
IsLock int `json:"is_lock"`
LatestRepliedOn int64 `json:"latest_replied_on"`
Tags string `json:"tags"`
AttachmentPrice int64 `json:"attachment_price"`
IP string `json:"ip"`
IPLoc string `json:"ip_loc"`
}
type PostFormated struct {
@ -31,6 +42,7 @@ type PostFormated struct {
CommentCount int64 `json:"comment_count"`
CollectionCount int64 `json:"collection_count"`
UpvoteCount int64 `json:"upvote_count"`
Visibility PostVisibleT `json:"visibility"`
IsTop int `json:"is_top"`
IsEssence int `json:"is_essence"`
IsLock int `json:"is_lock"`
@ -56,6 +68,7 @@ func (p *Post) Format() *PostFormated {
CommentCount: p.CommentCount,
CollectionCount: p.CollectionCount,
UpvoteCount: p.UpvoteCount,
Visibility: p.Visibility,
IsTop: p.IsTop,
IsEssence: p.IsEssence,
IsLock: p.IsLock,
@ -144,3 +157,18 @@ func (p *Post) Count(db *gorm.DB, conditions *ConditionsT) (int64, error) {
func (p *Post) Update(db *gorm.DB) error {
return db.Model(&Post{}).Where("id = ? AND is_del = ?", p.Model.ID, 0).Save(p).Error
}
func (p PostVisibleT) String() string {
switch p {
case PostVisitPublic:
return "public"
case PostVisitPrivate:
return "private"
case PostVisitFriend:
return "friend"
case PostVisitInvalid:
return "invalid"
default:
return "unknow"
}
}

@ -8,22 +8,26 @@ import (
type PostCollection struct {
*Model
Post *Post `json:"-"`
PostID int64 `json:"post_id"`
UserID int64 `json:"user_id"`
}
func (p *PostCollection) Get(db *gorm.DB) (*PostCollection, error) {
var star PostCollection
tn := db.NamingStrategy.TableName("PostCollection") + "."
if p.Model != nil && p.ID > 0 {
db = db.Where("id = ? AND is_del = ?", p.ID, 0)
db = db.Where(tn+"id = ? AND "+tn+"is_del = ?", p.ID, 0)
}
if p.PostID > 0 {
db = db.Where("post_id = ?", p.PostID)
db = db.Where(tn+"post_id = ?", p.PostID)
}
if p.UserID > 0 {
db = db.Where("user_id = ?", p.UserID)
db = db.Where(tn+"user_id = ?", p.UserID)
}
db = db.Joins("Post").Where("Post.visibility <> ?", PostVisitPrivate).Order("Post.id DESC")
err := db.First(&star).Error
if err != nil {
return &star, err
@ -33,13 +37,13 @@ func (p *PostCollection) Get(db *gorm.DB) (*PostCollection, error) {
}
func (p *PostCollection) Create(db *gorm.DB) (*PostCollection, error) {
err := db.Create(&p).Error
err := db.Omit("Post").Create(&p).Error
return p, err
}
func (p *PostCollection) Delete(db *gorm.DB) error {
return db.Model(&PostCollection{}).Where("id = ? AND is_del = ?", p.Model.ID, 0).Updates(map[string]interface{}{
return db.Model(&PostCollection{}).Omit("Post").Where("id = ? AND is_del = ?", p.Model.ID, 0).Updates(map[string]interface{}{
"deleted_on": time.Now().Unix(),
"is_del": 1,
}).Error
@ -48,22 +52,25 @@ func (p *PostCollection) Delete(db *gorm.DB) error {
func (p *PostCollection) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([]*PostCollection, error) {
var collections []*PostCollection
var err error
tn := db.NamingStrategy.TableName("PostCollection") + "."
if offset >= 0 && limit > 0 {
db = db.Offset(offset).Limit(limit)
}
if p.UserID > 0 {
db = db.Where("user_id = ?", p.UserID)
db = db.Where(tn+"user_id = ?", p.UserID)
}
for k, v := range *conditions {
if k == "ORDER" {
db = db.Order(v)
} else {
db = db.Where(k, v)
db = db.Where(tn+k, v)
}
}
if err = db.Where("is_del = ?", 0).Find(&collections).Error; err != nil {
db = db.Joins("Post").Where("Post.visibility <> ?", PostVisitPrivate).Order("Post.id DESC")
if err = db.Where(tn+"is_del = ?", 0).Find(&collections).Error; err != nil {
return nil, err
}
@ -72,17 +79,21 @@ func (p *PostCollection) List(db *gorm.DB, conditions *ConditionsT, offset, limi
func (p *PostCollection) Count(db *gorm.DB, conditions *ConditionsT) (int64, error) {
var count int64
tn := db.NamingStrategy.TableName("PostCollection") + "."
if p.PostID > 0 {
db = db.Where("post_id = ?", p.PostID)
db = db.Where(tn+"post_id = ?", p.PostID)
}
if p.UserID > 0 {
db = db.Where("user_id = ?", p.UserID)
db = db.Where(tn+"user_id = ?", p.UserID)
}
for k, v := range *conditions {
if k != "ORDER" {
db = db.Where(k, v)
db = db.Where(tn+k, v)
}
}
db = db.Joins("Post").Where("Post.visibility <> ?", PostVisitPrivate)
if err := db.Model(p).Count(&count).Error; err != nil {
return 0, err
}

@ -8,38 +8,40 @@ import (
type PostStar struct {
*Model
Post *Post `json:"-"`
PostID int64 `json:"post_id"`
UserID int64 `json:"user_id"`
}
func (p *PostStar) Get(db *gorm.DB) (*PostStar, error) {
var star PostStar
tn := db.NamingStrategy.TableName("PostStar") + "."
if p.Model != nil && p.ID > 0 {
db = db.Where("id = ? AND is_del = ?", p.ID, 0)
db = db.Where(tn+"id = ? AND "+tn+"is_del = ?", p.ID, 0)
}
if p.PostID > 0 {
db = db.Where("post_id = ?", p.PostID)
db = db.Where(tn+"post_id = ?", p.PostID)
}
if p.UserID > 0 {
db = db.Where("user_id = ?", p.UserID)
db = db.Where(tn+"user_id = ?", p.UserID)
}
err := db.First(&star).Error
if err != nil {
return &star, err
db = db.Joins("Post").Where("Post.visibility <> ?", PostVisitPrivate).Order("Post.id DESC")
if err := db.First(&star).Error; err != nil {
return nil, err
}
return &star, nil
}
func (p *PostStar) Create(db *gorm.DB) (*PostStar, error) {
err := db.Create(&p).Error
err := db.Omit("Post").Create(&p).Error
return p, err
}
func (p *PostStar) Delete(db *gorm.DB) error {
return db.Model(&PostStar{}).Where("id = ? AND is_del = ?", p.Model.ID, 0).Updates(map[string]interface{}{
return db.Model(&PostStar{}).Omit("Post").Where("id = ? AND is_del = ?", p.Model.ID, 0).Updates(map[string]interface{}{
"deleted_on": time.Now().Unix(),
"is_del": 1,
}).Error
@ -48,44 +50,49 @@ func (p *PostStar) Delete(db *gorm.DB) error {
func (p *PostStar) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([]*PostStar, error) {
var stars []*PostStar
var err error
tn := db.NamingStrategy.TableName("PostStar") + "."
if offset >= 0 && limit > 0 {
db = db.Offset(offset).Limit(limit)
}
if p.UserID > 0 {
db = db.Where("user_id = ?", p.UserID)
db = db.Where(tn+"user_id = ?", p.UserID)
}
for k, v := range *conditions {
if k == "ORDER" {
db = db.Order(v)
} else {
db = db.Where(k, v)
db = db.Where(tn+k, v)
}
}
if err = db.Where("is_del = ?", 0).Find(&stars).Error; err != nil {
db = db.Joins("Post").Where("Post.visibility <> ?", PostVisitPrivate).Order("Post.id DESC")
if err = db.Find(&stars).Error; err != nil {
return nil, err
}
return stars, nil
}
func (p *PostStar) Count(db *gorm.DB, conditions *ConditionsT) (int64, error) {
var count int64
tn := db.NamingStrategy.TableName("PostStar") + "."
if p.PostID > 0 {
db = db.Where("post_id = ?", p.PostID)
db = db.Where(tn+"post_id = ?", p.PostID)
}
if p.UserID > 0 {
db = db.Where("user_id = ?", p.UserID)
db = db.Where(tn+"user_id = ?", p.UserID)
}
for k, v := range *conditions {
if k != "ORDER" {
db = db.Where(k, v)
db = db.Where(tn+k, v)
}
}
db = db.Joins("Post").Where("Post.visibility <> ?", PostVisitPrivate)
if err := db.Model(p).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}

@ -12,5 +12,5 @@ var (
func Initialize() {
objectStorage = dao.NewObjectStorageService()
attachmentChecker = dao.NewAttachmentCheckerService()
attachmentChecker = dao.NewAttachmentCheckService()
}

@ -16,6 +16,7 @@ import (
"github.com/rocboss/paopao-ce/internal/service"
"github.com/rocboss/paopao-ce/pkg/app"
"github.com/rocboss/paopao-ce/pkg/convert"
"github.com/rocboss/paopao-ce/pkg/debug"
"github.com/rocboss/paopao-ce/pkg/errcode"
"github.com/rocboss/paopao-ce/pkg/util"
"github.com/sirupsen/logrus"
@ -29,7 +30,7 @@ const MAX_PHONE_CAPTCHA = 10
func Version(c *gin.Context) {
response := app.NewResponse(c)
response.ToResponse(gin.H{
"version": "PaoPao Service v1.0",
"BuildInfo": debug.ReadBuildInfo(),
})
}

@ -14,7 +14,7 @@ import (
func GetPostList(c *gin.Context) {
response := app.NewResponse(c)
q := &core.QueryT{
q := &core.QueryReq{
Query: c.Query("query"),
Type: "search",
}
@ -22,21 +22,23 @@ func GetPostList(c *gin.Context) {
q.Type = "tag"
}
userId, _ := userIdFrom(c)
offset, limit := app.GetPageOffset(c)
if q.Query == "" && q.Type == "search" {
offset, limit := app.GetPageOffset(c)
posts, err := service.GetIndexPosts(offset, limit)
posts, err := service.GetIndexPosts(userId, offset, limit)
if err != nil {
logrus.Errorf("service.GetPostList err: %v\n", err)
response.ToErrorResponse(errcode.GetPostsFailed)
return
}
totalRows, _ := service.GetPostCount(&model.ConditionsT{
"ORDER": "latest_replied_on DESC",
"visibility IN ?": []model.PostVisibleT{model.PostVisitPublic, model.PostVisitFriend},
"ORDER": "latest_replied_on DESC",
})
response.ToResponseList(posts, totalRows)
} else {
posts, totalRows, err := service.GetPostListFromSearch(q, (app.GetPage(c)-1)*app.GetPageSize(c), app.GetPageSize(c))
posts, totalRows, err := service.GetPostListFromSearch(q, offset, limit)
if err != nil {
logrus.Errorf("service.GetPostListFromSearch err: %v\n", err)
@ -155,11 +157,16 @@ func PostStar(c *gin.Context) {
star, err := service.GetPostStar(param.ID, userID.(int64))
if err != nil {
// 创建Star
service.CreatePostStar(param.ID, userID.(int64))
_, err = service.CreatePostStar(param.ID, userID.(int64))
status = true
} else {
// 取消Star
service.DeletePostStar(star)
err = service.DeletePostStar(star)
}
if err != nil {
response.ToErrorResponse(errcode.NoPermission)
return
}
response.ToResponse(gin.H{
@ -203,11 +210,16 @@ func PostCollection(c *gin.Context) {
collection, err := service.GetPostCollection(param.ID, userID.(int64))
if err != nil {
// 创建collection
service.CreatePostCollection(param.ID, userID.(int64))
_, err = service.CreatePostCollection(param.ID, userID.(int64))
status = true
} else {
// 取消Star
service.DeletePostCollection(collection)
err = service.DeletePostCollection(collection)
}
if err != nil {
response.ToErrorResponse(errcode.NoPermission)
return
}
response.ToResponse(gin.H{
@ -287,6 +299,28 @@ func StickPost(c *gin.Context) {
})
}
func VisiblePost(c *gin.Context) {
param := service.PostVisibilityReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
logrus.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
user, _ := userFrom(c)
if err := service.VisiblePost(user, param.ID, param.Visibility); err != nil {
logrus.Errorf("service.VisiblePost err: %v\n", err)
response.ToErrorResponse(err)
return
}
response.ToResponse(gin.H{
"visibility": param.Visibility,
})
}
func GetPostTags(c *gin.Context) {
param := service.PostTagsReq{}
response := app.NewResponse(c)

@ -313,13 +313,23 @@ func GetUserPosts(c *gin.Context) {
return
}
conditions := &model.ConditionsT{
"user_id": user.ID,
"ORDER": "latest_replied_on DESC",
visibilities := []model.PostVisibleT{model.PostVisitPublic}
if u, exists := c.Get("USER"); exists {
self := u.(*model.User)
if self.ID == user.ID || self.IsAdmin {
visibilities = append(visibilities, model.PostVisitPrivate, model.PostVisitFriend)
} else if service.IsFriend(user.ID, self.ID) {
visibilities = append(visibilities, model.PostVisitFriend)
}
}
conditions := model.ConditionsT{
"user_id": user.ID,
"visibility IN ?": visibilities,
"ORDER": "latest_replied_on DESC",
}
posts, err := service.GetPostList(&service.PostListReq{
Conditions: conditions,
Conditions: &conditions,
Offset: (app.GetPage(c) - 1) * app.GetPageSize(c),
Limit: app.GetPageSize(c),
})
@ -328,7 +338,7 @@ func GetUserPosts(c *gin.Context) {
response.ToErrorResponse(errcode.GetPostsFailed)
return
}
totalRows, _ := service.GetPostCount(conditions)
totalRows, _ := service.GetPostCount(&conditions)
response.ToResponseList(posts, totalRows)
}
@ -554,3 +564,19 @@ func GetUserWalletBills(c *gin.Context) {
response.ToResponseList(bills, totalRows)
}
func userFrom(c *gin.Context) (*model.User, bool) {
if u, exists := c.Get("USER"); exists {
user, ok := u.(*model.User)
return user, ok
}
return nil, false
}
func userIdFrom(c *gin.Context) (int64, bool) {
if u, exists := c.Get("UID"); exists {
uid, ok := u.(int64)
return uid, ok
}
return -1, false
}

@ -0,0 +1,13 @@
//go:build !docs
// +build !docs
package routers
import (
"github.com/gin-gonic/gin"
)
// registerDocs stub function for register docs asset route
func registerDocs(e *gin.Engine) {
// empty
}

@ -0,0 +1,14 @@
//go:build docs
// +build docs
package routers
import (
"github.com/gin-gonic/gin"
"github.com/rocboss/paopao-ce/docs"
)
// registerDocs register docs asset route
func registerDocs(e *gin.Engine) {
e.StaticFS("/docs", docs.NewFileSystem())
}

@ -24,8 +24,9 @@ func NewRouter() *gin.Engine {
corsConfig.AddAllowHeaders("Authorization")
e.Use(cors.New(corsConfig))
// 按需注册 静态资源、LocalOSS 路由
// 按需注册 docs、静态资源、LocalOSS 路由
{
registerDocs(e)
registerStatick(e)
routeLocalOSS(e)
}
@ -54,9 +55,6 @@ func NewRouter() *gin.Engine {
// 无鉴权路由组
noAuthApi := r.Group("/")
{
// 获取广场流
noAuthApi.GET("/posts", api.GetPostList)
// 获取动态详情
noAuthApi.GET("/post", api.GetPost)
@ -68,9 +66,16 @@ func NewRouter() *gin.Engine {
// 获取用户基本信息
noAuthApi.GET("/user/profile", api.GetUserProfile)
}
// 宽松鉴权路由组
looseApi := r.Group("/").Use(middleware.JwtLoose())
{
// 获取广场流
looseApi.GET("/posts", api.GetPostList)
// 获取用户动态列表
noAuthApi.GET("/user/posts", api.GetUserPosts)
looseApi.GET("/user/posts", api.GetUserPosts)
}
// 鉴权路由组
@ -162,6 +167,9 @@ func NewRouter() *gin.Engine {
// 置顶动态
privApi.POST("/post/stick", api.StickPost)
// 修改动态可见度
privApi.POST("/post/visibility", api.VisiblePost)
// 发布动态评论
privApi.POST("/post/comment", api.CreatePostComment)

@ -136,7 +136,7 @@ func CreatePostComment(ctx *gin.Context, userID int64, param CommentCreationReq)
ds.UpdatePost(post)
// 更新索引
go PushPostToSearch(post)
PushPostToSearch(post)
// 创建用户消息提醒
postMaster, err := ds.GetUserByID(post.UserID)
@ -251,7 +251,7 @@ func CreatePostCommentReply(ctx *gin.Context, commentID int64, content string, u
ds.UpdatePost(post)
// 更新索引
go PushPostToSearch(post)
PushPostToSearch(post)
// 创建用户消息提醒
commentMaster, err := ds.GetUserByID(comment.UserID)
@ -325,7 +325,7 @@ func DeletePostCommentReply(reply *model.CommentReply) error {
ds.UpdatePost(post)
// 更新索引
go PushPostToSearch(post)
PushPostToSearch(post)
return nil
}

@ -1,7 +1,7 @@
package service
import (
"encoding/json"
"errors"
"fmt"
"math"
"strings"
@ -11,8 +11,8 @@ import (
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/model"
"github.com/rocboss/paopao-ce/pkg/errcode"
"github.com/rocboss/paopao-ce/pkg/util"
"github.com/rocboss/paopao-ce/pkg/zinc"
"github.com/sirupsen/logrus"
)
@ -36,6 +36,7 @@ type PostCreationReq struct {
Tags []string `json:"tags" binding:"required"`
Users []string `json:"users" binding:"required"`
AttachmentPrice int64 `json:"attachment_price"`
Visibility model.PostVisibleT `json:"visibility"`
}
type PostDelReq struct {
@ -50,6 +51,11 @@ type PostStickReq struct {
ID int64 `json:"id" binding:"required"`
}
type PostVisibilityReq struct {
ID int64 `json:"id" binding:"required"`
Visibility model.PostVisibleT `json:"visibility"`
}
type PostStarReq struct {
ID int64 `json:"id" binding:"required"`
}
@ -83,30 +89,39 @@ func (p *PostContentItem) Check() error {
return nil
}
func tagsFrom(originTags []string) []string {
tags := make([]string, 0, len(originTags))
for _, tag := range originTags {
// TODO: 优化tag有效性检测
if tag = strings.TrimSpace(tag); len(tag) > 0 {
tags = append(tags, tag)
}
}
return tags
}
// CreatePost 创建文章
// TODO: maybe have bug need optimize for use transaction to create post
func CreatePost(c *gin.Context, userID int64, param PostCreationReq) (*model.Post, error) {
ip := c.ClientIP()
if len(ip) == 0 {
ip = "未知"
}
tags := tagsFrom(param.Tags)
post := &model.Post{
UserID: userID,
Tags: strings.Join(param.Tags, ","),
Tags: strings.Join(tags, ","),
IP: ip,
IPLoc: util.GetIPLoc(ip),
AttachmentPrice: param.AttachmentPrice,
Visibility: param.Visibility,
}
post, err := ds.CreatePost(post)
if err != nil {
return nil, err
}
// 创建标签
for _, t := range param.Tags {
tag := &model.Tag{
UserID: userID,
Tag: t,
}
ds.CreateTag(tag)
}
for _, item := range param.Contents {
if err = item.Check(); err != nil {
// 属性非法
@ -125,27 +140,42 @@ func CreatePost(c *gin.Context, userID int64, param PostCreationReq) (*model.Pos
Type: item.Type,
Sort: item.Sort,
}
ds.CreatePostContent(postContent)
if _, err := ds.CreatePostContent(postContent); err != nil {
return nil, err
}
}
// 推送Search
go PushPostToSearch(post)
// 创建用户消息提醒
for _, u := range param.Users {
user, err := ds.GetUserByUsername(u)
if err != nil || user.ID == userID {
continue
// TODO: 目前非私密文章才能有如下操作,后续再优化
if post.Visibility != model.PostVisitPrivate {
// 创建标签
for _, t := range tags {
tag := &model.Tag{
UserID: userID,
Tag: t,
}
if _, err := ds.CreateTag(tag); err != nil {
return nil, err
}
}
// 创建用户消息提醒
for _, u := range param.Users {
user, err := ds.GetUserByUsername(u)
if err != nil || user.ID == userID {
continue
}
// 创建消息提醒
go ds.CreateMessage(&model.Message{
SenderUserID: userID,
ReceiverUserID: user.ID,
Type: model.MESSAGE_POST,
Brief: "在新发布的泡泡动态中@了你",
PostID: post.ID,
})
// 创建消息提醒
// TODO: 优化消息提醒处理机制
go ds.CreateMessage(&model.Message{
SenderUserID: userID,
ReceiverUserID: user.ID,
Type: model.MESSAGE_POST,
Brief: "在新发布的泡泡动态中@了你",
PostID: post.ID,
})
}
// 推送Search
PushPostToSearch(post)
}
return post, nil
@ -171,7 +201,7 @@ func DeletePost(id int64) error {
}
// 删除索引
go DeleteSearchPost(post)
DeleteSearchPost(post)
return nil
}
@ -200,6 +230,45 @@ func StickPost(id int64) error {
return nil
}
func VisiblePost(user *model.User, postId int64, visibility model.PostVisibleT) *errcode.Error {
if visibility >= model.PostVisitInvalid {
return errcode.InvalidParams
}
post, err := ds.GetPostByID(postId)
if err != nil {
return errcode.GetPostFailed
}
if err := checkPermision(user, post.UserID); err != nil {
return err
}
// 相同属性,不需要操作了
oldVisibility := post.Visibility
if oldVisibility == visibility {
logrus.Infof("sample visibility no need operate postId: %d", postId)
return nil
}
if err = ds.VisiblePost(post, visibility); err != nil {
logrus.Warnf("update post failure: %v", err)
return errcode.VisblePostFailed
}
// 搜索处理
if oldVisibility == model.PostVisitPrivate {
// 从私密转为非私密需要push
logrus.Debugf("visible post set to re-public to add search index: %d, visibility: %s", post.ID, visibility)
PushPostToSearch(post)
} else if visibility == model.PostVisitPrivate {
// 从非私密转为私密需要删除索引
logrus.Debugf("visible post set to private to delete search index: %d, visibility: %s", post.ID, visibility)
DeleteSearchPost(post)
}
return nil
}
func GetPostStar(postID, userID int64) (*model.PostStar, error) {
return ds.GetUserPostStar(postID, userID)
}
@ -210,6 +279,12 @@ func CreatePostStar(postID, userID int64) (*model.PostStar, error) {
if err != nil {
return nil, err
}
// 私密post不可操作
if post.Visibility == model.PostVisitPrivate {
return nil, errors.New("no permision")
}
star, err := ds.CreatePostStar(postID, userID)
if err != nil {
return nil, err
@ -220,7 +295,7 @@ func CreatePostStar(postID, userID int64) (*model.PostStar, error) {
ds.UpdatePost(post)
// 更新索引
go PushPostToSearch(post)
PushPostToSearch(post)
return star, nil
}
@ -235,12 +310,18 @@ func DeletePostStar(star *model.PostStar) error {
if err != nil {
return err
}
// 私密post不可操作
if post.Visibility == model.PostVisitPrivate {
return errors.New("no permision")
}
// 更新Post点赞数
post.UpvoteCount--
ds.UpdatePost(post)
// 更新索引
go PushPostToSearch(post)
PushPostToSearch(post)
return nil
}
@ -255,6 +336,12 @@ func CreatePostCollection(postID, userID int64) (*model.PostCollection, error) {
if err != nil {
return nil, err
}
// 私密post不可操作
if post.Visibility == model.PostVisitPrivate {
return nil, errors.New("no permision")
}
collection, err := ds.CreatePostCollection(postID, userID)
if err != nil {
return nil, err
@ -265,7 +352,7 @@ func CreatePostCollection(postID, userID int64) (*model.PostCollection, error) {
ds.UpdatePost(post)
// 更新索引
go PushPostToSearch(post)
PushPostToSearch(post)
return collection, nil
}
@ -280,12 +367,18 @@ func DeletePostCollection(collection *model.PostCollection) error {
if err != nil {
return err
}
// 私密post不可操作
if post.Visibility == model.PostVisitPrivate {
return errors.New("no permision")
}
// 更新Post点赞数
post.CollectionCount--
ds.UpdatePost(post)
// 更新索引
go PushPostToSearch(post)
PushPostToSearch(post)
return nil
}
@ -324,8 +417,8 @@ func GetPostContentByID(id int64) (*model.PostContent, error) {
return ds.GetPostContentByID(id)
}
func GetIndexPosts(offset int, limit int) ([]*model.PostFormated, error) {
return ds.IndexPosts(offset, limit)
func GetIndexPosts(userId int64, offset int, limit int) ([]*model.PostFormated, error) {
return ds.IndexPosts(userId, offset, limit)
}
func GetPostList(req *PostListReq) ([]*model.PostFormated, error) {
@ -335,83 +428,38 @@ func GetPostList(req *PostListReq) ([]*model.PostFormated, error) {
return nil, err
}
return FormatPosts(posts)
}
func FormatPosts(posts []*model.Post) ([]*model.PostFormated, error) {
postIds := []int64{}
userIds := []int64{}
for _, post := range posts {
postIds = append(postIds, post.ID)
userIds = append(userIds, post.UserID)
}
postContents, err := ds.GetPostContentsByIDs(postIds)
if err != nil {
return nil, err
}
users, err := ds.GetUsersByIDs(userIds)
if err != nil {
return nil, err
}
// 数据整合
postsFormated := []*model.PostFormated{}
for _, post := range posts {
postFormated := post.Format()
for _, user := range users {
if user.ID == postFormated.UserID {
postFormated.User = user.Format()
}
}
for _, content := range postContents {
if content.PostID == post.ID {
postFormated.Contents = append(postFormated.Contents, content.Format())
}
}
postsFormated = append(postsFormated, postFormated)
}
return postsFormated, nil
return ds.MergePosts(posts)
}
func GetPostCount(conditions *model.ConditionsT) (int64, error) {
return ds.GetPostCount(conditions)
}
func GetPostListFromSearch(q *core.QueryT, offset, limit int) ([]*model.PostFormated, int64, error) {
queryResult, err := ds.QueryAll(q, conf.ZincSetting.Index, offset, limit)
func GetPostListFromSearch(q *core.QueryReq, offset, limit int) ([]*model.PostFormated, int64, error) {
resp, err := ts.Search(q, offset, limit)
if err != nil {
return nil, 0, err
}
posts, err := FormatZincPost(queryResult)
posts, err := ds.RevampPosts(resp.Items)
if err != nil {
return nil, 0, err
}
return posts, queryResult.Hits.Total.Value, nil
return posts, resp.Total, nil
}
func GetPostListFromSearchByQuery(query string, offset, limit int) ([]*model.PostFormated, int64, error) {
queryResult, err := ds.QuerySearch(conf.ZincSetting.Index, query, offset, limit)
if err != nil {
return nil, 0, err
}
posts, err := FormatZincPost(queryResult)
if err != nil {
return nil, 0, err
q := &core.QueryReq{
Query: query,
Type: "search",
}
return posts, queryResult.Hits.Total.Value, nil
return GetPostListFromSearch(q, offset, limit)
}
func PushPostToSearch(post *model.Post) {
indexName := conf.ZincSetting.Index
// TODO: 暂时不索引私密文章,后续再完善
if post.Visibility == model.PostVisitPrivate {
return
}
postFormated := post.Format()
postFormated.User = &model.UserFormated{
@ -435,18 +483,13 @@ func PushPostToSearch(post *model.Post) {
tagMaps[tag] = 1
}
data := []map[string]interface{}{}
data = append(data, map[string]interface{}{
"index": map[string]interface{}{
"_index": indexName,
"_id": fmt.Sprintf("%d", post.ID),
},
}, map[string]interface{}{
data := core.DocItems{{
"id": post.ID,
"user_id": post.UserID,
"comment_count": post.CommentCount,
"collection_count": post.CollectionCount,
"upvote_count": post.UpvoteCount,
"visibility": post.Visibility,
"is_top": post.IsTop,
"is_essence": post.IsEssence,
"content": contentFormated,
@ -456,36 +499,32 @@ func PushPostToSearch(post *model.Post) {
"attachment_price": post.AttachmentPrice,
"created_on": post.CreatedOn,
"modified_on": post.ModifiedOn,
})
}}
ds.BulkPushDoc(data)
ts.AddDocuments(data, fmt.Sprintf("%d", post.ID))
}
func DeleteSearchPost(post *model.Post) error {
indexName := conf.ZincSetting.Index
return ds.DelDoc(indexName, fmt.Sprintf("%d", post.ID))
return ts.DeleteDocuments([]string{fmt.Sprintf("%d", post.ID)})
}
func PushPostsToSearch(c *gin.Context) {
if ok, _ := conf.Redis.SetNX(c, "JOB_PUSH_TO_SEARCH", 1, time.Hour).Result(); ok {
splitNum := 1000
totalRows, _ := GetPostCount(&model.ConditionsT{})
totalRows, _ := GetPostCount(&model.ConditionsT{
"visibility IN ?": []model.PostVisibleT{model.PostVisitPublic, model.PostVisitFriend},
})
pages := math.Ceil(float64(totalRows) / float64(splitNum))
nums := int(pages)
indexName := conf.ZincSetting.Index
// 创建索引
ds.CreateSearchIndex(indexName)
for i := 0; i < nums; i++ {
data := []map[string]interface{}{}
posts, _ := GetPostList(&PostListReq{
Conditions: &model.ConditionsT{},
Offset: i * splitNum,
Limit: splitNum,
Conditions: &model.ConditionsT{
"visibility IN ?": []model.PostVisibleT{model.PostVisitPublic, model.PostVisitFriend},
},
Offset: i * splitNum,
Limit: splitNum,
})
for _, post := range posts {
@ -497,17 +536,13 @@ func PushPostsToSearch(c *gin.Context) {
}
}
data = append(data, map[string]interface{}{
"index": map[string]interface{}{
"_index": indexName,
"_id": fmt.Sprintf("%d", post.ID),
},
}, map[string]interface{}{
docs := core.DocItems{{
"id": post.ID,
"user_id": post.User.ID,
"comment_count": post.CommentCount,
"collection_count": post.CollectionCount,
"upvote_count": post.UpvoteCount,
"visibility": post.Visibility,
"is_top": post.IsTop,
"is_essence": post.IsEssence,
"content": contentFormated,
@ -517,11 +552,8 @@ func PushPostsToSearch(c *gin.Context) {
"attachment_price": post.AttachmentPrice,
"created_on": post.CreatedOn,
"modified_on": post.ModifiedOn,
})
}
if len(data) > 0 {
ds.BulkPushDoc(data)
}}
ts.AddDocuments(docs, fmt.Sprintf("%d", post.ID))
}
}
@ -529,54 +561,6 @@ func PushPostsToSearch(c *gin.Context) {
}
}
func FormatZincPost(queryResult *zinc.QueryResultT) ([]*model.PostFormated, error) {
posts := []*model.PostFormated{}
for _, hit := range queryResult.Hits.Hits {
item := &model.PostFormated{}
raw, _ := json.Marshal(hit.Source)
err := json.Unmarshal(raw, item)
if err == nil {
posts = append(posts, item)
}
}
postIds := []int64{}
userIds := []int64{}
for _, post := range posts {
postIds = append(postIds, post.ID)
userIds = append(userIds, post.UserID)
}
postContents, err := ds.GetPostContentsByIDs(postIds)
if err != nil {
return nil, err
}
users, err := ds.GetUsersByIDs(userIds)
if err != nil {
return nil, err
}
// 数据整合
for _, post := range posts {
for _, user := range users {
if user.ID == post.UserID {
post.User = user.Format()
}
}
if post.Contents == nil {
post.Contents = []*model.PostContentFormated{}
}
for _, content := range postContents {
if content.PostID == post.ID {
post.Contents = append(post.Contents, content.Format())
}
}
}
return posts, nil
}
func GetPostTags(param *PostTagsReq) ([]*model.TagFormated, error) {
num := param.Num
if num > conf.AppSetting.MaxPageSize {

@ -4,19 +4,18 @@ import (
"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/pkg/zinc"
)
var (
ds core.DataService
ts core.TweetSearchService
attachmentChecker core.AttachmentCheckService
DisablePhoneVerify bool
)
func Initialize() {
zincClient := zinc.NewClient(conf.ZincSetting)
ds = dao.NewDataService(conf.DBEngine, zincClient)
attachmentChecker = dao.NewAttachmentCheckerService()
ds = dao.NewDataService()
ts = dao.NewTweetSearchService()
attachmentChecker = dao.NewAttachmentCheckService()
DisablePhoneVerify = !conf.CfgIf("Sms")
}

@ -274,21 +274,11 @@ func GetUserCollections(userID int64, offset, limit int) ([]*model.PostFormated,
if err != nil {
return nil, 0, err
}
postIDs := []int64{}
var posts []*model.Post
for _, collection := range collections {
postIDs = append(postIDs, collection.PostID)
posts = append(posts, collection.Post)
}
// 获取Posts
posts, err := ds.GetPosts(&model.ConditionsT{
"id IN ?": postIDs,
"ORDER": "id DESC",
}, 0, 0)
if err != nil {
return nil, 0, err
}
postsFormated, err := FormatPosts(posts)
postsFormated, err := ds.MergePosts(posts)
if err != nil {
return nil, 0, err
}
@ -306,21 +296,12 @@ func GetUserStars(userID int64, offset, limit int) ([]*model.PostFormated, int64
if err != nil {
return nil, 0, err
}
postIDs := []int64{}
for _, star := range stars {
postIDs = append(postIDs, star.PostID)
}
// 获取Posts
posts, err := ds.GetPosts(&model.ConditionsT{
"id IN ?": postIDs,
"ORDER": "id DESC",
}, 0, 0)
if err != nil {
return nil, 0, err
var posts []*model.Post
for _, star := range stars {
posts = append(posts, star.Post)
}
postsFormated, err := FormatPosts(posts)
postsFormated, err := ds.MergePosts(posts)
if err != nil {
return nil, 0, err
}
@ -390,3 +371,15 @@ func GetSuggestTags(keyword string) ([]string, error) {
return ts, nil
}
func IsFriend(userId, friendId int64) bool {
return ds.IsFriend(userId, friendId)
}
// checkPermision 检查是否拥有者或管理员
func checkPermision(user *model.User, targetUserId int64) *errcode.Error {
if user == nil || (user.ID != targetUserId && !user.IsAdmin) {
return errcode.NoPermission
}
return nil
}

@ -11,11 +11,12 @@ import (
"github.com/rocboss/paopao-ce/internal"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/internal/routers"
"github.com/rocboss/paopao-ce/pkg/debug"
"github.com/rocboss/paopao-ce/pkg/util"
)
var (
version, buildDate, commitID string
version, commitID, buildDate string
noDefaultFeatures bool
features suites
@ -59,7 +60,7 @@ func main() {
MaxHeaderBytes: 1 << 20,
}
util.PrintHelloBanner(fmt.Sprintf("paopao %s (build:%s %s)", version, commitID, buildDate))
util.PrintHelloBanner(debug.VersionInfo())
fmt.Fprintf(color.Output, "PaoPao service listen on %s\n",
color.GreenString(fmt.Sprintf("http://%s:%s", conf.ServerSetting.HttpIp, conf.ServerSetting.HttpPort)),
)

@ -0,0 +1,25 @@
package debug
import (
"fmt"
)
var version, commitID, buildDate string
type BuildInfo struct {
Version string
Sum string
BuildDate string
}
func VersionInfo() string {
return fmt.Sprintf("paopao %s (build:%s %s)", version, commitID, buildDate)
}
func ReadBuildInfo() *BuildInfo {
return &BuildInfo{
Version: version,
Sum: commitID,
BuildDate: buildDate,
}
}

@ -35,6 +35,7 @@ var (
InsuffientDownloadMoney = NewError(30009, "附件下载失败:账户资金不足")
DownloadExecFail = NewError(30010, "附件下载失败:扣费失败")
StickPostFailed = NewError(30011, "动态置顶失败")
VisblePostFailed = NewError(30012, "更新可见性失败")
GetCommentsFailed = NewError(40001, "获取评论列表失败")
CreateCommentFailed = NewError(40002, "评论发布失败")

@ -0,0 +1,23 @@
// Copyright 2017 Bo-Yi Wu. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build go_json
// +build go_json
package json
import json "github.com/goccy/go-json"
var (
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
)

@ -0,0 +1,23 @@
// Copyright 2017 Bo-Yi Wu. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !jsoniter && !go_json
// +build !jsoniter,!go_json
package json
import "encoding/json"
var (
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
)

@ -0,0 +1,24 @@
// Copyright 2017 Bo-Yi Wu. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build jsoniter
// +build jsoniter
package json
import jsoniter "github.com/json-iterator/go"
var (
json = jsoniter.ConfigCompatibleWithStandardLibrary
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
)

@ -25,7 +25,12 @@ func Find(ip string) (string, string) {
if strings.Trim(ip, " ") == "" {
return "", ""
}
offset := searchIndex(binary.BigEndian.Uint32(net.ParseIP(ip).To4()))
// If ip is "::1", To4 returns nil.
to4 := net.ParseIP(ip).To4()
if to4 == nil {
to4 = net.ParseIP("127.0.0.1").To4()
}
offset := searchIndex(binary.BigEndian.Uint32(to4))
if offset <= 0 {
return "", ""
}

@ -1,7 +1,6 @@
package zinc
import (
"encoding/json"
"errors"
"fmt"
"net/http"
@ -9,6 +8,7 @@ import (
"github.com/go-resty/resty/v2"
"github.com/rocboss/paopao-ce/internal/conf"
"github.com/rocboss/paopao-ce/pkg/json"
)
type ZincClient struct {
@ -74,7 +74,7 @@ type HitItem struct {
func NewClient(conf *conf.ZincSettingS) *ZincClient {
return &ZincClient{
ZincClientConfig: &ZincClientConfig{
ZincHost: conf.Host,
ZincHost: conf.Endpoint(),
ZincUser: conf.User,
ZincPassword: conf.Password,
},
@ -90,9 +90,7 @@ func (c *ZincClient) CreateIndex(name string, p *ZincIndexProperty) bool {
Properties: p,
},
}
client := resty.New() // 创建一个restry客户端
client.DisableWarn = true
resp, err := client.R().SetBody(data).SetBasicAuth(c.ZincUser, c.ZincPassword).Put(c.ZincHost + "/api/index")
resp, err := c.request().SetBody(data).Put("/api/index")
if err != nil || resp.StatusCode() != http.StatusOK {
return false
@ -103,9 +101,7 @@ func (c *ZincClient) CreateIndex(name string, p *ZincIndexProperty) bool {
// 检查索引是否存在
func (c *ZincClient) ExistIndex(name string) bool {
client := resty.New()
client.DisableWarn = true
resp, err := client.R().SetBasicAuth(c.ZincUser, c.ZincPassword).Get(c.ZincHost + "/api/index")
resp, err := c.request().Get("/api/index")
if err != nil || resp.StatusCode() != http.StatusOK {
return false
@ -126,9 +122,7 @@ func (c *ZincClient) ExistIndex(name string) bool {
// 新增/更新文档
func (c *ZincClient) PutDoc(name string, id int64, doc interface{}) (bool, error) {
client := resty.New()
client.DisableWarn = true
resp, err := client.R().SetBody(doc).SetBasicAuth(c.ZincUser, c.ZincPassword).Put(fmt.Sprintf("%s/api/%s/_doc/%d", c.ZincHost, name, id))
resp, err := c.request().SetBody(doc).Put(fmt.Sprintf("/api/%s/_doc/%d", name, id))
if err != nil {
return false, err
@ -151,9 +145,7 @@ func (c *ZincClient) BulkPushDoc(docs []map[string]interface{}) (bool, error) {
}
}
client := resty.New()
client.DisableWarn = true
resp, err := client.R().SetBody(dataStr).SetBasicAuth(c.ZincUser, c.ZincPassword).Post(fmt.Sprintf("%s/api/_bulk", c.ZincHost))
resp, err := c.request().SetBody(dataStr).Post("/api/_bulk")
if err != nil {
return false, err
}
@ -166,9 +158,7 @@ func (c *ZincClient) BulkPushDoc(docs []map[string]interface{}) (bool, error) {
}
func (c *ZincClient) EsQuery(indexName string, q interface{}) (*QueryResultT, error) {
client := resty.New()
client.DisableWarn = true
resp, err := client.R().SetBody(q).SetBasicAuth(c.ZincUser, c.ZincPassword).Post(fmt.Sprintf("%s/es/%s/_search", c.ZincHost, indexName))
resp, err := c.request().SetBody(q).Post(fmt.Sprintf("/es/%s/_search", indexName))
if err != nil {
return nil, err
}
@ -187,9 +177,7 @@ func (c *ZincClient) EsQuery(indexName string, q interface{}) (*QueryResultT, er
}
func (c *ZincClient) ApiQuery(indexName string, q interface{}) (*QueryResultT, error) {
client := resty.New()
client.DisableWarn = true
resp, err := client.R().SetBody(q).SetBasicAuth(c.ZincUser, c.ZincPassword).Post(fmt.Sprintf("%s/api/%s/_search", c.ZincHost, indexName))
resp, err := c.request().SetBody(q).Post(fmt.Sprintf("/api/%s/_search", indexName))
if err != nil {
return nil, err
}
@ -208,9 +196,7 @@ func (c *ZincClient) ApiQuery(indexName string, q interface{}) (*QueryResultT, e
}
func (c *ZincClient) DelDoc(indexName, id string) error {
client := resty.New()
client.DisableWarn = true
resp, err := client.R().SetBasicAuth(c.ZincUser, c.ZincPassword).Delete(fmt.Sprintf("%s/api/%s/_doc/%s", c.ZincHost, indexName, id))
resp, err := c.request().Delete(fmt.Sprintf("/api/%s/_doc/%s", indexName, id))
if err != nil {
return err
}
@ -221,3 +207,12 @@ func (c *ZincClient) DelDoc(indexName, id string) error {
return nil
}
func (c *ZincClient) request() *resty.Request {
client := resty.New()
client.DisableWarn = true
client.SetBaseURL(c.ZincHost)
client.SetBasicAuth(c.ZincUser, c.ZincPassword)
return client.R()
}

@ -0,0 +1,9 @@
-- ----------------------------
-- Table p_post alter add visibility column
-- ----------------------------
ALTER TABLE `p_post` ADD COLUMN `visibility` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '可见性 0公开 1私密 2好友可见';
-- ----------------------------
-- Indexes structure for table p_post
-- ----------------------------
CREATE INDEX `idx_visibility` ON `p_post` ( `visibility` ) USING BTREE;

@ -0,0 +1,310 @@
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for p_attachment
-- ----------------------------
DROP TABLE IF EXISTS `p_attachment`;
CREATE TABLE `p_attachment` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned NOT NULL DEFAULT '0',
`file_size` bigint unsigned NOT NULL,
`img_width` bigint unsigned NOT NULL DEFAULT '0',
`img_height` bigint unsigned NOT NULL DEFAULT '0',
`type` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '1图片2视频3其他附件',
`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`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 为已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_user` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=100041 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='附件';
-- ----------------------------
-- Table structure for p_captcha
-- ----------------------------
DROP TABLE IF EXISTS `p_captcha`;
CREATE TABLE `p_captcha` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '验证码ID',
`phone` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '手机号',
`captcha` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '验证码',
`use_times` int unsigned NOT NULL DEFAULT '0' COMMENT '使用次数',
`expired_on` bigint unsigned NOT NULL DEFAULT '0' COMMENT '过期时间',
`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 为已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_phone` (`phone`) USING BTREE,
KEY `idx_expired_on` (`expired_on`) USING BTREE,
KEY `idx_use_times` (`use_times`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1021 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='手机验证码';
-- ----------------------------
-- Table structure for p_comment
-- ----------------------------
DROP TABLE IF EXISTS `p_comment`;
CREATE TABLE `p_comment` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '评论ID',
`post_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'POST ID',
`user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`ip` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'IP地址',
`ip_loc` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'IP城市地址',
`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 为已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_post` (`post_id`) USING BTREE,
KEY `idx_user` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=6001736 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='评论';
-- ----------------------------
-- Table structure for p_comment_content
-- ----------------------------
DROP TABLE IF EXISTS `p_comment_content`;
CREATE TABLE `p_comment_content` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '内容ID',
`comment_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '评论ID',
`user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '内容',
`type` tinyint unsigned NOT NULL DEFAULT '2' COMMENT '类型1标题2文字段落3图片地址4视频地址5语音地址6链接地址',
`sort` bigint unsigned NOT NULL DEFAULT '100' COMMENT '排序,越小越靠前',
`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 为已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_reply` (`comment_id`) USING BTREE,
KEY `idx_user` (`user_id`) USING BTREE,
KEY `idx_type` (`type`) USING BTREE,
KEY `idx_sort` (`sort`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=11001738 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='评论内容';
-- ----------------------------
-- Table structure for p_comment_reply
-- ----------------------------
DROP TABLE IF EXISTS `p_comment_reply`;
CREATE TABLE `p_comment_reply` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '回复ID',
`comment_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '评论ID',
`user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`at_user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '@用户ID',
`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '内容',
`ip` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'IP地址',
`ip_loc` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'IP城市地址',
`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 为已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_comment` (`comment_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=12000015 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='评论回复';
-- ----------------------------
-- Table structure for p_message
-- ----------------------------
DROP TABLE IF EXISTS `p_message`;
CREATE TABLE `p_message` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '消息通知ID',
`sender_user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '发送方用户ID',
`receiver_user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '接收方用户ID',
`type` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '通知类型1动态2评论3回复4私信99系统通知',
`brief` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '摘要说明',
`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '详细内容',
`post_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '动态ID',
`comment_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '评论ID',
`reply_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '回复ID',
`is_read` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否已读',
`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 为已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_receiver` (`receiver_user_id`) USING BTREE,
KEY `idx_is_read` (`is_read`) USING BTREE,
KEY `idx_type` (`type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=16000033 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='消息通知';
-- ----------------------------
-- Table structure for p_post
-- ----------------------------
DROP TABLE IF EXISTS `p_post`;
CREATE TABLE `p_post` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主题ID',
`user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`comment_count` bigint unsigned NOT NULL DEFAULT '0' COMMENT '评论数',
`collection_count` bigint unsigned NOT NULL DEFAULT '0' COMMENT '收藏数',
`upvote_count` bigint unsigned NOT NULL DEFAULT '0' COMMENT '点赞数',
`is_top` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否置顶',
`is_essence` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否精华',
`is_lock` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否锁定',
`latest_replied_on` bigint unsigned NOT NULL DEFAULT '0' COMMENT '最新回复时间',
`tags` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '标签',
`attachment_price` bigint unsigned NOT NULL DEFAULT '0' COMMENT '附件价格(分)',
`ip` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'IP地址',
`ip_loc` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'IP城市地址',
`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 为已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_user` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1080017989 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='冒泡/文章';
-- ----------------------------
-- Table structure for p_post_attachment_bill
-- ----------------------------
DROP TABLE IF EXISTS `p_post_attachment_bill`;
CREATE TABLE `p_post_attachment_bill` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '购买记录ID',
`post_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'POST ID',
`user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`paid_amount` bigint unsigned NOT NULL DEFAULT '0' COMMENT '支付金额',
`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 为已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_post` (`post_id`) USING BTREE,
KEY `idx_user` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5000002 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='冒泡/文章附件账单';
-- ----------------------------
-- Table structure for p_post_collection
-- ----------------------------
DROP TABLE IF EXISTS `p_post_collection`;
CREATE TABLE `p_post_collection` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '收藏ID',
`post_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'POST ID',
`user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`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 为已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_post` (`post_id`) USING BTREE,
KEY `idx_user` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=6000012 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='冒泡/文章收藏';
-- ----------------------------
-- Table structure for p_post_content
-- ----------------------------
DROP TABLE IF EXISTS `p_post_content`;
CREATE TABLE `p_post_content` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '内容ID',
`post_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'POST ID',
`user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`content` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '内容',
`type` tinyint unsigned NOT NULL DEFAULT '2' COMMENT '类型1标题2文字段落3图片地址4视频地址5语音地址6链接地址7附件资源8收费资源',
`sort` int unsigned NOT NULL DEFAULT '100' COMMENT '排序,越小越靠前',
`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 为已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_post` (`post_id`) USING BTREE,
KEY `idx_user` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=180022546 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='冒泡/文章内容';
-- ----------------------------
-- Table structure for p_post_star
-- ----------------------------
DROP TABLE IF EXISTS `p_post_star`;
CREATE TABLE `p_post_star` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '收藏ID',
`post_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'POST ID',
`user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`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 为已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_post` (`post_id`) USING BTREE,
KEY `idx_user` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=6000028 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='冒泡/文章点赞';
-- ----------------------------
-- Table structure for p_tag
-- ----------------------------
DROP TABLE IF EXISTS `p_tag`;
CREATE TABLE `p_tag` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '标签ID',
`user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '创建者ID',
`tag` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '标签名',
`quote_num` bigint unsigned NOT NULL DEFAULT '0' COMMENT '引用数',
`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 为已删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `idx_tag` (`tag`) USING BTREE,
KEY `idx_user` (`user_id`) USING BTREE,
KEY `idx_num` (`quote_num`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=9000065 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='标签';
-- ----------------------------
-- Table structure for p_user
-- ----------------------------
DROP TABLE IF EXISTS `p_user`;
CREATE TABLE `p_user` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`nickname` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '昵称',
`username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名',
`phone` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '手机号',
`password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'MD5密码',
`salt` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '盐值',
`status` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '状态1正常2停用',
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户头像',
`balance` bigint unsigned NOT NULL COMMENT '用户余额(分)',
`is_admin` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否管理员',
`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 为已删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `idx_username` (`username`) USING BTREE,
KEY `idx_phone` (`phone`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=100058 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户';
-- ----------------------------
-- Table structure for p_wallet_recharge
-- ----------------------------
DROP TABLE IF EXISTS `p_wallet_recharge`;
CREATE TABLE `p_wallet_recharge` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '充值ID',
`user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`amount` bigint NOT NULL DEFAULT '0' COMMENT '充值金额',
`trade_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '支付宝订单号',
`trade_status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '交易状态',
`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 为已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_user` (`user_id`) USING BTREE,
KEY `idx_trade_no` (`trade_no`) USING BTREE,
KEY `idx_trade_status` (`trade_status`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=10023 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='钱包流水';
-- ----------------------------
-- Table structure for p_wallet_statement
-- ----------------------------
DROP TABLE IF EXISTS `p_wallet_statement`;
CREATE TABLE `p_wallet_statement` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '账单ID',
`user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`change_amount` bigint NOT NULL DEFAULT '0' COMMENT '变动金额',
`balance_snapshot` bigint NOT NULL DEFAULT '0' COMMENT '资金快照',
`reason` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '变动原因',
`post_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '关联动态',
`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 为已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_user` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=10010 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='钱包流水';
SET FOREIGN_KEY_CHECKS = 1;

@ -0,0 +1,309 @@
PRAGMA foreign_keys = false;
-- ----------------------------
-- Table structure for p_attachment
-- ----------------------------
DROP TABLE IF EXISTS "p_attachment";
CREATE TABLE "p_attachment" (
"id" integer NOT NULL,
"user_id" integer NOT NULL,
"file_size" integer NOT NULL,
"img_width" integer NOT NULL,
"img_height" integer NOT NULL,
"type" integer NOT NULL,
"content" text(255) NOT NULL,
"created_on" integer NOT NULL,
"modified_on" integer NOT NULL,
"deleted_on" integer NOT NULL,
"is_del" integer NOT NULL,
PRIMARY KEY ("id")
);
-- ----------------------------
-- Table structure for p_captcha
-- ----------------------------
DROP TABLE IF EXISTS "p_captcha";
CREATE TABLE "p_captcha" (
"id" integer NOT NULL,
"phone" text(16) NOT NULL,
"captcha" text(16) NOT NULL,
"use_times" integer NOT NULL,
"expired_on" integer NOT NULL,
"created_on" integer NOT NULL,
"modified_on" integer NOT NULL,
"deleted_on" integer NOT NULL,
"is_del" integer NOT NULL,
PRIMARY KEY ("id")
);
-- ----------------------------
-- Table structure for p_comment
-- ----------------------------
DROP TABLE IF EXISTS "p_comment";
CREATE TABLE "p_comment" (
"id" integer NOT NULL,
"post_id" integer NOT NULL,
"user_id" integer NOT NULL,
"ip" text(64) NOT NULL,
"ip_loc" text(64) NOT NULL,
"created_on" integer NOT NULL,
"modified_on" integer NOT NULL,
"deleted_on" integer NOT NULL,
"is_del" integer NOT NULL,
PRIMARY KEY ("id")
);
-- ----------------------------
-- Table structure for p_comment_content
-- ----------------------------
DROP TABLE IF EXISTS "p_comment_content";
CREATE TABLE "p_comment_content" (
"id" integer NOT NULL,
"comment_id" integer NOT NULL,
"user_id" integer NOT NULL,
"content" text(255) NOT NULL,
"type" integer NOT NULL,
"sort" integer NOT NULL,
"created_on" integer NOT NULL,
"modified_on" integer NOT NULL,
"deleted_on" integer NOT NULL,
"is_del" integer NOT NULL,
PRIMARY KEY ("id")
);
-- ----------------------------
-- Table structure for p_comment_reply
-- ----------------------------
DROP TABLE IF EXISTS "p_comment_reply";
CREATE TABLE "p_comment_reply" (
"id" integer NOT NULL,
"comment_id" integer NOT NULL,
"user_id" integer NOT NULL,
"at_user_id" integer NOT NULL,
"content" text(255) NOT NULL,
"ip" text(64) NOT NULL,
"ip_loc" text(64) NOT NULL,
"created_on" integer NOT NULL,
"modified_on" integer NOT NULL,
"deleted_on" integer NOT NULL,
"is_del" integer NOT NULL,
PRIMARY KEY ("id")
);
-- ----------------------------
-- Table structure for p_message
-- ----------------------------
DROP TABLE IF EXISTS "p_message";
CREATE TABLE "p_message" (
"id" integer NOT NULL,
"sender_user_id" integer NOT NULL,
"receiver_user_id" integer NOT NULL,
"type" integer NOT NULL,
"brief" text(255) NOT NULL,
"content" text(255) NOT NULL,
"post_id" integer NOT NULL,
"comment_id" integer NOT NULL,
"reply_id" integer NOT NULL,
"is_read" integer NOT NULL,
"created_on" integer NOT NULL,
"modified_on" integer NOT NULL,
"deleted_on" integer NOT NULL,
"is_del" integer NOT NULL,
PRIMARY KEY ("id")
);
-- ----------------------------
-- Table structure for p_post
-- ----------------------------
DROP TABLE IF EXISTS "p_post";
CREATE TABLE "p_post" (
"id" integer NOT NULL,
"user_id" integer NOT NULL,
"comment_count" integer NOT NULL,
"collection_count" integer NOT NULL,
"upvote_count" integer NOT NULL,
"is_top" integer NOT NULL,
"is_essence" integer NOT NULL,
"is_lock" integer NOT NULL,
"latest_replied_on" integer NOT NULL,
"tags" text(255) NOT NULL,
"attachment_price" integer NOT NULL,
"ip" text(64) NOT NULL,
"ip_loc" text(64) NOT NULL,
"created_on" integer NOT NULL,
"modified_on" integer NOT NULL,
"deleted_on" integer NOT NULL,
"is_del" integer NOT NULL,
PRIMARY KEY ("id")
);
-- ----------------------------
-- Table structure for p_post_attachment_bill
-- ----------------------------
DROP TABLE IF EXISTS "p_post_attachment_bill";
CREATE TABLE "p_post_attachment_bill" (
"id" integer NOT NULL,
"post_id" integer NOT NULL,
"user_id" integer NOT NULL,
"paid_amount" integer NOT NULL,
"created_on" integer NOT NULL,
"modified_on" integer NOT NULL,
"deleted_on" integer NOT NULL,
"is_del" integer NOT NULL,
PRIMARY KEY ("id")
);
-- ----------------------------
-- Table structure for p_post_collection
-- ----------------------------
DROP TABLE IF EXISTS "p_post_collection";
CREATE TABLE "p_post_collection" (
"id" integer NOT NULL,
"post_id" integer NOT NULL,
"user_id" integer NOT NULL,
"created_on" integer NOT NULL,
"modified_on" integer NOT NULL,
"deleted_on" integer NOT NULL,
"is_del" integer NOT NULL,
PRIMARY KEY ("id")
);
-- ----------------------------
-- Table structure for p_post_content
-- ----------------------------
DROP TABLE IF EXISTS "p_post_content";
CREATE TABLE "p_post_content" (
"id" integer NOT NULL,
"post_id" integer NOT NULL,
"user_id" integer NOT NULL,
"content" text(2000) NOT NULL,
"type" integer NOT NULL,
"sort" integer NOT NULL,
"created_on" integer NOT NULL,
"modified_on" integer NOT NULL,
"deleted_on" integer NOT NULL,
"is_del" integer NOT NULL,
PRIMARY KEY ("id")
);
-- ----------------------------
-- Table structure for p_post_star
-- ----------------------------
DROP TABLE IF EXISTS "p_post_star";
CREATE TABLE "p_post_star" (
"id" integer NOT NULL,
"post_id" integer NOT NULL,
"user_id" integer NOT NULL,
"created_on" integer NOT NULL,
"modified_on" integer NOT NULL,
"deleted_on" integer NOT NULL,
"is_del" integer NOT NULL,
PRIMARY KEY ("id")
);
-- ----------------------------
-- Table structure for p_tag
-- ----------------------------
DROP TABLE IF EXISTS "p_tag";
CREATE TABLE "p_tag" (
"id" integer NOT NULL,
"user_id" integer NOT NULL,
"tag" text(255) NOT NULL,
"quote_num" integer NOT NULL,
"created_on" integer NOT NULL,
"modified_on" integer NOT NULL,
"deleted_on" integer NOT NULL,
"is_del" integer NOT NULL,
PRIMARY KEY ("id")
);
-- ----------------------------
-- Table structure for p_user
-- ----------------------------
DROP TABLE IF EXISTS "p_user";
CREATE TABLE "p_user" (
"id" integer NOT NULL,
"nickname" text(32) NOT NULL,
"username" text(32) NOT NULL,
"phone" text(16) NOT NULL,
"password" text(32) NOT NULL,
"salt" text(16) NOT NULL,
"status" integer NOT NULL,
"avatar" text(255) NOT NULL,
"balance" integer NOT NULL,
"is_admin" integer NOT NULL,
"created_on" integer NOT NULL,
"modified_on" integer NOT NULL,
"deleted_on" integer NOT NULL,
"is_del" integer NOT NULL,
PRIMARY KEY ("id")
);
-- ----------------------------
-- Table structure for p_wallet_recharge
-- ----------------------------
DROP TABLE IF EXISTS "p_wallet_recharge";
CREATE TABLE "p_wallet_recharge" (
"id" integer NOT NULL,
"user_id" integer NOT NULL,
"amount" integer NOT NULL,
"trade_no" text(64) NOT NULL,
"trade_status" text(32) NOT NULL,
"created_on" integer NOT NULL,
"modified_on" integer NOT NULL,
"deleted_on" integer NOT NULL,
"is_del" integer NOT NULL,
PRIMARY KEY ("id")
);
-- ----------------------------
-- Table structure for p_wallet_statement
-- ----------------------------
DROP TABLE IF EXISTS "p_wallet_statement";
CREATE TABLE "p_wallet_statement" (
"id" integer NOT NULL,
"user_id" integer NOT NULL,
"change_amount" integer NOT NULL,
"balance_snapshot" integer NOT NULL,
"reason" text(255) NOT NULL,
"post_id" integer NOT NULL,
"created_on" integer NOT NULL,
"modified_on" integer NOT NULL,
"deleted_on" integer NOT NULL,
"is_del" integer NOT NULL,
PRIMARY KEY ("id")
);
-- ----------------------------
-- Indexes structure for table p_attachment
-- ----------------------------
CREATE INDEX "main"."idx_user"
ON "p_attachment" (
"user_id" ASC
);
-- ----------------------------
-- Indexes structure for table p_captcha
-- ----------------------------
CREATE INDEX "main"."idx_expired_on"
ON "p_captcha" (
"expired_on" ASC
);
CREATE INDEX "main"."idx_phone"
ON "p_captcha" (
"phone" ASC
);
CREATE INDEX "main"."idx_use_times"
ON "p_captcha" (
"use_times" ASC
);
-- ----------------------------
-- Indexes structure for table p_comment
-- ----------------------------
CREATE INDEX "main"."idx_post"
ON "p_comment" (
"post_id" ASC
);
PRAGMA foreign_keys = true;

@ -0,0 +1,12 @@
-- ----------------------------
-- Table p_post alter add visibility column
-- ----------------------------
ALTER TABLE `p_post` ADD COLUMN `visibility` integer NOT NULL DEFAULT '0';
-- ----------------------------
-- Indexes structure for table p_post
-- ----------------------------
CREATE INDEX "main"."idx_visibility"
ON "p_post" (
"visibility" ASC
);

@ -137,6 +137,7 @@ CREATE TABLE `p_post` (
`comment_count` bigint unsigned NOT NULL DEFAULT '0' COMMENT '评论数',
`collection_count` bigint unsigned NOT NULL DEFAULT '0' COMMENT '收藏数',
`upvote_count` bigint unsigned NOT NULL DEFAULT '0' COMMENT '点赞数',
`visibility` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '可见性 0公开 1私密 2好友可见',
`is_top` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否置顶',
`is_essence` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否精华',
`is_lock` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否锁定',
@ -150,7 +151,8 @@ CREATE TABLE `p_post` (
`deleted_on` bigint unsigned NOT NULL DEFAULT '0' COMMENT '删除时间',
`is_del` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否删除 0 为未删除、1 为已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_user` (`user_id`) USING BTREE
KEY `idx_user` (`user_id`) USING BTREE,
KEY `idx_visibility` (`visibility`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1080017989 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='冒泡/文章';
-- ----------------------------

@ -1,16 +1,3 @@
/*
Navicat Premium Data Transfer
Source Server : paopao-ce
Source Server Type : SQLite
Source Server Version : 3035005
Source Schema : main
Target Server Type : SQLite
Target Server Version : 3035005
File Encoding : 65001
*/
PRAGMA foreign_keys = false;
-- ----------------------------
@ -135,6 +122,7 @@ CREATE TABLE "p_post" (
"comment_count" integer NOT NULL,
"collection_count" integer NOT NULL,
"upvote_count" integer NOT NULL,
"visibility" integer NOT NULL,
"is_top" integer NOT NULL,
"is_essence" integer NOT NULL,
"is_lock" integer NOT NULL,
@ -181,6 +169,14 @@ CREATE TABLE "p_post_collection" (
PRIMARY KEY ("id")
);
-- ----------------------------
-- Indexes structure for table p_post
-- ----------------------------
CREATE INDEX "main"."idx_visibility"
ON "p_post" (
"visibility" ASC
);
-- ----------------------------
-- Table structure for p_post_content
-- ----------------------------

@ -1,4 +1,4 @@
{
"version": "3",
"buildTime": "2022-06-08 23:29:43"
"version": "8",
"buildTime": "2022-06-13 17:16:22"
}

@ -30,7 +30,7 @@
"vuex": "^4.0.2"
},
"devDependencies": {
"@tauri-apps/cli": "^1.0.0-rc.7",
"@tauri-apps/cli": "^1.0.0",
"@types/node": "^17.0.35",
"@types/qrcode": "^1.4.2",
"@vitejs/plugin-vue": "^2.3.3",

@ -2,4 +2,3 @@
# will have compiled files and executables
/target/
WixTools
Cargo.lock

File diff suppressed because it is too large Load Diff

@ -9,10 +9,10 @@ edition = "2021"
rust-version = "1.57"
[build-dependencies]
tauri-build = { version = "1.0.0-rc.12", features = [] }
tauri-build = { version = "1.0", features = [] }
[dependencies]
tauri = { version = "1.0.0-rc.12", features = ["api-all"] }
tauri = { version = "1.0", features = ["api-all", "macos-private-api"] }
[features]
# by default Tauri runs in production mode

@ -10,6 +10,7 @@
"withGlobalTauri": true
},
"tauri": {
"macOSPrivateApi": true,
"bundle": {
"active": true,
"targets": "all",
@ -60,11 +61,13 @@
},
"windows": [
{
"title": "泡泡 - 一个清新文艺的微社区",
"title": "泡泡 | 一个清新文艺的微社区",
"width": 1080,
"height": 800,
"resizable": true,
"fullscreen": false
"resizable": false,
"fullscreen": false,
"transparent": true,
"decorations": true
}
],
"security": {

@ -108,6 +108,15 @@ export const stickPost = (data: NetParams.PostStickPost): Promise<NetReq.PostSti
});
};
/** 置顶/取消置顶动态 */
export const visibilityPost = (data: NetParams.PostVisibilityPost): Promise<NetReq.PostVisibilityPost> => {
return request({
method: 'post',
url: '/v1/post/visibility',
data
});
};
/** 发布动态评论 */
export const createComment = (data: NetParams.PostCreateComment): Promise<NetReq.PostCreateComment> => {
return request({

@ -141,6 +141,19 @@
</n-icon>
</template>
</n-button>
<n-button
quaternary
circle
type="primary"
@click.stop="switchEye"
>
<template #icon>
<n-icon size="20" color="var(--primary-color)">
<eye-outline />
</n-icon>
</template>
</n-button>
</div>
<div class="submit-wrap">
@ -200,6 +213,19 @@
<template #create-button-default> </template>
</n-dynamic-input>
</div>
<div class="eye-wrap" v-if="showEyeSet">
<n-radio-group v-model:value="visitType" name="radiogroup">
<n-space>
<n-radio
v-for="visit in visibilities"
:key="visit.value"
:value="visit.value"
:label="visit.label"
/>
</n-space>
</n-radio-group>
</div>
</div>
<div class="compose-wrap" v-else>
@ -242,10 +268,14 @@ import {
VideocamOutline,
AttachOutline,
CompassOutline,
EyeOutline,
} from '@vicons/ionicons5';
import { createPost } from '@/api/post';
import { parsePostTag } from '@/utils/content';
import type { MentionOption, UploadFileInfo, UploadInst } from 'naive-ui';
import { VisibilityEnum, PostItemTypeEnum } from '@/utils/IEnum';
const emit = defineEmits<{
(e: 'post-success', post: Item.PostProps): void;
@ -257,8 +287,10 @@ const optionsRef = ref<MentionOption[]>([]);
const loading = ref(false);
const submitting = ref(false);
const showLinkSet = ref(false);
const showEyeSet = ref(false);
const content = ref('');
const links = ref([]);
const uploadRef = ref<UploadInst>();
const attachmentPrice = ref(0);
const uploadType = ref('public/image');
@ -266,6 +298,12 @@ const fileQueue = ref<UploadFileInfo[]>([]);
const imageContents = ref<Item.CommentItemProps[]>([]);
const videoContents = ref<Item.CommentItemProps[]>([]);
const attachmentContents = ref<Item.AttachmentProps[]>([]);
const visitType = ref<VisibilityEnum>(VisibilityEnum.PUBLIC);
const visibilities = [
{value: VisibilityEnum.PUBLIC, label: "公开"}
, {value: VisibilityEnum.PRIVATE, label: "私密"}
, {value: VisibilityEnum.FRIEND, label: "好友可见"}
];
const uploadGateway = import.meta.env.VITE_HOST + '/v1/attachment';
const uploadToken = ref();
@ -277,6 +315,10 @@ const switchLink = () => {
}
};
const switchEye = () => {
showEyeSet.value = !showEyeSet.value;
};
// 加载at用户列表
const loadSuggestionUsers = debounce((k) => {
getSuggestUsers({
@ -468,7 +510,7 @@ const submitPost = () => {
contents.push({
content: content.value,
type: 2, // 文字
type: PostItemTypeEnum.TEXT, // 文字
sort,
});
@ -476,7 +518,7 @@ const submitPost = () => {
sort++;
contents.push({
content: img.content,
type: 3, // 图片
type: PostItemTypeEnum.IMAGEURL, // 图片
sort,
});
});
@ -484,7 +526,7 @@ const submitPost = () => {
sort++;
contents.push({
content: video.content,
type: 4, // 图片
type: PostItemTypeEnum.VIDEOURL, // 视频
sort,
});
});
@ -492,7 +534,7 @@ const submitPost = () => {
sort++;
contents.push({
content: attachment.content,
type: 7, // 附件
type: PostItemTypeEnum.ATTACHMENT, // 附件
sort,
});
});
@ -501,7 +543,7 @@ const submitPost = () => {
sort++;
contents.push({
content: link,
type: 6, // 链接
type: PostItemTypeEnum.LINKURL, // 链接
sort,
});
});
@ -513,6 +555,7 @@ const submitPost = () => {
tags: Array.from(new Set(tags)),
users: Array.from(new Set(users)),
attachment_price: +attachmentPrice.value * 100,
visibility: visitType.value
})
.then((res) => {
window.$message.success('');
@ -521,6 +564,7 @@ const submitPost = () => {
// 置空
showLinkSet.value = false;
showEyeSet.value = false;
uploadRef.value?.clear();
fileQueue.value = [];
content.value = '';
@ -528,6 +572,7 @@ const submitPost = () => {
imageContents.value = [];
videoContents.value = [];
attachmentContents.value = [];
visitType.value = VisibilityEnum.PUBLIC;
})
.catch((err) => {
submitting.value = false;
@ -597,4 +642,7 @@ onMounted(() => {
overflow: hidden;
}
}
.eye-wrap {
margin-left: 64px;
}
</style>

@ -16,6 +16,33 @@
{{ post.user.nickname }}
</router-link>
<span class="username-wrap"> @{{ post.user.username }} </span>
<n-tag
v-if="post.is_top"
class="top-tag"
type="warning"
size="small"
round
>
</n-tag>
<n-tag
v-if="post.visibility == VisibilityEnum.PRIVATE"
class="top-tag"
type="error"
size="small"
round
>
</n-tag>
<n-tag
v-if="post.visibility == VisibilityEnum.FRIEND"
class="top-tag"
type="info"
size="small"
round
>
</n-tag>
</template>
<template #header-extra>
<div
@ -83,6 +110,21 @@
negative-text="取消"
@positive-click="execStickAction"
/>
<!-- -->
<n-modal
v-model:show="showVisibilityModal"
:mask-closable="false"
preset="dialog"
title="提示"
:content="
'' +
(tempVisibility == 0 ? '' : (tempVisibility == 1 ? '' : '')) +
''
"
positive-text="确认"
negative-text="取消"
@positive-click="execVisibilityAction"
/>
</template>
<div v-if="post.texts.length > 0">
<span
@ -174,7 +216,10 @@ import {
deletePost,
lockPost,
stickPost,
visibilityPost
} from '@/api/post';
import type { DropdownOption } from 'naive-ui';
import { VisibilityEnum } from '@/utils/IEnum';
const store = useStore();
const router = useRouter();
@ -189,7 +234,9 @@ const props = withDefaults(
const showDelModal = ref(false);
const showLockModal = ref(false);
const showStickModal = ref(false);
const showVisibilityModal = ref(false);
const loading = ref(false);
const tempVisibility = ref<VisibilityEnum>(VisibilityEnum.PUBLIC);
const emit = defineEmits<{
(e: 'reload'): void;
@ -238,7 +285,7 @@ const post = computed({
});
const adminOptions = computed(() => {
let options = [
let options: DropdownOption[] = [
{
label: '',
key: 'delete',
@ -268,6 +315,34 @@ const adminOptions = computed(() => {
});
}
}
if (post.value.visibility === VisibilityEnum.PUBLIC) {
options.push({
label: '',
key: 'vpublic',
children: [
{ label: '', key: 'vprivate' }
, { label: '', key: 'vfriend' }
]
})
} else if (post.value.visibility === VisibilityEnum.PRIVATE) {
options.push({
label: '',
key: 'vprivate',
children: [
{ label: '', key: 'vpublic' }
, { label: '', key: 'vfriend' }
]
})
} else {
options.push({
label: '',
key: 'vfriend',
children: [
{ label: '', key: 'vpublic' }
, { label: '', key: 'vprivate' }
]
})
}
return options;
});
@ -306,16 +381,34 @@ const doClickText = (e: MouseEvent, id: number) => {
goPostDetail(id);
};
const handlePostAction = (
item: 'delete' | 'lock' | 'unlock' | 'stick' | 'unstick'
item: 'delete' | 'lock' | 'unlock' | 'stick' | 'unstick' | 'vpublic' | 'vprivate' | 'vfriend'
) => {
if (item === 'delete') {
showDelModal.value = true;
}
if (item === 'lock' || item === 'unlock') {
showLockModal.value = true;
}
if (item === 'stick' || item === 'unstick') {
showStickModal.value = true;
switch (item) {
case 'delete':
showDelModal.value = true;
break;
case 'lock':
case 'unlock':
showLockModal.value = true;
break;
case 'stick':
case 'unstick':
showStickModal.value = true;
break;
case 'vpublic':
tempVisibility.value = 0;
showVisibilityModal.value = true;
break;
case 'vprivate':
tempVisibility.value = 1;
showVisibilityModal.value = true;
break;
case 'vfriend':
tempVisibility.value = 2;
showVisibilityModal.value = true;
break;
default:
break;
}
};
const execDelAction = () => {
@ -366,6 +459,19 @@ const execStickAction = () => {
loading.value = false;
});
};
const execVisibilityAction = () => {
visibilityPost({
id: post.value.id,
visibility: tempVisibility.value
})
.then((res) => {
emit('reload');
window.$message.success('');
})
.catch((err) => {
loading.value = false;
});
};
const handlePostStar = () => {
postStar({
id: post.value.id,
@ -450,7 +556,9 @@ onMounted(() => {
font-size: 14px;
opacity: 0.75;
}
.top-tag {
transform: scale(0.75);
}
.options {
opacity: 0.75;
}

@ -27,6 +27,24 @@
>
</n-tag>
<n-tag
v-if="post.visibility == 1"
class="top-tag"
type="error"
size="small"
round
>
</n-tag>
<n-tag
v-if="post.visibility == 2"
class="top-tag"
type="info"
size="small"
round
>
</n-tag>
</template>
<template #header-extra>
<span class="timestamp">

@ -17,4 +17,4 @@ declare global {
$message: MessageApiInjection,
$store: any
}
}
}

@ -28,7 +28,7 @@ declare module Item {
/** 评论者UID */
user_id: number,
/** 类别1为标题2为文字段落3为图片地址4为视频地址5为语音地址6为链接地址 */
type: number,
type: import('@/utils/IEnum').CommentItemTypeEnum,
/** 内容 */
content: string,
/** 排序,越小越靠前 */
@ -111,7 +111,7 @@ declare module Item {
/** 内容ID */
id: number,
/** 类型1为标题2为文字段落3为图片地址4为视频地址5为语音地址6为链接地址7为附件资源8为收费资源 */
type: number,
type: import('@/utils/IEnum').PostItemTypeEnum,
/** POST ID */
post_id: number,
/** 内容 */
@ -161,6 +161,8 @@ declare module Item {
contents: PostItemProps[],
/** 标签列表 */
tags: { [key: string]: number } | string,
/** 可见性0为公开1为私密2为好友可见 */
visibility: import('@/utils/IEnum').VisibilityEnum,
/** 是否锁定 */
is_lock: number,
/** 是否置顶 */
@ -190,7 +192,7 @@ declare module Item {
interface MessageProps {
id: number,
/** 类型1为动态2为评论3为回复4为私信99为系统通知 */
type: 1 | 2 | 3 | 4 | 99,
type: import('@/utils/IEnum').MessageTypeEnum,
/** 摘要说明 */
brief: string,
/** 详细内容 */
@ -228,7 +230,7 @@ declare module Item {
interface AttachmentProps {
id: number,
/** 类别1为图片2为视频3为其他附件 */
type: 1 | 2 | 3,
type: import('@/utils/IEnum').AttachmentTypeEnum,
/** 发布者用户UID */
user_id: number,
/** 发布者用户数据 */
@ -287,4 +289,4 @@ declare module Item {
created_on: number
}
}
}

@ -124,6 +124,12 @@ declare module NetParams {
id: number
}
interface PostVisibilityPost {
id: number,
/** 可见性0为公开1为私密2为好友可见 */
visibility: import('@/utils/IEnum').VisibilityEnum
}
interface PostGetPostStar {
id: number
}
@ -157,7 +163,9 @@ declare module NetParams {
/** 艾特用户列表 */
users: string[],
/** 附件价格 */
attachment_price: number
attachment_price: number,
/** 可见性0为公开1为私密2为好友可见 */
visibility: import('@/utils/IEnum').VisibilityEnum
}
interface PostDeletePost {

@ -116,6 +116,11 @@ declare module NetReq {
top_status: 0 | 1
}
interface PostVisibilityPost {
/** 可见性0为公开1为私密2为好友可见 */
visibility_status: import('@/utils/IEnum').VisibilityEnum
}
interface PostGetPostStar {
status: boolean
}

@ -0,0 +1,69 @@
/** 动态内容类型枚举 */
export enum PostItemTypeEnum {
/** 标题 */
TITLE = 1,
/** 文字段落 */
TEXT = 2,
/** 图片地址 */
IMAGEURL = 3,
/** 视频地址 */
VIDEOURL = 4,
/** 音频地址 */
AUDIOURL = 5,
/** 链接地址 */
LINKURL = 6,
/** 附件资源 */
ATTACHMENT = 7,
/** 收费资源 */
CHARGEATTACHMENT = 8
}
/** 回复内容类型枚举 */
export enum CommentItemTypeEnum {
/** 标题 */
TITLE = 1,
/** 文字段落 */
TEXT = 2,
/** 图片地址 */
IMAGEURL = 3,
/** 视频地址 */
VIDEOURL = 4,
/** 音频地址 */
AUDIOURL = 5,
/** 链接地址 */
LINKURL = 6
}
/** 附件类型枚举 */
export enum AttachmentTypeEnum {
/** 图片 */
IMAGE = 1,
/** 视频 */
VIDEO = 2,
/** 其他 */
OTHER = 3
}
/** 消息类型枚举 */
export enum MessageTypeEnum {
/** 动态 */
POST = 1,
/** 评论 */
COMMENT = 2,
/** 回复 */
REPLY = 3,
/** 私信 */
PRIVATELETTER = 4,
/** 系统通知 */
SYSTEMNOTICE = 99
}
/** 动态可见度枚举 */
export enum VisibilityEnum {
/** 公开 */
PUBLIC,
/** 私密 */
PRIVATE,
/** 好友可见 */
FRIEND
}

@ -70,4 +70,13 @@ const updatePage = (p: number) => {
onMounted(() => {
loadPosts();
});
</script>
</script>
<style lang="less" scoped>
.pagination-wrap {
padding: 10px;
display: flex;
justify-content: center;
overflow: hidden;
}
</style>

@ -102,7 +102,7 @@
<n-form-item path="phone" label="手机号">
<n-input
:value="modelData.phone"
@update:value="(v) => (modelData.phone = v.trim())"
@update:value="(v: string) => (modelData.phone = v.trim())"
placeholder="请输入中国大陆手机号"
@keydown.enter.prevent
/>

@ -84,65 +84,65 @@
estree-walker "^2.0.1"
picomatch "^2.2.2"
"@tauri-apps/cli-darwin-arm64@1.0.0-rc.13":
version "1.0.0-rc.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.0.0-rc.13.tgz#da73a770835ffd63a149d8becf50d3781e1c97dd"
integrity sha512-/EqOz7ASHOU98H58Ibbkg12pLG/P5oyQz8OlueaMYryajkJdmi+bHTkJ05DfbS0owAaHkRJ6f+NmoW/AnyqUbg==
"@tauri-apps/cli-darwin-x64@1.0.0-rc.13":
version "1.0.0-rc.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.0.0-rc.13.tgz#e120cc623fddca3eb9d7fea0c2de057c7440a821"
integrity sha512-bvZ0MBKFD1kc4gdVPXgwUA6tHNKj0EmlQK0Xolk6PYP9vZZeNTP1vejevW0bh2IqxC8DuqUArbG9USXwu+LFbQ==
"@tauri-apps/cli-linux-arm-gnueabihf@1.0.0-rc.13":
version "1.0.0-rc.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.0.0-rc.13.tgz#3052a59788ae57ad690d4bc09bf0756bc8808116"
integrity sha512-yODvfUkNvtYYdDTOJSDXMx9fpoEB66I2PTrYx1UKonKTEaLrQDcpw2exD/S9LPQzCYgyTuJ/kHRhG1uLdO/UUQ==
"@tauri-apps/cli-linux-arm64-gnu@1.0.0-rc.13":
version "1.0.0-rc.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.0.0-rc.13.tgz#9c4094473890c165a4fb22132229ed8212559f79"
integrity sha512-kVDJHERD8CmTeMcd2VTnD/nVCHdnNAK8a6ur3l0KTR1iF8A1AtN/sPahMQjK4f7Ar00UDjIzTw74liqakOeiZg==
"@tauri-apps/cli-linux-arm64-musl@1.0.0-rc.13":
version "1.0.0-rc.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.0.0-rc.13.tgz#16270a6d3b9289993b9b4d837f63dba4991d9be5"
integrity sha512-PFHz+0xKCGMqqn2TmbOSPvTRS61xJQV7srwTZjs5sHBvK536mdBnF/6V6BPEvTn5LzfRnxMu2A5X5GFkYnrZ7w==
"@tauri-apps/cli-linux-x64-gnu@1.0.0-rc.13":
version "1.0.0-rc.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.0.0-rc.13.tgz#d2f5031f9597300a5814dc8b4d3c59e8dc25a871"
integrity sha512-EWhTOUNHaaMM7mxp/ue+Osnzn6/o9/7qVle3MSnNI9pGQzumc/dOtBs+sWS/NPXdVEiWKET2mFMK120KJlYcQQ==
"@tauri-apps/cli-linux-x64-musl@1.0.0-rc.13":
version "1.0.0-rc.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.0.0-rc.13.tgz#9d8b02de7bd5af71c5d3d5be96ec88f3f29c8fbd"
integrity sha512-i8lsKw5iAGTAhqSQHeUCISLjhRXNrloHPoFCaSZtU0/GAPGbW/qST7u593h7cKWxRooeMwzo74ij4GhgmddClQ==
"@tauri-apps/cli-win32-ia32-msvc@1.0.0-rc.13":
version "1.0.0-rc.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.0.0-rc.13.tgz#1fcf6bed5a89af2cb30a2fe2e823ca486ded61b7"
integrity sha512-rJxSqWIQXeeT2oLzSiQyqZPgDKSGH5sK7MUr8cOCBitqy3T0COlOMX4O7hhqF3cJ/5s0aX+MuNZBzF/D0QUcxA==
"@tauri-apps/cli-win32-x64-msvc@1.0.0-rc.13":
version "1.0.0-rc.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.0.0-rc.13.tgz#ed2feaf3b3a120c1460cae8941443563d14840bb"
integrity sha512-ifOTrJVQoBAQUYX+EVnE4XJ/FCMHs4FQ8qxGNszqkSxrU24mmT7La6tzj77352q80KnxRa05xjjLL6GGhmzXRg==
"@tauri-apps/cli@^1.0.0-rc.7":
version "1.0.0-rc.13"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-1.0.0-rc.13.tgz#e58127ebe24c6cc81c3258229219056199421500"
integrity sha512-q7i45Mi1SMv5XllNoX09QS4Q/fYVFwD6piVYmqMSrKY/T5RwedQhytiVH60TxC2xk6o0akVHa7BdYiyJvXNR8A==
"@tauri-apps/cli-darwin-arm64@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.0.0.tgz#9ab439898b4d05a6e43df4451a42fa21e9d5eaa1"
integrity sha512-0ryomgLjdpylXypMPVXLU3PZCde3Sw5nwN4coUhBcHPBLFRb8QPet+nweVK/HiZ3mxg8WeIazvpx2s8hS0l2GQ==
"@tauri-apps/cli-darwin-x64@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.0.0.tgz#5ffc8d444c7dc1cab14c8c743a929e448f1b1e93"
integrity sha512-oejvYUT4dEfzBi+FWMj+CMz4cZ6C2gEFHrUtKVLdTXr8Flj5UTwdB1YPGQjiOqk73LOI7cB/vXxb9DZT+Lrxgg==
"@tauri-apps/cli-linux-arm-gnueabihf@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.0.0.tgz#c24d63b4637a0c20b9671f0284968b164482f6ee"
integrity sha512-yAu78v8TeXNx/ETS5F2G2Uw/HX+LQvZkX94zNiqFsAj7snfWI/IqSUM52OBrdh/D0EC9NCdjUJ7Vuo32uxf7tg==
"@tauri-apps/cli-linux-arm64-gnu@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.0.0.tgz#dfb107a3d5f56dc0356126135f941261ffad10a3"
integrity sha512-YFUN/S58AN317njAynzcQ+EHhRsCDXqmp5g9Oiqmcdg1vU7fPWZivVLc1WHz+0037C7JnsX5PtKpNYewP/+Oqw==
"@tauri-apps/cli-linux-arm64-musl@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.0.0.tgz#72ebfc5066c6fac82b513c20fbec6ab841b3ee05"
integrity sha512-al+TxMGoNVikEvRQfMyYE/mdjUcUNMo5brkCIAb+fL4rWQlAhAnYVzmg/rM8N4nhdXm1MOaYAagQmxr8898dNA==
"@tauri-apps/cli-linux-x64-gnu@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.0.0.tgz#76f791378468a9ca2678885f0d14e05f4fd0d0c8"
integrity sha512-KQmYlYyGpn6/2kSl9QivWG6EIepm6PJd57e6IKmYwAyNhLr2XfGl1CLuocUQQgO+jprjT70HXp+MXD0tcB0+Sw==
"@tauri-apps/cli-linux-x64-musl@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.0.0.tgz#12ccc0747c88e9c2cbdf21834b94718c8da689ca"
integrity sha512-Qpaq5lZz569Aea6jfrRchgfEJaOrfLpCRBATcF8CJFFwVKmfCUcoV+MxbCIW30Zqw5Y06njC/ffa3261AV/ZIQ==
"@tauri-apps/cli-win32-ia32-msvc@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.0.0.tgz#5d56df6dbb62c11cae28b826619fbbad9a158399"
integrity sha512-e2DzFqEMI+s+gv14UupdI91gPxTbUJTbbfQlTHdQlOsTk4HEZTsh+ibAYBcCLAaMRW38NEsFlAUe1lQA0iRu/w==
"@tauri-apps/cli-win32-x64-msvc@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.0.0.tgz#6227331d177118323cff13bc6bfb04894130c83f"
integrity sha512-lWSs90pJeQX+L31IqIzmRhwLayEeyTh7mga0AxX8G868hvdLtcXCQA/rKoFtGdVLuHAx4+M+CBF5SMYb76xGYA==
"@tauri-apps/cli@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-1.0.0.tgz#28f021a2db4c2087b569845d42b4fe7978ccee19"
integrity sha512-4eHnk3p0xnCXd9Zel3kLvdiiSURnN98GMFvWUAdirm5AjyOjcx8TIET/jqRYmYKE5yd+LMQqYMUfHRwA6JJUkg==
optionalDependencies:
"@tauri-apps/cli-darwin-arm64" "1.0.0-rc.13"
"@tauri-apps/cli-darwin-x64" "1.0.0-rc.13"
"@tauri-apps/cli-linux-arm-gnueabihf" "1.0.0-rc.13"
"@tauri-apps/cli-linux-arm64-gnu" "1.0.0-rc.13"
"@tauri-apps/cli-linux-arm64-musl" "1.0.0-rc.13"
"@tauri-apps/cli-linux-x64-gnu" "1.0.0-rc.13"
"@tauri-apps/cli-linux-x64-musl" "1.0.0-rc.13"
"@tauri-apps/cli-win32-ia32-msvc" "1.0.0-rc.13"
"@tauri-apps/cli-win32-x64-msvc" "1.0.0-rc.13"
"@tauri-apps/cli-darwin-arm64" "1.0.0"
"@tauri-apps/cli-darwin-x64" "1.0.0"
"@tauri-apps/cli-linux-arm-gnueabihf" "1.0.0"
"@tauri-apps/cli-linux-arm64-gnu" "1.0.0"
"@tauri-apps/cli-linux-arm64-musl" "1.0.0"
"@tauri-apps/cli-linux-x64-gnu" "1.0.0"
"@tauri-apps/cli-linux-x64-musl" "1.0.0"
"@tauri-apps/cli-win32-ia32-msvc" "1.0.0"
"@tauri-apps/cli-win32-x64-msvc" "1.0.0"
"@types/jest@^27.0.1":
version "27.4.1"

Loading…
Cancel
Save