initial commit.

pull/180/head
ROC 2 years ago
parent 8cb0b153ea
commit d6cd619898

@ -1,7 +1,3 @@
<<<<<<< HEAD
# paopao-ce
PaoPao Community Edition
=======
# PaoPao CE
官网 https://www.paopao.info
@ -60,4 +56,3 @@ build完成后可以在dist目录获取编译产出配置nginx指向至该
代码结构比较简单喜欢的朋友欢迎贡献PR。
>>>>>>> 76a0b2b (update yarn build command.)

@ -0,0 +1,5 @@
.vscode
__debug_bin
config.yaml
*.log
paopao-api

@ -0,0 +1,27 @@
# build app
FROM golang AS build-env
ADD . /paopao-api
WORKDIR /paopao-api
RUN CGO_ENABLED=0 go build .
# safe image
FROM alpine
ENV TZ=Asia/Shanghai
RUN apk update && apk add --no-cache ca-certificates && update-ca-certificates
COPY --from=build-env /paopao-api/paopao-api /usr/bin/paopao-api
COPY --from=build-env /paopao-api/comic.ttf /comic.ttf
COPY --from=build-env /paopao-api/qqwry.dat /qqwry.dat
COPY --from=build-env /paopao-api/configs /configs
EXPOSE 8000
CMD ["paopao-api"]
# HEALTHCHECK
HEALTHCHECK --interval=5s --timeout=3s --retries=3 CMD ps -ef | grep paopao-api || exit 1

Binary file not shown.

@ -0,0 +1,58 @@
App: # APP基础设置项
BarkToken:
AttachmentIncomeRate: 0.8
MaxCommentCount: 10
DefaultContextTimeout: 60
DefaultPageSize: 10
MaxPageSize: 100
SmsJuheKey:
SmsJuheTplID:
SmsJuheTplVal: "#code#=%d&#m#=%d"
AlipayAppID:
AlipayPrivateKey:
Server: # 服务设置
RunMode: debug
HttpIp: 0.0.0.0
HttpPort: 8008
ReadTimeout: 60
WriteTimeout: 60
Log: # 日志
LogType: zinc # 可选file或zinc
LogFileSavePath: storage/logs
LogFileName: app
LogFileExt: .log
LogZincHost: http://127.0.0.1:4080/es/_bulk
LogZincIndex: paopao-log
LogZincUser: admin
LogZincPassword: admin
JWT: # 鉴权加密
Secret: 18a6413dc4fe394c66345ebe501b2f26
Issuer: paopao-api
Expire: 86400
Search: # 搜索配置
ZincHost: http://127.0.0.1:4080
ZincIndex: paopao-data
ZincUser: admin
ZincPassword: admin
Storage: # 阿里云OSS存储配置
AliossAccessKeyID:
AliossAccessKeySecret:
AliossEndpoint:
AliossBucket:
AliossDomain:
Database: # 数据库
DBType: mysql
Username: root # 填写你的数据库账号
Password: root # 填写你的数据库密码
Host: 127.0.0.1:3306
DBName: paopao
TablePrefix: p_
Charset: utf8mb4
ParseTime: True
LogLevel: 2
MaxIdleConns: 10
MaxOpenConns: 30
Redis:
Host: 127.0.0.1:6379
Password:
DB:

@ -0,0 +1,11 @@
package global
import (
"github.com/go-redis/redis/v8"
"gorm.io/gorm"
)
var (
DBEngine *gorm.DB
Redis *redis.Client
)

@ -0,0 +1,21 @@
package global
import (
"sync"
"github.com/rocboss/paopao-api/pkg/setting"
"github.com/sirupsen/logrus"
)
var (
ServerSetting *setting.ServerSettingS
AppSetting *setting.AppSettingS
DatabaseSetting *setting.DatabaseSettingS
RedisSetting *setting.RedisSettingS
SearchSetting *setting.SearchSettingS
AliossSetting *setting.AliossSettingS
JWTSetting *setting.JWTSettingS
LoggerSetting *setting.LoggerSettingS
Logger *logrus.Logger
Mutex *sync.Mutex
)

@ -0,0 +1,37 @@
module github.com/rocboss/paopao-api
go 1.16
require (
github.com/afocus/captcha v0.0.0-20191010092841-4bd1f21c8868
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/disintegration/imaging v1.6.2 // indirect
github.com/ethereum/go-ethereum v1.10.16
github.com/fbsobreira/gotron-sdk v0.0.0-20211102183839-58a64f4da5f4
github.com/gin-contrib/cors v1.3.1
github.com/gin-gonic/gin v1.7.7
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 // indirect
github.com/gofrs/uuid v3.3.0+incompatible
github.com/golang/protobuf v1.5.2
github.com/google/btree v1.0.1
github.com/google/go-cmp v0.5.7 // indirect
github.com/sirupsen/logrus v1.8.1
github.com/smartwalle/alipay/v3 v3.1.7 // indirect
github.com/spf13/viper v1.10.1
github.com/ugorji/go v1.2.7 // indirect
github.com/yinheli/mahonia v0.0.0-20131226213531-0eef680515cc // indirect
github.com/yinheli/qqwry v0.0.0-20160229183603-f50680010f4a // indirect
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
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/resty.v1 v1.12.0
gorm.io/driver/mysql v1.3.2
gorm.io/gorm v1.23.2
gorm.io/plugin/dbresolver v1.1.0
gorm.io/plugin/soft_delete v1.1.0
)

File diff suppressed because it is too large Load Diff

@ -0,0 +1,100 @@
package main
import (
"log"
"sync"
"time"
"github.com/go-redis/redis/v8"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/model"
"github.com/rocboss/paopao-api/pkg/logger"
"github.com/rocboss/paopao-api/pkg/setting"
)
func init() {
err := setupSetting()
if err != nil {
log.Fatalf("init.setupSetting err: %v", err)
}
err = setupLogger()
if err != nil {
log.Fatalf("init.setupLogger err: %v", err)
}
err = setupDBEngine()
if err != nil {
log.Fatalf("init.setupDBEngine err: %v", err)
}
}
func setupSetting() error {
setting, err := setting.NewSetting()
if err != nil {
return err
}
err = setting.ReadSection("Server", &global.ServerSetting)
if err != nil {
return err
}
err = setting.ReadSection("App", &global.AppSetting)
if err != nil {
return err
}
err = setting.ReadSection("Log", &global.LoggerSetting)
if err != nil {
return err
}
err = setting.ReadSection("Database", &global.DatabaseSetting)
if err != nil {
return err
}
err = setting.ReadSection("Search", &global.SearchSetting)
if err != nil {
return err
}
err = setting.ReadSection("Redis", &global.RedisSetting)
if err != nil {
return err
}
err = setting.ReadSection("JWT", &global.JWTSetting)
if err != nil {
return err
}
err = setting.ReadSection("Storage", &global.AliossSetting)
if err != nil {
return err
}
global.JWTSetting.Expire *= time.Second
global.ServerSetting.ReadTimeout *= time.Second
global.ServerSetting.WriteTimeout *= time.Second
global.Mutex = &sync.Mutex{}
return nil
}
func setupLogger() error {
logger, err := logger.New(global.LoggerSetting)
if err != nil {
return err
}
global.Logger = logger
return nil
}
func setupDBEngine() error {
var err error
global.DBEngine, err = model.NewDBEngine(global.DatabaseSetting)
if err != nil {
return err
}
global.Redis = redis.NewClient(&redis.Options{
Addr: global.RedisSetting.Host,
Password: global.RedisSetting.Password,
DB: global.RedisSetting.DB,
})
return nil
}

@ -0,0 +1,7 @@
package dao
import "github.com/rocboss/paopao-api/internal/model"
func (d *Dao) CreateAttachment(attachment *model.Attachment) (*model.Attachment, error) {
return attachment.Create(d.engine)
}

@ -0,0 +1,45 @@
package dao
import "github.com/rocboss/paopao-api/internal/model"
func (d *Dao) GetComments(conditions *model.ConditionsT, offset, limit int) ([]*model.Comment, error) {
return (&model.Comment{}).List(d.engine, conditions, offset, limit)
}
func (d *Dao) GetCommentByID(id int64) (*model.Comment, error) {
comment := &model.Comment{
Model: &model.Model{
ID: id,
},
}
return comment.Get(d.engine)
}
func (d *Dao) DeleteComment(comment *model.Comment) error {
return comment.Delete(d.engine)
}
func (d *Dao) GetCommentCount(conditions *model.ConditionsT) (int64, error) {
return (&model.Comment{}).Count(d.engine, conditions)
}
func (d *Dao) CreateComment(comment *model.Comment) (*model.Comment, error) {
return comment.Create(d.engine)
}
func (d *Dao) CreateCommentReply(reply *model.CommentReply) (*model.CommentReply, error) {
return reply.Create(d.engine)
}
func (d *Dao) GetCommentReplyByID(id int64) (*model.CommentReply, error) {
reply := &model.CommentReply{
Model: &model.Model{
ID: id,
},
}
return reply.Get(d.engine)
}
func (d *Dao) DeleteCommentReply(reply *model.CommentReply) error {
return reply.Delete(d.engine)
}

@ -0,0 +1,50 @@
package dao
import "github.com/rocboss/paopao-api/internal/model"
func (d *Dao) GetCommentContentsByIDs(ids []int64) ([]*model.CommentContent, error) {
commentContent := &model.CommentContent{}
return commentContent.List(d.engine, &model.ConditionsT{
"comment_id IN ?": ids,
}, 0, 0)
}
func (d *Dao) GetCommentRepliesByID(ids []int64) ([]*model.CommentReplyFormated, error) {
CommentReply := &model.CommentReply{}
replies, err := CommentReply.List(d.engine, &model.ConditionsT{
"comment_id IN ?": ids,
}, 0, 0)
if err != nil {
return nil, err
}
userIds := []int64{}
for _, reply := range replies {
userIds = append(userIds, reply.UserID, reply.AtUserID)
}
users, err := d.GetUsersByIDs(userIds)
if err != nil {
return nil, err
}
repliesFormated := []*model.CommentReplyFormated{}
for _, reply := range replies {
replyFormated := reply.Format()
for _, user := range users {
if reply.UserID == user.ID {
replyFormated.User = user.Format()
}
if reply.AtUserID == user.ID {
replyFormated.AtUser = user.Format()
}
}
repliesFormated = append(repliesFormated, replyFormated)
}
return repliesFormated, nil
}
func (d *Dao) CreateCommentContent(content *model.CommentContent) (*model.CommentContent, error) {
return content.Create(d.engine)
}

@ -0,0 +1,18 @@
package dao
import (
"github.com/rocboss/paopao-api/pkg/zinc"
"gorm.io/gorm"
)
type Dao struct {
engine *gorm.DB
zinc *zinc.ZincClient
}
func New(engine *gorm.DB, zinc *zinc.ZincClient) *Dao {
return &Dao{
engine: engine,
zinc: zinc,
}
}

@ -0,0 +1,46 @@
package dao
import "github.com/rocboss/paopao-api/internal/model"
func (d *Dao) CreateMessage(msg *model.Message) (*model.Message, error) {
return msg.Create(d.engine)
}
func (d *Dao) GetUnreadCount(userID int64) (int64, error) {
return (&model.Message{}).Count(d.engine, &model.ConditionsT{
"receiver_user_id": userID,
"is_read": model.MSG_UNREAD,
})
}
func (d *Dao) GetMessageByID(id int64) (*model.Message, error) {
return (&model.Message{
Model: &model.Model{
ID: id,
},
}).Get(d.engine)
}
func (d *Dao) ReadMessage(message *model.Message) error {
message.IsRead = 1
return message.Update(d.engine)
}
func (d *Dao) GetMessages(conditions *model.ConditionsT, offset, limit int) ([]*model.MessageFormated, error) {
messages, err := (&model.Message{}).List(d.engine, conditions, offset, limit)
if err != nil {
return nil, err
}
mfs := []*model.MessageFormated{}
for _, message := range messages {
mf := message.Format()
mfs = append(mfs, mf)
}
return mfs, nil
}
func (d *Dao) GetMessageCount(conditions *model.ConditionsT) (int64, error) {
return (&model.Message{}).Count(d.engine, conditions)
}

@ -0,0 +1,145 @@
package dao
import (
"time"
"github.com/rocboss/paopao-api/internal/model"
)
func (d *Dao) CreatePost(post *model.Post) (*model.Post, error) {
post.LatestRepliedOn = time.Now().Unix()
return post.Create(d.engine)
}
func (d *Dao) DeletePost(post *model.Post) error {
return post.Delete(d.engine)
}
func (d *Dao) LockPost(post *model.Post) error {
post.IsLock = 1 - post.IsLock
return post.Update(d.engine)
}
func (d *Dao) GetPostByID(id int64) (*model.Post, error) {
post := &model.Post{
Model: &model.Model{
ID: id,
},
}
return post.Get(d.engine)
}
func (d *Dao) GetPosts(conditions *model.ConditionsT, offset, limit int) ([]*model.Post, error) {
return (&model.Post{}).List(d.engine, conditions, offset, limit)
}
func (d *Dao) GetPostCount(conditions *model.ConditionsT) (int64, error) {
return (&model.Post{}).Count(d.engine, conditions)
}
func (d *Dao) UpdatePost(post *model.Post) error {
return post.Update(d.engine)
}
func (d *Dao) GetUserPostStar(postID, userID int64) (*model.PostStar, error) {
star := &model.PostStar{
PostID: postID,
UserID: userID,
}
return star.Get(d.engine)
}
func (d *Dao) GetUserPostStars(userID int64, offset, limit int) ([]*model.PostStar, error) {
star := &model.PostStar{
UserID: userID,
}
return star.List(d.engine, &model.ConditionsT{
"ORDER": "id DESC",
}, offset, limit)
}
func (d *Dao) GetUserPostStarCount(userID int64) (int64, error) {
star := &model.PostStar{
UserID: userID,
}
return star.Count(d.engine, &model.ConditionsT{})
}
func (d *Dao) CreatePostStar(postID, userID int64) (*model.PostStar, error) {
star := &model.PostStar{
PostID: postID,
UserID: userID,
}
return star.Create(d.engine)
}
func (d *Dao) DeletePostStar(p *model.PostStar) error {
return p.Delete(d.engine)
}
func (d *Dao) GetUserPostCollection(postID, userID int64) (*model.PostCollection, error) {
star := &model.PostCollection{
PostID: postID,
UserID: userID,
}
return star.Get(d.engine)
}
func (d *Dao) GetUserPostCollections(userID int64, offset, limit int) ([]*model.PostCollection, error) {
collection := &model.PostCollection{
UserID: userID,
}
return collection.List(d.engine, &model.ConditionsT{
"ORDER": "id DESC",
}, offset, limit)
}
func (d *Dao) GetUserPostCollectionCount(userID int64) (int64, error) {
collection := &model.PostCollection{
UserID: userID,
}
return collection.Count(d.engine, &model.ConditionsT{})
}
func (d *Dao) GetUserWalletBills(userID int64, offset, limit int) ([]*model.WalletStatement, error) {
statement := &model.WalletStatement{
UserID: userID,
}
return statement.List(d.engine, &model.ConditionsT{
"ORDER": "id DESC",
}, offset, limit)
}
func (d *Dao) GetUserWalletBillCount(userID int64) (int64, error) {
statement := &model.WalletStatement{
UserID: userID,
}
return statement.Count(d.engine, &model.ConditionsT{})
}
func (d *Dao) CreatePostCollection(postID, userID int64) (*model.PostCollection, error) {
collection := &model.PostCollection{
PostID: postID,
UserID: userID,
}
return collection.Create(d.engine)
}
func (d *Dao) DeletePostCollection(p *model.PostCollection) error {
return p.Delete(d.engine)
}
func (d *Dao) GetPostAttatchmentBill(postID, userID int64) (*model.PostAttachmentBill, error) {
bill := &model.PostAttachmentBill{
PostID: postID,
UserID: userID,
}
return bill.Get(d.engine)
}

@ -0,0 +1,22 @@
package dao
import "github.com/rocboss/paopao-api/internal/model"
func (d *Dao) CreatePostContent(content *model.PostContent) (*model.PostContent, error) {
return content.Create(d.engine)
}
func (d *Dao) GetPostContentsByIDs(ids []int64) ([]*model.PostContent, error) {
return (&model.PostContent{}).List(d.engine, &model.ConditionsT{
"post_id IN ?": ids,
"ORDER": "sort ASC",
}, 0, 0)
}
func (d *Dao) GetPostContentByID(id int64) (*model.PostContent, error) {
return (&model.PostContent{
Model: &model.Model{
ID: id,
},
}).Get(d.engine)
}

@ -0,0 +1,173 @@
package dao
import (
"github.com/rocboss/paopao-api/pkg/zinc"
)
type SearchType string
const SearchTypeDefault SearchType = "search"
const SearchTypeTag SearchType = "tag"
type QueryT struct {
Query string
Type SearchType
}
func (d *Dao) 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,
},
})
}
func (d *Dao) BulkPushDoc(data []map[string]interface{}) (bool, error) {
return d.zinc.BulkPushDoc(data)
}
func (d *Dao) DelDoc(indexName, id string) error {
return d.zinc.DelDoc(indexName, id)
}
func (d *Dao) QueryAll(q *QueryT, indexName string, offset, limit int) (*zinc.QueryResultT, error) {
// 普通搜索
if q.Type == SearchTypeDefault && q.Query != "" {
return d.QuerySearch(indexName, q.Query, offset, limit)
}
// Tag分类
if q.Type == 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
}
func (d *Dao) 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,
},
},
"from": offset,
"size": limit,
})
if err != nil {
return nil, err
}
return rsp, err
}
func (d *Dao) 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": []string{"-is_top", "-latest_replied_on"},
"from": offset,
"max_results": limit,
})
if err != nil {
return nil, err
}
return rsp, err
}

@ -0,0 +1,35 @@
package dao
import "github.com/rocboss/paopao-api/internal/model"
func (d *Dao) CreateTag(tag *model.Tag) (*model.Tag, error) {
t, err := tag.Get(d.engine)
if err != nil {
tag.QuoteNum = 1
return tag.Create(d.engine)
}
// 更新
t.QuoteNum++
err = t.Update(d.engine)
if err != nil {
return nil, err
}
return t, nil
}
func (d *Dao) DeleteTag(tag *model.Tag) error {
tag, err := tag.Get(d.engine)
if err != nil {
return err
}
tag.QuoteNum--
return tag.Update(d.engine)
}
func (d *Dao) GetTags(conditions *model.ConditionsT, offset, limit int) ([]*model.Tag, error) {
return (&model.Tag{}).List(d.engine, conditions, offset, limit)
}

@ -0,0 +1,160 @@
package dao
import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/model"
"gopkg.in/resty.v1"
)
type JuhePhoneCaptchaRsp struct {
ErrorCode int `json:"error_code"`
Reason string `json:"reason"`
}
// 根据用户ID获取用户
func (d *Dao) GetUserByID(id int64) (*model.User, error) {
user := &model.User{
Model: &model.Model{
ID: id,
},
}
return user.Get(d.engine)
}
// 根据用户名获取用户
func (d *Dao) GetUserByUsername(username string) (*model.User, error) {
user := &model.User{
Username: username,
}
return user.Get(d.engine)
}
// 根据手机号获取用户
func (d *Dao) GetUserByPhone(phone string) (*model.User, error) {
user := &model.User{
Phone: phone,
}
return user.Get(d.engine)
}
// 根据IDs获取用户列表
func (d *Dao) GetUsersByIDs(ids []int64) ([]*model.User, error) {
user := &model.User{}
return user.List(d.engine, &model.ConditionsT{
"id IN ?": ids,
}, 0, 0)
}
// 根据关键词模糊获取用户列表
func (d *Dao) GetUsersByKeyword(keyword string) ([]*model.User, error) {
user := &model.User{}
if strings.Trim(keyword, "") == "" {
return user.List(d.engine, &model.ConditionsT{
"ORDER": "id ASC",
}, 0, 6)
} else {
return user.List(d.engine, &model.ConditionsT{
"username LIKE ?": strings.Trim(keyword, "") + "%",
}, 0, 6)
}
}
// 根据关键词模糊获取用户列表
func (d *Dao) GetTagsByKeyword(keyword string) ([]*model.Tag, error) {
tag := &model.Tag{}
if strings.Trim(keyword, "") == "" {
return tag.List(d.engine, &model.ConditionsT{
"ORDER": "quote_num DESC",
}, 0, 6)
} else {
return tag.List(d.engine, &model.ConditionsT{
"tag LIKE ?": "%" + strings.Trim(keyword, "") + "%",
"ORDER": "quote_num DESC",
}, 0, 6)
}
}
// 创建用户
func (d *Dao) CreateUser(user *model.User) (*model.User, error) {
return user.Create(d.engine)
}
// 更新用户
func (d *Dao) UpdateUser(user *model.User) error {
return user.Update(d.engine)
}
// 获取最新短信验证码
func (d *Dao) GetLatestPhoneCaptcha(phone string) (*model.Captcha, error) {
return (&model.Captcha{
Phone: phone,
}).Get(d.engine)
}
// 更新短信验证码
func (d *Dao) UsePhoneCaptcha(captcha *model.Captcha) error {
captcha.UseTimes++
return captcha.Update(d.engine)
}
// 发送短信验证码
func (d *Dao) SendPhoneCaptcha(phone string) error {
rand.Seed(time.Now().UnixNano())
captcha := rand.Intn(900000) + 100000
m := 5
gateway := "https://v.juhe.cn/sms/send"
client := resty.New()
client.DisableWarn = true
resp, err := client.R().
SetFormData(map[string]string{
"mobile": phone,
"tpl_id": global.AppSetting.SmsJuheTplID,
"tpl_value": fmt.Sprintf(global.AppSetting.SmsJuheTplVal, captcha, m),
"key": global.AppSetting.SmsJuheKey,
}).Post(gateway)
if err != nil {
return err
}
if resp.StatusCode() != http.StatusOK {
return errors.New(resp.Status())
}
result := &JuhePhoneCaptchaRsp{}
err = json.Unmarshal(resp.Body(), result)
if err != nil {
return err
}
if result.ErrorCode != 0 {
return errors.New(result.Reason)
}
// 写入表
captchaModel := &model.Captcha{
Phone: phone,
Captcha: strconv.Itoa(captcha),
ExpiredOn: time.Now().Add(time.Minute * time.Duration(m)).Unix(),
}
captchaModel.Create(d.engine)
return nil
}

@ -0,0 +1,122 @@
package dao
import (
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/model"
"gorm.io/gorm"
)
func (d *Dao) GetRechargeByID(id int64) (*model.WalletRecharge, error) {
recharge := &model.WalletRecharge{
Model: &model.Model{
ID: id,
},
}
return recharge.Get(d.engine)
}
func (d *Dao) CreateRecharge(userId, amount int64) (*model.WalletRecharge, error) {
recharge := &model.WalletRecharge{
UserID: userId,
Amount: amount,
}
return recharge.Create(d.engine)
}
func (d *Dao) HandleRechargeSuccess(recharge *model.WalletRecharge, tradeNo string) error {
user, _ := (&model.User{
Model: &model.Model{
ID: recharge.UserID,
},
}).Get(d.engine)
return d.engine.Transaction(func(tx *gorm.DB) error {
// 扣除金额
if err := tx.Model(user).Update("balance", gorm.Expr("balance + ?", recharge.Amount)).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}
// 新增账单
if err := tx.Create(&model.WalletStatement{
UserID: user.ID,
ChangeAmount: recharge.Amount,
BalanceSnapshot: user.Balance + recharge.Amount,
Reason: "用户充值",
}).Error; err != nil {
return err
}
// 标记为已付款
if err := tx.Model(recharge).Updates(map[string]interface{}{
"trade_no": tradeNo,
"trade_status": "TRADE_SUCCESS",
}).Error; err != nil {
return err
}
// 返回 nil 提交事务
return nil
})
}
func (d *Dao) HandlePostAttachmentBought(post *model.Post, user *model.User) error {
return d.engine.Transaction(func(tx *gorm.DB) error {
// 扣除金额
if err := tx.Model(user).Update("balance", gorm.Expr("balance - ?", post.AttachmentPrice)).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}
// 新增账单
if err := tx.Create(&model.WalletStatement{
PostID: post.ID,
UserID: user.ID,
ChangeAmount: -post.AttachmentPrice,
BalanceSnapshot: user.Balance - post.AttachmentPrice,
Reason: "购买附件支出",
}).Error; err != nil {
return err
}
// 新增附件购买记录
if err := tx.Create(&model.PostAttachmentBill{
PostID: post.ID,
UserID: user.ID,
PaidAmount: post.AttachmentPrice,
}).Error; err != nil {
return err
}
// 对附件主新增账单
income := int64(float64(post.AttachmentPrice) * global.AppSetting.AttachmentIncomeRate)
if income > 0 {
master := &model.User{
Model: &model.Model{
ID: post.UserID,
},
}
master, _ = master.Get(d.engine)
if err := tx.Model(master).Update("balance", gorm.Expr("balance + ?", income)).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}
// 新增账单
if err := tx.Create(&model.WalletStatement{
PostID: post.ID,
UserID: master.ID,
ChangeAmount: income,
BalanceSnapshot: master.Balance + income,
Reason: "出售附件收入",
}).Error; err != nil {
return err
}
}
// 返回 nil 提交事务
return nil
})
}

