1291 lines
38 KiB
Go
1291 lines
38 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"dd_fiber_api/internal/camp"
|
||
"dd_fiber_api/internal/camp/dao"
|
||
"dd_fiber_api/internal/order"
|
||
order_dao "dd_fiber_api/internal/order/dao"
|
||
order_service "dd_fiber_api/internal/order/service"
|
||
question_service "dd_fiber_api/internal/question/service"
|
||
"dd_fiber_api/pkg/snowflake"
|
||
"dd_fiber_api/pkg/utils"
|
||
)
|
||
|
||
// CampService 打卡营服务
|
||
type CampService struct {
|
||
campDAO *dao.CampDAO
|
||
sectionDAO *dao.SectionDAO
|
||
taskDAO *dao.TaskDAO
|
||
progressDAO *dao.ProgressDAO
|
||
userCampDAO *dao.UserCampDAO
|
||
orderDAO *order_dao.OrderDAO
|
||
orderService *order_service.OrderService
|
||
answerRecordService *question_service.AnswerRecordService
|
||
paperService *question_service.PaperService
|
||
}
|
||
|
||
// NewCampService 创建打卡营服务
|
||
func NewCampService(campDAO *dao.CampDAO) *CampService {
|
||
return &CampService{
|
||
campDAO: campDAO,
|
||
}
|
||
}
|
||
|
||
// SetDependencies 设置依赖(用于聚合接口)
|
||
func (s *CampService) SetDependencies(sectionDAO *dao.SectionDAO, taskDAO *dao.TaskDAO, progressDAO *dao.ProgressDAO, userCampDAO *dao.UserCampDAO, orderDAO *order_dao.OrderDAO) {
|
||
s.sectionDAO = sectionDAO
|
||
s.taskDAO = taskDAO
|
||
s.progressDAO = progressDAO
|
||
s.userCampDAO = userCampDAO
|
||
s.orderDAO = orderDAO
|
||
}
|
||
|
||
// SetOrderService 设置订单服务(用于任务完成后自动开启下一小节时创建 0 元订单)
|
||
func (s *CampService) SetOrderService(svc *order_service.OrderService) {
|
||
s.orderService = svc
|
||
}
|
||
|
||
// SetAnswerRecordService 设置答题记录服务(用于客观题完成状态校验)
|
||
func (s *CampService) SetAnswerRecordService(svc *question_service.AnswerRecordService) {
|
||
s.answerRecordService = svc
|
||
}
|
||
|
||
// SetPaperService 设置试卷服务(用于客观题完成状态:需答完所有题才算完成)
|
||
func (s *CampService) SetPaperService(svc *question_service.PaperService) {
|
||
s.paperService = svc
|
||
}
|
||
|
||
// CreateCamp 创建打卡营
|
||
func (s *CampService) CreateCamp(req *camp.CreateCampRequest) (*camp.CreateCampResponse, error) {
|
||
campID := snowflake.GetInstance().NextIDString()
|
||
|
||
campObj := &camp.Camp{
|
||
ID: campID,
|
||
Title: req.Title,
|
||
CoverImage: req.CoverImage,
|
||
Description: req.Description,
|
||
IntroType: req.IntroType,
|
||
IntroContent: req.IntroContent,
|
||
CategoryID: req.CategoryID,
|
||
IsRecommended: req.IsRecommended,
|
||
SectionCount: 0,
|
||
}
|
||
|
||
err := s.campDAO.Create(campObj)
|
||
if err != nil {
|
||
return &camp.CreateCampResponse{
|
||
Success: false,
|
||
Message: fmt.Sprintf("创建打卡营失败: %v", err),
|
||
}, nil
|
||
}
|
||
|
||
return &camp.CreateCampResponse{
|
||
ID: campID,
|
||
Success: true,
|
||
Message: "创建打卡营成功",
|
||
}, nil
|
||
}
|
||
|
||
// GetCamp 获取打卡营
|
||
func (s *CampService) GetCamp(id string) (*camp.GetCampResponse, error) {
|
||
campObj, err := s.campDAO.GetByID(id)
|
||
if err != nil {
|
||
return &camp.GetCampResponse{
|
||
Success: false,
|
||
Message: fmt.Sprintf("获取打卡营失败: %v", err),
|
||
}, nil
|
||
}
|
||
|
||
// 如果提供了 user_id,可以在这里获取用户相关的状态信息
|
||
// 例如:是否已加入、当前进度等
|
||
// 目前先返回基础信息,后续可以根据需要扩展
|
||
|
||
return &camp.GetCampResponse{
|
||
Camp: campObj,
|
||
Success: true,
|
||
Message: "获取打卡营成功",
|
||
}, nil
|
||
}
|
||
|
||
// UpdateCamp 更新打卡营
|
||
func (s *CampService) UpdateCamp(req *camp.UpdateCampRequest) (*camp.UpdateCampResponse, error) {
|
||
campObj := &camp.Camp{
|
||
ID: req.ID,
|
||
Title: req.Title,
|
||
CoverImage: req.CoverImage,
|
||
Description: req.Description,
|
||
IntroType: req.IntroType,
|
||
IntroContent: req.IntroContent,
|
||
CategoryID: req.CategoryID,
|
||
IsRecommended: req.IsRecommended,
|
||
SectionCount: 0, // 不更新 section_count,由系统自动维护
|
||
}
|
||
|
||
err := s.campDAO.Update(campObj)
|
||
if err != nil {
|
||
return &camp.UpdateCampResponse{
|
||
Success: false,
|
||
Message: fmt.Sprintf("更新打卡营失败: %v", err),
|
||
}, nil
|
||
}
|
||
|
||
return &camp.UpdateCampResponse{
|
||
Success: true,
|
||
Message: "更新打卡营成功",
|
||
}, nil
|
||
}
|
||
|
||
// DeleteCamp 删除打卡营
|
||
func (s *CampService) DeleteCamp(id string) (*camp.DeleteCampResponse, error) {
|
||
// TODO: 检查是否存在未删除的小节、任务和用户进度
|
||
// 暂时先允许删除,后续可以添加检查逻辑
|
||
|
||
err := s.campDAO.Delete(id)
|
||
if err != nil {
|
||
return &camp.DeleteCampResponse{
|
||
Success: false,
|
||
Message: fmt.Sprintf("删除打卡营失败: %v", err),
|
||
}, nil
|
||
}
|
||
|
||
return &camp.DeleteCampResponse{
|
||
Success: true,
|
||
Message: "删除打卡营成功",
|
||
}, nil
|
||
}
|
||
|
||
// ListCamps 列出打卡营(支持搜索和筛选;joined_only=1 时仅返回用户已加入的营)
|
||
func (s *CampService) ListCamps(req *camp.ListCampsRequest) (*camp.ListCampsResponse, error) {
|
||
if req.Page < 1 {
|
||
req.Page = 1
|
||
}
|
||
if req.PageSize < 1 {
|
||
req.PageSize = 10
|
||
}
|
||
if req.PageSize > 100 {
|
||
req.PageSize = 100
|
||
}
|
||
|
||
// 仅已加入:根据 user_id 查用户已加入的营,再补全营详情
|
||
if req.JoinedOnly == 1 && req.UserID != "" && s.userCampDAO != nil {
|
||
joined, total, err := s.userCampDAO.ListUserJoinedCamps(req.UserID, req.Page, req.PageSize)
|
||
if err != nil {
|
||
return &camp.ListCampsResponse{
|
||
Success: false,
|
||
Message: fmt.Sprintf("获取已加入打卡营列表失败: %v", err),
|
||
}, nil
|
||
}
|
||
camps := make([]*camp.Camp, 0, len(joined))
|
||
trueVal := true
|
||
for _, uc := range joined {
|
||
c, err := s.campDAO.GetByID(uc.CampID)
|
||
if err != nil || c == nil {
|
||
continue
|
||
}
|
||
c.IsJoined = &trueVal
|
||
camps = append(camps, c)
|
||
}
|
||
// 按 camp_sections 实时统计小节数
|
||
if len(camps) > 0 && s.sectionDAO != nil {
|
||
campIDs := make([]string, 0, len(camps))
|
||
for _, c := range camps {
|
||
campIDs = append(campIDs, c.ID)
|
||
}
|
||
if countMap, err := s.sectionDAO.CountByCampIDs(campIDs); err == nil {
|
||
for _, c := range camps {
|
||
if n, ok := countMap[c.ID]; ok {
|
||
c.SectionCount = int32(n)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return &camp.ListCampsResponse{
|
||
Camps: camps,
|
||
Total: total,
|
||
Success: true,
|
||
Message: "获取打卡营列表成功",
|
||
}, nil
|
||
}
|
||
|
||
var isRecommended *bool
|
||
switch req.RecommendFilter {
|
||
case camp.RecommendFilterOnlyTrue:
|
||
trueVal := true
|
||
isRecommended = &trueVal
|
||
case camp.RecommendFilterOnlyFalse:
|
||
falseVal := false
|
||
isRecommended = &falseVal
|
||
case camp.RecommendFilterAll:
|
||
isRecommended = nil
|
||
}
|
||
|
||
camps, total, err := s.campDAO.Search(req.Keyword, req.CategoryID, isRecommended, req.Page, req.PageSize)
|
||
if err != nil {
|
||
return &camp.ListCampsResponse{
|
||
Success: false,
|
||
Message: fmt.Sprintf("获取打卡营列表失败: %v", err),
|
||
}, nil
|
||
}
|
||
|
||
// 按 camp_sections 实时统计小节数,覆盖表中的 section_count,保证列表展示正确
|
||
if len(camps) > 0 && s.sectionDAO != nil {
|
||
campIDs := make([]string, 0, len(camps))
|
||
for _, c := range camps {
|
||
campIDs = append(campIDs, c.ID)
|
||
}
|
||
if countMap, err := s.sectionDAO.CountByCampIDs(campIDs); err == nil {
|
||
for _, c := range camps {
|
||
if n, ok := countMap[c.ID]; ok {
|
||
c.SectionCount = int32(n)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 当请求带了 user_id 时,为每个营填充是否已加入
|
||
if req.UserID != "" && s.userCampDAO != nil && len(camps) > 0 {
|
||
for _, c := range camps {
|
||
isJoined, _, _, _ := s.userCampDAO.CheckUserCampStatus(req.UserID, c.ID)
|
||
c.IsJoined = &isJoined
|
||
}
|
||
}
|
||
|
||
return &camp.ListCampsResponse{
|
||
Camps: camps,
|
||
Total: total,
|
||
Success: true,
|
||
Message: "获取打卡营列表成功",
|
||
}, nil
|
||
}
|
||
|
||
// GetCampDetailWithStatus 获取打卡营详情及状态(聚合多个数据源)
|
||
func (s *CampService) GetCampDetailWithStatus(ctx context.Context, req *camp.GetCampDetailWithStatusRequest) (*camp.GetCampDetailWithStatusResponse, error) {
|
||
if req.CampID == "" || req.UserID == "" {
|
||
return &camp.GetCampDetailWithStatusResponse{
|
||
Success: false,
|
||
Message: "参数缺失",
|
||
}, nil
|
||
}
|
||
|
||
// 检查依赖是否已设置
|
||
if s.sectionDAO == nil || s.taskDAO == nil || s.progressDAO == nil || s.userCampDAO == nil || s.orderDAO == nil {
|
||
return &camp.GetCampDetailWithStatusResponse{
|
||
Success: false,
|
||
Message: "服务依赖未初始化",
|
||
}, nil
|
||
}
|
||
|
||
// 使用 WaitGroup 进行并发控制(Go 1.25 语法)
|
||
var wg sync.WaitGroup
|
||
var mu sync.Mutex
|
||
var firstErr error
|
||
|
||
// 定义结果变量
|
||
var campObj *camp.Camp
|
||
var sections []*camp.Section
|
||
var isJoined bool
|
||
var joinedAt string
|
||
var currentSectionID string
|
||
var purchasedSectionIDs map[string]bool
|
||
var progressList []*camp.UserProgress
|
||
|
||
// 1. 并发获取基础数据
|
||
// 获取打卡营详情
|
||
wg.Go(func() {
|
||
campResp, err := s.GetCamp(req.CampID)
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
if err != nil {
|
||
if firstErr == nil {
|
||
firstErr = fmt.Errorf("获取打卡营详情失败: %v", err)
|
||
}
|
||
return
|
||
}
|
||
if !campResp.Success {
|
||
if firstErr == nil {
|
||
firstErr = fmt.Errorf("获取打卡营详情失败: %s", campResp.Message)
|
||
}
|
||
return
|
||
}
|
||
campObj = campResp.Camp
|
||
})
|
||
|
||
// 获取小节列表
|
||
wg.Go(func() {
|
||
sectionListReq := &camp.ListSectionsRequest{
|
||
CampID: req.CampID,
|
||
Page: 1,
|
||
PageSize: 1000,
|
||
}
|
||
sectionListResp, err := s.listSections(sectionListReq)
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
if err != nil {
|
||
if firstErr == nil {
|
||
firstErr = fmt.Errorf("获取小节列表失败: %v", err)
|
||
}
|
||
return
|
||
}
|
||
if !sectionListResp.Success {
|
||
if firstErr == nil {
|
||
firstErr = fmt.Errorf("获取小节列表失败: %s", sectionListResp.Message)
|
||
}
|
||
return
|
||
}
|
||
sections = sectionListResp.Sections
|
||
})
|
||
|
||
// 检查用户状态
|
||
wg.Go(func() {
|
||
statusResp, err := s.checkUserCampStatus(req.UserID, req.CampID)
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
if err != nil {
|
||
if firstErr == nil {
|
||
firstErr = fmt.Errorf("检查用户状态失败: %v", err)
|
||
}
|
||
return
|
||
}
|
||
if !statusResp.Success {
|
||
if firstErr == nil {
|
||
firstErr = fmt.Errorf("检查用户状态失败: %s", statusResp.Message)
|
||
}
|
||
return
|
||
}
|
||
isJoined = statusResp.IsJoined
|
||
joinedAt = statusResp.JoinedAt
|
||
currentSectionID = statusResp.CurrentSectionID
|
||
})
|
||
|
||
// 获取已购买的小节列表(通过订单查询)
|
||
wg.Go(func() {
|
||
orders, _, err := s.orderDAO.ListOrders(req.UserID, req.CampID, "", order.OrderStatusPaid, order.PaymentMethodUnknown, 1, 1000)
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
if err != nil {
|
||
if firstErr == nil {
|
||
firstErr = fmt.Errorf("获取订单列表失败: %v", err)
|
||
}
|
||
return
|
||
}
|
||
purchasedSectionIDs = make(map[string]bool)
|
||
for _, ord := range orders {
|
||
// 新的 Order 结构体使用 Status 字段,类型为 OrderStatus (string)
|
||
if ord.Status == order.OrderStatusPaid {
|
||
purchasedSectionIDs[ord.SectionID] = true
|
||
}
|
||
}
|
||
})
|
||
|
||
// 获取进度列表(不在这里计算小节进度,等 sections 获取完成后再计算)
|
||
wg.Go(func() {
|
||
list, _, err := s.progressDAO.List(req.UserID, "", "", "", req.CampID, "", 1, 1000)
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
if err != nil {
|
||
if firstErr == nil {
|
||
firstErr = fmt.Errorf("获取进度列表失败: %v", err)
|
||
}
|
||
return
|
||
}
|
||
progressList = list
|
||
})
|
||
|
||
// 等待所有基础数据获取完成
|
||
wg.Wait()
|
||
|
||
// 检查是否有错误
|
||
mu.Lock()
|
||
err := firstErr
|
||
mu.Unlock()
|
||
if err != nil {
|
||
return &camp.GetCampDetailWithStatusResponse{
|
||
Success: false,
|
||
Message: err.Error(),
|
||
}, nil
|
||
}
|
||
|
||
// 检查必要数据是否存在
|
||
if campObj == nil || sections == nil {
|
||
return &camp.GetCampDetailWithStatusResponse{
|
||
Success: false,
|
||
Message: "获取数据不完整",
|
||
}, nil
|
||
}
|
||
|
||
// 初始化映射
|
||
if purchasedSectionIDs == nil {
|
||
purchasedSectionIDs = make(map[string]bool)
|
||
}
|
||
|
||
// 构建进度映射,方便快速查找任务进度
|
||
progressMap := make(map[string]*camp.UserProgress)
|
||
for _, prog := range progressList {
|
||
progressMap[prog.TaskID] = prog
|
||
}
|
||
|
||
// 处理每个小节,获取任务列表和进度
|
||
aggregatedSections := make([]*camp.AggregatedSectionDetail, 0, len(sections))
|
||
for _, section := range sections {
|
||
sectionID := section.ID
|
||
|
||
// 判断是否已购买
|
||
isPurchased := purchasedSectionIDs[sectionID]
|
||
|
||
// 获取任务列表(循环获取所有任务,避免分页限制)
|
||
// 先获取准确的任务总数
|
||
totalCount, err := s.taskDAO.CountActiveBySection(sectionID)
|
||
if err != nil {
|
||
totalCount = 0
|
||
}
|
||
|
||
allTasks := make([]*camp.Task, 0)
|
||
page := 1
|
||
pageSize := 1000
|
||
for {
|
||
tasks, _, err := s.taskDAO.List("", "", sectionID, camp.TaskTypeUnknown, page, pageSize)
|
||
if err != nil {
|
||
// 查询出错,退出循环
|
||
break
|
||
}
|
||
|
||
// 添加任务到列表
|
||
allTasks = append(allTasks, tasks...)
|
||
|
||
// 如果已经获取了所有任务,退出循环
|
||
if len(allTasks) >= totalCount || len(tasks) < pageSize {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
tasks := allTasks
|
||
|
||
// 处理任务列表,获取每个任务的进度,同时统计完成情况
|
||
aggregatedTasks := make([]*camp.AggregatedTaskDetail, 0, len(tasks))
|
||
totalTasks := int32(len(tasks))
|
||
completedTasks := int32(0)
|
||
|
||
for _, task := range tasks {
|
||
// 判断是否需要审核(主观题和申论题需要审核)
|
||
needReview := task.TaskType == camp.TaskTypeSubjective || task.TaskType == camp.TaskTypeEssay
|
||
|
||
// 获取任务进度(优先从进度映射中获取,避免重复查询)
|
||
var taskProgress *camp.UserProgress
|
||
if prog, ok := progressMap[task.ID]; ok {
|
||
taskProgress = prog
|
||
} else {
|
||
taskProgress, _ = s.progressDAO.GetByUserAndTask(req.UserID, task.ID)
|
||
// 缓存到映射中
|
||
if taskProgress != nil {
|
||
progressMap[task.ID] = taskProgress
|
||
}
|
||
}
|
||
|
||
// 确定任务状态
|
||
status := "NotStarted"
|
||
reviewStatus := ""
|
||
isTaskCompleted := false
|
||
if taskProgress != nil {
|
||
if taskProgress.IsCompleted {
|
||
// 客观题:进度表已按“正确率达标即完成、达标后永久完成”维护,直接信任 is_completed
|
||
if task.TaskType == camp.TaskTypeObjective {
|
||
status = "Completed"
|
||
isTaskCompleted = true
|
||
} else if needReview {
|
||
reviewStatus = string(taskProgress.ReviewStatus)
|
||
switch taskProgress.ReviewStatus {
|
||
case camp.ReviewStatusApproved:
|
||
status = "Completed"
|
||
isTaskCompleted = true
|
||
case camp.ReviewStatusRejected:
|
||
status = "Rejected"
|
||
default:
|
||
status = "Reviewing"
|
||
}
|
||
} else {
|
||
status = "Completed"
|
||
isTaskCompleted = true
|
||
}
|
||
} else {
|
||
status = "InProgress"
|
||
}
|
||
}
|
||
|
||
// 统计已完成任务数
|
||
if isTaskCompleted {
|
||
completedTasks++
|
||
}
|
||
|
||
// 生成任务标题
|
||
taskTitle := s.getTaskTitle(task)
|
||
|
||
allowNextWhileReviewing := parseAllowNextWhileReviewing(task.Condition)
|
||
prereqID := task.PrerequisiteTaskID
|
||
prerequisites := []string{}
|
||
if prereqID != "" {
|
||
prerequisites = []string{prereqID}
|
||
}
|
||
aggregatedTask := &camp.AggregatedTaskDetail{
|
||
ID: task.ID,
|
||
TaskType: task.TaskType,
|
||
Title: taskTitle,
|
||
Status: status,
|
||
NeedReview: needReview,
|
||
AllowNextWhileReviewing: allowNextWhileReviewing,
|
||
ReviewStatus: reviewStatus,
|
||
Progress: taskProgress,
|
||
PrerequisiteTaskID: prereqID,
|
||
Prerequisites: prerequisites,
|
||
}
|
||
aggregatedTasks = append(aggregatedTasks, aggregatedTask)
|
||
}
|
||
|
||
// 按解锁关系计算 CanStart:仅看后台配置的前置任务;未配置 prerequisite_task_id 时默认都可开始(不按 1→2→3 顺序)
|
||
taskIDToAgg := make(map[string]*camp.AggregatedTaskDetail)
|
||
for _, at := range aggregatedTasks {
|
||
taskIDToAgg[at.ID] = at
|
||
}
|
||
for i, at := range aggregatedTasks {
|
||
task := tasks[i]
|
||
if task.PrerequisiteTaskID == "" {
|
||
// 未配置前置任务:默认可直接做,不按列表顺序
|
||
at.CanStart = true
|
||
} else {
|
||
// 配置了前置任务:必须前置任务完成(且需审核时已通过)才可开始
|
||
prereq, ok := taskIDToAgg[task.PrerequisiteTaskID]
|
||
if !ok {
|
||
at.CanStart = false
|
||
continue
|
||
}
|
||
if prereq.Status == "Completed" {
|
||
at.CanStart = true
|
||
} else if prereq.Status == "Reviewing" && prereq.NeedReview && prereq.AllowNextWhileReviewing {
|
||
at.CanStart = true
|
||
} else {
|
||
at.CanStart = false
|
||
}
|
||
}
|
||
}
|
||
|
||
// 计算小节进度
|
||
isCompleted := totalTasks > 0 && completedTasks == totalTasks
|
||
// 已开始:如果有已完成的任务,或者已购买(表示用户已经开始使用)
|
||
isStarted := completedTasks > 0 || isPurchased
|
||
sectionProgress := &camp.UserSectionProgress{
|
||
SectionID: sectionID,
|
||
TotalTasks: totalTasks,
|
||
CompletedTasks: completedTasks,
|
||
IsCompleted: isCompleted,
|
||
}
|
||
|
||
aggregatedSection := &camp.AggregatedSectionDetail{
|
||
ID: sectionID,
|
||
Title: section.Title,
|
||
SectionNumber: section.SectionNumber,
|
||
PriceFen: section.PriceFen,
|
||
IsPurchased: isPurchased,
|
||
IsStarted: isStarted,
|
||
IsCompleted: isCompleted,
|
||
IsCurrent: sectionID == currentSectionID,
|
||
RequirePreviousSection: section.RequirePreviousSection,
|
||
TimeIntervalType: section.TimeIntervalType,
|
||
TimeIntervalValue: section.TimeIntervalValue,
|
||
Tasks: aggregatedTasks,
|
||
SectionProgress: sectionProgress,
|
||
}
|
||
aggregatedSections = append(aggregatedSections, aggregatedSection)
|
||
}
|
||
|
||
// 计算打卡营状态
|
||
campStatus := camp.CampStatusNotStarted
|
||
if isJoined {
|
||
allSectionsCompleted := true
|
||
hasStartedTask := false
|
||
hasAnyTask := false
|
||
|
||
for _, section := range aggregatedSections {
|
||
if section.SectionProgress != nil {
|
||
totalTasks := section.SectionProgress.TotalTasks
|
||
completedTasks := section.SectionProgress.CompletedTasks
|
||
|
||
if totalTasks > 0 {
|
||
hasAnyTask = true
|
||
if completedTasks > 0 {
|
||
hasStartedTask = true
|
||
}
|
||
if completedTasks != totalTasks {
|
||
allSectionsCompleted = false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if hasAnyTask {
|
||
if allSectionsCompleted {
|
||
campStatus = camp.CampStatusCompleted
|
||
} else if hasStartedTask {
|
||
campStatus = camp.CampStatusInProgress
|
||
}
|
||
} else {
|
||
campStatus = camp.CampStatusCompleted
|
||
}
|
||
}
|
||
|
||
// 复制 Camp 对象,不返回 deleted_at
|
||
campCopy := &camp.Camp{
|
||
ID: campObj.ID,
|
||
Title: campObj.Title,
|
||
CoverImage: campObj.CoverImage,
|
||
Description: campObj.Description,
|
||
IntroType: campObj.IntroType,
|
||
IntroContent: campObj.IntroContent,
|
||
CategoryID: campObj.CategoryID,
|
||
IsRecommended: campObj.IsRecommended,
|
||
SectionCount: campObj.SectionCount,
|
||
DeletedAt: "", // 不返回 deleted_at
|
||
}
|
||
|
||
// 构建用户与打卡营的关系
|
||
userCamp := &camp.UserCamp{
|
||
IsJoined: isJoined,
|
||
JoinedAt: joinedAt,
|
||
CurrentSectionID: currentSectionID,
|
||
CampStatus: campStatus,
|
||
}
|
||
|
||
// 构建打卡营详情
|
||
campDetail := &camp.CampDetail{
|
||
Camp: campCopy,
|
||
UserCamp: userCamp,
|
||
}
|
||
|
||
return &camp.GetCampDetailWithStatusResponse{
|
||
CampDetail: campDetail,
|
||
Sections: aggregatedSections,
|
||
Success: true,
|
||
Message: "获取打卡营详情成功",
|
||
}, nil
|
||
}
|
||
|
||
// 辅助方法:获取小节列表(内部使用)
|
||
func (s *CampService) listSections(req *camp.ListSectionsRequest) (*camp.ListSectionsResponse, error) {
|
||
if s.sectionDAO == nil {
|
||
return &camp.ListSectionsResponse{
|
||
Success: false,
|
||
Message: "sectionDAO 未初始化",
|
||
}, nil
|
||
}
|
||
// 设置默认值
|
||
if req.Page < 1 {
|
||
req.Page = 1
|
||
}
|
||
if req.PageSize < 1 {
|
||
req.PageSize = 10
|
||
}
|
||
if req.PageSize > 1000 {
|
||
req.PageSize = 1000
|
||
}
|
||
|
||
sections, total, err := s.sectionDAO.List(req.Keyword, req.CampID, req.Page, req.PageSize)
|
||
if err != nil {
|
||
return &camp.ListSectionsResponse{
|
||
Success: false,
|
||
Message: fmt.Sprintf("获取小节列表失败: %v", err),
|
||
}, nil
|
||
}
|
||
|
||
return &camp.ListSectionsResponse{
|
||
Sections: sections,
|
||
Total: total,
|
||
Success: true,
|
||
Message: "获取小节列表成功",
|
||
}, nil
|
||
}
|
||
|
||
// 辅助方法:检查用户打卡营状态
|
||
func (s *CampService) checkUserCampStatus(userID, campID string) (*camp.CheckUserCampStatusResponse, error) {
|
||
if s.userCampDAO == nil {
|
||
return &camp.CheckUserCampStatusResponse{
|
||
Success: false,
|
||
Message: "userCampDAO 未初始化",
|
||
}, nil
|
||
}
|
||
isJoined, joinedAt, currentSectionID, err := s.userCampDAO.CheckUserCampStatus(userID, campID)
|
||
if err != nil {
|
||
return &camp.CheckUserCampStatusResponse{
|
||
Success: false,
|
||
Message: fmt.Sprintf("查询失败: %v", err),
|
||
IsJoined: false,
|
||
}, nil
|
||
}
|
||
|
||
// 格式化时间
|
||
formattedJoinedAt := utils.FormatNullTimeToStd(joinedAt)
|
||
|
||
var currentSectionIDStr string
|
||
if currentSectionID.Valid {
|
||
currentSectionIDStr = currentSectionID.String
|
||
}
|
||
|
||
return &camp.CheckUserCampStatusResponse{
|
||
Success: true,
|
||
Message: "查询成功",
|
||
IsJoined: isJoined,
|
||
JoinedAt: formattedJoinedAt,
|
||
CurrentSectionID: currentSectionIDStr,
|
||
}, nil
|
||
}
|
||
|
||
// CanUnlockSection 检查用户是否可开启指定小节(查询数据库:上一小节是否完成、是否已拥有等)
|
||
func (s *CampService) CanUnlockSection(req *camp.CanUnlockSectionRequest) (*camp.CanUnlockSectionResponse, error) {
|
||
if req.CampID == "" || req.SectionID == "" || req.UserID == "" {
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: false,
|
||
Message: "参数不完整",
|
||
}, nil
|
||
}
|
||
if s.sectionDAO == nil {
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: false,
|
||
Message: "服务未就绪",
|
||
}, nil
|
||
}
|
||
|
||
section, err := s.sectionDAO.GetByID(req.SectionID)
|
||
if err != nil || section == nil {
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: false,
|
||
Message: "小节不存在",
|
||
}, nil
|
||
}
|
||
if section.CampID != req.CampID {
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: false,
|
||
Message: "小节不属于该打卡营",
|
||
}, nil
|
||
}
|
||
|
||
// 已拥有该小节时,仍须校验时间间隔(未到间隔则不可视为“可进入”,不推进 current_section_id)
|
||
owned := false
|
||
if s.orderDAO != nil {
|
||
owned, _ = s.orderDAO.CheckUserHasSection(req.UserID, req.SectionID)
|
||
}
|
||
|
||
// 判断是否需要完成上一小节
|
||
sections, _, err := s.sectionDAO.List("", req.CampID, 1, 1000)
|
||
if err != nil || len(sections) == 0 {
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: true,
|
||
Message: "",
|
||
}, nil
|
||
}
|
||
|
||
var prevSection *camp.Section
|
||
for i, sec := range sections {
|
||
if sec.ID == req.SectionID {
|
||
if i > 0 {
|
||
prevSection = sections[i-1]
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
// 时间间隔限制:只要本节配置了间隔且存在上一节,就必须校验「上一节已完成 + 间隔已过」,与 require_previous_section 无关(收费类型不校验时间,由支付逻辑控制)
|
||
if section.TimeIntervalType != camp.TimeIntervalTypeNone && section.TimeIntervalType != camp.TimeIntervalTypePaid && section.TimeIntervalValue > 0 && prevSection != nil {
|
||
totalTasks := 0
|
||
if s.taskDAO != nil {
|
||
totalTasks, _ = s.taskDAO.CountActiveBySection(prevSection.ID)
|
||
}
|
||
if totalTasks > 0 {
|
||
completedTasks := 0
|
||
if s.progressDAO != nil {
|
||
completedTasks, _ = s.progressDAO.CountCompletedBySection(req.UserID, prevSection.ID)
|
||
}
|
||
if completedTasks < totalTasks {
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: false,
|
||
Message: "请先完成上一小节",
|
||
}, nil
|
||
}
|
||
}
|
||
if s.progressDAO == nil {
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: false,
|
||
Message: "无法获取上一小节开启时间,请稍后再试",
|
||
}, nil
|
||
}
|
||
// 时间间隔起点:从上一小节「开启时」算起(首次产生进度的 created_at),若无则用完成时间兜底
|
||
intervalStart, _ := s.progressDAO.GetUserSectionStartedAt(req.UserID, prevSection.ID)
|
||
if intervalStart == nil {
|
||
intervalStart, _ = s.progressDAO.GetUserSectionCompletedAt(req.UserID, prevSection.ID)
|
||
}
|
||
if intervalStart == nil {
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: false,
|
||
Message: "无法获取上一小节开启时间,请稍后再试",
|
||
}, nil
|
||
}
|
||
now := time.Now()
|
||
switch section.TimeIntervalType {
|
||
case camp.TimeIntervalTypeHour:
|
||
unlockAt := intervalStart.Add(time.Duration(section.TimeIntervalValue) * time.Hour)
|
||
if now.Before(unlockAt) {
|
||
msg := fmt.Sprintf("需在上一小节开启后 %d 小时才能解锁本小节", section.TimeIntervalValue)
|
||
hours := int(time.Until(unlockAt).Hours())
|
||
mins := int(time.Until(unlockAt).Minutes()) % 60
|
||
if hours > 0 || mins > 0 {
|
||
msg = fmt.Sprintf("%s,请 %d 小时 %d 分钟后再试", msg, hours, mins)
|
||
}
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: false,
|
||
Message: msg,
|
||
UnlockAt: unlockAt.Unix(),
|
||
}, nil
|
||
}
|
||
case camp.TimeIntervalTypeNaturalDay:
|
||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||
if loc == nil {
|
||
loc = intervalStart.Location()
|
||
}
|
||
prevInLoc := intervalStart.In(loc)
|
||
prevDay := time.Date(prevInLoc.Year(), prevInLoc.Month(), prevInLoc.Day(), 0, 0, 0, 0, loc)
|
||
unlockDay := prevDay.AddDate(0, 0, int(section.TimeIntervalValue))
|
||
nowInLoc := now.In(loc)
|
||
if nowInLoc.Before(unlockDay) {
|
||
msg := fmt.Sprintf("需在上一小节开启后 %d 个自然日才能解锁本小节", section.TimeIntervalValue)
|
||
if section.TimeIntervalValue == 1 {
|
||
msg = "需在上一小节开启的次日才能解锁本小节"
|
||
}
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: false,
|
||
Message: msg,
|
||
UnlockAt: unlockDay.Unix(),
|
||
}, nil
|
||
}
|
||
}
|
||
// 时间间隔已通过,若已拥有则直接返回可解锁
|
||
if owned {
|
||
return &camp.CanUnlockSectionResponse{Success: true, CanUnlock: true, Message: "已拥有"}, nil
|
||
}
|
||
}
|
||
|
||
// 无时间间隔或时间间隔已通过:若不需要完成上一小节,则允许解锁
|
||
needPrev := section.RequirePreviousSection
|
||
if !needPrev || prevSection == nil {
|
||
if owned {
|
||
return &camp.CanUnlockSectionResponse{Success: true, CanUnlock: true, Message: "已拥有"}, nil
|
||
}
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: true,
|
||
Message: "",
|
||
}, nil
|
||
}
|
||
|
||
// 上一小节任务总数与已完成数
|
||
totalTasks := 0
|
||
if s.taskDAO != nil {
|
||
totalTasks, err = s.taskDAO.CountActiveBySection(prevSection.ID)
|
||
if err != nil {
|
||
totalTasks = 0
|
||
}
|
||
}
|
||
if totalTasks == 0 {
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: true,
|
||
Message: "",
|
||
}, nil
|
||
}
|
||
|
||
completedTasks := 0
|
||
if s.progressDAO != nil {
|
||
completedTasks, err = s.progressDAO.CountCompletedBySection(req.UserID, prevSection.ID)
|
||
if err != nil {
|
||
completedTasks = 0
|
||
}
|
||
}
|
||
|
||
if completedTasks < totalTasks {
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: false,
|
||
Message: "请先完成上一小节",
|
||
}, nil
|
||
}
|
||
|
||
// 时间/自然天限制:起点从上一节「开启时」算起(首次产生进度的 created_at),若无则用完成时间兜底(收费类型不校验时间)
|
||
if section.TimeIntervalType != camp.TimeIntervalTypeNone && section.TimeIntervalType != camp.TimeIntervalTypePaid && section.TimeIntervalValue > 0 {
|
||
var intervalStart *time.Time
|
||
if s.progressDAO != nil {
|
||
intervalStart, _ = s.progressDAO.GetUserSectionStartedAt(req.UserID, prevSection.ID)
|
||
if intervalStart == nil {
|
||
intervalStart, _ = s.progressDAO.GetUserSectionCompletedAt(req.UserID, prevSection.ID)
|
||
}
|
||
}
|
||
if intervalStart == nil {
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: false,
|
||
Message: "无法获取上一小节开启时间,请稍后再试",
|
||
}, nil
|
||
}
|
||
now := time.Now()
|
||
switch section.TimeIntervalType {
|
||
case camp.TimeIntervalTypeHour:
|
||
unlockAt := intervalStart.Add(time.Duration(section.TimeIntervalValue) * time.Hour)
|
||
if now.Before(unlockAt) {
|
||
left := time.Until(unlockAt)
|
||
hours := int(left.Hours())
|
||
mins := int(left.Minutes()) % 60
|
||
msg := fmt.Sprintf("需在上一小节开启后 %d 小时才能解锁本小节", section.TimeIntervalValue)
|
||
if hours > 0 || mins > 0 {
|
||
msg = fmt.Sprintf("%s,请 %d 小时 %d 分钟后再试", msg, hours, mins)
|
||
}
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: false,
|
||
Message: msg,
|
||
UnlockAt: unlockAt.Unix(),
|
||
}, nil
|
||
}
|
||
case camp.TimeIntervalTypeNaturalDay:
|
||
// 自然日按中国时区计算,使「次日」为北京时间次日 0 点
|
||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||
if loc == nil {
|
||
loc = intervalStart.Location()
|
||
}
|
||
prevOpenInLoc := intervalStart.In(loc)
|
||
prevDay := time.Date(prevOpenInLoc.Year(), prevOpenInLoc.Month(), prevOpenInLoc.Day(), 0, 0, 0, 0, loc)
|
||
unlockDay := prevDay.AddDate(0, 0, int(section.TimeIntervalValue))
|
||
nowInLoc := now.In(loc)
|
||
if nowInLoc.Before(unlockDay) {
|
||
msg := fmt.Sprintf("需在上一小节开启后 %d 个自然日才能解锁本小节", section.TimeIntervalValue)
|
||
if section.TimeIntervalValue == 1 {
|
||
msg = "需在上一小节开启的次日才能解锁本小节"
|
||
}
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: false,
|
||
Message: msg,
|
||
UnlockAt: unlockDay.Unix(),
|
||
}, nil
|
||
}
|
||
}
|
||
}
|
||
|
||
return &camp.CanUnlockSectionResponse{
|
||
Success: true,
|
||
CanUnlock: true,
|
||
Message: "",
|
||
}, nil
|
||
}
|
||
|
||
// TryAutoOpenNextSectionIfEligible 在「当前小节全部任务已完成」时,若下一小节无限制且免费则自动开启(创建 0 元订单并更新 current_section_id)
|
||
// 供 ProgressService 在任务完成时调用,无需前端再请求 can_unlock_section + purchase_section
|
||
func (s *CampService) TryAutoOpenNextSectionIfEligible(userID, campID, completedSectionID string) {
|
||
if userID == "" || campID == "" || completedSectionID == "" {
|
||
return
|
||
}
|
||
if s.sectionDAO == nil || s.taskDAO == nil || s.progressDAO == nil || s.orderDAO == nil {
|
||
return
|
||
}
|
||
|
||
section, err := s.sectionDAO.GetByID(completedSectionID)
|
||
if err != nil || section == nil || section.CampID != campID {
|
||
return
|
||
}
|
||
|
||
totalTasks, err := s.taskDAO.CountActiveBySection(completedSectionID)
|
||
if err != nil || totalTasks == 0 {
|
||
return
|
||
}
|
||
completedTasks, err := s.progressDAO.CountCompletedBySection(userID, completedSectionID)
|
||
if err != nil || completedTasks < totalTasks {
|
||
return
|
||
}
|
||
|
||
sections, _, err := s.sectionDAO.List("", campID, 1, 1000)
|
||
if err != nil || len(sections) == 0 {
|
||
return
|
||
}
|
||
var nextSection *camp.Section
|
||
for i, sec := range sections {
|
||
if sec.ID == completedSectionID {
|
||
if i+1 < len(sections) {
|
||
nextSection = sections[i+1]
|
||
}
|
||
break
|
||
}
|
||
}
|
||
if nextSection == nil {
|
||
return
|
||
}
|
||
|
||
// 硬性时间间隔校验:下一节若配置了间隔,必须「上一节开启时间 + 间隔」已过才允许自动开启;收费类型不自动开启,需用户主动购买
|
||
if nextSection.TimeIntervalType == camp.TimeIntervalTypePaid {
|
||
return
|
||
}
|
||
if nextSection.TimeIntervalType != camp.TimeIntervalTypeNone && nextSection.TimeIntervalValue > 0 {
|
||
intervalStart, err := s.progressDAO.GetUserSectionStartedAt(userID, completedSectionID)
|
||
if err != nil || intervalStart == nil {
|
||
intervalStart, err = s.progressDAO.GetUserSectionCompletedAt(userID, completedSectionID)
|
||
}
|
||
if err != nil || intervalStart == nil {
|
||
return // 拿不到开启/完成时间则不自动开启
|
||
}
|
||
now := time.Now()
|
||
switch nextSection.TimeIntervalType {
|
||
case camp.TimeIntervalTypeHour:
|
||
unlockAt := intervalStart.Add(time.Duration(nextSection.TimeIntervalValue) * time.Hour)
|
||
if now.Before(unlockAt) {
|
||
return // 未到解锁时间,不创建订单、不更新 current_section_id
|
||
}
|
||
case camp.TimeIntervalTypeNaturalDay:
|
||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||
if loc == nil {
|
||
loc = intervalStart.Location()
|
||
}
|
||
prevDay := time.Date(intervalStart.In(loc).Year(), intervalStart.In(loc).Month(), intervalStart.In(loc).Day(), 0, 0, 0, 0, loc)
|
||
unlockDay := prevDay.AddDate(0, 0, int(nextSection.TimeIntervalValue))
|
||
if now.In(loc).Before(unlockDay) {
|
||
return
|
||
}
|
||
default:
|
||
// 其它类型不自动开启
|
||
return
|
||
}
|
||
}
|
||
|
||
owned, err := s.orderDAO.CheckUserHasSection(userID, nextSection.ID)
|
||
if err == nil && owned {
|
||
// 下一小节已拥有(如重启后):仍须通过 CanUnlockSection 校验(时间/自然日等限制),通过后再推进 current_section_id
|
||
resp, err := s.CanUnlockSection(&camp.CanUnlockSectionRequest{
|
||
UserID: userID,
|
||
CampID: campID,
|
||
SectionID: nextSection.ID,
|
||
})
|
||
if err == nil && resp != nil && resp.CanUnlock && s.userCampDAO != nil {
|
||
_ = s.userCampDAO.UpdateCurrentSection(userID, campID, nextSection.ID)
|
||
}
|
||
return
|
||
}
|
||
if nextSection.PriceFen > 0 {
|
||
return
|
||
}
|
||
|
||
resp, err := s.CanUnlockSection(&camp.CanUnlockSectionRequest{
|
||
UserID: userID,
|
||
CampID: campID,
|
||
SectionID: nextSection.ID,
|
||
})
|
||
if err != nil || resp == nil || !resp.CanUnlock {
|
||
return
|
||
}
|
||
|
||
if s.orderService == nil {
|
||
return
|
||
}
|
||
_, _ = s.orderService.CreateOrder(context.Background(), &order.CreateOrderRequest{
|
||
UserID: userID,
|
||
CampID: campID,
|
||
SectionID: nextSection.ID,
|
||
})
|
||
}
|
||
|
||
// parseAllowNextWhileReviewing 从任务 condition JSON 中解析 allow_next_while_reviewing(审核中是否允许开启下一任务),默认 true。
|
||
// 主观题已从管理端去掉该选项,不再下发该字段,缺省视为 true;申论题仍可配置。
|
||
func parseAllowNextWhileReviewing(condition json.RawMessage) bool {
|
||
if len(condition) == 0 {
|
||
return true
|
||
}
|
||
var raw map[string]any
|
||
if err := json.Unmarshal(condition, &raw); err != nil {
|
||
return true
|
||
}
|
||
// 根级别
|
||
if val, ok := raw["allow_next_while_reviewing"]; ok {
|
||
if b, ok := val.(bool); ok {
|
||
return b
|
||
}
|
||
}
|
||
// subjective
|
||
if sub, ok := raw["subjective"].(map[string]any); ok {
|
||
if val, ok := sub["allow_next_while_reviewing"]; ok {
|
||
if b, ok := val.(bool); ok {
|
||
return b
|
||
}
|
||
}
|
||
}
|
||
// essay
|
||
if essay, ok := raw["essay"].(map[string]any); ok {
|
||
if val, ok := essay["allow_next_while_reviewing"]; ok {
|
||
if b, ok := val.(bool); ok {
|
||
return b
|
||
}
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// getTaskTitle 获取任务标题(优先使用任务标题字段,否则按类型生成)
|
||
func (s *CampService) getTaskTitle(task *camp.Task) string {
|
||
if task != nil && strings.TrimSpace(task.Title) != "" {
|
||
return strings.TrimSpace(task.Title)
|
||
}
|
||
if task == nil {
|
||
return "未知任务"
|
||
}
|
||
switch task.TaskType {
|
||
case camp.TaskTypeImageText:
|
||
return "图文任务"
|
||
case camp.TaskTypeVideo:
|
||
return "视频任务"
|
||
case camp.TaskTypeSubjective:
|
||
return "主观题任务"
|
||
case camp.TaskTypeObjective:
|
||
return "客观题任务"
|
||
case camp.TaskTypeEssay:
|
||
return "申论题任务"
|
||
default:
|
||
return "未知任务"
|
||
}
|
||
}
|
||
|
||
// CanStartTask 检查用户是否可以开始指定任务(前置任务是否已完成)
|
||
func (s *CampService) CanStartTask(req *camp.CanStartTaskRequest) (*camp.CanStartTaskResponse, error) {
|
||
if req.UserID == "" || req.TaskID == "" {
|
||
return &camp.CanStartTaskResponse{
|
||
Success: true,
|
||
CanStart: false,
|
||
Reason: "参数不完整",
|
||
Message: "参数不完整",
|
||
}, nil
|
||
}
|
||
|
||
// 获取目标任务信息
|
||
task, err := s.taskDAO.GetByID(req.TaskID)
|
||
if err != nil || task == nil {
|
||
return &camp.CanStartTaskResponse{
|
||
Success: true,
|
||
CanStart: false,
|
||
Reason: "任务不存在",
|
||
Message: "任务不存在",
|
||
}, nil
|
||
}
|
||
|
||
// 获取同一小节下所有任务(按 id 升序)
|
||
allTasks, _, err := s.taskDAO.List("", "", task.SectionID, camp.TaskTypeUnknown, 1, 1000)
|
||
if err != nil {
|
||
return &camp.CanStartTaskResponse{
|
||
Success: false,
|
||
Message: fmt.Sprintf("获取任务列表失败: %v", err),
|
||
}, nil
|
||
}
|
||
|
||
// 找到目标任务在列表中的位置
|
||
targetIndex := -1
|
||
for i, t := range allTasks {
|
||
if t.ID == req.TaskID {
|
||
targetIndex = i
|
||
break
|
||
}
|
||
}
|
||
|
||
if targetIndex == -1 {
|
||
return &camp.CanStartTaskResponse{
|
||
Success: true,
|
||
CanStart: false,
|
||
Reason: "任务不在当前小节中",
|
||
Message: "任务不在当前小节中",
|
||
}, nil
|
||
}
|
||
|
||
// 第一个任务或未设置前置任务时,按“上一任务”顺序检查
|
||
if targetIndex == 0 {
|
||
return &camp.CanStartTaskResponse{
|
||
Success: true,
|
||
CanStart: true,
|
||
Message: "第一个任务,可以开始",
|
||
}, nil
|
||
}
|
||
|
||
// 若配置了前置任务(解锁关系),则必须完成该前置任务后才能开始
|
||
if task.PrerequisiteTaskID != "" {
|
||
prevProgress, _ := s.progressDAO.GetByUserAndTask(req.UserID, task.PrerequisiteTaskID)
|
||
if prevProgress == nil {
|
||
return &camp.CanStartTaskResponse{
|
||
Success: true,
|
||
CanStart: false,
|
||
Reason: "请先完成前置任务",
|
||
Message: "请先完成前置任务",
|
||
}, nil
|
||
}
|
||
if !prevProgress.IsCompleted {
|
||
return &camp.CanStartTaskResponse{
|
||
Success: true,
|
||
CanStart: false,
|
||
Reason: "请先完成前置任务",
|
||
Message: "请先完成前置任务",
|
||
}, nil
|
||
}
|
||
// 若前置任务需要审核,则需审核通过后才算“完成”
|
||
prevTask, _ := s.taskDAO.GetByID(task.PrerequisiteTaskID)
|
||
if prevTask != nil && (prevTask.TaskType == camp.TaskTypeSubjective || prevTask.TaskType == camp.TaskTypeEssay) {
|
||
if prevProgress.ReviewStatus != camp.ReviewStatusApproved && prevProgress.ReviewStatus != camp.ReviewStatusRejected {
|
||
return &camp.CanStartTaskResponse{
|
||
Success: true,
|
||
CanStart: false,
|
||
Reason: "前置任务审核中,请等待审核完成后继续",
|
||
Message: "前置任务审核中,请等待审核完成后继续",
|
||
}, nil
|
||
}
|
||
if prevProgress.ReviewStatus == camp.ReviewStatusRejected {
|
||
return &camp.CanStartTaskResponse{
|
||
Success: true,
|
||
CanStart: false,
|
||
Reason: "请先完成前置任务",
|
||
Message: "请先完成前置任务",
|
||
}, nil
|
||
}
|
||
}
|
||
return &camp.CanStartTaskResponse{
|
||
Success: true,
|
||
CanStart: true,
|
||
Message: "可以开始",
|
||
}, nil
|
||
}
|
||
|
||
// 未配置前置任务:默认可直接做,不按列表顺序要求上一任务
|
||
if task.PrerequisiteTaskID == "" {
|
||
return &camp.CanStartTaskResponse{
|
||
Success: true,
|
||
CanStart: true,
|
||
Message: "可以开始",
|
||
}, nil
|
||
}
|
||
|
||
return &camp.CanStartTaskResponse{
|
||
Success: true,
|
||
CanStart: true,
|
||
Message: "可以开始",
|
||
}, nil
|
||
}
|