书籍列表接口新增参数
This commit is contained in:
273
internal/logic/ad_event_logs/ad_event_logs.go
Normal file
273
internal/logic/ad_event_logs/ad_event_logs.go
Normal file
@ -0,0 +1,273 @@
|
||||
package ads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"server/internal/consts"
|
||||
"server/internal/dao"
|
||||
"server/internal/model"
|
||||
"server/internal/model/do"
|
||||
"server/internal/service"
|
||||
"server/utility/ecode"
|
||||
"server/utility/encrypt"
|
||||
"server/utility/mqtt"
|
||||
"server/utility/state_machine"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type sAds struct {
|
||||
stateMachine *state_machine.AdStateMachine
|
||||
}
|
||||
|
||||
func New() service.IAds {
|
||||
return &sAds{
|
||||
stateMachine: state_machine.NewAdStateMachine(),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAds(New())
|
||||
}
|
||||
|
||||
// Upload 广告数据上传 - 处理用户广告状态流转
|
||||
func (s *sAds) Upload(ctx context.Context, in *model.AdsUploadIn) (out *model.AdsUploadOut, err error) {
|
||||
// 解密广告数据
|
||||
adsData, err := encrypt.DecryptAdsData(in.Data)
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("data_decrypt_failed")
|
||||
}
|
||||
|
||||
// 验证数据
|
||||
if adsData.AdsPlatId <= 0 || adsData.AdsCategoryId <= 0 || adsData.AppPackage == "" {
|
||||
return nil, ecode.Params.Sub("invalid_ads_data")
|
||||
}
|
||||
|
||||
// 使用事务同时操作多个表
|
||||
err = dao.AdEventLogs.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// 1. 查找用户该广告的最新记录,确保状态流转是在同一条广告记录上进行的
|
||||
var fromState consts.AdState
|
||||
var adEventId int64 // 广告事件ID,用于关联状态流转记录
|
||||
var isNewLifecycle bool // 是否是新的生命周期
|
||||
|
||||
// 当前状态
|
||||
toState := consts.AdState(adsData.Status)
|
||||
|
||||
// 拉取成功和拉取失败总是新的广告生命周期的开始
|
||||
if toState == consts.StateFetchSuccess || toState == consts.StateFetchFailed {
|
||||
isNewLifecycle = true
|
||||
fromState = 0
|
||||
glog.Infof(ctx, "Starting new ad lifecycle with fetch state for user %d, app %s, state: %s",
|
||||
in.UserId, adsData.AppPackage, consts.GetStateDescription(toState))
|
||||
} else {
|
||||
// 查询条件:同一用户、同一平台、同一广告类型、同一APP包名
|
||||
latestLog, err := dao.AdEventLogs.Ctx(ctx).TX(tx).
|
||||
Where(do.AdEventLogs{
|
||||
UserId: in.UserId,
|
||||
AdsPlatId: adsData.AdsPlatId,
|
||||
AdsCategoryId: adsData.AdsCategoryId,
|
||||
AppPackage: adsData.AppPackage,
|
||||
}).
|
||||
Order("created_at DESC").
|
||||
One()
|
||||
|
||||
if err == nil && !latestLog.IsEmpty() {
|
||||
fromState = consts.AdState(latestLog["status"].Int())
|
||||
adEventId = latestLog["id"].Int64()
|
||||
|
||||
// 检查是否是终止状态,如果是终止状态,则需要创建新的广告记录
|
||||
if s.stateMachine.IsTerminalState(fromState) {
|
||||
// 如果是终止状态,则表示这是一个新的广告生命周期
|
||||
isNewLifecycle = true
|
||||
fromState = 0 // 重置状态,表示新的生命周期开始
|
||||
glog.Infof(ctx, "Starting new ad lifecycle after terminal state for user %d, app %s",
|
||||
in.UserId, adsData.AppPackage)
|
||||
} else {
|
||||
glog.Infof(ctx, "Continuing ad lifecycle for user %d, app %s, from state %d to %d",
|
||||
in.UserId, adsData.AppPackage, fromState, adsData.Status)
|
||||
}
|
||||
} else {
|
||||
// 没有找到记录,这是第一次上传该广告数据
|
||||
isNewLifecycle = true
|
||||
glog.Infof(ctx, "First ad upload for user %d, app %s", in.UserId, adsData.AppPackage)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 使用状态机验证状态转换
|
||||
flowID := generateFlowID(in.UserId, adsData.AppPackage)
|
||||
|
||||
// 如果不是新的生命周期,则验证状态转换
|
||||
if !isNewLifecycle {
|
||||
err = s.stateMachine.Transition(ctx, flowID, in.UserId, fromState, toState, "用户操作")
|
||||
if err != nil {
|
||||
glog.Warningf(ctx, "Invalid state transition: %s -> %s for user %d, app %s",
|
||||
consts.GetStateDescription(fromState), consts.GetStateDescription(toState),
|
||||
in.UserId, adsData.AppPackage)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 插入或更新广告事件日志
|
||||
var eventId int64
|
||||
if isNewLifecycle {
|
||||
// 新的生命周期,创建新记录
|
||||
eventId, err = dao.AdEventLogs.Ctx(ctx).TX(tx).Data(do.AdEventLogs{
|
||||
UserId: in.UserId,
|
||||
AdsPlatId: adsData.AdsPlatId,
|
||||
AdsCategoryId: adsData.AdsCategoryId,
|
||||
AppPackage: adsData.AppPackage,
|
||||
Status: adsData.Status,
|
||||
StatusDesc: consts.GetStateDescription(toState),
|
||||
}).InsertAndGetId()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("database_save_failed")
|
||||
}
|
||||
} else {
|
||||
// 继续现有生命周期,更新状态
|
||||
_, err = dao.AdEventLogs.Ctx(ctx).TX(tx).
|
||||
Where("id", adEventId).
|
||||
Data(do.AdEventLogs{
|
||||
Status: adsData.Status,
|
||||
StatusDesc: consts.GetStateDescription(toState),
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("database_update_failed")
|
||||
}
|
||||
eventId = adEventId
|
||||
}
|
||||
|
||||
// 4. 插入状态流转记录
|
||||
var fromStatus interface{}
|
||||
if fromState != 0 {
|
||||
fromStatus = int(fromState)
|
||||
}
|
||||
|
||||
_, err = dao.AdEventTransitions.Ctx(ctx).TX(tx).Data(do.AdEventTransitions{
|
||||
EventId: eventId,
|
||||
FromStatus: fromStatus,
|
||||
ToStatus: adsData.Status,
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("transition_save_failed")
|
||||
}
|
||||
|
||||
// 5. 如果状态是观看完成(StateWatched),则给用户增加积分
|
||||
if toState == consts.StateWatched {
|
||||
// 查询任务表中状态为1(启用)且任务类型为2(广告)的任务
|
||||
taskInfo, err := dao.Tasks.Ctx(ctx).TX(tx).
|
||||
Fields("id, reward_points").
|
||||
Where("task_type", 2).
|
||||
Where("status", 1).
|
||||
One()
|
||||
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "Failed to query ad task: %v", err)
|
||||
return ecode.Fail.Sub("task_query_failed")
|
||||
}
|
||||
|
||||
if !taskInfo.IsEmpty() {
|
||||
taskId := taskInfo["id"].Int64()
|
||||
rewardPoints := taskInfo["reward_points"].Uint()
|
||||
|
||||
if rewardPoints > 0 {
|
||||
// 更新用户积分
|
||||
_, err = dao.Users.Ctx(ctx).TX(tx).
|
||||
Where("id", in.UserId).
|
||||
Increment("points", rewardPoints)
|
||||
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "Failed to update user points: %v", err)
|
||||
return ecode.Fail.Sub("user_points_update_failed")
|
||||
}
|
||||
|
||||
// 记录积分日志
|
||||
_, err = dao.UserPointsLogs.Ctx(ctx).TX(tx).Data(do.UserPointsLogs{
|
||||
UserId: in.UserId,
|
||||
ChangeType: 2, // 2=收入(earn)
|
||||
PointsChange: int(rewardPoints),
|
||||
RelatedOrderId: eventId,
|
||||
Description: "观看广告奖励",
|
||||
}).Insert()
|
||||
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "Failed to create user points log: %v", err)
|
||||
return ecode.Fail.Sub("user_points_log_create_failed")
|
||||
}
|
||||
|
||||
// 记录任务日志
|
||||
_, err = dao.TaskLogs.Ctx(ctx).TX(tx).Data(do.TaskLogs{
|
||||
TaskId: taskId,
|
||||
UserId: in.UserId,
|
||||
RewardPoints: rewardPoints,
|
||||
}).Insert()
|
||||
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "Failed to create task log: %v", err)
|
||||
return ecode.Fail.Sub("task_log_create_failed")
|
||||
}
|
||||
|
||||
glog.Infof(ctx, "User %d earned %d points for watching ad", in.UserId, rewardPoints)
|
||||
}
|
||||
} else {
|
||||
glog.Warningf(ctx, "No active ad task found")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建MQTT消息
|
||||
mqttMessage := map[string]interface{}{
|
||||
"type": "ads",
|
||||
"node_uid": in.NodeUid,
|
||||
"device_code": in.DeviceCode,
|
||||
"data": map[string]interface{}{
|
||||
"ads_plat_id": adsData.AdsPlatId,
|
||||
"ads_category_id": adsData.AdsCategoryId,
|
||||
"app_package": adsData.AppPackage,
|
||||
"status": adsData.Status,
|
||||
"timestamp": time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
// 发送到MQTT
|
||||
s.publishToMQTT(ctx, mqttMessage)
|
||||
|
||||
return &model.AdsUploadOut{Success: true}, nil
|
||||
}
|
||||
|
||||
// generateFlowID 生成流转ID
|
||||
func generateFlowID(userID int64, appPackage string) string {
|
||||
return fmt.Sprintf("flow_%d_%s_%d", userID, appPackage, time.Now().Unix())
|
||||
}
|
||||
|
||||
// publishToMQTT 发送消息到MQTT
|
||||
func (s *sAds) publishToMQTT(ctx context.Context, message map[string]interface{}) {
|
||||
mqttClient := mqtt.GetMQTTClient("amazon_sqs")
|
||||
if mqttClient == nil {
|
||||
glog.Errorf(ctx, "MQTT client not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
queueName := g.Cfg().MustGet(ctx, "sqs.ads").String()
|
||||
if queueName == "" {
|
||||
glog.Errorf(ctx, "MQTT queue name not configured")
|
||||
return
|
||||
}
|
||||
|
||||
err := mqttClient.Publish(ctx, queueName, message)
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "Failed to publish message to MQTT: %v", err)
|
||||
} else {
|
||||
glog.Infof(ctx, "Message published to MQTT: %v", message)
|
||||
}
|
||||
}
|
||||
@ -62,8 +62,9 @@ func (s *sAdmin) Info(ctx context.Context, in *model.AdminInfoIn) (out *model.Ad
|
||||
return nil, ecode.Fail.Sub("admin_query_failed")
|
||||
}
|
||||
return &model.AdminInfoOut{
|
||||
AdminId: admin.Id,
|
||||
Id: admin.Id,
|
||||
Username: admin.Username,
|
||||
Role: consts.AdminRoleCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ func (s *sAuthor) List(ctx context.Context, in *model.AuthorListIn) (out *model.
|
||||
if in.Status != 0 {
|
||||
m = m.Where(dao.Authors.Columns().Status, in.Status)
|
||||
}
|
||||
if err = m.Page(in.Page, in.Size).ScanAndCount(&out.List, &out.Total, false); err != nil {
|
||||
if err = m.Page(in.Page, in.Size).WithAll().ScanAndCount(&out.List, &out.Total, false); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
@ -226,13 +226,8 @@ func (s *sAuthor) Delete(ctx context.Context, in *model.AuthorDelIn) (out *model
|
||||
|
||||
// Apply 允许用户申请成为作者
|
||||
func (s *sAuthor) Apply(ctx context.Context, in *model.AuthorApplyIn) (out *model.AuthorApplyOut, err error) {
|
||||
userIdVal := ctx.Value("id")
|
||||
userId, ok := userIdVal.(int64)
|
||||
if !ok || userId == 0 {
|
||||
return nil, ecode.Fail.Sub("user_id_invalid")
|
||||
}
|
||||
exist, err := dao.Authors.Ctx(ctx).
|
||||
Where(dao.Authors.Columns().UserId, userId).
|
||||
Where(dao.Authors.Columns().UserId, in.UserId).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("author_query_failed")
|
||||
@ -241,10 +236,10 @@ func (s *sAuthor) Apply(ctx context.Context, in *model.AuthorApplyIn) (out *mode
|
||||
return nil, ecode.Params.Sub("author_user_exists")
|
||||
}
|
||||
if _, err := dao.Authors.Ctx(ctx).Data(do.Authors{
|
||||
UserId: userId,
|
||||
UserId: in.UserId,
|
||||
PenName: in.PenName,
|
||||
Bio: in.Bio,
|
||||
Status: 1, // 默认正常
|
||||
Status: 2, // 默认禁用
|
||||
}).Insert(); err != nil {
|
||||
return nil, ecode.Fail.Sub("author_create_failed")
|
||||
}
|
||||
@ -252,17 +247,74 @@ func (s *sAuthor) Apply(ctx context.Context, in *model.AuthorApplyIn) (out *mode
|
||||
}
|
||||
func (s *sAuthor) Detail(ctx context.Context, in *model.AuthorDetailIn) (out *model.AuthorDetailOut, err error) {
|
||||
out = &model.AuthorDetailOut{}
|
||||
exist, err := dao.Authors.Ctx(ctx).
|
||||
WherePri(in.AuthorId).
|
||||
Exist()
|
||||
err = dao.Authors.Ctx(ctx).WherePri(in.AuthorId).WithAll().Scan(&out)
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("author_query_failed")
|
||||
}
|
||||
if out == nil {
|
||||
return nil, ecode.NotFound.Sub("author_not_found")
|
||||
}
|
||||
userId := ctx.Value("id")
|
||||
if userId != nil {
|
||||
exist, err := dao.UserFollowAuthors.Ctx(ctx).
|
||||
Where(dao.UserFollowAuthors.Columns().UserId, userId).
|
||||
Where(dao.UserFollowAuthors.Columns().AuthorId, in.AuthorId).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("user_follow_author_query_failed")
|
||||
}
|
||||
out.IsFollowed = exist
|
||||
}
|
||||
// 查询作者作品数量
|
||||
out.WorksCount, err = dao.Books.Ctx(ctx).
|
||||
Where(dao.Books.Columns().AuthorId, in.AuthorId).
|
||||
Count()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("author_book_count_failed")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AuthorInfo 获取作者信息
|
||||
func (s *sAuthor) AuthorInfo(ctx context.Context, in *model.AuthorInfoIn) (out *model.AuthorInfoOut, err error) {
|
||||
exist, err := dao.Authors.Ctx(ctx).Where(dao.Authors.Columns().UserId, in.UserId).Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("author_query_failed")
|
||||
}
|
||||
if !exist {
|
||||
return nil, ecode.NotFound.Sub("author_not_found")
|
||||
}
|
||||
if err = dao.Authors.Ctx(ctx).WherePri(in.AuthorId).WithAll().Scan(&out); err != nil {
|
||||
|
||||
var author struct {
|
||||
Id int64 `json:"id"`
|
||||
PenName string `json:"penName"`
|
||||
}
|
||||
err = dao.Authors.Ctx(ctx).Where(dao.Authors.Columns().UserId, in.UserId).Fields("id, pen_name").Scan(&author)
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("author_query_failed")
|
||||
}
|
||||
return out, nil
|
||||
|
||||
return &model.AuthorInfoOut{
|
||||
Id: author.Id,
|
||||
PenName: author.PenName,
|
||||
Role: "author",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 审核作者申请(通过/拒绝)
|
||||
func (s *sAuthor) Review(ctx context.Context, in *model.AuthorReviewIn) (out *model.AuthorReviewOut, err error) {
|
||||
exist, err := dao.Authors.Ctx(ctx).WherePri(in.AuthorId).Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("author_query_failed")
|
||||
}
|
||||
if !exist {
|
||||
return nil, ecode.NotFound.Sub("author_not_found")
|
||||
}
|
||||
_, err = dao.Authors.Ctx(ctx).WherePri(in.AuthorId).Data(do.Authors{
|
||||
Status: in.Status,
|
||||
}).Update()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("author_review_failed")
|
||||
}
|
||||
return &model.AuthorReviewOut{Success: true}, nil
|
||||
}
|
||||
|
||||
@ -232,6 +232,9 @@ func (s *sBook) AppList(ctx context.Context, in *model.BookAppListIn) (out *mode
|
||||
if in.IsFeatured {
|
||||
m = m.Where(dao.Books.Columns().IsFeatured, 1)
|
||||
}
|
||||
if in.IsHot {
|
||||
m = m.Where(dao.Books.Columns().IsHot, 1)
|
||||
}
|
||||
if in.Sort != "" {
|
||||
m = m.Order(in.Sort)
|
||||
}
|
||||
@ -278,6 +281,30 @@ func (s *sBook) AppList(ctx context.Context, in *model.BookAppListIn) (out *mode
|
||||
out.List[i].MyRating = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 查询用户书架
|
||||
type shelfRow struct {
|
||||
BookId int64 `json:"bookId"`
|
||||
}
|
||||
shelves := make([]shelfRow, 0)
|
||||
err = dao.Bookshelves.Ctx(ctx).
|
||||
Fields("book_id").
|
||||
Where(dao.Bookshelves.Columns().UserId, in.UserId).
|
||||
WhereIn(dao.Bookshelves.Columns().BookId, bookIds).
|
||||
Scan(&shelves)
|
||||
if err == nil && len(shelves) > 0 {
|
||||
shelfMap := make(map[int64]bool, len(shelves))
|
||||
for _, s := range shelves {
|
||||
shelfMap[s.BookId] = true
|
||||
}
|
||||
for i := range out.List {
|
||||
out.List[i].IsInBookshelf = shelfMap[out.List[i].Id]
|
||||
}
|
||||
} else {
|
||||
for i := range out.List {
|
||||
out.List[i].IsInBookshelf = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
@ -323,7 +350,7 @@ func (s *sBook) AppRate(ctx context.Context, in *model.BookAppRateIn) (out *mode
|
||||
// 开启事务处理评分
|
||||
if err := dao.BookRatings.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// 检查是否已经评分过
|
||||
exist, err := dao.BookRatings.Ctx(ctx).TX(tx).
|
||||
exist, err := dao.BookRatings.Ctx(ctx).
|
||||
Where(dao.BookRatings.Columns().UserId, in.UserId).
|
||||
Where(dao.BookRatings.Columns().BookId, in.BookId).
|
||||
Exist()
|
||||
@ -333,7 +360,7 @@ func (s *sBook) AppRate(ctx context.Context, in *model.BookAppRateIn) (out *mode
|
||||
|
||||
if exist {
|
||||
// 更新现有评分
|
||||
_, err = dao.BookRatings.Ctx(ctx).TX(tx).
|
||||
_, err = dao.BookRatings.Ctx(ctx).
|
||||
Where(dao.BookRatings.Columns().UserId, in.UserId).
|
||||
Where(dao.BookRatings.Columns().BookId, in.BookId).
|
||||
Data(do.BookRatings{
|
||||
@ -344,7 +371,7 @@ func (s *sBook) AppRate(ctx context.Context, in *model.BookAppRateIn) (out *mode
|
||||
}
|
||||
} else {
|
||||
// 创建新评分记录
|
||||
_, err = dao.BookRatings.Ctx(ctx).TX(tx).Data(do.BookRatings{
|
||||
_, err = dao.BookRatings.Ctx(ctx).Data(do.BookRatings{
|
||||
UserId: in.UserId,
|
||||
BookId: in.BookId,
|
||||
Score: in.Rating,
|
||||
@ -355,20 +382,22 @@ func (s *sBook) AppRate(ctx context.Context, in *model.BookAppRateIn) (out *mode
|
||||
}
|
||||
|
||||
// 重新计算书籍平均评分
|
||||
var avgRating float64
|
||||
err = dao.BookRatings.Ctx(ctx).TX(tx).
|
||||
var result struct {
|
||||
AvgRating float64 `json:"avg_rating"`
|
||||
}
|
||||
err = dao.BookRatings.Ctx(ctx).
|
||||
Where(dao.BookRatings.Columns().BookId, in.BookId).
|
||||
Fields("AVG(score) as avg_rating").
|
||||
Scan(&avgRating)
|
||||
Fields("COALESCE(AVG(" + dao.BookRatings.Columns().Score + "), 0) as avg_rating").
|
||||
Scan(&result)
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("rating_calculation_failed")
|
||||
}
|
||||
|
||||
// 更新书籍的平均评分
|
||||
_, err = dao.Books.Ctx(ctx).TX(tx).
|
||||
_, err = dao.Books.Ctx(ctx).
|
||||
Where(dao.Books.Columns().Id, in.BookId).
|
||||
Data(do.Books{
|
||||
Rating: avgRating,
|
||||
Rating: result.AvgRating,
|
||||
}).Update()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("book_rating_update_failed")
|
||||
@ -397,7 +426,7 @@ func (s *sBook) AppDetail(ctx context.Context, in *model.BookAppDetailIn) (out *
|
||||
m = m.Where(dao.Books.Columns().Id, in.Id)
|
||||
|
||||
// 执行查询
|
||||
if err = m.Scan(out); err != nil {
|
||||
if err = m.WithAll().Scan(out); err != nil {
|
||||
return nil, ecode.Fail.Sub("book_query_failed")
|
||||
}
|
||||
|
||||
@ -408,6 +437,29 @@ func (s *sBook) AppDetail(ctx context.Context, in *model.BookAppDetailIn) (out *
|
||||
|
||||
// 如果用户已登录,查询阅读进度
|
||||
if in.UserId > 0 {
|
||||
// 查询用户是否已将本书加入书架
|
||||
count, err := dao.Bookshelves.Ctx(ctx).
|
||||
Where(dao.Bookshelves.Columns().UserId, in.UserId).
|
||||
Where(dao.Bookshelves.Columns().BookId, in.Id).
|
||||
Count()
|
||||
if err == nil {
|
||||
out.IsInBookshelf = count > 0
|
||||
}
|
||||
|
||||
// 查询用户是否已评分
|
||||
var ratingRecord struct {
|
||||
Score float64 `json:"score"`
|
||||
}
|
||||
err = dao.BookRatings.Ctx(ctx).
|
||||
Fields("score").
|
||||
Where(dao.BookRatings.Columns().UserId, in.UserId).
|
||||
Where(dao.BookRatings.Columns().BookId, in.Id).
|
||||
Scan(&ratingRecord)
|
||||
if err == nil && ratingRecord.Score > 0 {
|
||||
out.HasRated = true
|
||||
out.MyRating = ratingRecord.Score
|
||||
}
|
||||
|
||||
// 查询用户对该书籍的历史记录
|
||||
var historyRecord struct {
|
||||
ChapterId int64 `json:"chapterId"`
|
||||
@ -435,7 +487,7 @@ func (s *sBook) AppDetail(ctx context.Context, in *model.BookAppDetailIn) (out *
|
||||
readChapters, err := dao.UserReadRecords.Ctx(ctx).
|
||||
Where(dao.UserReadRecords.Columns().UserId, in.UserId).
|
||||
Where(dao.UserReadRecords.Columns().BookId, in.Id).
|
||||
Where(dao.UserReadRecords.Columns().ChapterId, ">", 0). // 只统计有章节ID的记录
|
||||
WhereGT(dao.UserReadRecords.Columns().ChapterId, 0). // 只统计有章节ID的记录
|
||||
Count()
|
||||
|
||||
if err == nil {
|
||||
@ -454,274 +506,174 @@ func (s *sBook) AppDetail(ctx context.Context, in *model.BookAppDetailIn) (out *
|
||||
func (s *sBook) MyList(ctx context.Context, in *model.MyBookListIn) (out *model.MyBookListOut, err error) {
|
||||
out = &model.MyBookListOut{}
|
||||
|
||||
// 验证用户ID
|
||||
if in.UserId == 0 {
|
||||
return nil, ecode.Fail.Sub("user_id_required")
|
||||
}
|
||||
|
||||
// 验证类型参数
|
||||
if in.Type < 1 || in.Type > 3 {
|
||||
return nil, ecode.Fail.Sub("type_invalid")
|
||||
}
|
||||
|
||||
var list []model.MyBookItem
|
||||
var total int
|
||||
var (
|
||||
ids []int64
|
||||
extraMap map[int64]struct {
|
||||
Progress int
|
||||
LastReadAt string
|
||||
}
|
||||
total int
|
||||
)
|
||||
|
||||
switch in.Type {
|
||||
case 1: // 正在读
|
||||
// 查询书架表中read_status=1的记录
|
||||
case 1, 2:
|
||||
var bookshelves []struct {
|
||||
BookId int64 `json:"bookId"`
|
||||
LastReadPercent float64 `json:"lastReadPercent"`
|
||||
LastReadAt string `json:"lastReadAt"`
|
||||
}
|
||||
|
||||
if in.Sort != "" {
|
||||
err = dao.Bookshelves.Ctx(ctx).
|
||||
Fields("book_id, last_read_percent, last_read_at").
|
||||
Where(dao.Bookshelves.Columns().UserId, in.UserId).
|
||||
Where(dao.Bookshelves.Columns().ReadStatus, 1).
|
||||
Order(in.Sort).
|
||||
Page(in.Page, in.Size).
|
||||
ScanAndCount(&bookshelves, &total, false)
|
||||
} else {
|
||||
err = dao.Bookshelves.Ctx(ctx).
|
||||
Fields("book_id, last_read_percent, last_read_at").
|
||||
Where(dao.Bookshelves.Columns().UserId, in.UserId).
|
||||
Where(dao.Bookshelves.Columns().ReadStatus, 1).
|
||||
Page(in.Page, in.Size).
|
||||
ScanAndCount(&bookshelves, &total, false)
|
||||
readStatus := 1
|
||||
if in.Type == 2 {
|
||||
readStatus = 2
|
||||
}
|
||||
q := dao.Bookshelves.Ctx(ctx).
|
||||
Fields("book_id, last_read_percent, last_read_at").
|
||||
Where(dao.Bookshelves.Columns().UserId, in.UserId).
|
||||
Where(dao.Bookshelves.Columns().ReadStatus, readStatus)
|
||||
if in.Sort != "" {
|
||||
q = q.Order(in.Sort)
|
||||
} else {
|
||||
q = q
|
||||
}
|
||||
err = q.Page(in.Page, in.Size).ScanAndCount(&bookshelves, &total, false)
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("bookshelf_query_failed")
|
||||
}
|
||||
|
||||
// 获取书籍详细信息
|
||||
if len(bookshelves) > 0 {
|
||||
bookIds := make([]int64, 0, len(bookshelves))
|
||||
for _, bs := range bookshelves {
|
||||
bookIds = append(bookIds, bs.BookId)
|
||||
}
|
||||
|
||||
var books []model.Book
|
||||
err = dao.Books.Ctx(ctx).
|
||||
WhereIn(dao.Books.Columns().Id, bookIds).
|
||||
Scan(&books)
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_query_failed")
|
||||
}
|
||||
|
||||
// 构建书籍ID到书架信息的映射
|
||||
bookshelfMap := make(map[int64]struct {
|
||||
LastReadPercent float64
|
||||
LastReadAt string
|
||||
})
|
||||
for _, bs := range bookshelves {
|
||||
bookshelfMap[bs.BookId] = struct {
|
||||
LastReadPercent float64
|
||||
LastReadAt string
|
||||
}{
|
||||
LastReadPercent: bs.LastReadPercent,
|
||||
LastReadAt: bs.LastReadAt,
|
||||
}
|
||||
}
|
||||
|
||||
// 构建返回列表
|
||||
for _, book := range books {
|
||||
bsInfo := bookshelfMap[book.Id]
|
||||
list = append(list, model.MyBookItem{
|
||||
Id: book.Id,
|
||||
Title: book.Title,
|
||||
CoverUrl: book.CoverUrl,
|
||||
Description: book.Description,
|
||||
Progress: int(bsInfo.LastReadPercent),
|
||||
IsInShelf: true,
|
||||
LastReadAt: bsInfo.LastReadAt,
|
||||
Status: book.Status,
|
||||
AuthorId: book.AuthorId,
|
||||
CategoryId: book.CategoryId,
|
||||
})
|
||||
ids = make([]int64, 0, len(bookshelves))
|
||||
extraMap = make(map[int64]struct {
|
||||
Progress int
|
||||
LastReadAt string
|
||||
}, len(bookshelves))
|
||||
for _, bs := range bookshelves {
|
||||
ids = append(ids, bs.BookId)
|
||||
extraMap[bs.BookId] = struct {
|
||||
Progress int
|
||||
LastReadAt string
|
||||
}{
|
||||
Progress: int(bs.LastReadPercent),
|
||||
LastReadAt: bs.LastReadAt,
|
||||
}
|
||||
}
|
||||
|
||||
case 2: // 已读完
|
||||
// 查询书架表中read_status=2的记录
|
||||
var bookshelves []struct {
|
||||
BookId int64 `json:"bookId"`
|
||||
LastReadPercent float64 `json:"lastReadPercent"`
|
||||
LastReadAt string `json:"lastReadAt"`
|
||||
}
|
||||
|
||||
if in.Sort != "" {
|
||||
err = dao.Bookshelves.Ctx(ctx).
|
||||
Fields("book_id, last_read_percent, last_read_at").
|
||||
Where(dao.Bookshelves.Columns().UserId, in.UserId).
|
||||
Where(dao.Bookshelves.Columns().ReadStatus, 2).
|
||||
Order(in.Sort).
|
||||
Page(in.Page, in.Size).
|
||||
ScanAndCount(&bookshelves, &total, false)
|
||||
} else {
|
||||
err = dao.Bookshelves.Ctx(ctx).
|
||||
Fields("book_id, last_read_percent, last_read_at").
|
||||
Where(dao.Bookshelves.Columns().UserId, in.UserId).
|
||||
Where(dao.Bookshelves.Columns().ReadStatus, 2).
|
||||
Page(in.Page, in.Size).
|
||||
ScanAndCount(&bookshelves, &total, false)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("bookshelf_query_failed")
|
||||
}
|
||||
|
||||
// 获取书籍详细信息
|
||||
if len(bookshelves) > 0 {
|
||||
bookIds := make([]int64, 0, len(bookshelves))
|
||||
for _, bs := range bookshelves {
|
||||
bookIds = append(bookIds, bs.BookId)
|
||||
}
|
||||
|
||||
var books []model.Book
|
||||
err = dao.Books.Ctx(ctx).
|
||||
WhereIn(dao.Books.Columns().Id, bookIds).
|
||||
Scan(&books)
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_query_failed")
|
||||
}
|
||||
|
||||
// 构建书籍ID到书架信息的映射
|
||||
bookshelfMap := make(map[int64]struct {
|
||||
LastReadPercent float64
|
||||
LastReadAt string
|
||||
})
|
||||
for _, bs := range bookshelves {
|
||||
bookshelfMap[bs.BookId] = struct {
|
||||
LastReadPercent float64
|
||||
LastReadAt string
|
||||
}{
|
||||
LastReadPercent: bs.LastReadPercent,
|
||||
LastReadAt: bs.LastReadAt,
|
||||
}
|
||||
}
|
||||
|
||||
// 构建返回列表
|
||||
for _, book := range books {
|
||||
bsInfo := bookshelfMap[book.Id]
|
||||
list = append(list, model.MyBookItem{
|
||||
Id: book.Id,
|
||||
Title: book.Title,
|
||||
CoverUrl: book.CoverUrl,
|
||||
Description: book.Description,
|
||||
Progress: int(bsInfo.LastReadPercent),
|
||||
IsInShelf: true,
|
||||
LastReadAt: bsInfo.LastReadAt,
|
||||
Status: book.Status,
|
||||
AuthorId: book.AuthorId,
|
||||
CategoryId: book.CategoryId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
case 3: // 历史记录
|
||||
// 查询user_read_history表
|
||||
case 3:
|
||||
var histories []struct {
|
||||
BookId int64 `json:"bookId"`
|
||||
ChapterId int64 `json:"chapterId"`
|
||||
ReadAt string `json:"readAt"`
|
||||
}
|
||||
|
||||
q := dao.UserReadHistory.Ctx(ctx).
|
||||
Fields("book_id, chapter_id, read_at").
|
||||
Where(dao.UserReadHistory.Columns().UserId, in.UserId)
|
||||
if in.Sort != "" {
|
||||
err = dao.UserReadHistory.Ctx(ctx).
|
||||
Fields("book_id, chapter_id, read_at").
|
||||
Where(dao.UserReadHistory.Columns().UserId, in.UserId).
|
||||
Order(in.Sort).
|
||||
Page(in.Page, in.Size).
|
||||
ScanAndCount(&histories, &total, false)
|
||||
q = q.Order(in.Sort)
|
||||
} else {
|
||||
err = dao.UserReadHistory.Ctx(ctx).
|
||||
Fields("book_id, chapter_id, read_at").
|
||||
Where(dao.UserReadHistory.Columns().UserId, in.UserId).
|
||||
OrderDesc(dao.UserReadHistory.Columns().ReadAt).
|
||||
Page(in.Page, in.Size).
|
||||
ScanAndCount(&histories, &total, false)
|
||||
q = q.OrderDesc(dao.UserReadHistory.Columns().ReadAt)
|
||||
}
|
||||
err = q.Page(in.Page, in.Size).ScanAndCount(&histories, &total, false)
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("history_query_failed")
|
||||
}
|
||||
|
||||
// 获取书籍详细信息
|
||||
if len(histories) > 0 {
|
||||
bookIds := make([]int64, 0, len(histories))
|
||||
for _, history := range histories {
|
||||
bookIds = append(bookIds, history.BookId)
|
||||
}
|
||||
|
||||
var books []model.Book
|
||||
err = dao.Books.Ctx(ctx).
|
||||
WhereIn(dao.Books.Columns().Id, bookIds).
|
||||
Scan(&books)
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_query_failed")
|
||||
}
|
||||
|
||||
// 构建书籍ID到历史信息的映射
|
||||
historyMap := make(map[int64]struct {
|
||||
ChapterId int64
|
||||
ReadAt string
|
||||
})
|
||||
for _, history := range histories {
|
||||
historyMap[history.BookId] = struct {
|
||||
ChapterId int64
|
||||
ReadAt string
|
||||
}{
|
||||
ChapterId: history.ChapterId,
|
||||
ReadAt: history.ReadAt,
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否在书架中
|
||||
var bookshelves []struct {
|
||||
BookId int64 `json:"bookId"`
|
||||
}
|
||||
err = dao.Bookshelves.Ctx(ctx).
|
||||
Fields("book_id").
|
||||
Where(dao.Bookshelves.Columns().UserId, in.UserId).
|
||||
WhereIn(dao.Bookshelves.Columns().BookId, bookIds).
|
||||
Scan(&bookshelves)
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("bookshelf_query_failed")
|
||||
}
|
||||
|
||||
// 构建书架ID集合
|
||||
shelfBookIds := make(map[int64]bool)
|
||||
for _, bs := range bookshelves {
|
||||
shelfBookIds[bs.BookId] = true
|
||||
}
|
||||
|
||||
// 构建返回列表
|
||||
for _, book := range books {
|
||||
historyInfo := historyMap[book.Id]
|
||||
list = append(list, model.MyBookItem{
|
||||
Id: book.Id,
|
||||
Title: book.Title,
|
||||
CoverUrl: book.CoverUrl,
|
||||
Description: book.Description,
|
||||
Progress: 0, // 历史记录不显示进度
|
||||
IsInShelf: shelfBookIds[book.Id],
|
||||
LastReadAt: historyInfo.ReadAt,
|
||||
Status: book.Status,
|
||||
AuthorId: book.AuthorId,
|
||||
CategoryId: book.CategoryId,
|
||||
})
|
||||
ids = make([]int64, 0, len(histories))
|
||||
extraMap = make(map[int64]struct {
|
||||
Progress int
|
||||
LastReadAt string
|
||||
}, len(histories))
|
||||
for _, h := range histories {
|
||||
ids = append(ids, h.BookId)
|
||||
extraMap[h.BookId] = struct {
|
||||
Progress int
|
||||
LastReadAt string
|
||||
}{
|
||||
Progress: 0,
|
||||
LastReadAt: h.ReadAt,
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
// 返回空列表
|
||||
}
|
||||
|
||||
out = &model.MyBookListOut{
|
||||
Total: total,
|
||||
List: list,
|
||||
var books []model.MyBookItem
|
||||
if len(ids) > 0 {
|
||||
err = dao.Books.Ctx(ctx).WhereIn(dao.Books.Columns().Id, ids).Scan(&books)
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_query_failed")
|
||||
}
|
||||
}
|
||||
|
||||
// type3 需要查书架
|
||||
shelfBookIds := map[int64]bool{}
|
||||
if in.Type == 3 && len(ids) > 0 {
|
||||
var bookshelves []struct{ BookId int64 }
|
||||
err = dao.Bookshelves.Ctx(ctx).
|
||||
Fields("book_id").
|
||||
Where(dao.Bookshelves.Columns().UserId, in.UserId).
|
||||
WhereIn(dao.Bookshelves.Columns().BookId, ids).
|
||||
Scan(&bookshelves)
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("bookshelf_query_failed")
|
||||
}
|
||||
for _, bs := range bookshelves {
|
||||
shelfBookIds[bs.BookId] = true
|
||||
}
|
||||
}
|
||||
|
||||
for i := range books {
|
||||
if info, ok := extraMap[books[i].Id]; ok {
|
||||
books[i].Progress = info.Progress
|
||||
books[i].LastReadAt = info.LastReadAt
|
||||
}
|
||||
if in.Type == 1 || in.Type == 2 {
|
||||
books[i].IsInShelf = true
|
||||
} else if in.Type == 3 {
|
||||
books[i].IsInShelf = shelfBookIds[books[i].Id]
|
||||
}
|
||||
}
|
||||
|
||||
// 查询用户评分信息
|
||||
if len(books) > 0 {
|
||||
bookIds := make([]int64, 0, len(books))
|
||||
for _, book := range books {
|
||||
bookIds = append(bookIds, book.Id)
|
||||
}
|
||||
|
||||
type ratingRow struct {
|
||||
BookId int64 `json:"bookId"`
|
||||
Score float64 `json:"score"`
|
||||
}
|
||||
ratings := make([]ratingRow, 0)
|
||||
err = dao.BookRatings.Ctx(ctx).
|
||||
Fields("book_id, score").
|
||||
Where(dao.BookRatings.Columns().UserId, in.UserId).
|
||||
WhereIn(dao.BookRatings.Columns().BookId, bookIds).
|
||||
Scan(&ratings)
|
||||
if err == nil && len(ratings) > 0 {
|
||||
ratingMap := make(map[int64]float64, len(ratings))
|
||||
for _, r := range ratings {
|
||||
ratingMap[r.BookId] = r.Score
|
||||
}
|
||||
for i := range books {
|
||||
if score, ok := ratingMap[books[i].Id]; ok {
|
||||
books[i].HasRated = true
|
||||
books[i].MyRating = score
|
||||
} else {
|
||||
books[i].HasRated = false
|
||||
books[i].MyRating = 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := range books {
|
||||
books[i].HasRated = false
|
||||
books[i].MyRating = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.Total = total
|
||||
out.List = books
|
||||
return
|
||||
}
|
||||
|
||||
@ -762,3 +714,22 @@ func (s *sBook) SetRecommended(ctx context.Context, in *model.BookSetRecommended
|
||||
}
|
||||
return &model.BookCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
// SetHot: 单独修改书籍的热门状态
|
||||
func (s *sBook) SetHot(ctx context.Context, in *model.BookSetHotIn) (out *model.BookCRUDOut, err error) {
|
||||
// 检查书籍是否存在
|
||||
exist, err := dao.Books.Ctx(ctx).WherePri(in.Id).Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_query_failed")
|
||||
}
|
||||
if !exist {
|
||||
return nil, ecode.NotFound.Sub("book_not_found")
|
||||
}
|
||||
_, err = dao.Books.Ctx(ctx).WherePri(in.Id).Data(do.Books{
|
||||
IsHot: in.IsHot,
|
||||
}).Update()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_update_failed")
|
||||
}
|
||||
return &model.BookCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
226
internal/logic/book_recommendations/book_recommendations.go
Normal file
226
internal/logic/book_recommendations/book_recommendations.go
Normal file
@ -0,0 +1,226 @@
|
||||
package book_recommendations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"server/internal/dao"
|
||||
"server/internal/model"
|
||||
"server/internal/model/do"
|
||||
"server/internal/service"
|
||||
"server/utility/ecode"
|
||||
"server/utility/oss"
|
||||
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
type sBookRecommendations struct{}
|
||||
|
||||
func New() service.IBookRecommendations {
|
||||
return &sBookRecommendations{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterBookRecommendations(New())
|
||||
}
|
||||
|
||||
// List 获取推荐列表
|
||||
func (s *sBookRecommendations) List(ctx context.Context, in *model.BookRecommendationsListIn) (out *model.BookRecommendationsListOut, err error) {
|
||||
out = &model.BookRecommendationsListOut{}
|
||||
m := dao.BookRecommendations.Ctx(ctx)
|
||||
if in.Type != 0 {
|
||||
m = m.Where(dao.BookRecommendations.Columns().Type, in.Type)
|
||||
}
|
||||
if in.Status != 0 {
|
||||
m = m.Where(dao.BookRecommendations.Columns().Status, in.Status)
|
||||
}
|
||||
if in.BookId != 0 {
|
||||
m = m.Where(dao.BookRecommendations.Columns().BookId, in.BookId)
|
||||
}
|
||||
m = m.Order(dao.BookRecommendations.Columns().SortOrder)
|
||||
if err = m.Page(in.Page, in.Size).WithAll().ScanAndCount(&out.List, &out.Total, false); err != nil {
|
||||
return nil, ecode.Fail.Sub("book_recommendation_query_failed")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AppList 获取APP端推荐列表
|
||||
func (s *sBookRecommendations) AppList(ctx context.Context, in *model.BookRecommendationsListIn) (out *model.BookRecommendationsAppListOut, err error) {
|
||||
out = &model.BookRecommendationsAppListOut{}
|
||||
m := dao.BookRecommendations.Ctx(ctx)
|
||||
// 直接筛选状态为1的记录
|
||||
m = m.Where(dao.BookRecommendations.Columns().Status, 1)
|
||||
// 根据排序字段排序
|
||||
m = m.Order(dao.BookRecommendations.Columns().SortOrder)
|
||||
var list []model.RecommendAppItem
|
||||
if err = m.Page(in.Page, in.Size).ScanAndCount(&list, &out.Total, false); err != nil {
|
||||
return nil, ecode.Fail.Sub("book_recommendation_query_failed")
|
||||
}
|
||||
out.List = list
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Create 新增推荐
|
||||
func (s *sBookRecommendations) Create(ctx context.Context, in *model.BookRecommendationsCreateIn) (out *model.BookRecommendationsCRUDOut, err error) {
|
||||
// 检查同类型同书籍是否已存在(未软删除)
|
||||
exist, err := dao.BookRecommendations.Ctx(ctx).
|
||||
Where(dao.BookRecommendations.Columns().BookId, in.BookId).
|
||||
Where(dao.BookRecommendations.Columns().Type, in.Type).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_recommendation_query_failed")
|
||||
}
|
||||
if exist {
|
||||
return nil, ecode.Params.Sub("book_recommendation_exists")
|
||||
}
|
||||
// 插入数据
|
||||
_, err = dao.BookRecommendations.Ctx(ctx).Data(do.BookRecommendations{
|
||||
BookId: in.BookId,
|
||||
Type: in.Type,
|
||||
CoverUrl: in.CoverUrl,
|
||||
SortOrder: in.SortOrder,
|
||||
Status: in.Status,
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_recommendation_create_failed")
|
||||
}
|
||||
return &model.BookRecommendationsCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
// Update 编辑推荐
|
||||
func (s *sBookRecommendations) Update(ctx context.Context, in *model.BookRecommendationsUpdateIn) (out *model.BookRecommendationsCRUDOut, err error) {
|
||||
// 检查是否存在
|
||||
exist, err := dao.BookRecommendations.Ctx(ctx).
|
||||
Where(dao.BookRecommendations.Columns().Id, in.Id).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_recommendation_query_failed")
|
||||
}
|
||||
if !exist {
|
||||
return nil, ecode.NotFound.Sub("book_recommendation_not_found")
|
||||
}
|
||||
// 检查同类型同书籍是否有重复(排除自己)
|
||||
repeat, err := dao.BookRecommendations.Ctx(ctx).
|
||||
Where(dao.BookRecommendations.Columns().BookId, in.BookId).
|
||||
Where(dao.BookRecommendations.Columns().Type, in.Type).
|
||||
WhereNot(dao.BookRecommendations.Columns().Id, in.Id).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_recommendation_query_failed")
|
||||
}
|
||||
if repeat {
|
||||
return nil, ecode.Params.Sub("book_recommendation_exists")
|
||||
}
|
||||
_, err = dao.BookRecommendations.Ctx(ctx).
|
||||
Where(dao.BookRecommendations.Columns().Id, in.Id).
|
||||
Data(do.BookRecommendations{
|
||||
BookId: in.BookId,
|
||||
Type: in.Type,
|
||||
CoverUrl: in.CoverUrl,
|
||||
SortOrder: in.SortOrder,
|
||||
Status: in.Status,
|
||||
}).Update()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_recommendation_update_failed")
|
||||
}
|
||||
return &model.BookRecommendationsCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
// Delete 删除推荐
|
||||
func (s *sBookRecommendations) Delete(ctx context.Context, in *model.BookRecommendationsDeleteIn) (out *model.BookRecommendationsCRUDOut, err error) {
|
||||
// 检查是否存在
|
||||
exist, err := dao.BookRecommendations.Ctx(ctx).
|
||||
Where(dao.BookRecommendations.Columns().Id, in.Id).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_recommendation_query_failed")
|
||||
}
|
||||
if !exist {
|
||||
return nil, ecode.NotFound.Sub("book_recommendation_not_found")
|
||||
}
|
||||
// 直接调用 Delete
|
||||
_, err = dao.BookRecommendations.Ctx(ctx).
|
||||
Where(dao.BookRecommendations.Columns().Id, in.Id).
|
||||
Delete()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_recommendation_delete_failed")
|
||||
}
|
||||
return &model.BookRecommendationsCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
// SetStatus 启用/禁用推荐
|
||||
func (s *sBookRecommendations) SetStatus(ctx context.Context, in *model.BookRecommendationsSetStatusIn) (out *model.BookRecommendationsCRUDOut, err error) {
|
||||
// 检查是否存在
|
||||
exist, err := dao.BookRecommendations.Ctx(ctx).
|
||||
Where(dao.BookRecommendations.Columns().Id, in.Id).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_recommendation_query_failed")
|
||||
}
|
||||
if !exist {
|
||||
return nil, ecode.NotFound.Sub("book_recommendation_not_found")
|
||||
}
|
||||
_, err = dao.BookRecommendations.Ctx(ctx).
|
||||
Where(dao.BookRecommendations.Columns().Id, in.Id).
|
||||
Data(do.BookRecommendations{
|
||||
Status: in.Status,
|
||||
}).Update()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_recommendation_update_failed")
|
||||
}
|
||||
return &model.BookRecommendationsCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
// SortOrder 设置排序
|
||||
func (s *sBookRecommendations) SortOrder(ctx context.Context, in *model.BookRecommendationsSortOrderIn) (out *model.BookRecommendationsCRUDOut, err error) {
|
||||
// 检查是否存在
|
||||
exist, err := dao.BookRecommendations.Ctx(ctx).
|
||||
Where(dao.BookRecommendations.Columns().Id, in.Id).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_recommendation_query_failed")
|
||||
}
|
||||
if !exist {
|
||||
return nil, ecode.NotFound.Sub("book_recommendation_not_found")
|
||||
}
|
||||
_, err = dao.BookRecommendations.Ctx(ctx).
|
||||
Where(dao.BookRecommendations.Columns().Id, in.Id).
|
||||
Data(do.BookRecommendations{
|
||||
SortOrder: in.SortOrder,
|
||||
}).Update()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("book_recommendation_update_failed")
|
||||
}
|
||||
return &model.BookRecommendationsCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
// UploadCover 上传推荐封面图
|
||||
func (s *sBookRecommendations) UploadCover(ctx context.Context, file *ghttp.UploadFile) (url string, err error) {
|
||||
if file == nil {
|
||||
return "", ecode.Params.Sub("image_file_required")
|
||||
}
|
||||
// 校验文件类型(只允许图片)
|
||||
contentType := file.Header.Get("Content-Type")
|
||||
if contentType == "" || contentType[:6] != "image/" {
|
||||
return "", ecode.Params.Sub("image_type_invalid")
|
||||
}
|
||||
allowedTypes := map[string]bool{
|
||||
"image/jpeg": true,
|
||||
"image/png": true,
|
||||
"image/gif": true,
|
||||
"image/webp": true,
|
||||
}
|
||||
if !allowedTypes[contentType] {
|
||||
return "", ecode.Params.Sub("image_format_invalid")
|
||||
}
|
||||
if file.Size > 1*1024*1024 {
|
||||
return "", ecode.Params.Sub("image_size_exceeded")
|
||||
}
|
||||
client := oss.GetOSSClient("amazon_s3")
|
||||
if client == nil {
|
||||
return "", ecode.Fail.Sub("server_error")
|
||||
}
|
||||
url, err = client.Upload(file, "recommend")
|
||||
if err != nil {
|
||||
return "", ecode.Fail.Sub("image_upload_failed")
|
||||
}
|
||||
return url, nil
|
||||
}
|
||||
@ -55,3 +55,4 @@ func (s *sBookshelve) Delete(ctx context.Context, in *model.BookshelveDelIn) (ou
|
||||
}
|
||||
return &model.BookshelveCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
|
||||
@ -180,13 +180,34 @@ func (s *sChapter) AppList(ctx context.Context, in *model.ChapterAppListIn) (out
|
||||
return nil, ecode.Fail.Sub("read_record_query_failed")
|
||||
}
|
||||
|
||||
// 查询购买记录
|
||||
purchaseRecords := make([]struct {
|
||||
ChapterId int64 `json:"chapterId"`
|
||||
}, 0)
|
||||
|
||||
err = dao.UserChapterPurchases.Ctx(ctx).
|
||||
Fields("chapter_id").
|
||||
Where(dao.UserChapterPurchases.Columns().UserId, in.UserId).
|
||||
Where(dao.UserChapterPurchases.Columns().BookId, in.BookId).
|
||||
WhereIn(dao.UserChapterPurchases.Columns().ChapterId, chapterIds).
|
||||
Scan(&purchaseRecords)
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("purchase_record_query_failed")
|
||||
}
|
||||
|
||||
// 构建阅读记录映射
|
||||
readMap := make(map[int64]*gtime.Time)
|
||||
for _, record := range readRecords {
|
||||
readMap[record.ChapterId] = record.ReadAt
|
||||
}
|
||||
|
||||
// 为每个章节设置阅读进度
|
||||
// 构建购买记录映射
|
||||
purchaseMap := make(map[int64]bool)
|
||||
for _, record := range purchaseRecords {
|
||||
purchaseMap[record.ChapterId] = true
|
||||
}
|
||||
|
||||
// 为每个章节设置阅读进度和购买状态
|
||||
for i := range out.List {
|
||||
if readAt, exists := readMap[out.List[i].Id]; exists {
|
||||
out.List[i].ReadAt = readAt
|
||||
@ -195,6 +216,19 @@ func (s *sChapter) AppList(ctx context.Context, in *model.ChapterAppListIn) (out
|
||||
out.List[i].ReadProgress = 0 // 未读
|
||||
out.List[i].ReadAt = nil
|
||||
}
|
||||
|
||||
// 设置购买状态
|
||||
if out.List[i].IsLocked == 0 {
|
||||
// 免费章节,直接设置为已购买
|
||||
out.List[i].IsPurchased = true
|
||||
} else {
|
||||
// 付费章节,根据购买记录设置
|
||||
if purchaseMap[out.List[i].Id] {
|
||||
out.List[i].IsPurchased = true
|
||||
} else {
|
||||
out.List[i].IsPurchased = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -473,7 +507,7 @@ func (s *sChapter) AppProgress(ctx context.Context, in *model.ChapterAppProgress
|
||||
readChapters, err = dao.UserReadRecords.Ctx(ctx).TX(tx).
|
||||
Where(dao.UserReadRecords.Columns().UserId, in.UserId).
|
||||
Where(dao.UserReadRecords.Columns().BookId, in.BookId).
|
||||
Where(dao.UserReadRecords.Columns().ChapterId, ">", 0).
|
||||
WhereGT(dao.UserReadRecords.Columns().ChapterId, 0).
|
||||
Count()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("read_chapter_count_failed")
|
||||
@ -525,3 +559,211 @@ func (s *sChapter) AppProgress(ctx context.Context, in *model.ChapterAppProgress
|
||||
out.Success = true
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AppBatchProgress uploads batch reading progress for app
|
||||
func (s *sChapter) AppBatchProgress(ctx context.Context, in *model.ChapterAppBatchProgressIn) (out *model.ChapterAppProgressOut, err error) {
|
||||
out = &model.ChapterAppProgressOut{}
|
||||
|
||||
// 必须指定用户ID
|
||||
if in.UserId == 0 {
|
||||
return nil, ecode.Fail.Sub("user_id_required")
|
||||
}
|
||||
|
||||
// 必须指定书籍ID
|
||||
if in.BookId == 0 {
|
||||
return nil, ecode.Fail.Sub("book_id_required")
|
||||
}
|
||||
|
||||
// 必须指定章节列表
|
||||
if len(in.Chapters) == 0 {
|
||||
return nil, ecode.Fail.Sub("chapters_required")
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
exist, err := dao.Users.Ctx(ctx).Where(dao.Users.Columns().Id, in.UserId).Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("user_query_failed")
|
||||
}
|
||||
if !exist {
|
||||
return nil, ecode.NotFound.Sub("user_not_found")
|
||||
}
|
||||
|
||||
// 验证所有章节进度
|
||||
for _, chapter := range in.Chapters {
|
||||
if chapter.ChapterId == 0 {
|
||||
return nil, ecode.Fail.Sub("chapter_id_required")
|
||||
}
|
||||
if chapter.Progress < 0 || chapter.Progress > 100 {
|
||||
return nil, ecode.Fail.Sub("progress_invalid")
|
||||
}
|
||||
}
|
||||
|
||||
// 检查所有章节是否存在
|
||||
chapterIds := make([]int64, 0)
|
||||
for _, chapter := range in.Chapters {
|
||||
chapterIds = append(chapterIds, chapter.ChapterId)
|
||||
}
|
||||
|
||||
exist, err = dao.Chapters.Ctx(ctx).
|
||||
WhereIn(dao.Chapters.Columns().Id, chapterIds).
|
||||
Where(dao.Chapters.Columns().BookId, in.BookId).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("chapter_query_failed")
|
||||
}
|
||||
if !exist {
|
||||
return nil, ecode.NotFound.Sub("chapter_not_found")
|
||||
}
|
||||
|
||||
// 开启事务处理
|
||||
if err := dao.UserReadRecords.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// 批量处理每个章节的进度
|
||||
for _, chapter := range in.Chapters {
|
||||
// 1. 更新或创建阅读记录
|
||||
exist, err := dao.UserReadRecords.Ctx(ctx).TX(tx).
|
||||
Where(dao.UserReadRecords.Columns().UserId, in.UserId).
|
||||
Where(dao.UserReadRecords.Columns().BookId, in.BookId).
|
||||
Where(dao.UserReadRecords.Columns().ChapterId, chapter.ChapterId).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("read_record_query_failed")
|
||||
}
|
||||
|
||||
if exist {
|
||||
// 更新现有记录
|
||||
_, err = dao.UserReadRecords.Ctx(ctx).TX(tx).
|
||||
Where(dao.UserReadRecords.Columns().UserId, in.UserId).
|
||||
Where(dao.UserReadRecords.Columns().BookId, in.BookId).
|
||||
Where(dao.UserReadRecords.Columns().ChapterId, chapter.ChapterId).
|
||||
Data(do.UserReadRecords{
|
||||
Progress: chapter.Progress,
|
||||
ReadAt: gtime.Now(),
|
||||
}).Update()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("read_record_update_failed")
|
||||
}
|
||||
} else {
|
||||
// 创建新记录
|
||||
_, err = dao.UserReadRecords.Ctx(ctx).TX(tx).Data(do.UserReadRecords{
|
||||
UserId: in.UserId,
|
||||
BookId: in.BookId,
|
||||
ChapterId: chapter.ChapterId,
|
||||
Progress: chapter.Progress,
|
||||
ReadAt: gtime.Now(),
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("read_record_create_failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 更新或创建历史记录(使用最后一个章节作为当前阅读章节)
|
||||
lastChapter := in.Chapters[len(in.Chapters)-1]
|
||||
exist, err = dao.UserReadHistory.Ctx(ctx).TX(tx).
|
||||
Where(dao.UserReadHistory.Columns().UserId, in.UserId).
|
||||
Where(dao.UserReadHistory.Columns().BookId, in.BookId).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("history_query_failed")
|
||||
}
|
||||
|
||||
if exist {
|
||||
// 更新现有历史记录
|
||||
_, err = dao.UserReadHistory.Ctx(ctx).TX(tx).
|
||||
Where(dao.UserReadHistory.Columns().UserId, in.UserId).
|
||||
Where(dao.UserReadHistory.Columns().BookId, in.BookId).
|
||||
Data(do.UserReadHistory{
|
||||
ChapterId: lastChapter.ChapterId,
|
||||
ReadAt: gtime.Now(),
|
||||
}).Update()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("history_update_failed")
|
||||
}
|
||||
} else {
|
||||
// 创建新历史记录
|
||||
_, err = dao.UserReadHistory.Ctx(ctx).TX(tx).Data(do.UserReadHistory{
|
||||
UserId: in.UserId,
|
||||
BookId: in.BookId,
|
||||
ChapterId: lastChapter.ChapterId,
|
||||
ReadAt: gtime.Now(),
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("history_create_failed")
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 更新书架记录(如果存在)
|
||||
exist, err = dao.Bookshelves.Ctx(ctx).TX(tx).
|
||||
Where(dao.Bookshelves.Columns().UserId, in.UserId).
|
||||
Where(dao.Bookshelves.Columns().BookId, in.BookId).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("bookshelf_query_failed")
|
||||
}
|
||||
|
||||
if exist {
|
||||
// 计算阅读进度百分比
|
||||
totalChapters, err := dao.Chapters.Ctx(ctx).TX(tx).
|
||||
Where(dao.Chapters.Columns().BookId, in.BookId).
|
||||
Count()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("chapter_count_failed")
|
||||
}
|
||||
|
||||
var readChapters int
|
||||
if totalChapters > 0 {
|
||||
readChapters, err = dao.UserReadRecords.Ctx(ctx).TX(tx).
|
||||
Where(dao.UserReadRecords.Columns().UserId, in.UserId).
|
||||
Where(dao.UserReadRecords.Columns().BookId, in.BookId).
|
||||
WhereGT(dao.UserReadRecords.Columns().ChapterId, 0).
|
||||
Count()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("read_chapter_count_failed")
|
||||
}
|
||||
}
|
||||
|
||||
readPercent := 0.0
|
||||
if totalChapters > 0 {
|
||||
readPercent = float64(readChapters) / float64(totalChapters) * 100
|
||||
if readPercent > 100 {
|
||||
readPercent = 100
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否为最后一章
|
||||
lastChapterInfo, err := dao.Chapters.Ctx(ctx).TX(tx).
|
||||
Where(dao.Chapters.Columns().BookId, in.BookId).
|
||||
OrderDesc(dao.Chapters.Columns().Sort).
|
||||
One()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("chapter_query_failed")
|
||||
}
|
||||
|
||||
readStatus := 1 // 默认为正在读
|
||||
if lastChapterInfo != nil && lastChapterInfo["id"].Int64() == lastChapter.ChapterId {
|
||||
readStatus = 2 // 如果是最后一章,标记为已读完
|
||||
}
|
||||
|
||||
// 更新书架记录
|
||||
_, err = dao.Bookshelves.Ctx(ctx).TX(tx).
|
||||
Where(dao.Bookshelves.Columns().UserId, in.UserId).
|
||||
Where(dao.Bookshelves.Columns().BookId, in.BookId).
|
||||
Data(do.Bookshelves{
|
||||
LastReadChapterId: lastChapter.ChapterId,
|
||||
LastReadPercent: readPercent,
|
||||
LastReadAt: gtime.Now(),
|
||||
ReadStatus: readStatus,
|
||||
}).Update()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("bookshelf_update_failed")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.Success = true
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@ -5,14 +5,22 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
_ "server/internal/logic/ad_event_logs"
|
||||
_ "server/internal/logic/admin"
|
||||
_ "server/internal/logic/author"
|
||||
_ "server/internal/logic/book"
|
||||
_ "server/internal/logic/book_recommendations"
|
||||
_ "server/internal/logic/bookshelve"
|
||||
_ "server/internal/logic/category"
|
||||
_ "server/internal/logic/chapter"
|
||||
_ "server/internal/logic/feedback"
|
||||
_ "server/internal/logic/sign_in_reward_details"
|
||||
_ "server/internal/logic/sign_in_reward_rules"
|
||||
_ "server/internal/logic/system"
|
||||
_ "server/internal/logic/task"
|
||||
_ "server/internal/logic/upload"
|
||||
_ "server/internal/logic/user"
|
||||
_ "server/internal/logic/user_follow_author"
|
||||
_ "server/internal/logic/user_read_record"
|
||||
_ "server/internal/logic/user_read_history"
|
||||
_ "server/internal/logic/user_sign_in_logs"
|
||||
)
|
||||
|
||||
@ -0,0 +1,93 @@
|
||||
package sign_in_reward_details
|
||||
|
||||
import (
|
||||
"context"
|
||||
"server/internal/dao"
|
||||
"server/internal/model"
|
||||
"server/internal/model/do"
|
||||
"server/internal/service"
|
||||
"server/utility/ecode"
|
||||
)
|
||||
|
||||
type sSignInRewardDetails struct{}
|
||||
|
||||
func init() {
|
||||
service.RegisterSignInRewardDetails(New())
|
||||
}
|
||||
|
||||
func New() *sSignInRewardDetails {
|
||||
return &sSignInRewardDetails{}
|
||||
}
|
||||
|
||||
// Create 新增签到奖励明细
|
||||
func (s *sSignInRewardDetails) Create(ctx context.Context, in *model.SignInRewardDetail) (int64, error) {
|
||||
id, err := dao.SignInRewardDetails.Ctx(ctx).Data(do.SignInRewardDetails{
|
||||
RuleId: in.RuleId,
|
||||
DayNumber: in.DayNumber,
|
||||
RewardType: in.RewardType,
|
||||
Quantity: in.Quantity,
|
||||
Status: in.Status,
|
||||
}).InsertAndGetId()
|
||||
if err != nil {
|
||||
return 0, ecode.New(10001, "sign_in_reward_detail_create_failed")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Update 编辑签到奖励明细
|
||||
func (s *sSignInRewardDetails) Update(ctx context.Context, in *model.SignInRewardDetail) error {
|
||||
_, err := dao.SignInRewardDetails.Ctx(ctx).Data(do.SignInRewardDetails{
|
||||
RuleId: in.RuleId,
|
||||
DayNumber: in.DayNumber,
|
||||
RewardType: in.RewardType,
|
||||
Quantity: in.Quantity,
|
||||
Status: in.Status,
|
||||
}).Where(do.SignInRewardDetails{Id: in.Id}).Update()
|
||||
if err != nil {
|
||||
return ecode.New(10002, "sign_in_reward_detail_update_failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除签到奖励明细
|
||||
func (s *sSignInRewardDetails) Delete(ctx context.Context, in *model.SignInRewardDetailDeleteIn) (*model.SignInRewardDetailDeleteOut, error) {
|
||||
_, err := dao.SignInRewardDetails.Ctx(ctx).Where(do.SignInRewardDetails{Id: in.Id}).Delete()
|
||||
if err != nil {
|
||||
return &model.SignInRewardDetailDeleteOut{Success: false}, ecode.New(10003, "sign_in_reward_detail_delete_failed")
|
||||
}
|
||||
return &model.SignInRewardDetailDeleteOut{Success: true}, nil
|
||||
}
|
||||
|
||||
// Get 查询单个签到奖励明细
|
||||
func (s *sSignInRewardDetails) Get(ctx context.Context, in *model.SignInRewardDetailGetIn) (*model.SignInRewardDetailGetOut, error) {
|
||||
var detail model.SignInRewardDetail
|
||||
err := dao.SignInRewardDetails.Ctx(ctx).Where(do.SignInRewardDetails{Id: in.Id}).Scan(&detail)
|
||||
if err != nil {
|
||||
return nil, ecode.New(10004, "sign_in_reward_detail_query_failed")
|
||||
}
|
||||
if detail.Id == 0 {
|
||||
return nil, ecode.New(10006, "sign_in_reward_detail_not_found")
|
||||
}
|
||||
return &model.SignInRewardDetailGetOut{SignInRewardDetail: detail}, nil
|
||||
}
|
||||
|
||||
// List 根据 ruleId 查询签到奖励明细列表
|
||||
func (s *sSignInRewardDetails) List(ctx context.Context, in *model.SignInRewardDetailListIn) (*model.SignInRewardDetailListOut, error) {
|
||||
var list []model.SignInRewardDetail
|
||||
err := dao.SignInRewardDetails.Ctx(ctx).Where(do.SignInRewardDetails{RuleId: in.RuleId}).Order("day_number asc").Scan(&list)
|
||||
if err != nil {
|
||||
return nil, ecode.New(10005, "sign_in_reward_detail_query_failed")
|
||||
}
|
||||
return &model.SignInRewardDetailListOut{List: list}, nil
|
||||
}
|
||||
|
||||
// SetStatus 设置签到奖励明细状态
|
||||
func (s *sSignInRewardDetails) SetStatus(ctx context.Context, in *model.SignInRewardDetailSetStatusIn) (*model.SignInRewardDetailSetStatusOut, error) {
|
||||
_, err := dao.SignInRewardDetails.Ctx(ctx).Data(do.SignInRewardDetails{
|
||||
Status: in.Status,
|
||||
}).Where(do.SignInRewardDetails{Id: in.Id}).Update()
|
||||
if err != nil {
|
||||
return &model.SignInRewardDetailSetStatusOut{Success: false}, ecode.New(10007, "sign_in_reward_detail_update_failed")
|
||||
}
|
||||
return &model.SignInRewardDetailSetStatusOut{Success: true}, nil
|
||||
}
|
||||
207
internal/logic/sign_in_reward_rules/sign_in_reward_rules.go
Normal file
207
internal/logic/sign_in_reward_rules/sign_in_reward_rules.go
Normal file
@ -0,0 +1,207 @@
|
||||
package sign_in_reward_rules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"server/internal/dao"
|
||||
"server/internal/model"
|
||||
"server/internal/model/do"
|
||||
"server/internal/service"
|
||||
"server/utility/ecode"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type sSignInRewardRules struct{}
|
||||
|
||||
func New() service.ISignInRewardRules {
|
||||
return &sSignInRewardRules{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSignInRewardRules(New())
|
||||
}
|
||||
|
||||
// List 获取签到奖励规则列表
|
||||
func (s *sSignInRewardRules) List(ctx context.Context, in *model.SignInRewardRulesListIn) (out *model.SignInRewardRulesListOut, err error) {
|
||||
out = &model.SignInRewardRulesListOut{}
|
||||
m := dao.SignInRewardRules.Ctx(ctx)
|
||||
if in.Status != 0 {
|
||||
m = m.Where(dao.SignInRewardRules.Columns().Status, in.Status)
|
||||
}
|
||||
if in.RuleName != "" {
|
||||
m = m.WhereLike(dao.SignInRewardRules.Columns().RuleName, "%"+in.RuleName+"%")
|
||||
}
|
||||
m = m.OrderDesc(dao.SignInRewardRules.Columns().CreatedAt)
|
||||
if err = m.Page(in.Page, in.Size).WithAll().ScanAndCount(&out.List, &out.Total, false); err != nil {
|
||||
return nil, ecode.Fail.Sub("sign_in_reward_rule_query_failed")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Create 新增签到奖励规则
|
||||
func (s *sSignInRewardRules) Create(ctx context.Context, in *model.SignInRewardRulesCreateIn) (out *model.SignInRewardRulesCRUDOut, err error) {
|
||||
// 检查同名未删除规则是否已存在
|
||||
exist, err := dao.SignInRewardRules.Ctx(ctx).
|
||||
Where(dao.SignInRewardRules.Columns().RuleName, in.RuleName).
|
||||
WhereNull(dao.SignInRewardRules.Columns().DeletedAt).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("sign_in_reward_rule_query_failed")
|
||||
}
|
||||
if exist {
|
||||
return nil, ecode.Params.Sub("sign_in_reward_rule_exists")
|
||||
}
|
||||
// 开启事务
|
||||
err = dao.SignInRewardRules.Ctx(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
result, err := dao.SignInRewardRules.Ctx(ctx).TX(tx).Data(do.SignInRewardRules{
|
||||
RuleName: in.RuleName,
|
||||
CycleDays: in.CycleDays,
|
||||
StartDate: gtime.NewFromStr(in.StartDate),
|
||||
EndDate: gtime.NewFromStr(in.EndDate),
|
||||
Status: in.Status,
|
||||
}).InsertAndGetId()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("sign_in_reward_rule_create_failed")
|
||||
}
|
||||
// 自动插入明细
|
||||
var details []do.SignInRewardDetails
|
||||
for i := 1; i <= in.CycleDays; i++ {
|
||||
details = append(details, do.SignInRewardDetails{
|
||||
RuleId: result,
|
||||
DayNumber: i,
|
||||
RewardType: 1, // 默认积分
|
||||
Quantity: 0,
|
||||
Status: 0, // 默认禁用
|
||||
})
|
||||
}
|
||||
if len(details) > 0 {
|
||||
_, err = dao.SignInRewardDetails.Ctx(ctx).TX(tx).Data(details).Insert()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("sign_in_reward_rule_create_failed")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.SignInRewardRulesCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
// Update 编辑签到奖励规则
|
||||
func (s *sSignInRewardRules) Update(ctx context.Context, in *model.SignInRewardRulesUpdateIn) (out *model.SignInRewardRulesCRUDOut, err error) {
|
||||
// 检查是否存在
|
||||
exist, err := dao.SignInRewardRules.Ctx(ctx).
|
||||
Where(dao.SignInRewardRules.Columns().Id, in.Id).
|
||||
WhereNull(dao.SignInRewardRules.Columns().DeletedAt).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("sign_in_reward_rule_query_failed")
|
||||
}
|
||||
if !exist {
|
||||
return nil, ecode.NotFound.Sub("sign_in_reward_rule_not_found")
|
||||
}
|
||||
// 检查同名未删除规则是否有重复(排除自己)
|
||||
repeat, err := dao.SignInRewardRules.Ctx(ctx).
|
||||
Where(dao.SignInRewardRules.Columns().RuleName, in.RuleName).
|
||||
WhereNull(dao.SignInRewardRules.Columns().DeletedAt).
|
||||
WhereNot(dao.SignInRewardRules.Columns().Id, in.Id).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("sign_in_reward_rule_query_failed")
|
||||
}
|
||||
if repeat {
|
||||
return nil, ecode.Params.Sub("sign_in_reward_rule_exists")
|
||||
}
|
||||
_, err = dao.SignInRewardRules.Ctx(ctx).
|
||||
Where(dao.SignInRewardRules.Columns().Id, in.Id).
|
||||
Data(do.SignInRewardRules{
|
||||
RuleName: in.RuleName,
|
||||
CycleDays: in.CycleDays,
|
||||
StartDate: gtime.NewFromStr(in.StartDate),
|
||||
EndDate: gtime.NewFromStr(in.EndDate),
|
||||
Status: in.Status,
|
||||
}).Update()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("sign_in_reward_rule_update_failed")
|
||||
}
|
||||
return &model.SignInRewardRulesCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
// Delete 删除签到奖励规则
|
||||
func (s *sSignInRewardRules) Delete(ctx context.Context, in *model.SignInRewardRulesDeleteIn) (out *model.SignInRewardRulesCRUDOut, err error) {
|
||||
// 检查是否存在
|
||||
exist, err := dao.SignInRewardRules.Ctx(ctx).
|
||||
Where(dao.SignInRewardRules.Columns().Id, in.Id).
|
||||
WhereNull(dao.SignInRewardRules.Columns().DeletedAt).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("sign_in_reward_rule_query_failed")
|
||||
}
|
||||
if !exist {
|
||||
return nil, ecode.NotFound.Sub("sign_in_reward_rule_not_found")
|
||||
}
|
||||
_, err = dao.SignInRewardRules.Ctx(ctx).
|
||||
Where(dao.SignInRewardRules.Columns().Id, in.Id).
|
||||
Delete()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("sign_in_reward_rule_delete_failed")
|
||||
}
|
||||
return &model.SignInRewardRulesCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
// SetStatus 设置签到奖励规则状态
|
||||
func (s *sSignInRewardRules) SetStatus(ctx context.Context, in *model.SignInRewardRulesSetStatusIn) (out *model.SignInRewardRulesCRUDOut, err error) {
|
||||
// 检查是否存在
|
||||
exist, err := dao.SignInRewardRules.Ctx(ctx).
|
||||
Where(dao.SignInRewardRules.Columns().Id, in.Id).
|
||||
WhereNull(dao.SignInRewardRules.Columns().DeletedAt).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("sign_in_reward_rule_query_failed")
|
||||
}
|
||||
if !exist {
|
||||
return nil, ecode.NotFound.Sub("sign_in_reward_rule_not_found")
|
||||
}
|
||||
_, err = dao.SignInRewardRules.Ctx(ctx).
|
||||
Where(dao.SignInRewardRules.Columns().Id, in.Id).
|
||||
Data(do.SignInRewardRules{
|
||||
Status: in.Status,
|
||||
}).Update()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("sign_in_reward_rule_update_failed")
|
||||
}
|
||||
return &model.SignInRewardRulesCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
func (s *sSignInRewardRules) SignInList(ctx context.Context, in *model.SignInListIn) (out *model.SignInListOut, err error) {
|
||||
// 查询活动(只取一条)
|
||||
out = &model.SignInListOut{}
|
||||
err = dao.SignInRewardRules.Ctx(ctx).Where(do.SignInRewardRules{Status: 1}).OrderDesc("created_at").WithAll().Limit(1).Scan(&out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询用户在该活动下的签到记录
|
||||
if in.UserId == 0 {
|
||||
return
|
||||
}
|
||||
userSignLogs, err := dao.UserSignInLogs.Ctx(ctx).Where(do.UserSignInLogs{UserId: in.UserId, RuleId: out.Id}).Fields(dao.UserSignInLogs.Columns().RewardDetailId).Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claimedMap := make(map[int64]bool, 0)
|
||||
for _, log := range userSignLogs {
|
||||
claimedMap[log.Int64()] = true
|
||||
}
|
||||
|
||||
// 填充 IsClaimed
|
||||
for i := range out.List {
|
||||
if claimedMap[out.List[i].Id] {
|
||||
out.List[i].IsClaimed = true
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
50
internal/logic/system/system.go
Normal file
50
internal/logic/system/system.go
Normal file
@ -0,0 +1,50 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"server/internal/dao"
|
||||
"server/internal/model"
|
||||
"server/internal/service"
|
||||
)
|
||||
|
||||
type sSystem struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSystem(New())
|
||||
}
|
||||
func New() service.ISystem {
|
||||
return &sSystem{}
|
||||
}
|
||||
func (*sSystem) Unique(ctx context.Context, in *model.SystemUniqueInput) (*model.SystemOutput, error) {
|
||||
orm := dao.System.Ctx(ctx)
|
||||
if in.Key != "" {
|
||||
orm = orm.Where("`key`=?", in.Key)
|
||||
}
|
||||
if in.Lock {
|
||||
orm = orm.LockUpdate()
|
||||
}
|
||||
out := (*model.SystemOutput)(nil)
|
||||
err := orm.Scan(&out)
|
||||
return out, err
|
||||
}
|
||||
func (s *sSystem) Version(ctx context.Context) (*model.SystemVersionOutput, error) {
|
||||
system, err := s.Unique(ctx, &model.SystemUniqueInput{
|
||||
Key: "version",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out *model.SystemVersionOutput
|
||||
err = json.Unmarshal([]byte(system.Value), &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (s *sSystem) Save(ctx context.Context, in *model.SystemSaveInput) (err error) {
|
||||
_, err = dao.System.Ctx(ctx).Update(g.Map{
|
||||
"value": in.Value,
|
||||
}, g.Map{"key": in.Key})
|
||||
return
|
||||
}
|
||||
100
internal/logic/task/task.go
Normal file
100
internal/logic/task/task.go
Normal file
@ -0,0 +1,100 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"server/internal/dao"
|
||||
"server/internal/model"
|
||||
"server/internal/model/do"
|
||||
"server/internal/service"
|
||||
"server/utility/ecode"
|
||||
)
|
||||
|
||||
type sTask struct {
|
||||
}
|
||||
|
||||
func New() service.ITask {
|
||||
return &sTask{}
|
||||
}
|
||||
func init() {
|
||||
service.RegisterTask(New())
|
||||
}
|
||||
func (s *sTask) List(ctx context.Context, in *model.TaskListIn) (out *model.TaskListOut, err error) {
|
||||
out = &model.TaskListOut{}
|
||||
m := dao.Tasks.Ctx(ctx)
|
||||
if in.Title != "" {
|
||||
m = m.WhereLike("title", "%"+in.Title+"%")
|
||||
}
|
||||
if in.Status != 0 {
|
||||
m = m.Where("status", in.Status)
|
||||
}
|
||||
if err = m.Page(in.Page, in.Size).OrderDesc("id").ScanAndCount(&out.List, &out.Total, false); err != nil {
|
||||
return nil, ecode.Fail.Sub("task_query_failed")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *sTask) Add(ctx context.Context, in *model.TaskAddIn) (out *model.TaskCRUDOut, err error) {
|
||||
_, err = dao.Tasks.Ctx(ctx).Data(do.Tasks{
|
||||
TaskType: in.TaskType,
|
||||
Title: in.Title,
|
||||
Description: in.Description,
|
||||
RewardPoints: in.RewardPoints,
|
||||
Status: in.Status,
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("task_add_failed")
|
||||
}
|
||||
return &model.TaskCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
func (s *sTask) Edit(ctx context.Context, in *model.TaskEditIn) (out *model.TaskCRUDOut, err error) {
|
||||
exist, err := dao.Tasks.Ctx(ctx).WherePri(in.Id).Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("task_query_failed")
|
||||
}
|
||||
if !exist {
|
||||
return nil, ecode.NotFound.Sub("task_not_found")
|
||||
}
|
||||
_, err = dao.Tasks.Ctx(ctx).WherePri(in.Id).Data(do.Tasks{
|
||||
TaskType: in.TaskType,
|
||||
Title: in.Title,
|
||||
Description: in.Description,
|
||||
RewardPoints: in.RewardPoints,
|
||||
Status: in.Status,
|
||||
}).Update()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("task_edit_failed")
|
||||
}
|
||||
return &model.TaskCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
func (s *sTask) Delete(ctx context.Context, in *model.TaskDelIn) (out *model.TaskCRUDOut, err error) {
|
||||
exist, err := dao.Tasks.Ctx(ctx).WherePri(in.Id).Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("task_query_failed")
|
||||
}
|
||||
if !exist {
|
||||
return nil, ecode.NotFound.Sub("task_not_found")
|
||||
}
|
||||
_, err = dao.Tasks.Ctx(ctx).WherePri(in.Id).Delete()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("task_delete_failed")
|
||||
}
|
||||
return &model.TaskCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
// AppList 获取任务列表(只返回任务类型和奖励积分)
|
||||
func (s *sTask) AppList(ctx context.Context, in *model.TaskAppListIn) (out *model.TaskSimpleListOut, err error) {
|
||||
out = &model.TaskSimpleListOut{}
|
||||
|
||||
// 只查询任务类型和奖励积分字段
|
||||
if err = dao.Tasks.Ctx(ctx).
|
||||
Fields("task_type, reward_points").
|
||||
Where("status", 1).
|
||||
OrderDesc("id").
|
||||
Scan(&out.List); err != nil {
|
||||
return nil, ecode.Fail.Sub("task_query_failed")
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
66
internal/logic/upload/upload.go
Normal file
66
internal/logic/upload/upload.go
Normal file
@ -0,0 +1,66 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"server/internal/model"
|
||||
"server/internal/service"
|
||||
"server/utility/ecode"
|
||||
"server/utility/oss"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type sUpload struct{}
|
||||
|
||||
func New() service.IUpload {
|
||||
return &sUpload{}
|
||||
}
|
||||
func init() {
|
||||
service.RegisterUpload(New())
|
||||
}
|
||||
func (s *sUpload) UploadImage(ctx context.Context, in *model.UploadImageIn) (*model.UploadImageOut, error) {
|
||||
if in.File == nil {
|
||||
return nil, gerror.NewCode(ecode.Fail.Sub("image_file_required").Code())
|
||||
}
|
||||
// 校验文件类型(只允许图片)
|
||||
contentType := in.File.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
f, err := in.File.Open()
|
||||
if err != nil {
|
||||
return nil, gerror.NewCode(ecode.Fail.Sub("image_read_failed").Code())
|
||||
}
|
||||
defer f.Close()
|
||||
buf := make([]byte, 512)
|
||||
_, err = f.Read(buf)
|
||||
if err != nil {
|
||||
return nil, gerror.NewCode(ecode.Fail.Sub("image_read_failed").Code())
|
||||
}
|
||||
contentType = http.DetectContentType(buf)
|
||||
}
|
||||
if len(contentType) < 6 || contentType[:6] != "image/" {
|
||||
return nil, gerror.NewCode(ecode.Fail.Sub("image_type_invalid").Code())
|
||||
}
|
||||
allowedTypes := map[string]bool{
|
||||
"image/jpeg": true,
|
||||
"image/png": true,
|
||||
"image/gif": true,
|
||||
"image/webp": true,
|
||||
}
|
||||
if !allowedTypes[contentType] {
|
||||
return nil, gerror.NewCode(ecode.Fail.Sub("image_format_invalid").Code())
|
||||
}
|
||||
// 校验文件大小(不超过1MB)
|
||||
if in.File.Size > 1*1024*1024 {
|
||||
return nil, gerror.NewCode(ecode.Fail.Sub("image_size_exceeded").Code())
|
||||
}
|
||||
client := oss.GetOSSClient("amazon_s3")
|
||||
if client == nil {
|
||||
return nil, gerror.NewCode(ecode.Fail.Code())
|
||||
}
|
||||
url, err := client.Upload(in.File, in.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.UploadImageOut{ImageUrl: url}, nil
|
||||
}
|
||||
@ -90,12 +90,26 @@ func (s *sUser) Info(ctx context.Context, in *model.UserInfoIn) (out *model.User
|
||||
if err = user.Struct(&entityUser); err != nil {
|
||||
return nil, ecode.Fail.Sub("data_conversion_failed")
|
||||
}
|
||||
// 查询作者信息
|
||||
var authorStatus int
|
||||
isAuthor := false
|
||||
author, err := dao.Authors.Ctx(ctx).Where(do.Authors{UserId: entityUser.Id}).One()
|
||||
if err == nil && !author.IsEmpty() {
|
||||
authorStatus = author[dao.Authors.Columns().Status].Int()
|
||||
if authorStatus == 1 {
|
||||
isAuthor = true
|
||||
}
|
||||
}
|
||||
return &model.UserInfoOut{
|
||||
UserId: entityUser.Id,
|
||||
Username: entityUser.Username,
|
||||
Email: entityUser.Email,
|
||||
Avatar: entityUser.Avatar,
|
||||
Points: entityUser.Points, // 如有积分表可补充
|
||||
Id: entityUser.Id,
|
||||
Username: entityUser.Username,
|
||||
Email: entityUser.Email,
|
||||
Avatar: entityUser.Avatar,
|
||||
Points: entityUser.Points, // 如有积分表可补充
|
||||
BackgroundUrl: entityUser.BackgroundUrl,
|
||||
AttentionCount: entityUser.AttentionCount,
|
||||
IsAuthor: isAuthor,
|
||||
AuthorStatus: authorStatus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -246,3 +260,33 @@ func (s *sUser) EditPass(ctx context.Context, in *model.UserEditPassIn) (out *mo
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sUser) AuthorLogin(ctx context.Context, in *model.UserLoginIn) (out *model.UserLoginOut, err error) {
|
||||
user, err := dao.Users.Ctx(ctx).Where(do.Users{Email: in.Email}).One()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("database_query_failed")
|
||||
}
|
||||
if user == nil {
|
||||
return nil, ecode.Auth // 账户名或密码不正确
|
||||
}
|
||||
var entityUser entity.Users
|
||||
if err = user.Struct(&entityUser); err != nil {
|
||||
return nil, ecode.Fail.Sub("data_conversion_failed")
|
||||
}
|
||||
if !encrypt.ComparePassword(entityUser.PasswordHash, in.Password) {
|
||||
return nil, ecode.Password // 密码不正确
|
||||
}
|
||||
// 验证是否为作者
|
||||
author, err := dao.Authors.Ctx(ctx).Where(do.Authors{UserId: entityUser.Id, Status: 1}).One()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("author_query_failed")
|
||||
}
|
||||
if author == nil || author.IsEmpty() {
|
||||
return nil, ecode.Auth.Sub("not_author") // 不是作者
|
||||
}
|
||||
token, err := jwt.GenerateToken(&jwt.TokenIn{UserId: entityUser.Id, Role: "author"})
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("token_generation_failed")
|
||||
}
|
||||
return &model.UserLoginOut{Token: token}, nil
|
||||
}
|
||||
|
||||
@ -7,6 +7,8 @@ import (
|
||||
"server/internal/model/do"
|
||||
"server/internal/service"
|
||||
"server/utility/ecode"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
)
|
||||
|
||||
type sUserFollowAuthor struct{}
|
||||
@ -36,63 +38,82 @@ func (s *sUserFollowAuthor) List(ctx context.Context, in *model.UserFollowAuthor
|
||||
|
||||
// Create adds a new user follow author
|
||||
func (s *sUserFollowAuthor) Create(ctx context.Context, in *model.UserFollowAuthorAddIn) (out *model.UserFollowAuthorCRUDOut, err error) {
|
||||
exist, err := dao.UserFollowAuthors.Ctx(ctx).
|
||||
Where(dao.UserFollowAuthors.Columns().UserId, in.UserId).
|
||||
Where(dao.UserFollowAuthors.Columns().AuthorId, in.AuthorId).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("user_follow_author_query_failed")
|
||||
}
|
||||
if exist {
|
||||
return nil, ecode.Params.Sub("user_follow_author_exists")
|
||||
}
|
||||
if _, err := dao.UserFollowAuthors.Ctx(ctx).Data(do.UserFollowAuthors{
|
||||
UserId: in.UserId,
|
||||
AuthorId: in.AuthorId,
|
||||
}).Insert(); err != nil {
|
||||
return nil, ecode.Fail.Sub("user_follow_author_create_failed")
|
||||
}
|
||||
return &model.UserFollowAuthorCRUDOut{Success: true}, nil
|
||||
}
|
||||
err = dao.UserFollowAuthors.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
exist, err := dao.UserFollowAuthors.Ctx(ctx).TX(tx).
|
||||
Where(dao.UserFollowAuthors.Columns().UserId, in.UserId).
|
||||
Where(dao.UserFollowAuthors.Columns().AuthorId, in.AuthorId).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("user_follow_author_query_failed")
|
||||
}
|
||||
if exist {
|
||||
return ecode.Params.Sub("user_follow_author_exists")
|
||||
}
|
||||
if _, err := dao.UserFollowAuthors.Ctx(ctx).TX(tx).Data(do.UserFollowAuthors{
|
||||
UserId: in.UserId,
|
||||
AuthorId: in.AuthorId,
|
||||
}).Insert(); err != nil {
|
||||
return ecode.Fail.Sub("user_follow_author_create_failed")
|
||||
}
|
||||
|
||||
// Delete removes a user follow author by id
|
||||
func (s *sUserFollowAuthor) Delete(ctx context.Context, in *model.UserFollowAuthorDelIn) (out *model.UserFollowAuthorCRUDOut, err error) {
|
||||
exist, err := dao.UserFollowAuthors.Ctx(ctx).
|
||||
WherePri(in.Id).
|
||||
Exist()
|
||||
// 作者被关注数 +1
|
||||
if _, err := dao.Authors.Ctx(ctx).TX(tx).
|
||||
Where(dao.Authors.Columns().Id, in.AuthorId).
|
||||
Increment(dao.Authors.Columns().FollowerCount, 1); err != nil {
|
||||
return ecode.Fail.Sub("author_follow_count_update_failed")
|
||||
}
|
||||
// 用户关注数 +1
|
||||
if _, err := dao.Users.Ctx(ctx).TX(tx).
|
||||
Where(dao.Users.Columns().Id, in.UserId).
|
||||
Increment(dao.Users.Columns().AttentionCount, 1); err != nil {
|
||||
return ecode.Fail.Sub("user_attention_count_update_failed")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("user_follow_author_query_failed")
|
||||
}
|
||||
if !exist {
|
||||
return nil, ecode.NotFound.Sub("user_follow_author_not_found")
|
||||
}
|
||||
_, err = dao.UserFollowAuthors.Ctx(ctx).WherePri(in.Id).Delete()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("user_follow_author_delete_failed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.UserFollowAuthorCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
// Unfollow removes a user follow author by userId and authorId
|
||||
func (s *sUserFollowAuthor) Unfollow(ctx context.Context, userId int64, authorId int64) (out *model.UserFollowAuthorCRUDOut, err error) {
|
||||
if userId == 0 || authorId == 0 {
|
||||
return nil, ecode.Params.Sub("user_id_or_author_id_invalid")
|
||||
}
|
||||
// 查找关注记录
|
||||
var record struct{ Id int64 }
|
||||
err = dao.UserFollowAuthors.Ctx(ctx).
|
||||
Where(dao.UserFollowAuthors.Columns().UserId, userId).
|
||||
Where(dao.UserFollowAuthors.Columns().AuthorId, authorId).
|
||||
Fields("id").Scan(&record)
|
||||
err = dao.UserFollowAuthors.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
if userId == 0 || authorId == 0 {
|
||||
return ecode.Params.Sub("user_id_or_author_id_invalid")
|
||||
}
|
||||
// 查找关注记录
|
||||
result, err := dao.UserFollowAuthors.Ctx(ctx).TX(tx).
|
||||
Where(dao.UserFollowAuthors.Columns().UserId, userId).
|
||||
Where(dao.UserFollowAuthors.Columns().AuthorId, authorId).
|
||||
Delete()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("user_follow_author_delete_failed")
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return ecode.Fail.Sub("user_follow_author_delete_failed")
|
||||
}
|
||||
if affected == 0 {
|
||||
return ecode.NotFound.Sub("user_follow_author_not_found")
|
||||
}
|
||||
// 作者被关注数 -1
|
||||
if _, err := dao.Authors.Ctx(ctx).TX(tx).
|
||||
Where(dao.Authors.Columns().Id, authorId).
|
||||
Decrement(dao.Authors.Columns().FollowerCount, 1); err != nil {
|
||||
return ecode.Fail.Sub("author_follow_count_update_failed")
|
||||
}
|
||||
// 用户关注数 -1
|
||||
if _, err := dao.Users.Ctx(ctx).TX(tx).
|
||||
Where(dao.Users.Columns().Id, userId).
|
||||
Decrement(dao.Users.Columns().AttentionCount, 1); err != nil {
|
||||
return ecode.Fail.Sub("user_attention_count_update_failed")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("user_follow_author_query_failed")
|
||||
}
|
||||
if record.Id == 0 {
|
||||
return nil, ecode.NotFound.Sub("user_follow_author_not_found")
|
||||
}
|
||||
_, err = dao.UserFollowAuthors.Ctx(ctx).WherePri(record.Id).Delete()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("user_follow_author_delete_failed")
|
||||
return nil, err
|
||||
}
|
||||
return &model.UserFollowAuthorCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
36
internal/logic/user_read_history/user_read_history.go
Normal file
36
internal/logic/user_read_history/user_read_history.go
Normal file
@ -0,0 +1,36 @@
|
||||
package userreadhistory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"server/internal/dao"
|
||||
"server/internal/model"
|
||||
"server/internal/service"
|
||||
"server/utility/ecode"
|
||||
)
|
||||
|
||||
type sUserReadHistory struct{}
|
||||
|
||||
func New() service.IUserReadHistory {
|
||||
return &sUserReadHistory{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterUserReadHistory(New())
|
||||
}
|
||||
|
||||
func (s *sUserReadHistory) Remove(ctx context.Context, in *model.UserReadHistoryDelIn) (out *model.UserReadHistoryCRUDOut, err error) {
|
||||
// 参数校验
|
||||
if in.UserId == 0 || len(in.BookIds) == 0 {
|
||||
return nil, ecode.Params.Sub("user_read_history_remove_param_invalid")
|
||||
}
|
||||
|
||||
_, err = dao.UserReadHistory.Ctx(ctx).
|
||||
Where(dao.UserReadHistory.Columns().UserId, in.UserId).
|
||||
WhereIn(dao.UserReadHistory.Columns().BookId, in.BookIds).
|
||||
Delete()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("user_read_history_remove_failed")
|
||||
}
|
||||
|
||||
return &model.UserReadHistoryCRUDOut{Success: true}, nil
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
package user_read_record
|
||||
|
||||
import (
|
||||
"context"
|
||||
"server/internal/dao"
|
||||
"server/internal/model"
|
||||
"server/internal/model/do"
|
||||
"server/internal/service"
|
||||
"server/utility/ecode"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type sUserReadRecord struct{}
|
||||
|
||||
func New() service.IUserReadRecord {
|
||||
return &sUserReadRecord{}
|
||||
}
|
||||
func init() {
|
||||
service.RegisterUserReadRecord(New())
|
||||
}
|
||||
|
||||
// List retrieves a paginated list of user read records
|
||||
func (s *sUserReadRecord) List(ctx context.Context, in *model.UserReadRecordListIn) (out *model.UserReadRecordListOut, err error) {
|
||||
out = &model.UserReadRecordListOut{}
|
||||
m := dao.UserReadRecords.Ctx(ctx)
|
||||
if in.UserId != 0 {
|
||||
m = m.Where(dao.UserReadRecords.Columns().UserId, in.UserId)
|
||||
}
|
||||
if in.BookId != 0 {
|
||||
m = m.Where(dao.UserReadRecords.Columns().BookId, in.BookId)
|
||||
}
|
||||
if in.ChapterId != 0 {
|
||||
m = m.Where(dao.UserReadRecords.Columns().ChapterId, in.ChapterId)
|
||||
}
|
||||
if err = m.Page(in.Page, in.Size).ScanAndCount(&out.List, &out.Total, false); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Create adds a new user read record
|
||||
func (s *sUserReadRecord) Create(ctx context.Context, in *model.UserReadRecordAddIn) (out *model.UserReadRecordCRUDOut, err error) {
|
||||
// 检查是否已存在相同的阅读记录
|
||||
exist, err := dao.UserReadRecords.Ctx(ctx).
|
||||
Where(dao.UserReadRecords.Columns().UserId, in.UserId).
|
||||
Where(dao.UserReadRecords.Columns().BookId, in.BookId).
|
||||
Where(dao.UserReadRecords.Columns().ChapterId, in.ChapterId).
|
||||
Exist()
|
||||
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("read_record_query_failed")
|
||||
}
|
||||
|
||||
if exist {
|
||||
// 如果记录已存在,更新进度和时间
|
||||
_, err = dao.UserReadRecords.Ctx(ctx).
|
||||
Where(dao.UserReadRecords.Columns().UserId, in.UserId).
|
||||
Where(dao.UserReadRecords.Columns().BookId, in.BookId).
|
||||
Where(dao.UserReadRecords.Columns().ChapterId, in.ChapterId).
|
||||
Data(do.UserReadRecords{
|
||||
Progress: in.Progress,
|
||||
ReadAt: gtime.Now(),
|
||||
}).Update()
|
||||
} else {
|
||||
// 创建新记录
|
||||
_, err = dao.UserReadRecords.Ctx(ctx).Data(do.UserReadRecords{
|
||||
UserId: in.UserId,
|
||||
BookId: in.BookId,
|
||||
ChapterId: in.ChapterId,
|
||||
Progress: in.Progress,
|
||||
ReadAt: gtime.Now(),
|
||||
}).Insert()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("read_record_create_failed")
|
||||
}
|
||||
return &model.UserReadRecordCRUDOut{Success: true}, nil
|
||||
}
|
||||
|
||||
// Delete removes user read records by userId and bookIds
|
||||
func (s *sUserReadRecord) Delete(ctx context.Context, in *model.UserReadRecordDelIn) (out *model.UserReadRecordCRUDOut, err error) {
|
||||
if len(in.BookIds) == 0 {
|
||||
return nil, ecode.Params.Sub("bookshelve_bookids_empty")
|
||||
}
|
||||
|
||||
// 批量删除指定用户的指定书籍历史记录
|
||||
_, err = dao.UserReadRecords.Ctx(ctx).
|
||||
Where(dao.UserReadRecords.Columns().UserId, in.UserId).
|
||||
WhereIn(dao.UserReadRecords.Columns().BookId, in.BookIds).
|
||||
Delete()
|
||||
if err != nil {
|
||||
return nil, ecode.Fail.Sub("read_record_delete_failed")
|
||||
}
|
||||
return &model.UserReadRecordCRUDOut{Success: true}, nil
|
||||
}
|
||||
86
internal/logic/user_sign_in_logs/user_sign_in_logs.go
Normal file
86
internal/logic/user_sign_in_logs/user_sign_in_logs.go
Normal file
@ -0,0 +1,86 @@
|
||||
package user_sign_in_logs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"server/internal/dao"
|
||||
"server/internal/model"
|
||||
"server/internal/model/do"
|
||||
"server/internal/service"
|
||||
"server/utility/ecode"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
)
|
||||
|
||||
type sUserSignInLogs struct{}
|
||||
|
||||
func New() service.IUserSignInLogs {
|
||||
return &sUserSignInLogs{}
|
||||
}
|
||||
func init() {
|
||||
service.RegisterUserSignInLogs(New())
|
||||
}
|
||||
|
||||
// Sign 用户签到,只允许每日唯一签到
|
||||
func (s *sUserSignInLogs) Sign(ctx context.Context, in *model.UserSignInLogSignIn) (*model.UserSignInLogSignOut, error) {
|
||||
// 事务前查询奖励明细
|
||||
detail, err := dao.SignInRewardDetails.Ctx(ctx).Where(do.SignInRewardDetails{Id: in.RewardDetailId}).One()
|
||||
if err != nil {
|
||||
return nil, ecode.New(10014, "sign_in_reward_detail_query_failed")
|
||||
}
|
||||
if detail == nil || detail.IsEmpty() {
|
||||
return nil, ecode.New(10015, "sign_in_reward_detail_not_found")
|
||||
}
|
||||
quantity := detail["quantity"].Int()
|
||||
|
||||
var success bool
|
||||
err = dao.UserSignInLogs.Ctx(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// 检查是否已签到
|
||||
count, err := dao.UserSignInLogs.Ctx(ctx).TX(tx).Where(do.UserSignInLogs{
|
||||
UserId: in.UserId,
|
||||
RuleId: in.RuleId,
|
||||
SignInDate: in.SignInDate,
|
||||
DeletedAt: nil,
|
||||
}).Count()
|
||||
if err != nil {
|
||||
return ecode.New(10010, "user_sign_in_log_query_failed")
|
||||
}
|
||||
if count > 0 {
|
||||
success = false
|
||||
return nil
|
||||
}
|
||||
// 插入签到记录
|
||||
result, err := dao.UserSignInLogs.Ctx(ctx).TX(tx).Data(do.UserSignInLogs{
|
||||
UserId: in.UserId,
|
||||
RuleId: in.RuleId,
|
||||
RewardDetailId: in.RewardDetailId,
|
||||
SignInDate: in.SignInDate,
|
||||
Quantity: quantity,
|
||||
Status: 1,
|
||||
}).InsertAndGetId()
|
||||
if err != nil {
|
||||
return ecode.New(10011, "user_sign_in_log_create_failed")
|
||||
}
|
||||
// 插入积分日志
|
||||
_, err = dao.UserPointsLogs.Ctx(ctx).TX(tx).Data(do.UserPointsLogs{
|
||||
UserId: in.UserId,
|
||||
ChangeType: 2, // 2=收入(earn)
|
||||
PointsChange: quantity,
|
||||
RelatedOrderId: result,
|
||||
Description: "签到奖励",
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return ecode.New(10012, "user_points_log_create_failed")
|
||||
}
|
||||
// 更新用户积分
|
||||
_, err = dao.Users.Ctx(ctx).TX(tx).Where(do.Users{Id: in.UserId}).Increment("points", quantity)
|
||||
if err != nil {
|
||||
return ecode.New(10013, "user_points_update_failed")
|
||||
}
|
||||
success = true
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.UserSignInLogSignOut{Success: success}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user