@ -0,0 +1,76 @@
package middleware
import (
"strings"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/model"
"github.com/rocboss/paopao-api/pkg/app"
"github.com/rocboss/paopao-api/pkg/errcode"
)
func JWT() gin.HandlerFunc {
return func(c *gin.Context) {
var (
token string
ecode = errcode.Success
)
if s, exist := c.GetQuery("token"); exist {
token = s
} else {
token = c.GetHeader("Authorization")
// 验证前端传过来的token格式不为空开头为Bearer
if token == "" || !strings.HasPrefix(token, "Bearer ") {
response := app.NewResponse(c)
response.ToErrorResponse(errcode.UnauthorizedTokenError)
c.Abort()
return
}
// 验证通过提取有效部分除去Bearer)
token = token[7:]
}
if token == "" {
ecode = errcode.InvalidParams
} else {
claims, err := app.ParseToken(token)
if err != nil {
switch err.(*jwt.ValidationError).Errors {
case jwt.ValidationErrorExpired:
ecode = errcode.UnauthorizedTokenTimeout
default:
ecode = errcode.UnauthorizedTokenError
}
} else {
c.Set("UID", claims.UID)
c.Set("USERNAME", claims.Username)
// 加载用户信息
user := &model.User{
Model: &model.Model{
ID: claims.UID,
},
}
user, _ = user.Get(global.DBEngine)
c.Set("USER", user)
// 强制下线机制
if (global.JWTSetting.Issuer + ":" + user.Salt) != claims.Issuer {
ecode = errcode.UnauthorizedTokenTimeout
}
}
}
if ecode != errcode.Success {
response := app.NewResponse(c)
response.ToErrorResponse(ecode)
c.Abort()
return
}
c.Next()
}
}

@ -0,0 +1,33 @@
package middleware
import (
"github.com/gin-gonic/gin"
"github.com/rocboss/paopao-api/internal/model"
"github.com/rocboss/paopao-api/pkg/app"
"github.com/rocboss/paopao-api/pkg/errcode"
)
func Priv() gin.HandlerFunc {
return func(c *gin.Context) {
if user, exist := c.Get("USER"); exist {
if userModel, ok := user.(*model.User); ok {
if userModel.Status == model.UserStatusNormal {
if userModel.Phone == "" {
response := app.NewResponse(c)
response.ToErrorResponse(errcode.AccountNoPhoneBind)
c.Abort()
return
}
c.Next()
return
}
}
}
response := app.NewResponse(c)
response.ToErrorResponse(errcode.UserHasBeenBanned)
c.Abort()
}
}

@ -0,0 +1,27 @@
package model
import "gorm.io/gorm"
type AttachmentType int
const (
ATTACHMENT_TYPE_IMAGE AttachmentType = iota + 1
ATTACHMENT_TYPE_VIDEO
ATTACHMENT_TYPE_OTHER
)
type Attachment struct {
*Model
UserID int64 `json:"user_id"`
FileSize int64 `json:"file_size"`
ImgWidth int `json:"img_width"`
ImgHeight int `json:"img_height"`
Type AttachmentType `json:"type"`
Content string `json:"content"`
}
func (a *Attachment) Create(db *gorm.DB) (*Attachment, error) {
err := db.Create(&a).Error
return a, err
}

@ -0,0 +1,38 @@
package model
import "gorm.io/gorm"
type Captcha struct {
*Model
Phone string `json:"phone"`
Captcha string `json:"captcha"`
UseTimes int `json:"use_times"`
ExpiredOn int64 `json:"expired_on"`
}
func (c *Captcha) Create(db *gorm.DB) (*Captcha, error) {
err := db.Create(&c).Error
return c, err
}
func (c *Captcha) Update(db *gorm.DB) error {
return db.Model(&Captcha{}).Where("id = ? AND is_del = ?", c.Model.ID, 0).Save(c).Error
}
func (c *Captcha) Get(db *gorm.DB) (*Captcha, error) {
var captcha Captcha
if c.Model != nil && c.ID > 0 {
db = db.Where("id = ? AND is_del = ?", c.ID, 0)
}
if c.Phone != "" {
db = db.Where("phone = ?", c.Phone)
}
err := db.Last(&captcha).Error
if err != nil {
return &captcha, err
}
return &captcha, nil
}

@ -0,0 +1,115 @@
package model
import (
"time"
"gorm.io/gorm"
)
type Comment struct {
*Model
PostID int64 `json:"post_id"`
UserID int64 `json:"user_id"`
IP string `json:"ip"`
IPLoc string `json:"ip_loc"`
}
type CommentFormated struct {
ID int64 `json:"id"`
PostID int64 `json:"post_id"`
UserID int64 `json:"user_id"`
User *UserFormated `json:"user"`
Contents []*CommentContent `json:"contents"`
Replies []*CommentReplyFormated `json:"replies"`
IPLoc string `json:"ip_loc"`
CreatedOn int64 `json:"created_on"`
ModifiedOn int64 `json:"modified_on"`
}
func (c *Comment) Format() *CommentFormated {
if c.Model == nil {
return &CommentFormated{}
}
return &CommentFormated{
ID: c.Model.ID,
PostID: c.PostID,
UserID: c.UserID,
User: &UserFormated{},
Contents: []*CommentContent{},
Replies: []*CommentReplyFormated{},
IPLoc: c.IPLoc,
CreatedOn: c.CreatedOn,
ModifiedOn: c.ModifiedOn,
}
}
func (c *Comment) Get(db *gorm.DB) (*Comment, error) {
var comment Comment
if c.Model != nil && c.ID > 0 {
db = db.Where("id = ? AND is_del = ?", c.ID, 0)
} else {
return nil, gorm.ErrRecordNotFound
}
err := db.First(&comment).Error
if err != nil {
return &comment, err
}
return &comment, nil
}
func (c *Comment) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([]*Comment, error) {
var comments []*Comment
var err error
if offset >= 0 && limit > 0 {
db = db.Offset(offset).Limit(limit)
}
if c.PostID > 0 {
db = db.Where("id = ?", c.PostID)
}
for k, v := range *conditions {
if k == "ORDER" {
db = db.Order(v)
} else {
db = db.Where(k, v)
}
}
if err = db.Where("is_del = ?", 0).Find(&comments).Error; err != nil {
return nil, err
}
return comments, nil
}
func (c *Comment) Count(db *gorm.DB, conditions *ConditionsT) (int64, error) {
var count int64
if c.PostID > 0 {
db = db.Where("post_id = ?", c.PostID)
}
for k, v := range *conditions {
if k != "ORDER" {
db = db.Where(k, v)
}
}
if err := db.Model(c).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (c *Comment) Create(db *gorm.DB) (*Comment, error) {
err := db.Create(&c).Error
return c, err
}
func (c *Comment) Delete(db *gorm.DB) error {
return db.Model(&Comment{}).Where("id = ? AND is_del = ?", c.Model.ID, 0).Updates(map[string]interface{}{
"deleted_on": time.Now().Unix(),
"is_del": 1,
}).Error
}

@ -0,0 +1,43 @@
package model
import "gorm.io/gorm"
type CommentContent struct {
*Model
CommentID int64 `json:"comment_id"`
UserID int64 `json:"user_id"`
Content string `json:"content"`
Type PostContentT `json:"type"`
Sort int64 `json:"sort"`
}
func (c *CommentContent) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([]*CommentContent, error) {
var comments []*CommentContent
var err error
if offset >= 0 && limit > 0 {
db = db.Offset(offset).Limit(limit)
}
if c.CommentID > 0 {
db = db.Where("id = ?", c.CommentID)
}
for k, v := range *conditions {
if k == "ORDER" {
db = db.Order(v)
} else {
db = db.Where(k, v)
}
}
if err = db.Where("is_del = ?", 0).Find(&comments).Error; err != nil {
return nil, err
}
return comments, nil
}
func (c *CommentContent) Create(db *gorm.DB) (*CommentContent, error) {
err := db.Create(&c).Error
return c, err
}

@ -0,0 +1,103 @@
package model
import (
"time"
"gorm.io/gorm"
)
type CommentReply struct {
*Model
CommentID int64 `json:"comment_id"`
UserID int64 `json:"user_id"`
AtUserID int64 `json:"at_user_id"`
Content string `json:"content"`
IP string `json:"ip"`
IPLoc string `json:"ip_loc"`
}
type CommentReplyFormated struct {
ID int64 `json:"id"`
CommentID int64 `json:"comment_id"`
UserID int64 `json:"user_id"`
User *UserFormated `json:"user"`
AtUserID int64 `json:"at_user_id"`
AtUser *UserFormated `json:"at_user"`
Content string `json:"content"`
IPLoc string `json:"ip_loc"`
CreatedOn int64 `json:"created_on"`
ModifiedOn int64 `json:"modified_on"`
}
func (c *CommentReply) Format() *CommentReplyFormated {
if c.Model == nil {
return &CommentReplyFormated{}
}
return &CommentReplyFormated{
ID: c.ID,
CommentID: c.CommentID,
UserID: c.UserID,
User: &UserFormated{},
AtUserID: c.AtUserID,
AtUser: &UserFormated{},
Content: c.Content,
IPLoc: c.IPLoc,
CreatedOn: c.CreatedOn,
ModifiedOn: c.ModifiedOn,
}
}
func (c *CommentReply) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([]*CommentReply, error) {
var comments []*CommentReply
var err error
if offset >= 0 && limit > 0 {
db = db.Offset(offset).Limit(limit)
}
if c.CommentID > 0 {
db = db.Where("id = ?", c.CommentID)
}
for k, v := range *conditions {
if k == "ORDER" {
db = db.Order(v)
} else {
db = db.Where(k, v)
}
}
if err = db.Where("is_del = ?", 0).Find(&comments).Error; err != nil {
return nil, err
}
return comments, nil
}
func (c *CommentReply) Create(db *gorm.DB) (*CommentReply, error) {
err := db.Create(&c).Error
return c, err
}
func (c *CommentReply) Get(db *gorm.DB) (*CommentReply, error) {
var reply CommentReply
if c.Model != nil && c.ID > 0 {
db = db.Where("id = ? AND is_del = ?", c.ID, 0)
} else {
return nil, gorm.ErrRecordNotFound
}
err := db.First(&reply).Error
if err != nil {
return &reply, err
}
return &reply, nil
}
func (c *CommentReply) Delete(db *gorm.DB) error {
return db.Model(&CommentReply{}).Where("id = ? AND is_del = ?", c.Model.ID, 0).Updates(map[string]interface{}{
"deleted_on": time.Now().Unix(),
"is_del": 1,
}).Error
}

@ -0,0 +1,138 @@
package model
import "gorm.io/gorm"
type MessageT int8
const (
MESSAGE_POST MessageT = iota + 1
MESSAGE_COMMENT
MESSAGE_REPLY
MESSAGE_WHISPER
)
const MESSAGE_SYSTEM MessageT = 99
const MSG_UNREAD = 0
const MSG_READED = 1
type Message struct {
*Model
SenderUserID int64 `json:"sender_user_id"`
ReceiverUserID int64 `json:"receiver_user_id"`
Type MessageT `json:"type"`
Breif string `json:"breif"`
Content string `json:"content"`
PostID int64 `json:"post_id"`
CommentID int64 `json:"comment_id"`
ReplyID int64 `json:"reply_id"`
IsRead int8 `json:"is_read"`
}
type MessageFormated struct {
ID int64 `json:"id"`
SenderUserID int64 `json:"sender_user_id"`
SenderUser *UserFormated `json:"sender_user"`
ReceiverUserID int64 `json:"receiver_user_id"`
Type MessageT `json:"type"`
Breif string `json:"breif"`
Content string `json:"content"`
PostID int64 `json:"post_id"`
Post *PostFormated `json:"post"`
CommentID int64 `json:"comment_id"`
Comment *Comment `json:"comment"`
ReplyID int64 `json:"reply_id"`
Reply *CommentReply `json:"reply"`
IsRead int8 `json:"is_read"`
CreatedOn int64 `json:"created_on"`
ModifiedOn int64 `json:"modified_on"`
}
func (m *Message) Format() *MessageFormated {
if m.Model == nil || m.Model.ID == 0 {
return nil
}
mf := &MessageFormated{
ID: m.ID,
SenderUserID: m.SenderUserID,
SenderUser: &UserFormated{},
ReceiverUserID: m.ReceiverUserID,
Type: m.Type,
Breif: m.Breif,
Content: m.Content,
PostID: m.PostID,
Post: &PostFormated{},
CommentID: m.CommentID,
Comment: &Comment{},
ReplyID: m.ReplyID,
Reply: &CommentReply{},
IsRead: m.IsRead,
CreatedOn: m.CreatedOn,
ModifiedOn: m.ModifiedOn,
}
return mf
}
func (m *Message) Create(db *gorm.DB) (*Message, error) {
err := db.Create(&m).Error
return m, err
}
func (m *Message) Update(db *gorm.DB) error {
return db.Model(&Message{}).Where("id = ? AND is_del = ?", m.Model.ID, 0).Save(m).Error
}
func (m *Message) Get(db *gorm.DB) (*Message, error) {
var message Message
if m.Model != nil && m.ID > 0 {
db = db.Where("id = ? AND is_del = ?", m.ID, 0)
}
if m.ReceiverUserID > 0 {
db = db.Where("receiver_user_id = ?", m.ReceiverUserID)
}
err := db.First(&message).Error
if err != nil {
return &message, err
}
return &message, nil
}
func (c *Message) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([]*Message, error) {
var messages []*Message
var err error
if offset >= 0 && limit > 0 {
db = db.Offset(offset).Limit(limit)
}
for k, v := range *conditions {
if k == "ORDER" {
db = db.Order(v)
} else {
db = db.Where(k, v)
}
}
if err = db.Where("is_del = ?", 0).Find(&messages).Error; err != nil {
return nil, err
}
return messages, nil
}
func (m *Message) Count(db *gorm.DB, conditions *ConditionsT) (int64, error) {
var count int64
for k, v := range *conditions {
if k != "ORDER" {
db = db.Where(k, v)
}
}
if err := db.Model(m).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}

@ -0,0 +1,81 @@
package model
import (
"fmt"
"time"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/pkg/setting"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"gorm.io/plugin/dbresolver"
"gorm.io/plugin/soft_delete"
)
// 公共Model
type Model struct {
ID int64 `gorm:"primary_key" json:"id"`
CreatedOn int64 `json:"created_on"`
ModifiedOn int64 `json:"modified_on"`
DeletedOn int64 `json:"deleted_on"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag" json:"is_del"`
}
type ConditionsT map[string]interface{}
func NewDBEngine(databaseSetting *setting.DatabaseSettingS) (*gorm.DB, error) {
newLogger := logger.New(
global.Logger, // io writer日志输出的目标前缀和日志包含的内容
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: databaseSetting.LogLevel, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound记录未找到错误
Colorful: false, // 禁用彩色打印
},
)
s := "%s:%s@tcp(%s)/%s?charset=%s&parseTime=%t&loc=Local"
db, err := gorm.Open(mysql.Open(fmt.Sprintf(s,
databaseSetting.UserName,
databaseSetting.Password,
databaseSetting.Host,
databaseSetting.DBName,
databaseSetting.Charset,
databaseSetting.ParseTime,
)), &gorm.Config{
Logger: newLogger,
NamingStrategy: schema.NamingStrategy{
TablePrefix: databaseSetting.TablePrefix,
SingularTable: true,
},
})
if err != nil {
return nil, err
}
db.Use(dbresolver.Register(dbresolver.Config{}).
SetConnMaxIdleTime(time.Hour).
SetConnMaxLifetime(24 * time.Hour).
SetMaxIdleConns(databaseSetting.MaxIdleConns).
SetMaxOpenConns(databaseSetting.MaxOpenConns))
return db, nil
}
func (m *Model) BeforeCreate(tx *gorm.DB) (err error) {
nowTime := time.Now().Unix()
tx.Statement.SetColumn("created_on", nowTime)
tx.Statement.SetColumn("modified_on", nowTime)
return
}
func (m *Model) BeforeUpdate(tx *gorm.DB) (err error) {
if !tx.Statement.Changed("modified_on") {
tx.Statement.SetColumn("modified_on", time.Now().Unix())
}
return
}

@ -0,0 +1,146 @@
package model
import (
"strings"
"time"
"gorm.io/gorm"
)
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"`
}
type PostFormated struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
User *UserFormated `json:"user"`
Contents []*PostContentFormated `json:"contents"`
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"`
CreatedOn int64 `json:"created_on"`
ModifiedOn int64 `json:"modified_on"`
Tags map[string]int8 `json:"tags"`
AttachmentPrice int64 `json:"attachment_price"`
IPLoc string `json:"ip_loc"`
}
func (p *Post) Format() *PostFormated {
if p.Model != nil {
tagsMap := map[string]int8{}
for _, tag := range strings.Split(p.Tags, ",") {
tagsMap[tag] = 1
}
return &PostFormated{
ID: p.ID,
UserID: p.UserID,
User: &UserFormated{},
Contents: []*PostContentFormated{},
CommentCount: p.CommentCount,
CollectionCount: p.CollectionCount,
UpvoteCount: p.UpvoteCount,
IsTop: p.IsTop,
IsEssence: p.IsEssence,
IsLock: p.IsLock,
LatestRepliedOn: p.LatestRepliedOn,
CreatedOn: p.CreatedOn,
ModifiedOn: p.ModifiedOn,
AttachmentPrice: p.AttachmentPrice,
Tags: tagsMap,
IPLoc: p.IPLoc,
}
}
return nil
}
func (p *Post) Create(db *gorm.DB) (*Post, error) {
err := db.Create(&p).Error
return p, err
}
func (s *Post) Delete(db *gorm.DB) error {
return db.Model(&Post{}).Where("id = ? AND is_del = ?", s.Model.ID, 0).Updates(map[string]interface{}{
"deleted_on": time.Now().Unix(),
"is_del": 1,
}).Error
}
func (p *Post) Get(db *gorm.DB) (*Post, error) {
var post Post
if p.Model != nil && p.ID > 0 {
db = db.Where("id = ? AND is_del = ?", p.ID, 0)
} else {
return nil, gorm.ErrRecordNotFound
}
err := db.First(&post).Error
if err != nil {
return &post, err
}
return &post, nil
}
func (p *Post) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([]*Post, error) {
var posts []*Post
var err error
if offset >= 0 && limit > 0 {
db = db.Offset(offset).Limit(limit)
}
if p.UserID > 0 {
db = db.Where("user_id = ?", p.UserID)
}
for k, v := range *conditions {
if k == "ORDER" {
db = db.Order(v)
} else {
db = db.Where(k, v)
}
}
if err = db.Where("is_del = ?", 0).Find(&posts).Error; err != nil {
return nil, err
}
return posts, nil
}
func (p *Post) Count(db *gorm.DB, conditions *ConditionsT) (int64, error) {
var count int64
if p.UserID > 0 {
db = db.Where("user_id = ?", p.UserID)
}
for k, v := range *conditions {
if k != "ORDER" {
db = db.Where(k, v)
}
}
if err := db.Model(p).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (p *Post) Update(db *gorm.DB) error {
return db.Model(&Post{}).Where("id = ? AND is_del = ?", p.Model.ID, 0).Save(p).Error
}

@ -0,0 +1,36 @@
package model
import "gorm.io/gorm"
type PostAttachmentBill struct {
*Model
PostID int64 `json:"post_id"`
UserID int64 `json:"user_id"`
PaidAmount int64 `json:"paid_amount"`
}
func (p *PostAttachmentBill) Get(db *gorm.DB) (*PostAttachmentBill, error) {
var pas PostAttachmentBill
if p.Model != nil && p.ID > 0 {
db = db.Where("id = ? AND is_del = ?", p.ID, 0)
}
if p.PostID > 0 {
db = db.Where("post_id = ?", p.PostID)
}
if p.UserID > 0 {
db = db.Where("user_id = ?", p.UserID)
}
err := db.First(&pas).Error
if err != nil {
return &pas, err
}
return &pas, nil
}
func (p *PostAttachmentBill) Create(db *gorm.DB) (*PostAttachmentBill, error) {
err := db.Create(&p).Error
return p, err
}

@ -0,0 +1,91 @@
package model
import (
"time"
"gorm.io/gorm"
)
type PostCollection struct {
*Model
PostID int64 `json:"post_id"`
UserID int64 `json:"user_id"`
}
func (p *PostCollection) Get(db *gorm.DB) (*PostCollection, error) {
var star PostCollection
if p.Model != nil && p.ID > 0 {
db = db.Where("id = ? AND is_del = ?", p.ID, 0)
}
if p.PostID > 0 {
db = db.Where("post_id = ?", p.PostID)
}
if p.UserID > 0 {
db = db.Where("user_id = ?", p.UserID)
}
err := db.First(&star).Error
if err != nil {
return &star, err
}
return &star, nil
}
func (p *PostCollection) Create(db *gorm.DB) (*PostCollection, error) {
err := db.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{}{
"deleted_on": time.Now().Unix(),
"is_del": 1,
}).Error
}
func (p *PostCollection) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([]*PostCollection, error) {
var collections []*PostCollection
var err error
if offset >= 0 && limit > 0 {
db = db.Offset(offset).Limit(limit)
}
if p.UserID > 0 {
db = db.Where("user_id = ?", p.UserID)
}
for k, v := range *conditions {
if k == "ORDER" {
db = db.Order(v)
} else {
db = db.Where(k, v)
}
}
if err = db.Where("is_del = ?", 0).Find(&collections).Error; err != nil {
return nil, err
}
return collections, nil
}
func (p *PostCollection) Count(db *gorm.DB, conditions *ConditionsT) (int64, error) {
var count int64
if p.PostID > 0 {
db = db.Where("post_id = ?", p.PostID)
}
if p.UserID > 0 {
db = db.Where("user_id = ?", p.UserID)
}
for k, v := range *conditions {
if k != "ORDER" {
db = db.Where(k, v)
}
}
if err := db.Model(p).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}

@ -0,0 +1,94 @@
package model
import "gorm.io/gorm"
// 类型1标题2文字段落3图片地址4视频地址5语音地址6链接地址7附件资源
type PostContentT int
const (
CONTENT_TYPE_TITLE PostContentT = iota + 1
CONTENT_TYPE_TEXT
CONTENT_TYPE_IMAGE
CONTENT_TYPE_VIDEO
CONTENT_TYPE_AUDIO
CONTENT_TYPE_LINK
CONTENT_TYPE_ATTACHMENT
CONTENT_TYPE_CHARGE_ATTACHMENT
)
type PostContent struct {
*Model
PostID int64 `json:"post_id"`
UserID int64 `json:"user_id"`
Content string `json:"content"`
Type PostContentT `json:"type"`
Sort int64 `json:"sort"`
}
type PostContentFormated struct {
ID int64 `json:"id"`
PostID int64 `json:"post_id"`
Content string `json:"content"`
Type PostContentT `json:"type"`
Sort int64 `json:"sort"`
}
func (p *PostContent) Create(db *gorm.DB) (*PostContent, error) {
err := db.Create(&p).Error
return p, err
}
func (p *PostContent) Format() *PostContentFormated {
if p.Model == nil {
return nil
}
return &PostContentFormated{
ID: p.ID,
PostID: p.ID,
Content: p.Content,
Type: p.Type,
Sort: p.Sort,
}
}
func (p *PostContent) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([]*PostContent, error) {
var contents []*PostContent
var err error
if offset >= 0 && limit > 0 {
db = db.Offset(offset).Limit(limit)
}
if p.PostID > 0 {
db = db.Where("id = ?", p.PostID)
}
for k, v := range *conditions {
if k == "ORDER" {
db = db.Order(v)
} else {
db = db.Where(k, v)
}
}
if err = db.Where("is_del = ?", 0).Find(&contents).Error; err != nil {
return nil, err
}
return contents, nil
}
func (p *PostContent) Get(db *gorm.DB) (*PostContent, error) {
var content PostContent
if p.Model != nil && p.ID > 0 {
db = db.Where("id = ? AND is_del = ?", p.ID, 0)
} else {
return nil, gorm.ErrRecordNotFound
}
err := db.First(&content).Error
if err != nil {
return &content, err
}
return &content, nil
}

@ -0,0 +1,91 @@
package model
import (
"time"
"gorm.io/gorm"
)
type PostStar struct {
*Model
PostID int64 `json:"post_id"`
UserID int64 `json:"user_id"`
}
func (p *PostStar) Get(db *gorm.DB) (*PostStar, error) {
var star PostStar
if p.Model != nil && p.ID > 0 {
db = db.Where("id = ? AND is_del = ?", p.ID, 0)
}
if p.PostID > 0 {
db = db.Where("post_id = ?", p.PostID)
}
if p.UserID > 0 {
db = db.Where("user_id = ?", p.UserID)
}
err := db.First(&star).Error
if err != nil {
return &star, err
}
return &star, nil
}
func (p *PostStar) Create(db *gorm.DB) (*PostStar, error) {
err := db.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{}{
"deleted_on": time.Now().Unix(),
"is_del": 1,
}).Error
}
func (p *PostStar) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([]*PostStar, error) {
var stars []*PostStar
var err error
if offset >= 0 && limit > 0 {
db = db.Offset(offset).Limit(limit)
}
if p.UserID > 0 {
db = db.Where("user_id = ?", p.UserID)
}
for k, v := range *conditions {
if k == "ORDER" {
db = db.Order(v)
} else {
db = db.Where(k, v)
}
}
if err = db.Where("is_del = ?", 0).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
if p.PostID > 0 {
db = db.Where("post_id = ?", p.PostID)
}
if p.UserID > 0 {
db = db.Where("user_id = ?", p.UserID)
}
for k, v := range *conditions {
if k != "ORDER" {
db = db.Where(k, v)
}
}
if err := db.Model(p).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}

@ -0,0 +1,81 @@
package model
import "gorm.io/gorm"
type Tag struct {
*Model
UserID int64 `json:"user_id"`
Tag string `json:"tag"`
QuoteNum int64 `json:"quote_num"`
}
type TagFormated struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
User *UserFormated `json:"user"`
Tag string `json:"tag"`
QuoteNum int64 `json:"quote_num"`
}
func (t *Tag) Format() *TagFormated {
if t.Model == nil {
return &TagFormated{}
}
return &TagFormated{
ID: t.ID,
UserID: t.UserID,
User: &UserFormated{},
Tag: t.Tag,
QuoteNum: t.QuoteNum,
}
}
func (t *Tag) Get(db *gorm.DB) (*Tag, error) {
var tag Tag
if t.Model != nil && t.Model.ID > 0 {
db = db.Where("id= ? AND is_del = ?", t.Model.ID, 0)
} else {
db = db.Where("tag = ? AND is_del = ?", t.Tag, 0)
}
err := db.First(&tag).Error
if err != nil {
return &tag, err
}
return &tag, nil
}
func (t *Tag) Create(db *gorm.DB) (*Tag, error) {
err := db.Create(&t).Error
return t, err
}
func (t *Tag) Update(db *gorm.DB) error {
return db.Model(&Tag{}).Where("id = ? AND is_del = ?", t.Model.ID, 0).Save(t).Error
}
func (t *Tag) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([]*Tag, error) {
var tags []*Tag
var err error
if offset >= 0 && limit > 0 {
db = db.Offset(offset).Limit(limit)
}
if t.UserID > 0 {
db = db.Where("user_id = ?", t.UserID)
}
for k, v := range *conditions {
if k == "ORDER" {
db = db.Order(v)
} else {
db = db.Where(k, v)
}
}
if err = db.Where("is_del = ?", 0).Find(&tags).Error; err != nil {
return nil, err
}
return tags, nil
}

@ -0,0 +1,94 @@
package model
import "gorm.io/gorm"
const (
UserStatusNormal int = iota + 1
UserStatusClosed
)
type User struct {
*Model
Nickname string `json:"nickname"`
Username string `json:"username"`
Phone string `json:"phone"`
Password string `json:"password"`
Salt string `json:"salt"`
Status int `json:"status"`
Avatar string `json:"avatar"`
Balance int64 `json:"balance"`
IsAdmin bool `json:"is_admin"`
}
type UserFormated struct {
ID int64 `json:"id"`
Nickname string `json:"nickname"`
Username string `json:"username"`
Status int `json:"status"`
Avatar string `json:"avatar"`
IsAdmin bool `json:"is_admin"`
}
func (u *User) Format() *UserFormated {
if u.Model != nil {
return &UserFormated{
ID: u.ID,
Nickname: u.Nickname,
Username: u.Username,
Status: u.Status,
Avatar: u.Avatar,
IsAdmin: u.IsAdmin,
}
}
return nil
}
func (u *User) Get(db *gorm.DB) (*User, error) {
var user User
if u.Model != nil && u.Model.ID > 0 {
db = db.Where("id= ? AND is_del = ?", u.Model.ID, 0)
} else if u.Phone != "" {
db = db.Where("phone = ? AND is_del = ?", u.Phone, 0)
} else {
db = db.Where("username = ? AND is_del = ?", u.Username, 0)
}
err := db.First(&user).Error
if err != nil {
return &user, err
}
return &user, nil
}
func (u *User) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([]*User, error) {
var users []*User
var err error
if offset >= 0 && limit > 0 {
db = db.Offset(offset).Limit(limit)
}
for k, v := range *conditions {
if k == "ORDER" {
db = db.Order(v)
} else {
db = db.Where(k, v)
}
}
if err = db.Where("is_del = ?", 0).Find(&users).Error; err != nil {
return nil, err
}
return users, nil
}
func (u *User) Create(db *gorm.DB) (*User, error) {
err := db.Create(&u).Error
return u, err
}
func (u *User) Update(db *gorm.DB) error {
return db.Model(&User{}).Where("id = ? AND is_del = ?", u.Model.ID, 0).Save(u).Error
}

@ -0,0 +1,34 @@
package model
import "gorm.io/gorm"
type WalletRecharge struct {
*Model
UserID int64 `json:"user_id"`
Amount int64 `json:"amount"`
TradeNo string `json:"trade_no"`
TradeStatus string `json:"trade_status"`
}
func (p *WalletRecharge) Get(db *gorm.DB) (*WalletRecharge, error) {
var pas WalletRecharge
if p.Model != nil && p.ID > 0 {
db = db.Where("id = ? AND is_del = ?", p.ID, 0)
}
if p.UserID > 0 {
db = db.Where("user_id = ?", p.UserID)
}
err := db.First(&pas).Error
if err != nil {
return &pas, err
}
return &pas, nil
}
func (p *WalletRecharge) Create(db *gorm.DB) (*WalletRecharge, error) {
err := db.Create(&p).Error
return p, err
}

@ -0,0 +1,83 @@
package model
import "gorm.io/gorm"
type WalletStatement struct {
*Model
UserID int64 `json:"user_id"`
ChangeAmount int64 `json:"change_amount"`
BalanceSnapshot int64 `json:"balance_snapshot"`
Reason string `json:"reason"`
PostID int64 `json:"post_id"`
}
func (w *WalletStatement) Get(db *gorm.DB) (*WalletStatement, error) {
var ws WalletStatement
if w.Model != nil && w.ID > 0 {
db = db.Where("id = ? AND is_del = ?", w.ID, 0)
}
if w.PostID > 0 {
db = db.Where("post_id = ?", w.PostID)
}
if w.UserID > 0 {
db = db.Where("user_id = ?", w.UserID)
}
err := db.First(&ws).Error
if err != nil {
return &ws, err
}
return &ws, nil
}
func (w *WalletStatement) Create(db *gorm.DB) (*WalletStatement, error) {
err := db.Create(&w).Error
return w, err
}
func (w *WalletStatement) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([]*WalletStatement, error) {
var records []*WalletStatement
var err error
if offset >= 0 && limit > 0 {
db = db.Offset(offset).Limit(limit)
}
if w.UserID > 0 {
db = db.Where("user_id = ?", w.UserID)
}
for k, v := range *conditions {
if k == "ORDER" {
db = db.Order(v)
} else {
db = db.Where(k, v)
}
}
if err = db.Where("is_del = ?", 0).Find(&records).Error; err != nil {
return nil, err
}
return records, nil
}
func (w *WalletStatement) Count(db *gorm.DB, conditions *ConditionsT) (int64, error) {
var count int64
if w.PostID > 0 {
db = db.Where("post_id = ?", w.PostID)
}
if w.UserID > 0 {
db = db.Where("user_id = ?", w.UserID)
}
for k, v := range *conditions {
if k != "ORDER" {
db = db.Where(k, v)
}
}
if err := db.Model(w).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}

@ -0,0 +1,299 @@
package api
import (
"image"
"net/url"
"strings"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/disintegration/imaging"
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/model"
"github.com/rocboss/paopao-api/internal/service"
"github.com/rocboss/paopao-api/pkg/app"
"github.com/rocboss/paopao-api/pkg/convert"
"github.com/rocboss/paopao-api/pkg/errcode"
)
func GeneratePath(s string) string {
n := len(s)
if n <= 2 {
return s
}
return GeneratePath(s[:n-2]) + "/" + s[n-2:]
}
func GetFileExt(s string) (string, error) {
switch s {
case "image/png":
return ".png", nil
case "image/jpg":
return ".jpg", nil
case "image/jpeg":
return ".jpeg", nil
case "image/gif":
return ".gif", nil
case "video/mp4":
return ".mp4", nil
case "video/quicktime":
return ".mov", nil
case "application/zip":
return ".zip", nil
default:
return "", errcode.FileInvalidExt.WithDetails("仅允许 png/jpg/gif/mp4/mov/zip 类型")
}
}
func GetImageSize(img image.Rectangle) (int, int) {
b := img.Bounds()
width := b.Max.X
height := b.Max.Y
return width, height
}
func UploadAttachment(c *gin.Context) {
response := app.NewResponse(c)
svc := service.New(c)
uploadType := c.Request.FormValue("type")
file, fileHeader, err := c.Request.FormFile("file")
if err != nil {
global.Logger.Errorf("api.UploadAttachment err: %v", err)
response.ToErrorResponse(errcode.FileUploadFailed)
return
}
defer file.Close()
if uploadType != "public/video" &&
uploadType != "public/image" &&
uploadType != "public/avatar" &&
uploadType != "attachment" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
if fileHeader.Size > 1024*1024*100 {
response.ToErrorResponse(errcode.FileInvalidSize.WithDetails("最大允许100MB"))
return
}
fileExt, err := GetFileExt(fileHeader.Header.Get("Content-Type"))
if err != nil {
global.Logger.Errorf("GetFileExt err: %v", err)
response.ToErrorResponse(err.(*errcode.Error))
return
}
fileReader, err := fileHeader.Open()
if err != nil {
global.Logger.Errorf("Attachment file read err: %v", err)
response.ToErrorResponse(errcode.FileUploadFailed)
return
}
defer fileReader.Close()
// 生成随机路径
randomPath := uuid.Must(uuid.NewV4()).String()
ossSavePath := uploadType + "/" + GeneratePath(randomPath[:8]) + "/" + randomPath[9:] + fileExt
client, err := oss.New(global.AliossSetting.AliossEndpoint, global.AliossSetting.AliossAccessKeyID, global.AliossSetting.AliossAccessKeySecret)
if err != nil {
global.Logger.Errorf("oss.New err: %v", err)
response.ToErrorResponse(errcode.FileUploadFailed)
return
}
bucket, err := client.Bucket("paopao-assets")
if err != nil {
global.Logger.Errorf("client.Bucket err: %v", err)
response.ToErrorResponse(errcode.FileUploadFailed)
return
}
err = bucket.PutObject(ossSavePath, fileReader)
if err != nil {
global.Logger.Errorf("bucket.PutObject err: %v", err)
response.ToErrorResponse(errcode.FileUploadFailed)
return
}
// 构造附件Model
attachment := &model.Attachment{
FileSize: fileHeader.Size,
Content: "https://" + global.AliossSetting.AliossDomain + "/" + ossSavePath,
}
if userID, exists := c.Get("UID"); exists {
attachment.UserID = userID.(int64)
}
if uploadType == "public/image" || uploadType == "public/avatar" {
attachment.Type = model.ATTACHMENT_TYPE_IMAGE
src, err := imaging.Decode(file)
if err == nil {
attachment.ImgWidth, attachment.ImgHeight = GetImageSize(src.Bounds())
}
}
if uploadType == "public/video" {
attachment.Type = model.ATTACHMENT_TYPE_VIDEO
}
if uploadType == "attachment" {
attachment.Type = model.ATTACHMENT_TYPE_OTHER
}
attachment, err = svc.CreateAttachment(attachment)
if err != nil {
global.Logger.Errorf("svc.CreateAttachment err: %v", err)
response.ToErrorResponse(errcode.FileUploadFailed)
}
response.ToResponse(attachment)
}
func DownloadAttachmentPrecheck(c *gin.Context) {
response := app.NewResponse(c)
svc := service.New(c)
contentID := convert.StrTo(c.Query("id")).MustInt64()
// 加载content
content, err := svc.GetPostContentByID(contentID)
if err != nil {
global.Logger.Errorf("svc.GetPostContentByID err: %v", err)
response.ToErrorResponse(errcode.InvalidDownloadReq)
}
user, _ := c.Get("USER")
if content.Type == model.CONTENT_TYPE_CHARGE_ATTACHMENT {
// 加载post
post, err := svc.GetPost(content.PostID)
if err != nil {
global.Logger.Errorf("svc.GetPost err: %v", err)
response.ToResponse(gin.H{
"paid": false,
})
return
}
// 发布者或管理员免费下载
if post.UserID == user.(*model.User).ID || user.(*model.User).IsAdmin {
response.ToResponse(gin.H{
"paid": true,
})
return
}
// 检测是否有购买记录
response.ToResponse(gin.H{
"paid": svc.CheckPostAttachmentIsPaid(post.ID, user.(*model.User).ID),
})
return
}
response.ToResponse(gin.H{
"paid": false,
})
}
func DownloadAttachment(c *gin.Context) {
response := app.NewResponse(c)
svc := service.New(c)
contentID := convert.StrTo(c.Query("id")).MustInt64()
// 加载content
content, err := svc.GetPostContentByID(contentID)
if err != nil {
global.Logger.Errorf("svc.GetPostContentByID err: %v", err)
response.ToErrorResponse(errcode.InvalidDownloadReq)
}
// 收费附件
if content.Type == model.CONTENT_TYPE_CHARGE_ATTACHMENT {
user, _ := c.Get("USER")
// 加载post
post, err := svc.GetPost(content.PostID)
if err != nil {
global.Logger.Errorf("svc.GetPost err: %v", err)
response.ToResponse(gin.H{
"paid": false,
})
return
}
paidFlag := false
// 发布者或管理员免费下载
if post.UserID == user.(*model.User).ID || user.(*model.User).IsAdmin {
paidFlag = true
}
// 检测是否有购买记录
if svc.CheckPostAttachmentIsPaid(post.ID, user.(*model.User).ID) {
paidFlag = true
}
if !paidFlag {
// 未购买,则尝试购买
err := svc.BuyPostAttachment(&model.Post{
Model: &model.Model{
ID: post.ID,
},
UserID: post.UserID,
AttachmentPrice: post.AttachmentPrice,
}, user.(*model.User))
if err != nil {
global.Logger.Errorf("svc.BuyPostAttachment err: %v", err)
if err == errcode.InsuffientDownloadMoney {
response.ToErrorResponse(errcode.InsuffientDownloadMoney)
} else {
response.ToErrorResponse(errcode.DownloadExecFail)
}
return
}
}
}
// 开始构造下载地址
client, err := oss.New(global.AliossSetting.AliossEndpoint, global.AliossSetting.AliossAccessKeyID, global.AliossSetting.AliossAccessKeySecret)
if err != nil {
global.Logger.Errorf("oss.New err: %v", err)
response.ToErrorResponse(errcode.DownloadReqError)
return
}
bucket, err := client.Bucket("paopao-assets")
if err != nil {
global.Logger.Errorf("client.Bucket err: %v", err)
response.ToErrorResponse(errcode.DownloadReqError)
return
}
// 签名
objectKey := strings.Replace(content.Content, "https://"+global.AliossSetting.AliossDomain+"/", "", -1)
signedURL, err := bucket.SignURL(objectKey, oss.HTTPGet, 60)
if err != nil {
global.Logger.Errorf("client.SignURL err: %v", err)
response.ToErrorResponse(errcode.DownloadReqError)
return
}
ur, err := url.Parse(signedURL)
if err != nil {
global.Logger.Errorf("url.Parse err: %v", err)
response.ToErrorResponse(errcode.DownloadReqError)
return
}
epath, err := url.PathUnescape(ur.Path)
if err != nil {
global.Logger.Errorf("url.PathUnescape err: %v", err)
response.ToErrorResponse(errcode.DownloadReqError)
return
}
ur.Path = epath
ur.RawPath = epath
response.ToResponse(ur.String())
}

@ -0,0 +1,148 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/model"
"github.com/rocboss/paopao-api/internal/service"
"github.com/rocboss/paopao-api/pkg/app"
"github.com/rocboss/paopao-api/pkg/convert"
"github.com/rocboss/paopao-api/pkg/errcode"
)
func GetPostComments(c *gin.Context) {
postID := convert.StrTo(c.Query("id")).MustInt64()
response := app.NewResponse(c)
svc := service.New(c)
contents, totalRows, err := svc.GetPostComments(postID, "id ASC", 0, 0)
if err != nil {
global.Logger.Errorf("svc.GetPostComments err: %v\n", err)
response.ToErrorResponse(errcode.GetCommentsFailed)
return
}
response.ToResponseList(contents, totalRows)
}
func CreatePostComment(c *gin.Context) {
param := service.CommentCreationReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
userID, _ := c.Get("UID")
svc := service.New(c)
comment, err := svc.CreatePostComment(userID.(int64), param)
if err != nil {
if err == errcode.MaxCommentCount {
response.ToErrorResponse(errcode.MaxCommentCount)
} else {
global.Logger.Errorf("svc.CreatePostComment err: %v\n", err)
response.ToErrorResponse(errcode.CreateCommentFailed)
}
return
}
response.ToResponse(comment)
}
func DeletePostComment(c *gin.Context) {
param := service.CommentDelReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
user, _ := c.Get("USER")
svc := service.New(c)
comment, err := svc.GetPostComment(param.ID)
if err != nil {
global.Logger.Errorf("svc.GetPostComment err: %v\n", err)
response.ToErrorResponse(errcode.GetCommentFailed)
return
}
if user.(*model.User).ID != comment.UserID && !user.(*model.User).IsAdmin {
response.ToErrorResponse(errcode.NoPermission)
return
}
// 执行删除
err = svc.DeletePostComment(comment)
if err != nil {
global.Logger.Errorf("svc.DeletePostComment err: %v\n", err)
response.ToErrorResponse(errcode.DeleteCommentFailed)
return
}
response.ToResponse(nil)
}
func CreatePostCommentReply(c *gin.Context) {
param := service.CommentReplyCreationReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
user, _ := c.Get("USER")
svc := service.New(c)
comment, err := svc.CreatePostCommentReply(param.CommentID, param.Content, user.(*model.User).ID, param.AtUserID)
if err != nil {
global.Logger.Errorf("svc.CreatePostCommentReply err: %v\n", err)
response.ToErrorResponse(errcode.CreateReplyFailed)
return
}
response.ToResponse(comment)
}
func DeletePostCommentReply(c *gin.Context) {
param := service.ReplyDelReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
user, _ := c.Get("USER")
svc := service.New(c)
reply, err := svc.GetPostCommentReply(param.ID)
if err != nil {
global.Logger.Errorf("svc.GetPostCommentReply err: %v\n", err)
response.ToErrorResponse(errcode.GetReplyFailed)
return
}
if user.(*model.User).ID != reply.UserID && !user.(*model.User).IsAdmin {
response.ToErrorResponse(errcode.NoPermission)
return
}
// 执行删除
err = svc.DeletePostCommentReply(reply)
if err != nil {
global.Logger.Errorf("svc.DeletePostCommentReply err: %v\n", err)
response.ToErrorResponse(errcode.DeleteCommentFailed)
return
}
response.ToResponse(nil)
}

@ -0,0 +1,103 @@
package api
import (
"bytes"
"encoding/base64"
"image/color"
"image/png"
"time"
"github.com/afocus/captcha"
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/model"
"github.com/rocboss/paopao-api/internal/service"
"github.com/rocboss/paopao-api/pkg/app"
"github.com/rocboss/paopao-api/pkg/convert"
"github.com/rocboss/paopao-api/pkg/errcode"
"github.com/rocboss/paopao-api/pkg/util"
)
const MAX_PHONE_CAPTCHA = 10
func Version(c *gin.Context) {
response := app.NewResponse(c)
response.ToResponse(gin.H{
"version": "PaoPao Service v1.0",
})
}
func SyncSearchIndex(c *gin.Context) {
response := app.NewResponse(c)
svc := service.New(c)
user, _ := c.Get("USER")
if user.(*model.User).IsAdmin {
go svc.PushPostsToSearch()
}
response.ToResponse(nil)
}
func GetCaptcha(c *gin.Context) {
cap := captcha.New()
if err := cap.SetFont("comic.ttf"); err != nil {
panic(err.Error())
}
cap.SetSize(160, 64)
cap.SetDisturbance(captcha.MEDIUM)
cap.SetFrontColor(color.RGBA{0, 0, 0, 255})
cap.SetBkgColor(color.RGBA{218, 240, 228, 255})
img, password := cap.Create(6, captcha.NUM)
emptyBuff := bytes.NewBuffer(nil)
_ = png.Encode(emptyBuff, img)
key := util.EncodeMD5(uuid.Must(uuid.NewV4()).String())
// 五分钟有效期
global.Redis.SetEX(c, "PaoPaoCaptcha:"+key, password, time.Minute*5)
response := app.NewResponse(c)
response.ToResponse(gin.H{
"id": key,
"b64s": "data:image/png;base64," + base64.StdEncoding.EncodeToString(emptyBuff.Bytes()),
})
}
func PostCaptcha(c *gin.Context) {
param := service.PhoneCaptchaReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
svc := service.New(c)
// 验证图片验证码
if res, err := global.Redis.Get(c.Request.Context(), "PaoPaoCaptcha:"+param.ImgCaptchaID).Result(); err != nil || res != param.ImgCaptcha {
response.ToErrorResponse(errcode.ErrorCaptchaPassword)
return
}
global.Redis.Del(c.Request.Context(), "PaoPaoCaptcha:"+param.ImgCaptchaID).Result()
// 今日频次限制
if res, _ := global.Redis.Get(c.Request.Context(), "PaoPaoSmsCaptcha:"+param.Phone).Result(); convert.StrTo(res).MustInt() >= MAX_PHONE_CAPTCHA {
response.ToErrorResponse(errcode.TooManyPhoneCaptchaSend)
return
}
err := svc.SendPhoneCaptcha(param.Phone)
if err != nil {
global.Logger.Errorf("app.SendPhoneCaptcha errs: %v", errs)
response.ToErrorResponse(errcode.GetPhoneCaptchaError)
return
}
response.ToResponse(nil)
}

@ -0,0 +1,64 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/model"
"github.com/rocboss/paopao-api/internal/service"
"github.com/rocboss/paopao-api/pkg/app"
"github.com/rocboss/paopao-api/pkg/errcode"
)
func GetUnreadMsgCount(c *gin.Context) {
response := app.NewResponse(c)
user := &model.User{}
if u, exists := c.Get("USER"); exists {
user = u.(*model.User)
}
svc := service.New(c)
count, _ := svc.GetUnreadCount(user.ID)
response.ToResponse(gin.H{
"count": count,
})
}
func GetMessages(c *gin.Context) {
response := app.NewResponse(c)
userID, _ := c.Get("UID")
svc := service.New(c)
messages, totalRows, err := svc.GetMessages(userID.(int64), (app.GetPage(c)-1)*app.GetPageSize(c), app.GetPageSize(c))
if err != nil {
global.Logger.Errorf("svc.GetMessages err: %v\n", err)
response.ToErrorResponse(errcode.GetMessagesFailed)
return
}
response.ToResponseList(messages, totalRows)
}
func ReadMessage(c *gin.Context) {
param := service.ReadMessageReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
userID, _ := c.Get("UID")
svc := service.New(c)
err := svc.ReadMessage(param.ID, userID.(int64))
if err != nil {
global.Logger.Errorf("svc.ReadMessage err: %v\n", err)
response.ToErrorResponse(errcode.ReadMessageFailed)
return
}
response.ToResponse(nil)
}

@ -0,0 +1,290 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/dao"
"github.com/rocboss/paopao-api/internal/model"
"github.com/rocboss/paopao-api/internal/service"
"github.com/rocboss/paopao-api/pkg/app"
"github.com/rocboss/paopao-api/pkg/convert"
"github.com/rocboss/paopao-api/pkg/errcode"
)
func GetPostList(c *gin.Context) {
response := app.NewResponse(c)
q := &dao.QueryT{
Query: c.Query("query"),
Type: "search",
}
if c.Query("type") == "tag" {
q.Type = "tag"
}
svc := service.New(c)
if q.Query == "" && q.Type == "search" {
// 直接读库
posts, err := svc.GetPostList(&service.PostListReq{
Conditions: &model.ConditionsT{
"ORDER": "is_top DESC, latest_replied_on DESC",
},
Offset: (app.GetPage(c) - 1) * app.GetPageSize(c),
Limit: app.GetPageSize(c),
})
if err != nil {
global.Logger.Errorf("svc.GetPostList err: %v\n", err)
response.ToErrorResponse(errcode.GetPostsFailed)
return
}
totalRows, _ := svc.GetPostCount(&model.ConditionsT{
"ORDER": "latest_replied_on DESC",
})
response.ToResponseList(posts, totalRows)
} else {
posts, totalRows, err := svc.GetPostListFromSearch(q, (app.GetPage(c)-1)*app.GetPageSize(c), app.GetPageSize(c))
if err != nil {
global.Logger.Errorf("svc.GetPostListFromSearch err: %v\n", err)
response.ToErrorResponse(errcode.GetPostsFailed)
return
}
response.ToResponseList(posts, totalRows)
}
}
func GetPost(c *gin.Context) {
postID := convert.StrTo(c.Query("id")).MustInt64()
response := app.NewResponse(c)
svc := service.New(c)
postFormated, err := svc.GetPost(postID)
if err != nil {
global.Logger.Errorf("svc.GetPost err: %v\n", err)
response.ToErrorResponse(errcode.GetPostFailed)
return
}
response.ToResponse(postFormated)
}
func CreatePost(c *gin.Context) {
param := service.PostCreationReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
userID, _ := c.Get("UID")
svc := service.New(c)
post, err := svc.CreatePost(userID.(int64), param)
if err != nil {
global.Logger.Errorf("svc.CreatePost err: %v\n", err)
response.ToErrorResponse(errcode.CreatePostFailed)
return
}
response.ToResponse(post)
}
func DeletePost(c *gin.Context) {
param := service.PostDelReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
user, _ := c.Get("USER")
svc := service.New(c)
// 获取Post
postFormated, err := svc.GetPost(param.ID)
if err != nil {
global.Logger.Errorf("svc.GetPost err: %v\n", err)
response.ToErrorResponse(errcode.GetPostFailed)
return
}
if postFormated.UserID != user.(*model.User).ID && !user.(*model.User).IsAdmin {
response.ToErrorResponse(errcode.NoPermission)
return
}
err = svc.DeletePost(param.ID)
if err != nil {
global.Logger.Errorf("svc.DeletePost err: %v\n", err)
response.ToErrorResponse(errcode.DeletePostFailed)
return
}
response.ToResponse(nil)
}
func GetPostStar(c *gin.Context) {
postID := convert.StrTo(c.Query("id")).MustInt64()
response := app.NewResponse(c)
svc := service.New(c)
userID, _ := c.Get("UID")
_, err := svc.GetPostStar(postID, userID.(int64))
if err != nil {
response.ToResponse(gin.H{
"status": false,
})
return
}
response.ToResponse(gin.H{
"status": true,
})
}
func PostStar(c *gin.Context) {
param := service.PostStarReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
svc := service.New(c)
userID, _ := c.Get("UID")
status := false
star, err := svc.GetPostStar(param.ID, userID.(int64))
if err != nil {
// 创建Star
svc.CreatePostStar(param.ID, userID.(int64))
status = true
} else {
// 取消Star
svc.DeletePostStar(star)
}
response.ToResponse(gin.H{
"status": status,
})
}
func GetPostCollection(c *gin.Context) {
postID := convert.StrTo(c.Query("id")).MustInt64()
response := app.NewResponse(c)
svc := service.New(c)
userID, _ := c.Get("UID")
_, err := svc.GetPostCollection(postID, userID.(int64))
if err != nil {
response.ToResponse(gin.H{
"status": false,
})
return
}
response.ToResponse(gin.H{
"status": true,
})
}
func PostCollection(c *gin.Context) {
param := service.PostCollectionReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
svc := service.New(c)
userID, _ := c.Get("UID")
status := false
collection, err := svc.GetPostCollection(param.ID, userID.(int64))
if err != nil {
// 创建collection
svc.CreatePostCollection(param.ID, userID.(int64))
status = true
} else {
// 取消Star
svc.DeletePostCollection(collection)
}
response.ToResponse(gin.H{
"status": status,
})
}
func LockPost(c *gin.Context) {
param := service.PostLockReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
user, _ := c.Get("USER")
svc := service.New(c)
// 获取Post
postFormated, err := svc.GetPost(param.ID)
if err != nil {
global.Logger.Errorf("svc.GetPost err: %v\n", err)
response.ToErrorResponse(errcode.GetPostFailed)
return
}
if postFormated.UserID != user.(*model.User).ID && !user.(*model.User).IsAdmin {
response.ToErrorResponse(errcode.NoPermission)
return
}
err = svc.LockPost(param.ID)
if err != nil {
global.Logger.Errorf("svc.LockPost err: %v\n", err)
response.ToErrorResponse(errcode.LockPostFailed)
return
}
response.ToResponse(gin.H{
"lock_status": 1 - postFormated.IsLock,
})
}
func GetPostTags(c *gin.Context) {
param := service.PostTagsReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
svc := service.New(c)
tags, err := svc.GetPostTags(&param)
if err != nil {
global.Logger.Errorf("svc.GetPostTags err: %v\n", err)
response.ToErrorResponse(errcode.GetPostTagsFailed)
return
}
response.ToResponse(tags)
}

@ -0,0 +1,547 @@
package api
import (
"fmt"
"net/http"
"strings"
"unicode/utf8"
"github.com/gin-gonic/gin"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/model"
"github.com/rocboss/paopao-api/internal/service"
"github.com/rocboss/paopao-api/pkg/app"
"github.com/rocboss/paopao-api/pkg/convert"
"github.com/rocboss/paopao-api/pkg/errcode"
"github.com/smartwalle/alipay/v3"
)
// 用户登录
func Login(c *gin.Context) {
param := service.AuthRequest{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
svc := service.New(c)
user, err := svc.DoLogin(&param)
if err != nil {
global.Logger.Errorf("svc.DoLogin err: %v", err)
response.ToErrorResponse(err.(*errcode.Error))
return
}
token, err := app.GenerateToken(user)
if err != nil {
global.Logger.Errorf("app.GenerateToken err: %v", err)
response.ToErrorResponse(errcode.UnauthorizedTokenGenerate)
return
}
response.ToResponse(gin.H{
"token": token,
})
}
// 用户注册
func Register(c *gin.Context) {
param := service.RegisterRequest{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
svc := service.New(c)
// 用户名检查
err := svc.ValidUsername(param.Username)
if err != nil {
global.Logger.Errorf("svc.Register err: %v", err)
response.ToErrorResponse(err.(*errcode.Error))
return
}
// 密码检查
err = svc.CheckPassword(param.Password)
if err != nil {
global.Logger.Errorf("svc.Register err: %v", err)
response.ToErrorResponse(err.(*errcode.Error))
return
}
user, err := svc.Register(
param.Username,
param.Password,
)
if err != nil {
global.Logger.Errorf("svc.Register err: %v", err)
response.ToErrorResponse(errcode.UserRegisterFailed)
return
}
response.ToResponse(gin.H{
"id": user.ID,
"username": user.Username,
})
}
// 获取用户基本信息
func GetUserInfo(c *gin.Context) {
param := service.AuthRequest{}
response := app.NewResponse(c)
svc := service.New(c)
if username, exists := c.Get("USERNAME"); exists {
param.Username = username.(string)
}
user, err := svc.GetUserInfo(&param)
if err != nil {
response.ToErrorResponse(errcode.UnauthorizedAuthNotExist)
return
}
phone := ""
if user.Phone != "" && len(user.Phone) == 11 {
phone = user.Phone[0:3] + "****" + user.Phone[7:]
}
response.ToResponse(gin.H{
"id": user.ID,
"nickname": user.Nickname,
"username": user.Username,
"status": user.Status,
"avatar": user.Avatar,
"balance": user.Balance,
"phone": phone,
"is_admin": user.IsAdmin,
})
}
// 修改密码
func ChangeUserPassword(c *gin.Context) {
param := service.ChangePasswordReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
user := &model.User{}
if u, exists := c.Get("USER"); exists {
user = u.(*model.User)
}
svc := service.New(c)
// 密码检查
err := svc.CheckPassword(param.Password)
if err != nil {
global.Logger.Errorf("svc.Register err: %v", err)
response.ToErrorResponse(err.(*errcode.Error))
return
}
// 旧密码校验
if !svc.ValidPassword(user.Password, param.OldPassword, user.Salt) {
response.ToErrorResponse(errcode.ErrorOldPassword)
return
}
// 更新入库
user.Password, user.Salt = svc.EncryptPasswordAndSalt(param.Password)
svc.UpdateUserInfo(user)
response.ToResponse(nil)
}
// 修改昵称
func ChangeNickname(c *gin.Context) {
param := service.ChangeNicknameReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
user := &model.User{}
if u, exists := c.Get("USER"); exists {
user = u.(*model.User)
}
svc := service.New(c)
if utf8.RuneCountInString(param.Nickname) < 2 || utf8.RuneCountInString(param.Nickname) > 12 {
response.ToErrorResponse(errcode.NicknameLengthLimit)
return
}
// 执行绑定
user.Nickname = param.Nickname
svc.UpdateUserInfo(user)
response.ToResponse(nil)
}
// 修改头像
func ChangeAvatar(c *gin.Context) {
param := service.ChangeAvatarReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
user := &model.User{}
if u, exists := c.Get("USER"); exists {
user = u.(*model.User)
}
svc := service.New(c)
if strings.Index(param.Avatar, "https://"+global.AliossSetting.AliossDomain) != 0 {
response.ToErrorResponse(errcode.InvalidParams)
return
}
// 执行绑定
user.Avatar = param.Avatar
svc.UpdateUserInfo(user)
response.ToResponse(nil)
}
// 用户绑定手机号
func BindUserPhone(c *gin.Context) {
param := service.UserPhoneBindReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
user := &model.User{}
if u, exists := c.Get("USER"); exists {
user = u.(*model.User)
}
svc := service.New(c)
// 手机重复性检查
if svc.CheckPhoneExist(user.ID, param.Phone) {
response.ToErrorResponse(errcode.ExistedUserPhone)
return
}
// 验证短信验证码
err := svc.CheckPhoneCaptcha(param.Phone, param.Captcha)
if err != nil {
global.Logger.Errorf("svc.CheckPhoneCaptcha err: %v\n", err)
response.ToErrorResponse(err)
return
}
// 执行绑定
user.Phone = param.Phone
svc.UpdateUserInfo(user)
response.ToResponse(nil)
}
func GetUserProfile(c *gin.Context) {
response := app.NewResponse(c)
username := c.Query("username")
svc := service.New(c)
user, err := svc.GetUserByUsername(username)
if err != nil {
global.Logger.Errorf("svc.GetUserByUsername err: %v\n", err)
response.ToErrorResponse(errcode.NoExistUsername)
return
}
response.ToResponse(gin.H{
"id": user.ID,
"nickname": user.Nickname,
"username": user.Username,
"status": user.Status,
"avatar": user.Avatar,
"is_admin": user.IsAdmin,
})
}
func GetUserPosts(c *gin.Context) {
response := app.NewResponse(c)
username := c.Query("username")
svc := service.New(c)
user, err := svc.GetUserByUsername(username)
if err != nil {
global.Logger.Errorf("svc.GetUserByUsername err: %v\n", err)
response.ToErrorResponse(errcode.NoExistUsername)
return
}
conditions := &model.ConditionsT{
"user_id": user.ID,
"ORDER": "latest_replied_on DESC",
}
posts, err := svc.GetPostList(&service.PostListReq{
Conditions: conditions,
Offset: (app.GetPage(c) - 1) * app.GetPageSize(c),
Limit: app.GetPageSize(c),
})
if err != nil {
global.Logger.Errorf("svc.GetPostList err: %v\n", err)
response.ToErrorResponse(errcode.GetPostsFailed)
return
}
totalRows, _ := svc.GetPostCount(conditions)
response.ToResponseList(posts, totalRows)
}
func GetUserCollections(c *gin.Context) {
response := app.NewResponse(c)
userID, _ := c.Get("UID")
svc := service.New(c)
posts, totalRows, err := svc.GetUserCollections(userID.(int64), (app.GetPage(c)-1)*app.GetPageSize(c), app.GetPageSize(c))
if err != nil {
global.Logger.Errorf("svc.GetUserCollections err: %v\n", err)
response.ToErrorResponse(errcode.GetCollectionsFailed)
return
}
response.ToResponseList(posts, totalRows)
}
func GetUserStars(c *gin.Context) {
response := app.NewResponse(c)
userID, _ := c.Get("UID")
svc := service.New(c)
posts, totalRows, err := svc.GetUserStars(userID.(int64), (app.GetPage(c)-1)*app.GetPageSize(c), app.GetPageSize(c))
if err != nil {
global.Logger.Errorf("svc.GetUserStars err: %v\n", err)
response.ToErrorResponse(errcode.GetCollectionsFailed)
return
}
response.ToResponseList(posts, totalRows)
}
func GetSuggestUsers(c *gin.Context) {
keyword := c.Query("k")
response := app.NewResponse(c)
svc := service.New(c)
usernames, err := svc.GetSuggestUsers(keyword)
if err != nil {
global.Logger.Errorf("svc.GetSuggestUsers err: %v\n", err)
response.ToErrorResponse(errcode.GetCollectionsFailed)
return
}
response.ToResponse(usernames)
}
func GetSuggestTags(c *gin.Context) {
keyword := c.Query("k")
response := app.NewResponse(c)
svc := service.New(c)
tags, err := svc.GetSuggestTags(keyword)
if err != nil {
global.Logger.Errorf("svc.GetSuggestTags err: %v\n", err)
response.ToErrorResponse(errcode.GetCollectionsFailed)
return
}
response.ToResponse(tags)
}
func GetUserRechargeLink(c *gin.Context) {
param := service.RechargeReq{}
response := app.NewResponse(c)
valid, errs := app.BindAndValid(c, &param)
if !valid {
global.Logger.Errorf("app.BindAndValid errs: %v", errs)
response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
return
}
// 下单
userID, _ := c.Get("UID")
svc := service.New(c)
recharge, err := svc.CreateRecharge(userID.(int64), param.Amount)
if err != nil {
global.Logger.Errorf("svc.CreateRecharge err: %v\n", err)
response.ToErrorResponse(errcode.RechargeReqFail)
return
}
client, err := alipay.New(global.AppSetting.AlipayAppID, global.AppSetting.AlipayPrivateKey, true)
// 将 key 的验证调整到初始化阶段
if err != nil {
global.Logger.Errorf("alipay.New err: %v\n", err)
response.ToErrorResponse(errcode.RechargeReqFail)
return
}
err = client.LoadAppPublicCertFromFile("configs/alipayAppCertPublicKey.crt") // 加载应用公钥证书
if err != nil {
global.Logger.Errorf("client.LoadAppPublicCertFromFile err: %v\n", err)
response.ToErrorResponse(errcode.RechargeReqFail)
return
}
err = client.LoadAliPayRootCertFromFile("configs/alipayRootCert.crt") // 加载支付宝根证书
if err != nil {
global.Logger.Errorf("client.LoadAliPayRootCertFromFile err: %v\n", err)
response.ToErrorResponse(errcode.RechargeReqFail)
return
}
err = client.LoadAliPayPublicCertFromFile("configs/alipayCertPublicKey_RSA2.crt") // 加载支付宝公钥证书
if err != nil {
global.Logger.Errorf("client.LoadAliPayPublicCertFromFile err: %v\n", err)
response.ToErrorResponse(errcode.RechargeReqFail)
return
}
p := alipay.TradePreCreate{}
p.OutTradeNo = fmt.Sprintf("%d", recharge.ID)
p.Subject = "PaoPao用户钱包充值"
p.TotalAmount = fmt.Sprintf("%.2f", float64(recharge.Amount)/100.0)
p.NotifyURL = "https://" + c.Request.Host + "/alipay/notify"
rsp, err := client.TradePreCreate(p)
if err != nil {
global.Logger.Errorf("client.TradePreCreate err: %v\n", err)
response.ToErrorResponse(errcode.RechargeReqFail)
return
}
if rsp.Content.Code != alipay.CodeSuccess {
response.ToErrorResponse(errcode.RechargeReqFail)
return
}
response.ToResponse(gin.H{
"id": recharge.ID,
"pay": rsp.Content.QRCode,
})
}
func GetUserRechargeResult(c *gin.Context) {
response := app.NewResponse(c)
id := c.Query("id")
userID, _ := c.Get("UID")
svc := service.New(c)
recharge, err := svc.GetRechargeByID(convert.StrTo(id).MustInt64())
if err != nil {
response.ToErrorResponse(errcode.GetRechargeFailed)
return
}
if recharge.UserID != userID.(int64) {
response.ToErrorResponse(errcode.GetRechargeFailed)
return
}
response.ToResponse(gin.H{
"id": recharge.ID,
"status": recharge.TradeStatus,
})
}
func AlipayNotify(c *gin.Context) {
response := app.NewResponse(c)
c.Request.ParseForm()
aliClient, err := alipay.New(global.AppSetting.AlipayAppID, global.AppSetting.AlipayPrivateKey, true)
// 将 key 的验证调整到初始化阶段
if err != nil {
global.Logger.Errorf("alipay.New err: %v\n", err)
response.ToErrorResponse(errcode.RechargeReqFail)
return
}
err = aliClient.LoadAppPublicCertFromFile("configs/alipayAppCertPublicKey.crt") // 加载应用公钥证书
if err != nil {
global.Logger.Errorf("client.LoadAppPublicCertFromFile err: %v\n", err)
response.ToErrorResponse(errcode.RechargeReqFail)
return
}
err = aliClient.LoadAliPayRootCertFromFile("configs/alipayRootCert.crt") // 加载支付宝根证书
if err != nil {
global.Logger.Errorf("client.LoadAliPayRootCertFromFile err: %v\n", err)
response.ToErrorResponse(errcode.RechargeReqFail)
return
}
err = aliClient.LoadAliPayPublicCertFromFile("configs/alipayCertPublicKey_RSA2.crt") // 加载支付宝公钥证书
if err != nil {
global.Logger.Errorf("client.LoadAliPayPublicCertFromFile err: %v\n", err)
response.ToErrorResponse(errcode.RechargeReqFail)
return
}
_, err = aliClient.GetTradeNotification(c.Request)
if err != nil {
global.Logger.Errorf("aliClient.GetTradeNotification err: %v\n", err)
global.Logger.Infoln(c.Request.Form)
response.ToErrorResponse(errcode.RechargeNotifyError)
return
}
svc := service.New(c)
id := c.Request.Form.Get("out_trade_no")
tradeNo := c.Request.Form.Get("trade_no")
tradeStatus := c.Request.Form.Get("trade_status")
if tradeStatus == "TRADE_SUCCESS" {
// 交易支付成功
err = svc.FinishRecharge(convert.StrTo(id).MustInt64(), tradeNo)
if err != nil {
global.Logger.Errorf("svc.FinishRecharge err: %v\n", err)
response.ToErrorResponse(errcode.RechargeNotifyError)
return
}
}
response.Ctx.String(http.StatusOK, "success")
}
func GetUserWalletBills(c *gin.Context) {
response := app.NewResponse(c)
userID, _ := c.Get("UID")
svc := service.New(c)
bills, totalRows, err := svc.GetUserWalletBills(userID.(int64), (app.GetPage(c)-1)*app.GetPageSize(c), app.GetPageSize(c))
if err != nil {
global.Logger.Errorf("svc.GetUserWalletBills err: %v\n", err)
response.ToErrorResponse(errcode.GetCollectionsFailed)
return
}
response.ToResponseList(bills, totalRows)
}

@ -0,0 +1,174 @@
package routers
import (
"net/http"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/rocboss/paopao-api/internal/middleware"
"github.com/rocboss/paopao-api/internal/routers/api"
)
func NewRouter() *gin.Engine {
r := gin.New()
r.HandleMethodNotAllowed = true
r.Use(gin.Logger())
r.Use(gin.Recovery())
// 跨域配置
corsConfig := cors.DefaultConfig()
corsConfig.AllowAllOrigins = true
corsConfig.AddAllowHeaders("Authorization")
r.Use(cors.New(corsConfig))
// 获取version
r.GET("/", api.Version)
// 用户登录
r.POST("/auth/login", api.Login)
// 用户注册
r.POST("/auth/register", api.Register)
// 获取验证码
r.GET("/captcha", api.GetCaptcha)
// 发送验证码
r.POST("/captcha", api.PostCaptcha)
// 支付宝回调
r.POST("/alipay/notify", api.AlipayNotify)
// 无鉴权路由组
noAuthApi := r.Group("/")
{
// 获取广场流
noAuthApi.GET("/posts", api.GetPostList)
// 获取动态详情
noAuthApi.GET("/post", api.GetPost)
// 获取动态评论
noAuthApi.GET("/post/comments", api.GetPostComments)
// 获取话题列表
noAuthApi.GET("/tags", api.GetPostTags)
// 获取用户基本信息
noAuthApi.GET("/user/profile", api.GetUserProfile)
// 获取用户动态列表
noAuthApi.GET("/user/posts", api.GetUserPosts)
}
// 鉴权路由组
authApi := r.Group("/").Use(middleware.JWT())
privApi := r.Group("/").Use(middleware.JWT()).Use(middleware.Priv())
{
// 同步索引
authApi.GET("/sync/index", api.SyncSearchIndex)
// 获取当前用户信息
authApi.GET("/user/info", api.GetUserInfo)
// 获取当前用户未读消息数量
authApi.GET("/user/msgcount/unread", api.GetUnreadMsgCount)
// 获取消息列表
authApi.GET("/user/messages", api.GetMessages)
// 标记消息已读
authApi.POST("/user/message/read", api.ReadMessage)
// 获取用户收藏列表
authApi.GET("/user/collections", api.GetUserCollections)
// 获取用户点赞列表
authApi.GET("/user/stars", api.GetUserStars)
// 绑定用户手机号
authApi.POST("/user/phone", api.BindUserPhone)
// 修改密码
authApi.POST("/user/password", api.ChangeUserPassword)
// 修改昵称
authApi.POST("/user/nickname", api.ChangeNickname)
// 修改头像
authApi.POST("/user/avatar", api.ChangeAvatar)
// 检索用户
authApi.GET("/suggest/users", api.GetSuggestUsers)
// 检索标签
authApi.GET("/suggest/tags", api.GetSuggestTags)
// 用户充值
authApi.POST("/user/recharge", api.GetUserRechargeLink)
// 用户充值
authApi.GET("/user/recharge", api.GetUserRechargeResult)
// 获取用户账单
authApi.GET("/user/wallet/bills", api.GetUserWalletBills)
// 上传资源
privApi.POST("/attachment", api.UploadAttachment)
// 下载资源预检
privApi.GET("/attachment/precheck", api.DownloadAttachmentPrecheck)
// 下载资源
privApi.GET("/attachment", api.DownloadAttachment)
// 发布动态
privApi.POST("/post", api.CreatePost)
// 删除动态
privApi.DELETE("/post", api.DeletePost)
// 获取动态点赞状态
authApi.GET("/post/star", api.GetPostStar)
// 动态点赞操作
privApi.POST("/post/star", api.PostStar)
// 获取动态收藏状态
authApi.GET("/post/collection", api.GetPostCollection)
// 动态收藏操作
privApi.POST("/post/collection", api.PostCollection)
// 锁定动态
privApi.POST("/post/lock", api.LockPost)
// 发布动态评论
privApi.POST("/post/comment", api.CreatePostComment)
// 删除动态评论
privApi.DELETE("/post/comment", api.DeletePostComment)
// 发布评论回复
privApi.POST("/post/comment/reply", api.CreatePostCommentReply)
// 删除评论回复
privApi.DELETE("/post/comment/reply", api.DeletePostCommentReply)
}
// 默认404
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"msg": "Not Found",
})
})
// 默认405
r.NoMethod(func(c *gin.Context) {
c.JSON(http.StatusMethodNotAllowed, gin.H{
"code": 405,
"msg": "Method Not Allowed",
})
})
return r
}

@ -0,0 +1,7 @@
package service
import "github.com/rocboss/paopao-api/internal/model"
func (svc *Service) CreateAttachment(attachment *model.Attachment) (*model.Attachment, error) {
return svc.dao.CreateAttachment(attachment)
}

@ -0,0 +1,64 @@
package service
import (
"math/rand"
"time"
)
var defaultAvatars = []string{
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/zoe.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/william.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/walter.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/thomas.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/taylor.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/sophia.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/sam.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/ryan.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/ruby.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/quinn.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/paul.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/owen.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/olivia.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/norman.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/nora.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/natalie.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/naomi.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/miley.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/mike.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/lucas.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/kylie.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/julia.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/joshua.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/john.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/jane.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/jackson.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/ivy.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/isaac.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/henry.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/harry.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/harold.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/hanna.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/grace.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/george.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/freddy.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/frank.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/finn.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/emma.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/emily.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/edward.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/clara.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/claire.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/chloe.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/audrey.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/arthur.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/anna.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/andy.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/alfred.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/alexa.png",
"https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/avatar/default/abigail.png",
}
func (s *Service) GetRandomAvatar() string {
rand.Seed(time.Now().UnixMicro())
return defaultAvatars[rand.Intn(len(defaultAvatars))]
}

@ -0,0 +1,317 @@
package service
import (
"strings"
"time"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/model"
"github.com/rocboss/paopao-api/pkg/errcode"
"github.com/rocboss/paopao-api/pkg/util"
)
type CommentCreationReq struct {
PostID int64 `json:"post_id" binding:"required"`
Contents []*PostContentItem `json:"contents" binding:"required"`
Users []string `json:"users" binding:"required"`
}
type CommentReplyCreationReq struct {
CommentID int64 `json:"comment_id" binding:"required"`
Content string `json:"content" binding:"required"`
AtUserID int64 `json:"at_user_id"`
}
type CommentDelReq struct {
ID int64 `json:"id" binding:"required"`
}
type ReplyDelReq struct {
ID int64 `json:"id" binding:"required"`
}
func (svc *Service) GetPostComments(postID int64, sort string, offset, limit int) ([]*model.CommentFormated, int64, error) {
conditions := &model.ConditionsT{
"post_id": postID,
"ORDER": sort,
}
comments, err := svc.dao.GetComments(conditions, offset, limit)
if err != nil {
return nil, 0, err
}
userIDs := []int64{}
commentIDs := []int64{}
for _, comment := range comments {
userIDs = append(userIDs, comment.UserID)
commentIDs = append(commentIDs, comment.ID)
}
users, err := svc.dao.GetUsersByIDs(userIDs)
if err != nil {
return nil, 0, err
}
contents, err := svc.dao.GetCommentContentsByIDs(commentIDs)
if err != nil {
return nil, 0, err
}
replies, err := svc.dao.GetCommentRepliesByID(commentIDs)
if err != nil {
return nil, 0, err
}
commentsFormated := []*model.CommentFormated{}
for _, comment := range comments {
commentFormated := comment.Format()
for _, content := range contents {
if content.CommentID == comment.ID {
commentFormated.Contents = append(commentFormated.Contents, content)
}
}
for _, reply := range replies {
if reply.CommentID == commentFormated.ID {
commentFormated.Replies = append(commentFormated.Replies, reply)
}
}
for _, user := range users {
if user.ID == comment.UserID {
commentFormated.User = user.Format()
}
}
commentsFormated = append(commentsFormated, commentFormated)
}
// 获取总量
totalRows, _ := svc.dao.GetCommentCount(conditions)
return commentsFormated, totalRows, nil
}
func (svc *Service) CreatePostComment(userID int64, param CommentCreationReq) (*model.Comment, error) {
// 加载Post
post, err := svc.dao.GetPostByID(param.PostID)
if err != nil {
return nil, err
}
if post.CommentCount >= global.AppSetting.MaxCommentCount {
return nil, errcode.MaxCommentCount
}
ip := svc.ctx.ClientIP()
comment := &model.Comment{
PostID: post.ID,
UserID: userID,
IP: ip,
IPLoc: util.GetIPLoc(ip),
}
comment, err = svc.dao.CreateComment(comment)
if err != nil {
return nil, err
}
for _, item := range param.Contents {
// 检查附件是否是本站资源
if item.Type == model.CONTENT_TYPE_IMAGE || item.Type == model.CONTENT_TYPE_VIDEO || item.Type == model.CONTENT_TYPE_ATTACHMENT {
if strings.Index(item.Content, "https://"+global.AliossSetting.AliossDomain) != 0 {
continue
}
}
postContent := &model.CommentContent{
CommentID: comment.ID,
UserID: userID,
Content: item.Content,
Type: item.Type,
Sort: item.Sort,
}
svc.dao.CreateCommentContent(postContent)
}
// 更新Post回复数
post.CommentCount++
post.LatestRepliedOn = time.Now().Unix()
svc.dao.UpdatePost(post)
// 更新索引
go svc.PushPostToSearch(post)
// 创建用户消息提醒
postMaster, err := svc.dao.GetUserByID(post.UserID)
if err == nil && postMaster.ID != userID {
go svc.dao.CreateMessage(&model.Message{
SenderUserID: userID,
ReceiverUserID: postMaster.ID,
Type: model.MESSAGE_COMMENT,
Breif: "在泡泡中评论了你",
PostID: post.ID,
CommentID: comment.ID,
})
}
for _, u := range param.Users {
user, err := svc.dao.GetUserByUsername(u)
if err != nil || user.ID == userID || user.ID == postMaster.ID {
continue
}
// 创建消息提醒
go svc.dao.CreateMessage(&model.Message{
SenderUserID: userID,
ReceiverUserID: user.ID,
Type: model.MESSAGE_COMMENT,
Breif: "在泡泡评论中@了你",
PostID: post.ID,
CommentID: comment.ID,
})
}
return comment, nil
}
func (svc *Service) GetPostComment(id int64) (*model.Comment, error) {
return svc.dao.GetCommentByID(id)
}
func (svc *Service) DeletePostComment(comment *model.Comment) error {
// 加载post
post, err := svc.dao.GetPostByID(comment.PostID)
if err == nil {
// 更新post回复数
post.CommentCount--
svc.dao.UpdatePost(post)
}
return svc.dao.DeleteComment(comment)
}
func (svc *Service) CreatePostCommentReply(commentID int64, content string, userID, atUserID int64) (*model.CommentReply, error) {
// 加载Comment
comment, err := svc.dao.GetCommentByID(commentID)
if err != nil {
return nil, err
}
// 加载comment的post
post, err := svc.dao.GetPostByID(comment.PostID)
if err != nil {
return nil, err
}
if post.CommentCount >= global.AppSetting.MaxCommentCount {
return nil, errcode.MaxCommentCount
}
if userID == atUserID {
atUserID = 0
}
if atUserID > 0 {
// 检测目前用户是否存在
users, _ := svc.dao.GetUsersByIDs([]int64{atUserID})
if len(users) == 0 {
atUserID = 0
}
}
// 创建评论
ip := svc.ctx.ClientIP()
reply := &model.CommentReply{
CommentID: commentID,
UserID: userID,
Content: content,
AtUserID: atUserID,
IP: ip,
IPLoc: util.GetIPLoc(ip),
}
reply, err = svc.dao.CreateCommentReply(reply)
if err != nil {
return nil, err
}
// 更新Post回复数
post.CommentCount++
post.LatestRepliedOn = time.Now().Unix()
svc.dao.UpdatePost(post)
// 更新索引
go svc.PushPostToSearch(post)
// 创建用户消息提醒
commentMaster, err := svc.dao.GetUserByID(comment.UserID)
if err == nil && commentMaster.ID != userID {
go svc.dao.CreateMessage(&model.Message{
SenderUserID: userID,
ReceiverUserID: commentMaster.ID,
Type: model.MESSAGE_REPLY,
Breif: "在泡泡评论下回复了你",
PostID: post.ID,
CommentID: comment.ID,
ReplyID: reply.ID,
})
}
postMaster, err := svc.dao.GetUserByID(post.UserID)
if err == nil && postMaster.ID != userID && commentMaster.ID != postMaster.ID {
go svc.dao.CreateMessage(&model.Message{
SenderUserID: userID,
ReceiverUserID: postMaster.ID,
Type: model.MESSAGE_REPLY,
Breif: "在泡泡评论下发布了新回复",
PostID: post.ID,
CommentID: comment.ID,
ReplyID: reply.ID,
})
}
if atUserID > 0 {
user, err := svc.dao.GetUserByID(atUserID)
if err == nil && user.ID != userID && commentMaster.ID != user.ID && postMaster.ID != user.ID {
// 创建消息提醒
go svc.dao.CreateMessage(&model.Message{
SenderUserID: userID,
ReceiverUserID: user.ID,
Type: model.MESSAGE_REPLY,
Breif: "在泡泡评论的回复中@了你",
PostID: post.ID,
CommentID: comment.ID,
ReplyID: reply.ID,
})
}
}
return reply, nil
}
func (svc *Service) GetPostCommentReply(id int64) (*model.CommentReply, error) {
return svc.dao.GetCommentReplyByID(id)
}
func (svc *Service) DeletePostCommentReply(reply *model.CommentReply) error {
err := svc.dao.DeleteCommentReply(reply)
if err != nil {
return err
}
// 加载Comment
comment, err := svc.dao.GetCommentByID(reply.CommentID)
if err != nil {
return err
}
// 加载comment的post
post, err := svc.dao.GetPostByID(comment.PostID)
if err != nil {
return err
}
// 更新Post回复数
post.CommentCount--
post.LatestRepliedOn = time.Now().Unix()
svc.dao.UpdatePost(post)
// 更新索引
go svc.PushPostToSearch(post)
return nil
}

@ -0,0 +1,81 @@
package service
import (
"github.com/rocboss/paopao-api/internal/model"
"github.com/rocboss/paopao-api/pkg/errcode"
)
type ReadMessageReq struct {
ID int64 `json:"id" binding:"required"`
}
func (svc *Service) CreateMessage(msg *model.Message) (*model.Message, error) {
return svc.dao.CreateMessage(msg)
}
func (svc *Service) GetUnreadCount(userID int64) (int64, error) {
return svc.dao.GetUnreadCount(userID)
}
func (svc *Service) ReadMessage(id, userID int64) error {
// 获取message
message, err := svc.dao.GetMessageByID(id)
if err != nil {
return err
}
if message.ReceiverUserID != userID {
return errcode.NoPermission
}
// 已读消息
return svc.dao.ReadMessage(message)
}
func (svc *Service) GetMessages(userID int64, offset, limit int) ([]*model.MessageFormated, int64, error) {
conditions := &model.ConditionsT{
"receiver_user_id": userID,
"ORDER": "id DESC",
}
messages, err := svc.dao.GetMessages(conditions, offset, limit)
for _, mf := range messages {
if mf.SenderUserID > 0 {
user, err := svc.dao.GetUserByID(mf.SenderUserID)
if err == nil {
mf.SenderUser = user.Format()
}
}
if mf.PostID > 0 {
post, err := svc.GetPost(mf.PostID)
if err == nil {
mf.Post = post
if mf.CommentID > 0 {
comment, err := svc.GetPostComment(mf.CommentID)
if err == nil {
mf.Comment = comment
if mf.ReplyID > 0 {
reply, err := svc.GetPostCommentReply(mf.ReplyID)
if err == nil {
mf.Reply = reply
}
}
}
}
}
}
}
if err != nil {
return nil, 0, err
}
// 获取总量
totalRows, _ := svc.dao.GetMessageCount(conditions)
return messages, totalRows, nil
}

@ -0,0 +1,594 @@
package service
import (
"encoding/json"
"fmt"
"math"
"strings"
"time"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/dao"
"github.com/rocboss/paopao-api/internal/model"
"github.com/rocboss/paopao-api/pkg/util"
"github.com/rocboss/paopao-api/pkg/zinc"
)
type TagType string
const TagTypeHot TagType = "hot"
const TagTypeNew TagType = "new"
type PostListReq struct {
Conditions *model.ConditionsT
Offset int
Limit int
}
type PostTagsReq struct {
Type TagType `json:"type" form:"type" binding:"required"`
Num int `json:"num" form:"num" binding:"required"`
}
type PostCreationReq struct {
Contents []*PostContentItem `json:"contents" binding:"required"`
Tags []string `json:"tags" binding:"required"`
Users []string `json:"users" binding:"required"`
AttachmentPrice int64 `json:"attachment_price"`
}
type PostDelReq struct {
ID int64 `json:"id" binding:"required"`
}
type PostLockReq struct {
ID int64 `json:"id" binding:"required"`
}
type PostStarReq struct {
ID int64 `json:"id" binding:"required"`
}
type PostCollectionReq struct {
ID int64 `json:"id" binding:"required"`
}
type PostContentItem struct {
Content string `json:"content" binding:"required"`
Type model.PostContentT `json:"type" binding:"required"`
Sort int64 `json:"sort" binding:"required"`
}
func (svc *Service) CreatePost(userID int64, param PostCreationReq) (*model.Post, error) {
ip := svc.ctx.ClientIP()
post := &model.Post{
UserID: userID,
Tags: strings.Join(param.Tags, ","),
IP: ip,
IPLoc: util.GetIPLoc(ip),
AttachmentPrice: param.AttachmentPrice,
}
post, err := svc.dao.CreatePost(post)
if err != nil {
return nil, err
}
// 创建标签
for _, t := range param.Tags {
tag := &model.Tag{
UserID: userID,
Tag: t,
}
svc.dao.CreateTag(tag)
}
for _, item := range param.Contents {
// 检查附件是否是本站资源
if item.Type == model.CONTENT_TYPE_IMAGE || item.Type == model.CONTENT_TYPE_VIDEO || item.Type == model.CONTENT_TYPE_ATTACHMENT {
if strings.Index(item.Content, "https://"+global.AliossSetting.AliossDomain) != 0 {
continue
}
}
// 检查链接是否合法
if item.Type == model.CONTENT_TYPE_LINK {
if strings.Index(item.Content, "http://") != 0 && strings.Index(item.Content, "https://") != 0 {
continue
}
}
if item.Type == model.CONTENT_TYPE_ATTACHMENT && param.AttachmentPrice > 0 {
item.Type = model.CONTENT_TYPE_CHARGE_ATTACHMENT
}
postContent := &model.PostContent{
PostID: post.ID,
UserID: userID,
Content: item.Content,
Type: item.Type,
Sort: item.Sort,
}
svc.dao.CreatePostContent(postContent)
}
// 推送Search
go svc.PushPostToSearch(post)
// 创建用户消息提醒
for _, u := range param.Users {
user, err := svc.dao.GetUserByUsername(u)
if err != nil || user.ID == userID {
continue
}
// 创建消息提醒
go svc.dao.CreateMessage(&model.Message{
SenderUserID: userID,
ReceiverUserID: user.ID,
Type: model.MESSAGE_POST,
Breif: "在新发布的泡泡动态中@了你",
PostID: post.ID,
})
}
return post, nil
}
func (svc *Service) DeletePost(id int64) error {
post, _ := svc.dao.GetPostByID(id)
// tag删除
tags := strings.Split(post.Tags, ",")
for _, t := range tags {
tag := &model.Tag{
Tag: t,
}
svc.dao.DeleteTag(tag)
}
err := svc.dao.DeletePost(post)
if err != nil {
return err
}
// 删除索引
go svc.DeleteSearchPost(post)
return nil
}
func (svc *Service) LockPost(id int64) error {
post, _ := svc.dao.GetPostByID(id)
err := svc.dao.LockPost(post)
if err != nil {
return err
}
return nil
}
func (svc *Service) GetPostStar(postID, userID int64) (*model.PostStar, error) {
return svc.dao.GetUserPostStar(postID, userID)
}
func (svc *Service) CreatePostStar(postID, userID int64) (*model.PostStar, error) {
// 加载Post
post, err := svc.dao.GetPostByID(postID)
if err != nil {
return nil, err
}
star, err := svc.dao.CreatePostStar(postID, userID)
if err != nil {
return nil, err
}
// 更新Post点赞数
post.UpvoteCount++
svc.dao.UpdatePost(post)
// 更新索引
go svc.PushPostToSearch(post)
return star, nil
}
func (svc *Service) DeletePostStar(star *model.PostStar) error {
err := svc.dao.DeletePostStar(star)
if err != nil {
return err
}
// 加载Post
post, err := svc.dao.GetPostByID(star.PostID)
if err != nil {
return err
}
// 更新Post点赞数
post.UpvoteCount--
svc.dao.UpdatePost(post)
// 更新索引
go svc.PushPostToSearch(post)
return nil
}
func (svc *Service) GetPostCollection(postID, userID int64) (*model.PostCollection, error) {
return svc.dao.GetUserPostCollection(postID, userID)
}
func (svc *Service) CreatePostCollection(postID, userID int64) (*model.PostCollection, error) {
// 加载Post
post, err := svc.dao.GetPostByID(postID)
if err != nil {
return nil, err
}
collection, err := svc.dao.CreatePostCollection(postID, userID)
if err != nil {
return nil, err
}
// 更新Post点赞数
post.CollectionCount++
svc.dao.UpdatePost(post)
// 更新索引
go svc.PushPostToSearch(post)
return collection, nil
}
func (svc *Service) DeletePostCollection(collection *model.PostCollection) error {
err := svc.dao.DeletePostCollection(collection)
if err != nil {
return err
}
// 加载Post
post, err := svc.dao.GetPostByID(collection.PostID)
if err != nil {
return err
}
// 更新Post点赞数
post.CollectionCount--
svc.dao.UpdatePost(post)
// 更新索引
go svc.PushPostToSearch(post)
return nil
}
func (svc *Service) GetPost(id int64) (*model.PostFormated, error) {
post, err := svc.dao.GetPostByID(id)
if err != nil {
return nil, err
}
postContents, err := svc.dao.GetPostContentsByIDs([]int64{post.ID})
if err != nil {
return nil, err
}
users, err := svc.dao.GetUsersByIDs([]int64{post.UserID})
if err != nil {
return nil, err
}
// 数据整合
postFormated := post.Format()
for _, user := range users {
postFormated.User = user.Format()
}
for _, content := range postContents {
if content.PostID == post.ID {
postFormated.Contents = append(postFormated.Contents, content.Format())
}
}
return postFormated, nil
}
func (svc *Service) GetPostContentByID(id int64) (*model.PostContent, error) {
return svc.dao.GetPostContentByID(id)
}
func (svc *Service) GetPostList(req *PostListReq) ([]*model.PostFormated, error) {
posts, err := svc.dao.GetPosts(req.Conditions, req.Offset, req.Limit)
if err != nil {
return nil, err
}
return svc.FormatPosts(posts)
}
func (svc *Service) 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 := svc.dao.GetPostContentsByIDs(postIds)
if err != nil {
return nil, err
}
users, err := svc.dao.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
}
func (svc *Service) GetPostCount(conditions *model.ConditionsT) (int64, error) {
return svc.dao.GetPostCount(conditions)
}
func (svc *Service) GetPostListFromSearch(q *dao.QueryT, offset, limit int) ([]*model.PostFormated, int64, error) {
queryResult, err := svc.dao.QueryAll(q, global.SearchSetting.ZincIndex, offset, limit)
if err != nil {
return nil, 0, err
}
posts, err := svc.FormatZincPost(queryResult)
if err != nil {
return nil, 0, err
}
return posts, queryResult.Hits.Total.Value, nil
}
func (svc *Service) GetPostListFromSearchByQuery(query string, offset, limit int) ([]*model.PostFormated, int64, error) {
queryResult, err := svc.dao.QuerySearch(global.SearchSetting.ZincIndex, query, offset, limit)
if err != nil {
return nil, 0, err
}
posts, err := svc.FormatZincPost(queryResult)
if err != nil {
return nil, 0, err
}
return posts, queryResult.Hits.Total.Value, nil
}
func (svc *Service) PushPostToSearch(post *model.Post) {
indexName := global.SearchSetting.ZincIndex
postFormated := post.Format()
postFormated.User = &model.UserFormated{
ID: post.UserID,
}
contents, _ := svc.dao.GetPostContentsByIDs([]int64{post.ID})
for _, content := range contents {
postFormated.Contents = append(postFormated.Contents, content.Format())
}
contentFormated := ""
for _, content := range postFormated.Contents {
if content.Type == model.CONTENT_TYPE_TEXT || content.Type == model.CONTENT_TYPE_TITLE {
contentFormated = contentFormated + content.Content + "\n"
}
}
tagMaps := map[string]int8{}
for _, tag := range strings.Split(post.Tags, ",") {
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{}{
"id": post.ID,
"user_id": post.UserID,
"comment_count": post.CommentCount,
"collection_count": post.CollectionCount,
"upvote_count": post.UpvoteCount,
"is_top": post.IsTop,
"is_essence": post.IsEssence,
"content": contentFormated,
"tags": tagMaps,
"ip_loc": post.IPLoc,
"latest_replied_on": post.LatestRepliedOn,
"attachment_price": post.AttachmentPrice,
"created_on": post.CreatedOn,
"modified_on": post.ModifiedOn,
})
svc.dao.BulkPushDoc(data)
}
func (svc *Service) DeleteSearchPost(post *model.Post) error {
indexName := global.SearchSetting.ZincIndex
return svc.dao.DelDoc(indexName, fmt.Sprintf("%d", post.ID))
}
func (svc *Service) PushPostsToSearch() {
if ok, _ := global.Redis.SetNX(svc.ctx, "JOB_PUSH_TO_SEARCH", 1, time.Hour).Result(); ok {
splitNum := 1000
totalRows, _ := svc.GetPostCount(&model.ConditionsT{})
pages := math.Ceil(float64(totalRows) / float64(splitNum))
nums := int(pages)
indexName := global.SearchSetting.ZincIndex
// 创建索引
svc.dao.CreateSearchIndex(indexName)
for i := 0; i < nums; i++ {
data := []map[string]interface{}{}
posts, _ := svc.GetPostList(&PostListReq{
Conditions: &model.ConditionsT{},
Offset: i * splitNum,
Limit: splitNum,
})
for _, post := range posts {
contentFormated := ""
for _, content := range post.Contents {
if content.Type == model.CONTENT_TYPE_TEXT || content.Type == model.CONTENT_TYPE_TITLE {
contentFormated = contentFormated + content.Content + "\n"
}
}
data = append(data, map[string]interface{}{
"index": map[string]interface{}{
"_index": indexName,
"_id": fmt.Sprintf("%d", post.ID),
},
}, map[string]interface{}{
"id": post.ID,
"user_id": post.User.ID,
"comment_count": post.CommentCount,
"collection_count": post.CollectionCount,
"upvote_count": post.UpvoteCount,
"is_top": post.IsTop,
"is_essence": post.IsEssence,
"content": contentFormated,
"tags": post.Tags,
"ip_loc": post.IPLoc,
"latest_replied_on": post.LatestRepliedOn,
"attachment_price": post.AttachmentPrice,
"created_on": post.CreatedOn,
"modified_on": post.ModifiedOn,
})
}
if len(data) > 0 {
svc.dao.BulkPushDoc(data)
}
}
global.Redis.Del(svc.ctx, "JOB_PUSH_TO_SEARCH")
}
}
func (svc *Service) 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 := svc.dao.GetPostContentsByIDs(postIds)
if err != nil {
return nil, err
}
users, err := svc.dao.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 (svc *Service) GetPostTags(param *PostTagsReq) ([]*model.TagFormated, error) {
num := param.Num
if num > global.AppSetting.MaxPageSize {
num = global.AppSetting.MaxPageSize
}
conditions := &model.ConditionsT{}
if param.Type == TagTypeHot {
// 热门标签
conditions = &model.ConditionsT{
"ORDER": "quote_num DESC",
}
}
if param.Type == TagTypeNew {
// 热门标签
conditions = &model.ConditionsT{
"ORDER": "id DESC",
}
}
tags, err := svc.dao.GetTags(conditions, 0, num)
if err != nil {
return nil, err
}
// 获取创建者User IDs
userIds := []int64{}
for _, tag := range tags {
userIds = append(userIds, tag.UserID)
}
users, _ := svc.dao.GetUsersByIDs(userIds)
tagsFormated := []*model.TagFormated{}
for _, tag := range tags {
tagFormated := tag.Format()
for _, user := range users {
if user.ID == tagFormated.UserID {
tagFormated.User = user.Format()
}
}
tagsFormated = append(tagsFormated, tagFormated)
}
return tagsFormated, nil
}
func (svc *Service) CheckPostAttachmentIsPaid(postID, userID int64) bool {
bill, err := svc.dao.GetPostAttatchmentBill(postID, userID)
return err == nil && bill.Model != nil && bill.ID > 0
}

@ -0,0 +1,26 @@
package service
import (
"github.com/gin-gonic/gin"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/dao"
"github.com/rocboss/paopao-api/pkg/zinc"
)
type Service struct {
ctx *gin.Context
dao *dao.Dao
}
func New(ctx *gin.Context) Service {
svc := Service{ctx: ctx}
svc.dao = dao.New(global.DBEngine, &zinc.ZincClient{
ZincClientConfig: &zinc.ZincClientConfig{
ZincHost: global.SearchSetting.ZincHost,
ZincUser: global.SearchSetting.ZincUser,
ZincPassword: global.SearchSetting.ZincPassword,
},
})
return svc
}

@ -0,0 +1,34 @@
package service
import (
"fmt"
"sort"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/pkg/util"
)
func (svc *Service) GetParamSign(param map[string]interface{}, secretKey string) string {
signRaw := ""
rawStrs := []string{}
for k, v := range param {
if k != "sign" {
rawStrs = append(rawStrs, k+"="+fmt.Sprintf("%v", v))
}
}
sort.Strings(rawStrs)
for _, v := range rawStrs {
signRaw += v
}
if global.ServerSetting.RunMode == "debug" {
global.Logger.Info(map[string]string{
"signRaw": signRaw,
"sysSign": util.EncodeMD5(signRaw + secretKey),
})
}
return util.EncodeMD5(signRaw + secretKey)
}

@ -0,0 +1,364 @@
package service
import (
"fmt"
"regexp"
"strings"
"time"
"unicode/utf8"
"github.com/gofrs/uuid"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/model"
"github.com/rocboss/paopao-api/pkg/convert"
"github.com/rocboss/paopao-api/pkg/errcode"
"github.com/rocboss/paopao-api/pkg/util"
)
const MAX_CAPTCHA_TIMES = 2
type PhoneCaptchaReq struct {
Phone string `json:"phone" form:"phone" binding:"required"`
ImgCaptcha string `json:"img_captcha" form:"img_captcha" binding:"required"`
ImgCaptchaID string `json:"img_captcha_id" form:"img_captcha_id" binding:"required"`
}
type UserPhoneBindReq struct {
Phone string `json:"phone" form:"phone" binding:"required"`
Captcha string `json:"captcha" form:"captcha" binding:"required"`
}
type AuthRequest struct {
Username string `json:"username" form:"username" binding:"required"`
Password string `json:"password" form:"password" binding:"required"`
}
type RegisterRequest struct {
Username string `json:"username" form:"username" binding:"required"`
Password string `json:"password" form:"password" binding:"required"`
}
type ChangePasswordReq struct {
Password string `json:"password" form:"password" binding:"required"`
OldPassword string `json:"old_password" form:"old_password" binding:"required"`
}
type ChangeNicknameReq struct {
Nickname string `json:"nickname" form:"nickname" binding:"required"`
}
type ChangeAvatarReq struct {
Avatar string `json:"avatar" form:"avatar" binding:"required"`
}
const LOGIN_ERR_KEY = "PaoPaoUserLoginErr"
const MAX_LOGIN_ERR_TIMES = 10
// 用户认证
func (svc *Service) DoLogin(param *AuthRequest) (*model.User, error) {
user, err := svc.dao.GetUserByUsername(param.Username)
if err != nil {
return nil, errcode.UnauthorizedAuthNotExist
}
if user.Model != nil && user.ID > 0 {
if errTimes, err := global.Redis.Get(svc.ctx, fmt.Sprintf("%s:%d", LOGIN_ERR_KEY, user.ID)).Result(); err == nil {
if convert.StrTo(errTimes).MustInt() >= MAX_LOGIN_ERR_TIMES {
return nil, errcode.TooManyLoginError
}
}
// 对比密码是否正确
if svc.ValidPassword(user.Password, param.Password, user.Salt) {
if user.Status == model.UserStatusClosed {
return nil, errcode.UserHasBeenBanned
}
// 清空登录计数
global.Redis.Del(svc.ctx, fmt.Sprintf("%s:%d", LOGIN_ERR_KEY, user.ID))
return user, nil
}
// 登录错误计数
_, err = global.Redis.Incr(svc.ctx, fmt.Sprintf("%s:%d", LOGIN_ERR_KEY, user.ID)).Result()
if err == nil {
global.Redis.Expire(svc.ctx, fmt.Sprintf("%s:%d", LOGIN_ERR_KEY, user.ID), time.Hour).Result()
}
return nil, errcode.UnauthorizedAuthFailed
}
return nil, errcode.UnauthorizedAuthNotExist
}
// 检查密码是否一致
func (svc *Service) ValidPassword(dbPassword, password, salt string) bool {
return strings.Compare(dbPassword, util.EncodeMD5(util.EncodeMD5(password)+salt)) == 0
}
// 检测用户权限
func (svc *Service) CheckStatus(user *model.User) bool {
return user.Status == model.UserStatusNormal
}
// 验证用户
func (svc *Service) ValidUsername(username string) error {
// 检测用户是否合规
if utf8.RuneCountInString(username) < 3 || utf8.RuneCountInString(username) > 12 {
return errcode.UsernameLengthLimit
}
if !regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(username) {
return errcode.UsernameCharLimit
}
// 重复检查
user, _ := svc.dao.GetUserByUsername(username)
if user.Model != nil && user.ID > 0 {
return errcode.UsernameHasExisted
}
return nil
}
// 密码检查
func (svc *Service) CheckPassword(password string) error {
// 检测用户是否合规
if utf8.RuneCountInString(password) < 6 || utf8.RuneCountInString(password) > 16 {
return errcode.PasswordLengthLimit
}
return nil
}
// 验证手机验证码
func (svc *Service) CheckPhoneCaptcha(phone, captcha string) *errcode.Error {
c, err := svc.dao.GetLatestPhoneCaptcha(phone)
if err != nil {
return errcode.ErrorPhoneCaptcha
}
if c.Captcha != captcha {
return errcode.ErrorPhoneCaptcha
}
if c.ExpiredOn < time.Now().Unix() {
return errcode.ErrorPhoneCaptcha
}
if c.UseTimes >= MAX_CAPTCHA_TIMES {
return errcode.MaxPhoneCaptchaUseTimes
}
// 更新检测次数
svc.dao.UsePhoneCaptcha(c)
return nil
}
// 检测手机号是否存在
func (svc *Service) CheckPhoneExist(uid int64, phone string) bool {
u, err := svc.dao.GetUserByPhone(phone)
if err != nil {
return false
}
if u.Model == nil || u.ID == 0 {
return false
}
if u.ID == uid {
return false
}
return true
}
// 密码加密&生成salt
func (svc *Service) EncryptPasswordAndSalt(password string) (string, string) {
salt := uuid.Must(uuid.NewV4()).String()[:8]
password = util.EncodeMD5(util.EncodeMD5(password) + salt)
return password, salt
}
// 用户注册
func (svc *Service) Register(username, password string) (*model.User, error) {
password, salt := svc.EncryptPasswordAndSalt(password)
user := &model.User{
Nickname: username,
Username: username,
Password: password,
Avatar: svc.GetRandomAvatar(),
Salt: salt,
Status: model.UserStatusNormal,
}
user, err := svc.dao.CreateUser(user)
if err != nil {
return nil, err
}
return user, nil
}
// 获取用户信息
func (svc *Service) GetUserInfo(param *AuthRequest) (*model.User, error) {
user, err := svc.dao.GetUserByUsername(param.Username)
if err != nil {
return nil, err
}
if user.Model != nil && user.ID > 0 {
return user, nil
}
return nil, errcode.UnauthorizedAuthNotExist
}
func (svc *Service) GetUserByUsername(username string) (*model.User, error) {
user, err := svc.dao.GetUserByUsername(username)
if err != nil {
return nil, err
}
if user.Model != nil && user.ID > 0 {
return user, nil
}
return nil, errcode.NoExistUsername
}
// 更新用户信息
func (svc *Service) UpdateUserInfo(user *model.User) error {
return svc.dao.UpdateUser(user)
}
// 获取用户收藏列表
func (svc *Service) GetUserCollections(userID int64, offset, limit int) ([]*model.PostFormated, int64, error) {
collections, err := svc.dao.GetUserPostCollections(userID, offset, limit)
if err != nil {
return nil, 0, err
}
totalRows, err := svc.dao.GetUserPostCollectionCount(userID)
if err != nil {
return nil, 0, err
}
postIDs := []int64{}
for _, collection := range collections {
postIDs = append(postIDs, collection.PostID)
}
// 获取Posts
posts, err := svc.dao.GetPosts(&model.ConditionsT{
"id IN ?": postIDs,
"ORDER": "id DESC",
}, 0, 0)
if err != nil {
return nil, 0, err
}
postsFormated, err := svc.FormatPosts(posts)
if err != nil {
return nil, 0, err
}
return postsFormated, totalRows, nil
}
// 获取用户点赞列表
func (svc *Service) GetUserStars(userID int64, offset, limit int) ([]*model.PostFormated, int64, error) {
stars, err := svc.dao.GetUserPostStars(userID, offset, limit)
if err != nil {
return nil, 0, err
}
totalRows, err := svc.dao.GetUserPostStarCount(userID)
if err != nil {
return nil, 0, err
}
postIDs := []int64{}
for _, star := range stars {
postIDs = append(postIDs, star.PostID)
}
// 获取Posts
posts, err := svc.dao.GetPosts(&model.ConditionsT{
"id IN ?": postIDs,
"ORDER": "id DESC",
}, 0, 0)
if err != nil {
return nil, 0, err
}
postsFormated, err := svc.FormatPosts(posts)
if err != nil {
return nil, 0, err
}
return postsFormated, totalRows, nil
}
// 获取用户账单列表
func (svc *Service) GetUserWalletBills(userID int64, offset, limit int) ([]*model.WalletStatement, int64, error) {
bills, err := svc.dao.GetUserWalletBills(userID, offset, limit)
if err != nil {
return nil, 0, err
}
totalRows, err := svc.dao.GetUserWalletBillCount(userID)
if err != nil {
return nil, 0, err
}
return bills, totalRows, nil
}
// 发送短信验证码
func (svc *Service) SendPhoneCaptcha(phone string) error {
err := svc.dao.SendPhoneCaptcha(phone)
if err != nil {
return err
}
// 写入计数缓存
global.Redis.Incr(svc.ctx, "PaoPaoSmsCaptcha:"+phone).Result()
currentTime := time.Now()
endTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), 23, 59, 59, 0, currentTime.Location())
global.Redis.Expire(svc.ctx, "PaoPaoSmsCaptcha:"+phone, endTime.Sub(currentTime))
return nil
}
// 根据关键词获取用户推荐
func (svc *Service) GetSuggestUsers(keyword string) ([]string, error) {
users, err := svc.dao.GetUsersByKeyword(keyword)
if err != nil {
return nil, err
}
usernames := []string{}
for _, user := range users {
usernames = append(usernames, user.Username)
}
return usernames, nil
}
// 根据关键词获取标签推荐
func (svc *Service) GetSuggestTags(keyword string) ([]string, error) {
tags, err := svc.dao.GetTagsByKeyword(keyword)
if err != nil {
return nil, err
}
ts := []string{}
for _, t := range tags {
ts = append(ts, t.Tag)
}
return ts, nil
}

@ -0,0 +1,53 @@
package service
import (
"time"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/model"
"github.com/rocboss/paopao-api/pkg/errcode"
)
type RechargeReq struct {
Amount int64 `json:"amount" form:"amount" binding:"required"`
}
func (svc *Service) GetRechargeByID(id int64) (*model.WalletRecharge, error) {
return svc.dao.GetRechargeByID(id)
}
func (svc *Service) CreateRecharge(userID, amount int64) (*model.WalletRecharge, error) {
return svc.dao.CreateRecharge(userID, amount)
}
func (svc *Service) FinishRecharge(id int64, tradeNo string) error {
if ok, _ := global.Redis.SetNX(svc.ctx, "PaoPaoRecharge:"+tradeNo, 1, time.Second*5).Result(); ok {
recharge, err := svc.dao.GetRechargeByID(id)
if err != nil {
return err
}
if recharge.TradeStatus != "TRADE_SUCCESS" {
// 标记为已付款
err := svc.dao.HandleRechargeSuccess(recharge, tradeNo)
defer global.Redis.Del(svc.ctx, "PaoPaoRecharge:"+tradeNo)
if err != nil {
return err
}
}
}
return nil
}
func (svc *Service) BuyPostAttachment(post *model.Post, user *model.User) error {
if user.Balance < post.AttachmentPrice {
return errcode.InsuffientDownloadMoney
}
// 执行购买
return svc.dao.HandlePostAttachmentBought(post, user)
}

@ -0,0 +1,26 @@
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/routers"
)
func main() {
gin.SetMode(global.ServerSetting.RunMode)
router := routers.NewRouter()
s := &http.Server{
Addr: global.ServerSetting.HttpIp + ":" + global.ServerSetting.HttpPort,
Handler: router,
ReadTimeout: global.ServerSetting.ReadTimeout,
WriteTimeout: global.ServerSetting.WriteTimeout,
MaxHeaderBytes: 1 << 20,
}
fmt.Printf("\x1b[36m%s\x1b[0m\n", "PaoPao service listen on http://"+global.ServerSetting.HttpIp+":"+global.ServerSetting.HttpPort)
s.ListenAndServe()
}

@ -0,0 +1,326 @@
/*
Navicat Premium Data Transfer
Source Server : t-roc
Source Server Type : MySQL
Source Server Version : 80029
Source Host : localhost:3306
Source Schema : paopao
Target Server Type : MySQL
Target Server Version : 80029
File Encoding : 65001
Date: 26/05/2022 17:12:03
*/
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系统通知',
`breif` 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,63 @@
package app
import (
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/rocboss/paopao-api/pkg/errcode"
)
type Response struct {
Ctx *gin.Context
}
type Pager struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalRows int64 `json:"total_rows"`
}
func NewResponse(ctx *gin.Context) *Response {
return &Response{Ctx: ctx}
}
func (r *Response) ToResponse(data interface{}) {
hostname, _ := os.Hostname()
if data == nil {
data = gin.H{
"code": 0,
"msg": "success",
"tracehost": hostname,
}
} else {
data = gin.H{
"code": 0,
"msg": "success",
"data": data,
"tracehost": hostname,
}
}
r.Ctx.JSON(http.StatusOK, data)
}
func (r *Response) ToResponseList(list interface{}, totalRows int64) {
r.ToResponse(gin.H{
"list": list,
"pager": Pager{
Page: GetPage(r.Ctx),
PageSize: GetPageSize(r.Ctx),
TotalRows: totalRows,
},
})
}
func (r *Response) ToErrorResponse(err *errcode.Error) {
response := gin.H{"code": err.Code(), "msg": err.Msg()}
details := err.Details()
if len(details) > 0 {
response["details"] = details
}
r.Ctx.JSON(err.StatusCode(), response)
}

@ -0,0 +1,44 @@
package app
import (
"strings"
"github.com/gin-gonic/gin"
)
type ValidError struct {
Message string
}
type ValidErrors []*ValidError
func (v *ValidError) Error() string {
return v.Message
}
func (v ValidErrors) Error() string {
return strings.Join(v.Errors(), ",")
}
func (v ValidErrors) Errors() []string {
var errs []string
for _, err := range v {
errs = append(errs, err.Error())
}
return errs
}
func BindAndValid(c *gin.Context, v interface{}) (bool, ValidErrors) {
var errs ValidErrors
err := c.ShouldBind(v)
if err != nil {
errs = append(errs, &ValidError{
Message: err.Error(),
})
return false, errs
}
return true, nil
}

@ -0,0 +1,52 @@
package app
import (
"time"
"github.com/dgrijalva/jwt-go"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/internal/model"
)
type Claims struct {
UID int64 `json:"uid"`
Username string `json:"username"`
jwt.StandardClaims
}
func GetJWTSecret() []byte {
return []byte(global.JWTSetting.Secret)
}
func GenerateToken(User *model.User) (string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(global.JWTSetting.Expire)
claims := Claims{
UID: User.ID,
Username: User.Username,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expireTime.Unix(),
Issuer: global.JWTSetting.Issuer + ":" + User.Salt,
},
}
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := tokenClaims.SignedString(GetJWTSecret())
return token, err
}
func ParseToken(token string) (*Claims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return GetJWTSecret(), nil
})
if err != nil {
return nil, err
}
if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}

@ -0,0 +1,37 @@
package app
import (
"github.com/gin-gonic/gin"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/pkg/convert"
)
func GetPage(c *gin.Context) int {
page := convert.StrTo(c.Query("page")).MustInt()
if page <= 0 {
return 1
}
return page
}
func GetPageSize(c *gin.Context) int {
pageSize := convert.StrTo(c.Query("page_size")).MustInt()
if pageSize <= 0 {
return global.AppSetting.DefaultPageSize
}
if pageSize > global.AppSetting.MaxPageSize {
return global.AppSetting.MaxPageSize
}
return pageSize
}
func GetPageOffset(page, pageSize int) int {
result := 0
if page > 0 {
result = (page - 1) * pageSize
}
return result
}

@ -0,0 +1,48 @@
package convert
import "strconv"
type StrTo string
func (s StrTo) String() string {
return string(s)
}
func (s StrTo) Int() (int, error) {
v, err := strconv.Atoi(s.String())
return v, err
}
func (s StrTo) MustInt() int {
v, _ := s.Int()
return v
}
func (s StrTo) UInt32() (uint32, error) {
v, err := strconv.Atoi(s.String())
return uint32(v), err
}
func (s StrTo) MustUInt32() uint32 {
v, _ := s.UInt32()
return v
}
func (s StrTo) Int64() (int64, error) {
v, err := strconv.ParseInt(s.String(), 10, 64)
return v, err
}
func (s StrTo) MustInt64() int64 {
v, _ := s.Int64()
return v
}
func (s StrTo) Float64() (float64, error) {
return strconv.ParseFloat(s.String(), 64)
}
func (s StrTo) MustFloat64() float64 {
v, _ := strconv.ParseFloat(s.String(), 64)
return v
}

@ -0,0 +1,55 @@
package crypto
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/ecdsa"
"github.com/ethereum/go-ethereum/crypto"
)
func Sign(hash []byte, privateKey *ecdsa.PrivateKey) ([]byte, error) {
return crypto.Sign(hash, privateKey)
}
func VerifySignature(publicKey, hash, signature []byte) bool {
return crypto.VerifySignature(publicKey, hash, signature)
}
func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
func PKCS7UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
func AesEncrypt(origData, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockSize := block.BlockSize()
origData = PKCS7Padding(origData, blockSize)
blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
crypted := make([]byte, len(origData))
blockMode.CryptBlocks(crypted, origData)
return crypted, nil
}
func AesDecrypt(crypted, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockSize := block.BlockSize()
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
origData := make([]byte, len(crypted))
blockMode.CryptBlocks(origData, crypted)
origData = PKCS7UnPadding(origData)
return origData, nil
}

@ -0,0 +1,26 @@
package errcode
var (
Success = NewError(0, "成功")
ServerError = NewError(10000, "服务内部错误")
InvalidParams = NewError(10001, "入参错误")
NotFound = NewError(10002, "找不到")
UnauthorizedAuthNotExist = NewError(10003, "账户不存在")
UnauthorizedAuthFailed = NewError(10004, "账户密码错误")
UnauthorizedTokenError = NewError(10005, "鉴权失败Token 错误或丢失")
UnauthorizedTokenTimeout = NewError(10006, "鉴权失败Token 超时")
UnauthorizedTokenGenerate = NewError(10007, "鉴权失败Token 生成失败")
TooManyRequests = NewError(10008, "请求过多")
GatewayMethodsLimit = NewError(10109, "网关仅接受GET或POST请求")
GatewayLostSign = NewError(10110, "网关请求缺少签名")
GatewayLostAppKey = NewError(10111, "网关请求缺少APP KEY")
GatewayAppKeyInvalid = NewError(10112, "网关请求无效APP KEY")
GatewayAppKeyClosed = NewError(10113, "网关请求APP KEY已停用")
GatewayParamSignError = NewError(10114, "网关请求参数签名错误")
GatewayTooManyRequests = NewError(10115, "网关请求频次超限")
FileUploadFailed = NewError(10200, "文件上传失败")
FileInvalidExt = NewError(10201, "文件类型不合法")
FileInvalidSize = NewError(10202, "文件大小超限")
)

@ -0,0 +1,75 @@
package errcode
import (
"fmt"
"net/http"
)
type Error struct {
code int
msg string
details []string
}
var codes = map[int]string{}
func NewError(code int, msg string) *Error {
if _, ok := codes[code]; ok {
panic(fmt.Sprintf("错误码 %d 已经存在,请更换一个", code))
}
codes[code] = msg
return &Error{code: code, msg: msg}
}
func (e *Error) Error() string {
return fmt.Sprintf("错误码: %d, 错误信息: %s", e.Code(), e.Msg())
}
func (e *Error) Code() int {
return e.code
}
func (e *Error) Msg() string {
return e.msg
}
func (e *Error) Msgf(args []interface{}) string {
return fmt.Sprintf(e.msg, args...)
}
func (e *Error) Details() []string {
return e.details
}
func (e *Error) WithDetails(details ...string) *Error {
newError := *e
newError.details = []string{}
newError.details = append(newError.details, details...)
return &newError
}
func (e *Error) StatusCode() int {
switch e.Code() {
case Success.Code():
return http.StatusOK
case ServerError.Code():
return http.StatusInternalServerError
case InvalidParams.Code():
return http.StatusBadRequest
case UnauthorizedAuthNotExist.Code():
fallthrough
case UnauthorizedAuthFailed.Code():
fallthrough
case UnauthorizedTokenError.Code():
fallthrough
case UnauthorizedTokenGenerate.Code():
fallthrough
case UnauthorizedTokenTimeout.Code():
return http.StatusUnauthorized
case TooManyRequests.Code():
return http.StatusTooManyRequests
}
return http.StatusInternalServerError
}

@ -0,0 +1,54 @@
package errcode
var (
UsernameHasExisted = NewError(20001, "用户名已存在")
UsernameLengthLimit = NewError(20002, "用户名长度3~12")
UsernameCharLimit = NewError(20003, "用户名只能包含字母、数字")
PasswordLengthLimit = NewError(20004, "密码长度6~16")
UserRegisterFailed = NewError(20005, "用户注册失败")
UserHasBeenBanned = NewError(20006, "该账户已被封停")
NoPermission = NewError(20007, "无权限执行该请求")
UserHasBindOTP = NewError(20008, "当前用户已绑定二次验证")
UserOTPInvalid = NewError(20009, "二次验证码验证失败")
UserNoBindOTP = NewError(20010, "当前用户未绑定二次验证")
ErrorOldPassword = NewError(20011, "当前用户密码验证失败")
ErrorCaptchaPassword = NewError(20012, "图形验证码验证失败")
AccountNoPhoneBind = NewError(20013, "拒绝操作: 账户未绑定手机号")
TooManyLoginError = NewError(20014, "登录失败次数过多,请稍后再试")
GetPhoneCaptchaError = NewError(20015, "短信验证码获取失败")
TooManyPhoneCaptchaSend = NewError(20016, "短信验证码获取次数已达今日上限")
ExistedUserPhone = NewError(20017, "该手机号已被绑定")
ErrorPhoneCaptcha = NewError(20018, "手机验证码不正确")
MaxPhoneCaptchaUseTimes = NewError(20019, "手机验证码已达最大使用次数")
NicknameLengthLimit = NewError(20020, "昵称长度2~12")
NoExistUsername = NewError(20021, "用户不存在")
GetPostsFailed = NewError(30001, "获取动态列表失败")
CreatePostFailed = NewError(30002, "动态发布失败")
GetPostFailed = NewError(30003, "获取动态详情失败")
DeletePostFailed = NewError(30004, "动态删除失败")
LockPostFailed = NewError(30005, "动态锁定失败")
GetPostTagsFailed = NewError(30006, "获取话题列表失败")
InvalidDownloadReq = NewError(30007, "附件下载请求不合法")
DownloadReqError = NewError(30008, "附件下载请求失败")
InsuffientDownloadMoney = NewError(30009, "附件下载失败:账户资金不足")
DownloadExecFail = NewError(30010, "附件下载失败:扣费失败")
GetCommentsFailed = NewError(40001, "获取评论列表失败")
CreateCommentFailed = NewError(40002, "评论发布失败")
GetCommentFailed = NewError(40003, "获取评论详情失败")
DeleteCommentFailed = NewError(40004, "评论删除失败")
CreateReplyFailed = NewError(40005, "评论回复失败")
GetReplyFailed = NewError(40006, "获取评论详情失败")
MaxCommentCount = NewError(40007, "评论数已达最大限制")
GetMessagesFailed = NewError(50001, "获取消息列表失败")
ReadMessageFailed = NewError(50002, "标记消息已读失败")
GetCollectionsFailed = NewError(60001, "获取收藏列表失败")
GetStarsFailed = NewError(60002, "获取点赞列表失败")
RechargeReqFail = NewError(70001, "充值请求失败")
RechargeNotifyError = NewError(70002, "充值回调失败")
GetRechargeFailed = NewError(70003, "充值详情获取失败")
)

@ -0,0 +1,83 @@
package logger
import (
"encoding/json"
"fmt"
"io"
"time"
"github.com/rocboss/paopao-api/global"
"github.com/rocboss/paopao-api/pkg/setting"
"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 {
Fired bool
}
func (hook *ZincLogHook) Fire(entry *logrus.Entry) error {
index := &ZincLogIndex{
Index: map[string]string{
"_index": global.LoggerSetting.LogZincIndex,
},
}
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(global.LoggerSetting.LogZincUser, global.LoggerSetting.LogZincPassword).
SetBody(logStr).
Post(global.LoggerSetting.LogZincHost); err != nil {
fmt.Println(err.Error())
}
return nil
}
func (hook *ZincLogHook) Levels() []logrus.Level {
return logrus.AllLevels
}
func New(s *setting.LoggerSettingS) (*logrus.Logger, error) {
log := logrus.New()
log.Formatter = &logrus.JSONFormatter{}
switch s.LogType {
case setting.LogFileType:
log.Out = &lumberjack.Logger{
Filename: s.LogFileSavePath + "/" + s.LogFileName + s.LogFileExt,
MaxSize: 600,
MaxAge: 10,
LocalTime: true,
}
case setting.LogZincType:
log.Out = io.Discard
log.AddHook(&ZincLogHook{})
}
return log, nil
}

@ -0,0 +1,115 @@
package setting
import (
"time"
"github.com/spf13/viper"
"gorm.io/gorm/logger"
)
type Setting struct {
vp *viper.Viper
}
type LogType string
const LogFileType LogType = "file"
const LogZincType LogType = "zinc"
type LoggerSettingS struct {
LogType LogType
LogFileSavePath string
LogFileName string
LogFileExt string
LogZincHost string
LogZincIndex string
LogZincUser string
LogZincPassword string
}
type ServerSettingS struct {
RunMode string
HttpIp string
HttpPort string
ReadTimeout time.Duration
WriteTimeout time.Duration
}
type AppSettingS struct {
BarkToken string
MaxCommentCount int64
AttachmentIncomeRate float64
DefaultContextTimeout time.Duration
DefaultPageSize int
MaxPageSize int
IsShastaTestnet bool
TronApiKeys []string
SmsJuheKey string
SmsJuheTplID string
SmsJuheTplVal string
AlipayAppID string
AlipayPrivateKey string
}
type SearchSettingS struct {
ZincHost string
ZincIndex string
ZincUser string
ZincPassword string
}
type DatabaseSettingS struct {
DBType string
UserName string
Password string
Host string
DBName string
TablePrefix string
Charset string
ParseTime bool
LogLevel logger.LogLevel
MaxIdleConns int
MaxOpenConns int
}
type AliossSettingS struct {
AliossAccessKeyID string
AliossAccessKeySecret string
AliossEndpoint string
AliossBucket string
AliossDomain string
}
type RedisSettingS struct {
Host string
Password string
DB int
}
type JWTSettingS struct {
Secret string
Issuer string
Expire time.Duration
}
func NewSetting() (*Setting, error) {
vp := viper.New()
vp.SetConfigName("config")
vp.AddConfigPath(".")
vp.AddConfigPath("configs/")
vp.SetConfigType("yaml")
err := vp.ReadInConfig()
if err != nil {
return nil, err
}
return &Setting{vp}, nil
}
func (s *Setting) ReadSection(k string, v interface{}) error {
err := s.vp.UnmarshalKey(k, v)
if err != nil {
return err
}
return nil
}

@ -0,0 +1,42 @@
package sign
import (
"crypto/ecdsa"
"crypto/sha256"
"time"
"github.com/fbsobreira/gotron-sdk/pkg/proto/core"
"github.com/golang/protobuf/proto"
"github.com/rocboss/paopao-api/pkg/crypto"
)
// SignTransaction 签名交易
func SignTransaction(transaction *core.Transaction, key *ecdsa.PrivateKey) ([]byte, error) {
transaction.GetRawData().Timestamp = time.Now().UnixNano() / 1000000
rawData, err := proto.Marshal(transaction.GetRawData())
if err != nil {
return nil, err
}
h256h := sha256.New()
h256h.Write(rawData)
hash := h256h.Sum(nil)
contractList := transaction.GetRawData().GetContract()
for range contractList {
signature, err := crypto.Sign(hash, key)
if err != nil {
return nil, err
}
transaction.Signature = append(transaction.Signature, signature)
}
return hash, nil
}
func TrimLeftZeroes(s []byte) []byte {
idx := 0
for ; idx < len(s); idx++ {
if s[idx] != 48 {
break
}
}
return s[idx:]
}

@ -0,0 +1,12 @@
package util
import (
"github.com/yinheli/qqwry"
)
func GetIPLoc(ip string) string {
q := qqwry.NewQQwry("qqwry.dat")
q.Find(ip)
return q.Country
}

@ -0,0 +1,13 @@
package util
import (
"crypto/md5"
"encoding/hex"
)
func EncodeMD5(value string) string {
m := md5.New()
m.Write([]byte(value))
return hex.EncodeToString(m.Sum(nil))
}

@ -0,0 +1,39 @@
package util
import (
"math/rand"
"time"
)
type StrType int
const (
NUM StrType = iota // 数字
LOWER // 小写字母
UPPER // 大写字母
ALL // 全部
CLEAR // 去除部分易混淆的字符
)
var fontKinds = [][]int{{10, 48}, {26, 97}, {26, 65}}
var letters = []byte("34578acdefghjkmnpqstwxyABCDEFGHJKMNPQRSVWXY")
// 生成随机字符串
// size 个数 kind 模式
func RandStr(size int, kind StrType) []byte {
ikind, result := kind, make([]byte, size)
isAll := kind > 2 || kind < 0
rand.Seed(time.Now().UnixNano())
for i := 0; i < size; i++ {
if isAll {
ikind = StrType(rand.Intn(int(ALL)))
}
scope, base := fontKinds[ikind][0], fontKinds[ikind][1]
result[i] = uint8(base + rand.Intn(scope))
// 不易混淆字符模式:重新生成字符
if kind == 4 {
result[i] = letters[rand.Intn(len(letters))]
}
}
return result
}

@ -0,0 +1,208 @@
package zinc
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/go-resty/resty/v2"
)
type ZincClient struct {
*ZincClientConfig
}
type ZincClientConfig struct {
ZincHost string
ZincUser string
ZincPassword string
}
type ZincIndex struct {
Name string `json:"name"`
StorageType string `json:"storage_type"`
Mappings *ZincIndexMappings `json:"mappings"`
}
type ZincIndexMappings struct {
Properties *ZincIndexProperty `json:"properties"`
}
type ZincIndexProperty map[string]*ZincIndexPropertyT
type ZincIndexPropertyT struct {
Type string `json:"type"`
Index bool `json:"index"`
Store bool `json:"store"`
Sortable bool `json:"sortable"`
Aggregatable bool `json:"aggregatable"`
Highlightable bool `json:"highlightable"`
Analyzer string `json:"analyzer"`
SearchAnalyzer string `json:"search_analyzer"`
Format string `json:"format"`
}
type QueryResultT struct {
Took int `json:"took"`
TimedOut bool `json:"timed_out"`
Hits *HitsResultT `json:"hits"`
}
type HitsResultT struct {
Total *HitsResultTotalT `json:"total"`
MaxScore float64 `json:"max_score"`
Hits []*HitItem `json:"hits"`
}
type HitsResultTotalT struct {
Value int64 `json:"value"`
}
type HitItem struct {
Index string `json:"_index"`
Type string `json:"_type"`
ID string `json:"_id"`
Score float64 `json:"_score"`
Timestamp time.Time `json:"@timestamp"`
Source interface{} `json:"_source"`
}
// 创建索引
func (c *ZincClient) CreateIndex(name string, p *ZincIndexProperty) bool {
data := &ZincIndex{
Name: name,
StorageType: "disk",
Mappings: &ZincIndexMappings{
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")
if err != nil || resp.StatusCode() != http.StatusOK {
return false
}
return true
}
// 检查索引是否存在
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")
if err != nil || resp.StatusCode() != http.StatusOK {
return false
}
retData := &map[string]interface{}{}
err = json.Unmarshal([]byte(resp.String()), retData)
if err != nil {
return false
}
if _, ok := (*retData)[name]; ok {
return true
}
return false
}
// 新增/更新文档
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))
if err != nil {
return false, err
}
if resp.StatusCode() != http.StatusOK {
return false, errors.New(resp.Status())
}
return true, nil
}
// 批量新增文档
func (c *ZincClient) BulkPushDoc(docs []map[string]interface{}) (bool, error) {
dataStr := ""
for _, doc := range docs {
str, err := json.Marshal(doc)
if err == nil {
dataStr = dataStr + string(str) + "\n"
}
}
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))
if err != nil {
return false, err
}
if resp.StatusCode() != http.StatusOK {
return false, errors.New(resp.Status())
}
return true, nil
}
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))
if err != nil {
return nil, err
}
if resp.StatusCode() != http.StatusOK {
return nil, errors.New(resp.Status())
}
result := &QueryResultT{}
err = json.Unmarshal(resp.Body(), result)
if err != nil {
return nil, err
}
return result, nil
}
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))
if err != nil {
return nil, err
}
if resp.StatusCode() != http.StatusOK {
return nil, errors.New(resp.Status())
}
result := &QueryResultT{}
err = json.Unmarshal(resp.Body(), result)
if err != nil {
return nil, err
}
return result, nil
}
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))
if err != nil {
return err
}
if resp.StatusCode() != http.StatusOK {
return errors.New(resp.Status())
}
return nil
}

Binary file not shown.

@ -0,0 +1 @@
VITE_HOST="https://api.paopao.info"

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,3 @@
{
"recommendations": ["johnsoncodehk.volar"]
}

@ -0,0 +1,9 @@
FROM library/nginx
USER root
# copy static files
COPY ./dist/ /usr/share/nginx/html/
# HEALTHCHECK
HEALTHCHECK --interval=5s --timeout=3s --retries=3 CMD service nginx status | grep running || exit 1

@ -0,0 +1,7 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar)

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0" />
<title>泡泡</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

@ -0,0 +1,11 @@
{
"compilerOptions": {
"baseUrl": "src",
"module": "esnext",
"moduleResolution": "node",
"jsx": "preserve"
},
"include": [
"src"
],
}

@ -0,0 +1,35 @@
{
"name": "paopao-web",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@vicons/carbon": "^0.12.0",
"@vicons/fa": "^0.12.0",
"@vicons/ionicons5": "^0.12.0",
"@vicons/material": "^0.12.0",
"@vicons/tabler": "^0.12.0",
"axios": "^0.26.1",
"less": "^4.1.2",
"lodash": "^4.17.21",
"moment": "^2.29.3",
"naive-ui": "^2.28.0",
"nonesir-video": "^1.0.3",
"qrcanvas-vue": "^3.0.0",
"qrcode": "^1.5.0",
"unplugin-vue-components": "^0.19.3",
"vfonts": "^0.0.3",
"vue": "^3.2.25",
"vue-router": "4",
"vue3-player-video": "^1.2.5",
"vuex": "^4.0.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.3.1",
"vite": "^2.9.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@ -0,0 +1,49 @@
<template>
<n-config-provider :theme="theme">
<n-message-provider>
<div
class="app-container"
:class="{ dark: theme?.name === 'dark' }"
>
<div has-sider class="main-wrap" position="static">
<!-- 侧边栏 -->
<sidebar />
<div class="content-wrap">
<router-view class="app-wrap" v-slot="{ Component }">
<keep-alive>
<component
v-if="$route.meta.keepAlive"
:is="Component"
/>
</keep-alive>
<component
v-if="!$route.meta.keepAlive"
:is="Component"
/>
</router-view>
</div>
<!-- 右侧 -->
<rightbar />
</div>
<!-- 登录/注册公共组件 -->
<auth />
</div>
</n-message-provider>
<n-global-style />
</n-config-provider>
</template>
<script setup>
import { computed } from 'vue';
import { useStore } from 'vuex';
import { darkTheme } from 'naive-ui';
const store = useStore();
const theme = computed(() => (store.state.theme === 'dark' ? darkTheme : null));
</script>
<style lang="less">
@import '@/assets/css/main.less';
</style>

@ -0,0 +1,62 @@
import request from '@/utils/request';
/**
* 用户登录
* @param {Object} params
* - @param {string} username
* - @param {string} password
* @returns Promise
*/
export const userLogin = (params = {}) => {
return request({
method: 'post',
url: '/auth/login',
data: params,
});
};
/**
* 注册用户
* @param {Object} params
* - @param {string} username
* - @param {string} password
* @returns Promise
*/
export const userRegister = (params = {}) => {
return request({
method: 'post',
url: '/auth/register',
data: params,
});
};
/**
* 用户信息
* @param {Object} params
* @returns Promise
*/
export const userInfo = (token = '') => {
return request({
method: 'get',
url: '/user/info',
headers: {
Authorization: `Bearer ${token}`,
},
});
};
/**
* 修改用户密码
* @param {Object} params
* - @param {string} password 新密码
* - @param {string} old_password 旧密码
* @returns Promise
*/
export const updateUserPassword = data => {
return request({
method: 'post',
url: '/api/user/password',
data,
});
};

@ -0,0 +1,208 @@
import request from '@/utils/request';
/**
* 获取动态列表
* @param {Object} params
* @returns Promise
*/
export const getPosts = params => {
return request({
method: 'get',
url: '/posts',
params
});
};
/**
* 获取标签列表
* @param {Object} params
* @returns Promise
*/
export const getTags = params => {
return request({
method: 'get',
url: '/tags',
params
});
};
/**
* 获取动态详情
* @param {Object} params
* @returns Promise
*/
export const getPost = params => {
return request({
method: 'get',
url: '/post',
params
});
};
/**
* 获取动态点赞状态
* @param {Object} params
* @returns Promise
*/
export const getPostStar = params => {
return request({
method: 'get',
url: '/post/star',
params
});
};
/**
* 动态点赞
* @param {Object} data
* @returns Promise
*/
export const postStar = data => {
return request({
method: 'post',
url: '/post/star',
data
});
};
/**
* 获取动态收藏状态
* @param {Object} params
* @returns Promise
*/
export const getPostCollection = params => {
return request({
method: 'get',
url: '/post/collection',
params
});
};
/**
* 动态收藏
* @param {Object} data
* @returns Promise
*/
export const postCollection = data => {
return request({
method: 'post',
url: '/post/collection',
data
});
};
/**
* 获取动态评论列表
* @param {Object} params
* @returns Promise
*/
export const getPostComments = params => {
return request({
method: 'get',
url: '/post/comments',
params
});
};
/**
* 发布动态
* @param {Object} data
* - @param {array} contents 内容
* - @param {array} users at用户
* - @param {array} tags 话题
* @returns Promise
*/
export const createPost = data => {
return request({
method: 'post',
url: '/post',
data
});
};
/**
* 删除动态
* @param {Object} data
* - @param {number} id
* @returns Promise
*/
export const deletePost = data => {
return request({
method: 'delete',
url: '/post',
data
});
};
/**
* 锁定/解锁动态
* @param {Object} data
* - @param {number} id
* @returns Promise
*/
export const lockPost = data => {
return request({
method: 'post',
url: '/post/lock',
data
});
};
/**
* 发布动态评论
* @param {Object} data
* - @param {array} contents 内容
* - @param {array} users at用户
* @returns Promise
*/
export const createComment = data => {
return request({
method: 'post',
url: '/post/comment',
data
});
};
/**
* 删除评论
* @param {Object} data
* - @param {number} id
* @returns Promise
*/
export const deleteComment = data => {
return request({
method: 'delete',
url: '/post/comment',
data
});
};
/**
* 发布评论回复
* @param {Object} data
* - @param {string} content 内容
* - @param {number} comment_id 评论ID
* - @param {number} at_user_id at用户ID
* @returns Promise
*/
export const createCommentReply = data => {
return request({
method: 'post',
url: '/post/comment/reply',
data
});
};
/**
* 删除评论回复
* @param {Object} data
* - @param {number} id 评论ID
* @returns Promise
*/
export const deleteCommentReply = data => {
return request({
method: 'delete',
url: '/post/comment/reply',
data
});
};

@ -0,0 +1,261 @@
import request from '@/utils/request';
/**
* 获取验证码
* @param {Object} params
* @returns Promise
*/
export const getCaptcha = params => {
return request({
method: 'get',
url: '/captcha',
params
});
};
/**
* 发送短信验证码
* @param {Object} data
* @returns Promise
*/
export const sendCaptcha = data => {
return request({
method: 'post',
url: '/captcha',
data
});
};
/**
* 绑定用户手机
* @param {Object} data
* @returns Promise
*/
export const bindUserPhone = data => {
return request({
method: 'post',
url: '/user/phone',
data
});
};
/**
* 更改密码
* @param {Object} data
* @returns Promise
*/
export const changePassword = data => {
return request({
method: 'post',
url: '/user/password',
data
});
};
/**
* 更改昵称
* @param {Object} data
* @returns Promise
*/
export const changeNickname = data => {
return request({
method: 'post',
url: '/user/nickname',
data
});
};
/**
* 更改头像
* @param {Object} data
* @returns Promise
*/
export const changeAvatar = data => {
return request({
method: 'post',
url: '/user/avatar',
data
});
};
/**
* 获取未读消息数
* @param {Object} params
* @returns Promise
*/
export const getUnreadMsgCount = params => {
return request({
method: 'get',
url: '/user/msgcount/unread',
params
});
};
/**
* 获取消息列表
* @param {Object} params
* @returns Promise
*/
export const getMessages = params => {
return request({
method: 'get',
url: '/user/messages',
params
});
};
/**
* 阅读消息
* @param {Object} data
* @returns Promise
*/
export const readMessage = data => {
return request({
method: 'post',
url: '/user/message/read',
data
});
};
/**
* 获取收藏列表
* @param {Object} params
* @returns Promise
*/
export const getCollections = params => {
return request({
method: 'get',
url: '/user/collections',
params
});
};
/**
* 获取点赞列表
* @param {Object} params
* @returns Promise
*/
export const getStars = params => {
return request({
method: 'get',
url: '/user/stars',
params
});
};
/**
* 获取用户基础信息
* @param {Object} params
* @returns Promise
*/
export const getUserProfile = params => {
return request({
method: 'get',
url: '/user/profile',
params
});
};
/**
* 获取点赞列表
* @param {Object} params
* @returns Promise
*/
export const getUserPosts = params => {
return request({
method: 'get',
url: '/user/posts',
params
});
};
/**
* 获取账单列表
* @param {Object} params
* @returns Promise
*/
export const getBills = params => {
return request({
method: 'get',
url: '/user/wallet/bills',
params
});
};
/**
* 发起充值请求
* @param {Object} data
* @returns Promise
*/
export const reqRecharge = data => {
return request({
method: 'post',
url: '/user/recharge',
data
});
};
/**
* 获取充值状态
* @param {Object} params
* @returns Promise
*/
export const getRecharge = params => {
return request({
method: 'get',
url: '/user/recharge',
params
});
};
/**
* 获取推荐用户
* @param {Object} params
* @returns Promise
*/
export const getSuggestUsers = params => {
return request({
method: 'get',
url: '/suggest/users',
params
});
};
/**
* 获取推荐用户
* @param {Object} params
* @returns Promise
*/
export const getSuggestTags = params => {
return request({
method: 'get',
url: '/suggest/tags',
params
});
};
/**
* 获取附件
* @param {Object} params
* @returns Promise
*/
export const precheckAttachment = params => {
return request({
method: 'get',
url: '/attachment/precheck',
params
});
};
/**
* 获取附件
* @param {Object} params
* @returns Promise
*/
export const getAttachment = params => {
return request({
method: 'get',
url: '/attachment',
params
});
};

@ -0,0 +1,82 @@
.app-container {
margin: 0;
.app-wrap {
width: 100%;
// max-width: 1000px;
margin: 0 auto;
}
}
.main-wrap {
min-height: 100vh;
display: flex;
flex-direction: row;
justify-content: center;
.content-wrap {
width: 100%;
max-width: 500px;
position: relative;
}
.main-content-wrap {
margin: 0;
border-top: none;
border-radius: 0;
.n-list-item {
padding: 0;
}
}
}
.empty-wrap {
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
.hash-link,
.user-link {
color: #18a058;
text-decoration: none;
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
.username-link {
color: #000;
color: none;
text-decoration: none;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.dark {
.hash-link,
.user-link {
color: #63e2b7;
}
.username-link {
color: #eee;
}
}
@media screen and (max-width: 821px) {
.content-wrap {
top: 0;
left: 60px;
position: absolute !important;
width: calc(100% - 60px) !important;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@ -0,0 +1,257 @@
<template>
<n-modal
v-model:show="store.state.authModelShow"
class="auth-card"
preset="card"
size="small"
:mask-closable="false"
:bordered="false"
:style="{
width: '360px',
}"
>
<div class="auth-wrap">
<n-card :bordered="false">
<n-tabs
:default-value="store.state.authModelTab"
size="large"
justify-content="space-evenly"
>
<n-tab-pane name="signin" tab="登录">
<n-form
ref="loginRef"
:model="loginForm"
:rules="{
username: {
required: true,
message: '请输入账户名',
},
password: {
required: true,
message: '请输入密码',
},
}"
>
<n-form-item-row label="账户" path="username">
<n-input
v-model:value="loginForm.username"
placeholder="请输入用户名"
@keyup.enter.prevent="handleLogin"
/>
</n-form-item-row>
<n-form-item-row label="密码" path="password">
<n-input
type="password"
show-password-on="mousedown"
v-model:value="loginForm.password"
placeholder="请输入账户密码"
@keyup.enter.prevent="handleLogin"
/>
</n-form-item-row>
</n-form>
<n-button
type="primary"
block
secondary
strong
:loading="loading"
@click="handleLogin"
>
登录
</n-button>
</n-tab-pane>
<n-tab-pane name="signup" tab="注册">
<n-form
ref="registerRef"
:model="registerForm"
:rules="{
username: {
required: true,
message: '请输入账户名',
},
password: {
required: true,
message: '请输入密码',
},
repassword: [
{
required: true,
message: '请输入密码',
},
{
validator(rule, value) {
return (
!!registerForm.password &&
registerForm.password.startsWith(
value
) &&
registerForm.password.length >=
value.length
);
},
message: '两次密码输入不一致',
trigger: 'input',
},
],
}"
>
<n-form-item-row label="用户名" path="username">
<n-input
v-model:value="registerForm.username"
placeholder="用户名注册后无法修改"
/>
</n-form-item-row>
<n-form-item-row label="密码" path="password">
<n-input
type="password"
show-password-on="mousedown"
placeholder="密码不少于6位"
v-model:value="registerForm.password"
@keyup.enter.prevent="handleRegister"
/>
</n-form-item-row>
<n-form-item-row label="重复密码" path="repassword">
<n-input
type="password"
show-password-on="mousedown"
placeholder="请再次输入密码"
v-model:value="registerForm.repassword"
@keyup.enter.prevent="handleRegister"
/>
</n-form-item-row>
</n-form>
<n-button
type="primary"
block
secondary
strong
:loading="loading"
@click="handleRegister"
>
注册
</n-button>
</n-tab-pane>
</n-tabs>
</n-card>
</div>
</n-modal>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { useStore } from 'vuex';
import { userLogin, userRegister, userInfo } from '@/api/auth';
const store = useStore();
const loading = ref(false);
const loginRef = ref(null);
const loginForm = reactive({
username: '',
password: '',
});
const registerRef = ref(null);
const registerForm = reactive({
username: '',
password: '',
repassword: '',
});
const handleLogin = (e) => {
e.preventDefault();
e.stopPropagation();
loginRef.value?.validate((errors) => {
if (!errors) {
loading.value = true;
userLogin({
username: loginForm.username,
password: loginForm.password,
})
.then((res) => {
const token = res?.token || '';
//
localStorage.setItem('PAOPAO_TOKEN', token);
return userInfo(token);
})
.then((res) => {
window.$message.success('登录成功');
loading.value = false;
store.commit('updateUserinfo', res);
store.commit('triggerAuth', false);
loginForm.username = '';
loginForm.password = '';
})
.catch((err) => {
loading.value = false;
});
}
});
};
const handleRegister = (e) => {
e.preventDefault();
e.stopPropagation();
registerRef.value?.validate((errors) => {
if (!errors) {
loading.value = true;
userRegister({
username: registerForm.username,
password: registerForm.password,
})
.then((res) => {
return userLogin({
username: registerForm.username,
password: registerForm.password,
});
})
.then((res) => {
const token = res?.token || '';
//
localStorage.setItem('PAOPAO_TOKEN', token);
return userInfo(token);
})
.then((res) => {
window.$message.success('注册成功');
loading.value = false;
store.commit('updateUserinfo', res);
store.commit('triggerAuth', false);
registerForm.username = '';
registerForm.password = '';
registerForm.repassword = '';
})
.catch((err) => {
loading.value = false;
});
}
});
};
onMounted(() => {
const token = localStorage.getItem('PAOPAO_TOKEN') || '';
if (token) {
userInfo(token)
.then((res) => {
store.commit('updateUserinfo', res);
store.commit('triggerAuth', false);
})
.catch((err) => {
store.commit('userLogout');
});
} else {
store.commit('userLogout');
}
});
</script>
<style lang="less" scoped>
.auth-wrap {
margin-top: -30px;
}
</style>

@ -0,0 +1,249 @@
<template>
<div class="comment-item">
<n-thing content-indented>
<template #avatar>
<n-avatar round :size="30" :src="comment.user.avatar" />
</template>
<template #header>
<span class="nickname-wrap">
<router-link
@click.stop
class="username-link"
:to="{
name: 'user',
query: { username: comment.user.username },
}"
>
{{ comment.user.nickname }}
</router-link>
</span>
<span class="username-wrap">
@{{ comment.user.username }}
</span>
</template>
<template #header-extra>
<div class="opt-wrap">
<span class="timestamp">
{{
comment.ip_loc
? comment.ip_loc + ' · '
: comment.ip_loc
}}
{{ formatRelativeTime(comment.created_on) }}
</span>
<n-popconfirm
v-if="
store.state.userInfo.is_admin ||
store.state.userInfo.id === comment.user.id
"
negative-text="取消"
positive-text="确认"
@positive-click="execDelAction"
>
<template #trigger>
<n-button
quaternary
circle
size="tiny"
class="del-btn"
>
<template #icon>
<n-icon>
<trash />
</n-icon>
</template>
</n-button>
</template>
是否确认删除
</n-popconfirm>
</div>
</template>
<template #description v-if="comment.texts.length > 0">
<span
v-for="content in comment.texts"
:key="content.id"
class="comment-text"
@click.stop="doClickText($event, comment.id)"
v-html="parsePostTag(content.content).content"
>
</span>
</template>
<template #footer>
<post-image :imgs="comment.imgs" />
<!-- 回复列表 -->
<div class="reply-wrap">
<reply-item
v-for="reply in comment.replies"
:key="reply.id"
:reply="reply"
@focusReply="focusReply"
@reload="reload"
/>
</div>
<!-- 回复编辑器 -->
<compose-reply
ref="replyComposeRef"
v-if="store.state.userInfo.id > 0"
:comment-id="comment.id"
:at-userid="replyAtUserID"
:at-username="replyAtUsername"
@reload="reload"
@reset="resetReply"
/>
</template>
</n-thing>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
import { formatRelativeTime } from '@/utils/formatTime';
import { parsePostTag } from '@/utils/content';
import { Trash } from '@vicons/tabler';
import { deleteComment } from '@/api/post';
const store = useStore();
const router = useRouter();
const replyAtUserID = ref(0);
const replyAtUsername = ref('');
const replyComposeRef = ref();
const emit = defineEmits(['reload']);
const props = defineProps({
comment: {
type: Object,
default: () => {},
},
});
const comment = computed(() => {
let comment = Object.assign(
{
texts: [],
imgs: [],
},
props.comment
);
comment.contents.map((content) => {
if (+content.type === 1 || +content.type === 2) {
comment.texts.push(content);
}
if (+content.type === 3) {
comment.imgs.push(content);
}
});
return comment;
});
const doClickText = (e, id) => {
if (e.target.dataset.detail) {
const d = e.target.dataset.detail.split(':');
if (d.length === 2) {
store.commit('refresh');
if (d[0] === 'tag') {
window.$message.warning('评论内的无效话题');
} else {
router.push({
name: 'user',
query: {
username: d[1],
},
});
}
}
}
};
const focusReply = (reply) => {
replyAtUserID.value = reply.user_id;
replyAtUsername.value = reply.user?.username || '';
replyComposeRef.value?.switchReply(true);
};
const reload = () => {
emit('reload');
};
const resetReply = () => {
replyAtUserID.value = 0;
replyAtUsername.value = '';
};
const execDelAction = () => {
deleteComment({
id: comment.value.id,
})
.then((res) => {
window.$message.success('删除成功');
setTimeout(() => {
reload();
}, 50);
})
.catch((err) => {});
};
</script>
<style lang="less" scoped>
.comment-item {
width: 100%;
padding: 16px;
box-sizing: border-box;
.nickname-wrap {
font-size: 14px;
}
.username-wrap {
font-size: 14px;
opacity: 0.75;
}
.opt-wrap {
display: flex;
align-items: center;
.timestamp {
opacity: 0.75;
font-size: 12px;
}
.del-btn {
margin-left: 4px;
}
}
.comment-text {
display: block;
text-align: justify;
overflow: hidden;
white-space: pre-wrap;
word-break: break-all;
}
.opt-item {
display: flex;
align-items: center;
opacity: 0.7;
.opt-item-icon {
margin-right: 10px;
}
}
}
.reply-wrap {
margin-top: 10px;
border-radius: 5px;
background: #fafafc;
.reply-item {
&:last-child {
border-bottom: none;
}
}
}
.dark {
.reply-wrap {
background: #18181c;
}
}
</style>

@ -0,0 +1,428 @@
<template>
<div>
<div class="compose-wrap" v-if="store.state.userInfo.id > 0">
<div class="compose-line">
<div class="compose-user">
<n-avatar
round
:size="30"
:src="store.state.userInfo.avatar"
/>
</div>
<n-mention
type="textarea"
size="large"
autosize
:bordered="false"
:options="optionsRef"
:prefix="['@']"
:loading="loading"
:value="content"
:disabled="props.lock === 1"
@update:value="changeContent"
@search="handleSearch"
@focus="focusComment"
:placeholder="
props.lock === 1
? '泡泡已被锁定,回复功能已关闭'
: '快来评论两句吧...'
"
/>
</div>
<n-upload
v-if="showBtn"
ref="uploadRef"
abstract
list-type="image"
:multiple="true"
:max="9"
:action="uploadGateway"
:headers="{
Authorization: uploadToken,
}"
:data="{
type: uploadType,
}"
@before-upload="beforeUpload"
@finish="finishUpload"
@error="failUpload"
@remove="removeUpload"
@update:file-list="updateUpload"
>
<div class="compose-line compose-options">
<div class="attachment">
<n-upload-trigger #="{ handleClick }" abstract>
<n-button
:disabled="
(fileQueue.length > 0 &&
uploadType === 'public/video') ||
fileQueue.length === 9
"
@click="
() => {
setUploadType('public/image');
handleClick();
}
"
quaternary
circle
type="primary"
>
<template #icon>
<n-icon
size="20"
color="var(--primary-color)"
>
<image-outline />
</n-icon>
</template>
</n-button>
</n-upload-trigger>
<n-tooltip trigger="hover" placement="bottom">
<template #trigger>
<n-progress
class="text-statistic"
type="circle"
:show-indicator="false"
status="success"
:stroke-width="10"
:percentage="(content.length / 200) * 100"
/>
</template>
{{ content.length }} / 200
</n-tooltip>
</div>
<div class="submit-wrap">
<n-button
quaternary
round
type="tertiary"
class="cancel-btn"
size="small"
@click="cancelComment"
>
取消
</n-button>
<n-button
:loading="submitting"
@click="submitPost"
type="primary"
secondary
size="small"
round
>
发布
</n-button>
</div>
</div>
<div class="attachment-list-wrap">
<n-upload-file-list />
</div>
</n-upload>
</div>
<div class="compose-wrap" v-else>
<div class="login-wrap">
<span class="login-banner"> 登录后精彩更多</span>
</div>
<div class="login-wrap">
<n-button
strong
secondary
round
type="primary"
@click="triggerAuth('signin')"
>
登录
</n-button>
<n-button
strong
secondary
round
type="info"
@click="triggerAuth('signup')"
>
注册
</n-button>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import { useStore } from 'vuex';
import { debounce } from 'lodash';
import {
ImageOutline,
VideocamOutline,
AttachOutline,
CompassOutline,
} from '@vicons/ionicons5';
import { createComment } from '@/api/post';
import { getSuggestUsers } from '@/api/user';
import { parsePostTag } from '@/utils/content';
const emit = defineEmits(['post-success']);
const props = defineProps({
lock: {
type: Number,
default: 0,
},
postId: {
type: Number,
default: 0,
},
});
const store = useStore();
const optionsRef = ref([]);
const showBtn = ref(false);
const loading = ref(false);
const submitting = ref(false);
const content = ref('');
const uploadRef = ref(null);
const uploadType = ref('public/image');
const fileQueue = ref([]);
const imageContents = ref([]);
const uploadGateway = import.meta.env.VITE_HOST + '/attachment';
const uploadToken = ref();
// at
const loadSuggestionUsers = debounce((k) => {
getSuggestUsers({
k,
})
.then((res) => {
let options = [];
res.map((i) => {
options.push({
label: i,
value: i,
});
});
optionsRef.value = options;
loading.value = false;
})
.catch((err) => {
loading.value = false;
});
}, 200);
const handleSearch = (k, prefix) => {
if (loading.value) {
return;
}
loading.value = true;
if (prefix === '@') {
loadSuggestionUsers(k);
}
};
const changeContent = (v) => {
if (v.length > 200) {
return;
}
content.value = v;
};
const setUploadType = (type) => {
uploadType.value = type;
};
const updateUpload = (list) => {
fileQueue.value = list;
};
const beforeUpload = async (data) => {
//
if (
uploadType.value === 'public/image' &&
!['image/png', 'image/jpg', 'image/jpeg', 'image/gif'].includes(
data.file.file?.type
)
) {
window.$message.warning('图片仅允许 png/jpg/gif 格式');
return false;
}
if (uploadType.value === 'image' && data.file.file?.size > 10485760) {
window.$message.warning('图片大小不能超过10MB');
return false;
}
return true;
};
const finishUpload = ({ file, event }) => {
try {
let data = JSON.parse(event.target?.response);
if (data.code === 0) {
if (uploadType.value === 'public/image') {
imageContents.value.push({
id: file.id,
content: data.data.content,
});
}
}
} catch (error) {
window.$message.error('上传失败');
}
};
const failUpload = ({ file, event }) => {
try {
let data = JSON.parse(event.target?.response);
if (data.code !== 0) {
let errMsg = data.msg || '上传失败';
if (data.details && data.details.length > 0) {
data.details.map((detail) => {
errMsg += ':' + detail;
});
}
window.$message.error(errMsg);
}
} catch (error) {
window.$message.error('上传失败');
}
};
const removeUpload = ({ file }) => {
let idx = imageContents.value.findIndex((item) => item.id === file.id);
if (idx > -1) {
imageContents.value.splice(idx, 1);
}
};
const focusComment = () => {
showBtn.value = true;
};
const cancelComment = () => {
showBtn.value = false;
//
uploadRef.value?.clear();
fileQueue.value = [];
content.value = '';
imageContents.value = [];
};
//
const submitPost = () => {
if (content.value.trim().length === 0) {
window.$message.warning('请输入内容哦');
return;
}
// at
let { users } = parsePostTag(content.value);
const contents = [];
let sort = 100;
contents.push({
content: content.value,
type: 2, //
sort,
});
imageContents.value.map((img) => {
sort++;
contents.push({
content: img.content,
type: 3, //
sort,
});
});
submitting.value = true;
createComment({
contents,
post_id: props.postId,
users: Array.from(new Set(users)),
})
.then((res) => {
window.$message.success('发布成功');
submitting.value = false;
emit('post-success');
//
cancelComment();
})
.catch((err) => {
submitting.value = false;
});
};
const triggerAuth = (key) => {
store.commit('triggerAuth', true);
store.commit('triggerAuthKey', key);
};
onMounted(() => {
uploadToken.value = 'Bearer ' + localStorage.getItem('PAOPAO_TOKEN');
});
</script>
<style lang="less" scoped>
.compose-wrap {
width: 100%;
padding: 16px;
box-sizing: border-box;
.compose-line {
display: flex;
flex-direction: row;
.compose-user {
width: 42px;
height: 42px;
display: flex;
align-items: center;
}
&.compose-options {
margin-top: 6px;
padding-left: 42px;
display: flex;
justify-content: space-between;
.submit-wrap {
display: flex;
align-items: center;
.cancel-btn {
margin-right: 8px;
}
}
}
}
.login-wrap {
display: flex;
justify-content: center;
width: 100%;
.login-banner {
margin-bottom: 12px;
opacity: 0.8;
}
button {
margin: 0 4px;
}
}
}
.attachment {
display: flex;
align-items: center;
.text-statistic {
margin-left: 8px;
width: 18px;
height: 18px;
transform: rotate(180deg);
}
}
.attachment-list-wrap {
margin-top: 12px;
margin-left: 42px;
.n-upload-file-info__thumbnail {
overflow: hidden;
}
}
</style>

@ -0,0 +1,113 @@
<template>
<div class="reply-compose-wrap">
<div class="reply-switch">
<span class="show" v-if="!showReply" @click="switchReply(true)">
回复
</span>
<span class="hide" v-if="showReply" @click="switchReply(false)">
取消
</span>
</div>
<div class="reply-input-wrap" v-if="showReply">
<n-input-group>
<n-input
ref="inputInstRef"
size="small"
:placeholder="
props.atUsername
? '@' + props.atUsername
: '请输入回复内容..'
"
maxlength="100"
v-model:value="replyContent"
show-count
clearable
/>
<n-button
type="primary"
size="small"
ghost
:loading="submitting"
@click="submitReply"
>
回复
</n-button>
</n-input-group>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { createCommentReply } from '@/api/post';
const props = defineProps({
commentId: {
type: Number,
default: 0,
},
atUserid: {
type: Number,
default: 0,
},
atUsername: {
type: String,
default: '',
},
});
const emit = defineEmits(['reload', 'reset']);
const inputInstRef = ref(null);
const showReply = ref(false);
const replyContent = ref('');
const submitting = ref(false);
const switchReply = (status) => {
showReply.value = status;
if (status) {
setTimeout(() => {
inputInstRef.value?.focus();
}, 10);
} else {
submitting.value = false;
replyContent.value = '';
emit('reset');
}
};
const submitReply = () => {
submitting.value = true;
createCommentReply({
comment_id: props.commentId,
at_user_id: props.atUserid,
content: replyContent.value,
})
.then((res) => {
switchReply(false);
window.$message.success('评论成功');
emit('reload');
})
.catch((err) => {
submitting.value = false;
});
};
defineExpose({ switchReply });
</script>
<style lang="less" scoped>
.reply-compose-wrap {
.reply-switch {
text-align: right;
font-size: 12px;
margin: 10px 0;
.show {
color: #18a058;
cursor: pointer;
}
.hide {
opacity: 0.75;
cursor: pointer;
}
}
}
</style>

@ -0,0 +1,597 @@
<template>
<div>
<div class="compose-wrap" v-if="store.state.userInfo.id > 0">
<div class="compose-line">
<div class="compose-user">
<n-avatar
round
:size="30"
:src="store.state.userInfo.avatar"
/>
</div>
<n-mention
type="textarea"
size="large"
autosize
:bordered="false"
:loading="loading"
:value="content"
:prefix="['@', '#']"
:options="optionsRef"
@search="handleSearch"
@update:value="changeContent"
placeholder="说说您的新鲜事..."
/>
</div>
<n-upload
ref="uploadRef"
abstract
list-type="image"
:multiple="true"
:max="9"
:action="uploadGateway"
:headers="{
Authorization: uploadToken,
}"
:data="{
type: uploadType,
}"
@before-upload="beforeUpload"
@finish="finishUpload"
@error="failUpload"
@remove="removeUpload"
@update:file-list="updateUpload"
>
<div class="compose-line compose-options">
<div class="attachment">
<n-upload-trigger #="{ handleClick }" abstract>
<n-button
:disabled="
(fileQueue.length > 0 &&
uploadType === 'public/video') ||
fileQueue.length === 9
"
@click="
() => {
setUploadType('public/image');
handleClick();
}
"
quaternary
circle
type="primary"
>
<template #icon>
<n-icon
size="20"
color="var(--primary-color)"
>
<image-outline />
</n-icon>
</template>
</n-button>
</n-upload-trigger>
<n-upload-trigger #="{ handleClick }" abstract>
<n-button
:disabled="
(fileQueue.length > 0 &&
uploadType !== 'public/video') ||
fileQueue.length === 9
"
@click="
() => {
setUploadType('public/video');
handleClick();
}
"
quaternary
circle
type="primary"
>
<template #icon>
<n-icon
size="20"
color="var(--primary-color)"
>
<videocam-outline />
</n-icon>
</template>
</n-button>
</n-upload-trigger>
<n-upload-trigger #="{ handleClick }" abstract>
<n-button
:disabled="
(fileQueue.length > 0 &&
uploadType === 'public/video') ||
fileQueue.length === 9
"
@click="
() => {
setUploadType('attachment');
handleClick();
}
"
quaternary
circle
type="primary"
>
<template #icon>
<n-icon
size="20"
color="var(--primary-color)"
>
<attach-outline />
</n-icon>
</template>
</n-button>
</n-upload-trigger>
<n-button
quaternary
circle
type="primary"
@click.stop="switchLink"
>
<template #icon>
<n-icon size="20" color="var(--primary-color)">
<compass-outline />
</n-icon>
</template>
</n-button>
</div>
<div class="submit-wrap">
<n-tooltip trigger="hover" placement="bottom">
<template #trigger>
<n-progress
class="text-statistic"
type="circle"
:show-indicator="false"
status="success"
:stroke-width="10"
:percentage="(content.length / 200) * 100"
/>
</template>
{{ content.length }} / 200
</n-tooltip>
<n-button
:loading="submitting"
@click="submitPost"
type="primary"
secondary
round
>
发布
</n-button>
</div>
</div>
<div class="attachment-list-wrap">
<n-upload-file-list />
<div
class="attachment-price-wrap"
v-if="attachmentContents.length > 0"
>
<n-input-number
v-model:value="attachmentPrice"
:min="0"
:max="100000"
placeholder="请输入附件价格0为免费附件"
>
<template #prefix>
<span> 附件价格</span>
</template>
</n-input-number>
</div>
</div>
</n-upload>
<div class="link-wrap" v-if="showLinkSet">
<n-dynamic-input
v-model:value="links"
placeholder="请输入以http(s)://开头的链接"
:min="0"
:max="3"
>
<template #create-button-default> 创建链接 </template>
</n-dynamic-input>
</div>
</div>
<div class="compose-wrap" v-else>
<div class="login-wrap">
<span class="login-banner"> 登录后精彩更多</span>
</div>
<div class="login-wrap">
<n-button
strong
secondary
round
type="primary"
@click="triggerAuth('signin')"
>
登录
</n-button>
<n-button
strong
secondary
round
type="info"
@click="triggerAuth('signup')"
>
注册
</n-button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useStore } from 'vuex';
import { debounce } from 'lodash';
import { getSuggestUsers, getSuggestTags } from '@/api/user';
import {
ImageOutline,
VideocamOutline,
AttachOutline,
CompassOutline,
} from '@vicons/ionicons5';
import { createPost } from '@/api/post';
import { parsePostTag } from '@/utils/content';
const emit = defineEmits(['post-success']);
const store = useStore();
const optionsRef = ref([]);
const loading = ref(false);
const submitting = ref(false);
const showLinkSet = ref(false);
const content = ref('');
const links = ref([]);
const uploadRef = ref(null);
const attachmentPrice = ref(0);
const uploadType = ref('public/image');
const fileQueue = ref([]);
const imageContents = ref([]);
const videoContents = ref([]);
const attachmentContents = ref([]);
const uploadGateway = import.meta.env.VITE_HOST + '/attachment';
const uploadToken = ref();
const switchLink = () => {
showLinkSet.value = !showLinkSet.value;
if (!showLinkSet.value) {
links.value = [];
}
};
// at
const loadSuggestionUsers = debounce((k) => {
getSuggestUsers({
k,
})
.then((res) => {
let options = [];
res.map((i) => {
options.push({
label: i,
value: i,
});
});
optionsRef.value = options;
loading.value = false;
})
.catch((err) => {
loading.value = false;
});
}, 200);
// tag
const loadSuggestionTags = debounce((k) => {
getSuggestTags({
k,
})
.then((res) => {
let options = [];
res.map((i) => {
options.push({
label: i,
value: i,
});
});
optionsRef.value = options;
loading.value = false;
})
.catch((err) => {
loading.value = false;
});
}, 200);
const handleSearch = (k, prefix) => {
if (loading.value) {
return;
}
loading.value = true;
if (prefix === '@') {
loadSuggestionUsers(k);
} else {
loadSuggestionTags(k);
}
};
const changeContent = (v) => {
if (v.length > 200) {
return;
}
content.value = v;
};
const setUploadType = (type) => {
uploadType.value = type;
};
const updateUpload = (list) => {
fileQueue.value = list;
};
const beforeUpload = async (data) => {
//
if (
uploadType.value === 'public/image' &&
!['image/png', 'image/jpg', 'image/jpeg', 'image/gif'].includes(
data.file.file?.type
)
) {
window.$message.warning('图片仅允许 png/jpg/gif 格式');
return false;
}
if (uploadType.value === 'image' && data.file.file?.size > 10485760) {
window.$message.warning('图片大小不能超过10MB');
return false;
}
//
if (
uploadType.value === 'public/video' &&
!['video/mp4', 'video/quicktime'].includes(data.file.file?.type)
) {
window.$message.warning('视频仅允许 mp4/mov 格式');
return false;
}
if (
uploadType.value === 'public/video' &&
data.file.file?.size > 104857600
) {
window.$message.warning('视频大小不能超过100MB');
return false;
}
//
if (
uploadType.value === 'attachment' &&
!['application/zip'].includes(data.file.file?.type)
) {
window.$message.warning('附件仅允许 zip 格式');
return false;
}
if (uploadType.value === 'attachment' && data.file.file?.size > 104857600) {
window.$message.warning('附件大小不能超过100MB');
return false;
}
return true;
};
const finishUpload = ({ file, event }) => {
try {
let data = JSON.parse(event.target?.response);
if (data.code === 0) {
if (uploadType.value === 'public/image') {
imageContents.value.push({
id: file.id,
content: data.data.content,
});
}
if (uploadType.value === 'public/video') {
videoContents.value.push({
id: file.id,
content: data.data.content,
});
}
if (uploadType.value === 'attachment') {
attachmentContents.value.push({
id: file.id,
content: data.data.content,
});
}
}
} catch (error) {
window.$message.error('上传失败');
}
};
const failUpload = ({ file, event }) => {
try {
let data = JSON.parse(event.target?.response);
if (data.code !== 0) {
let errMsg = data.msg || '上传失败';
if (data.details && data.details.length > 0) {
data.details.map((detail) => {
errMsg += ':' + detail;
});
}
window.$message.error(errMsg);
}
} catch (error) {
window.$message.error('上传失败');
}
};
const removeUpload = ({ file }) => {
let idx = imageContents.value.findIndex((item) => item.id === file.id);
if (idx > -1) {
imageContents.value.splice(idx, 1);
}
idx = videoContents.value.findIndex((item) => item.id === file.id);
if (idx > -1) {
videoContents.value.splice(idx, 1);
}
idx = attachmentContents.value.findIndex((item) => item.id === file.id);
if (idx > -1) {
attachmentContents.value.splice(idx, 1);
}
};
//
const submitPost = () => {
if (content.value.trim().length === 0) {
window.$message.warning('请输入内容哦');
return;
}
// attag
let { tags, users } = parsePostTag(content.value);
const contents = [];
let sort = 100;
contents.push({
content: content.value,
type: 2, //
sort,
});
imageContents.value.map((img) => {
sort++;
contents.push({
content: img.content,
type: 3, //
sort,
});
});
videoContents.value.map((video) => {
sort++;
contents.push({
content: video.content,
type: 4, //
sort,
});
});
attachmentContents.value.map((attachment) => {
sort++;
contents.push({
content: attachment.content,
type: 7, //
sort,
});
});
if (links.value.length > 0) {
links.value.map((link) => {
sort++;
contents.push({
content: link,
type: 6, //
sort,
});
});
}
submitting.value = true;
createPost({
contents,
tags: Array.from(new Set(tags)),
users: Array.from(new Set(users)),
attachment_price: +attachmentPrice.value * 100,
})
.then((res) => {
window.$message.success('发布成功');
submitting.value = false;
emit('post-success', res);
//
showLinkSet.value = false;
uploadRef.value?.clear();
fileQueue.value = [];
content.value = '';
links.value = [];
imageContents.value = [];
videoContents.value = [];
attachmentContents.value = [];
})
.catch((err) => {
submitting.value = false;
});
};
const triggerAuth = (key) => {
store.commit('triggerAuth', true);
store.commit('triggerAuthKey', key);
};
onMounted(() => {
uploadToken.value = 'Bearer ' + localStorage.getItem('PAOPAO_TOKEN');
});
</script>
<style lang="less">
.compose-wrap {
width: 100%;
padding: 16px;
box-sizing: border-box;
.compose-line {
display: flex;
flex-direction: row;
.compose-user {
width: 42px;
height: 42px;
display: flex;
align-items: center;
}
&.compose-options {
margin-top: 6px;
padding-left: 42px;
display: flex;
justify-content: space-between;
.submit-wrap {
display: flex;
align-items: center;
.text-statistic {
margin-right: 8px;
width: 20px;
height: 20px;
transform: rotate(180deg);
}
}
}
}
.login-wrap {
display: flex;
justify-content: center;
width: 100%;
.login-banner {
margin-bottom: 12px;
opacity: 0.8;
}
button {
margin: 0 4px;
}
}
}
.attachment-list-wrap {
margin-top: 12px;
margin-left: 42px;
.n-upload-file-info__thumbnail {
overflow: hidden;
}
}
</style>

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

Loading…
Cancel
Save