package handler import ( "dd_fiber_api/internal/camp" camp_dao "dd_fiber_api/internal/camp/dao" "dd_fiber_api/internal/camp/service" "dd_fiber_api/internal/order" order_dao "dd_fiber_api/internal/order/dao" order_service "dd_fiber_api/internal/order/service" "github.com/gofiber/fiber/v2" ) // SectionHandler 小节处理器 type SectionHandler struct { sectionService *service.SectionService orderService *order_service.OrderService // 订单服务(用于购买小节) orderDAO *order_dao.OrderDAO // 订单DAO(用于查询是否已拥有) userCampDAO *camp_dao.UserCampDAO // 用户打卡营 DAO(用于更新当前小节 is_current) } // NewSectionHandler 创建小节处理器 func NewSectionHandler(sectionService *service.SectionService) *SectionHandler { return &SectionHandler{ sectionService: sectionService, } } // SetOrderService 设置订单服务 func (h *SectionHandler) SetOrderService(orderService *order_service.OrderService) { h.orderService = orderService } // SetOrderDAO 设置订单DAO func (h *SectionHandler) SetOrderDAO(orderDAO *order_dao.OrderDAO) { h.orderDAO = orderDAO } // SetUserCampDAO 设置用户打卡营 DAO(用于开启小节时更新 current_section_id,保证 is_current 只有最新小节为 true) func (h *SectionHandler) SetUserCampDAO(userCampDAO *camp_dao.UserCampDAO) { h.userCampDAO = userCampDAO } // CreateSection 创建小节 func (h *SectionHandler) CreateSection(c *fiber.Ctx) error { // 先解析为 map 以处理 time_interval_type 的数字到字符串转换 var rawReq map[string]interface{} if err := c.BodyParser(&rawReq); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": "请求参数解析失败: " + err.Error(), }) } // 转换 time_interval_type:如果前端传的是数字,转换为字符串 if timeIntervalTypeVal, ok := rawReq["time_interval_type"]; ok { switch v := timeIntervalTypeVal.(type) { case float64: // JSON 解析数字为 float64 switch int(v) { case 0: rawReq["time_interval_type"] = "none" case 1: rawReq["time_interval_type"] = "hour" case 2: rawReq["time_interval_type"] = "natural_day" case 3: rawReq["time_interval_type"] = "paid" default: rawReq["time_interval_type"] = "none" } case int: switch v { case 0: rawReq["time_interval_type"] = "none" case 1: rawReq["time_interval_type"] = "hour" case 2: rawReq["time_interval_type"] = "natural_day" case 3: rawReq["time_interval_type"] = "paid" default: rawReq["time_interval_type"] = "none" } } } // 将转换后的 map 转换为 CreateSectionRequest var req camp.CreateSectionRequest if campID, ok := rawReq["camp_id"].(string); ok { req.CampID = campID } if title, ok := rawReq["title"].(string); ok { req.Title = title } if sectionNumber, ok := rawReq["section_number"].(float64); ok { req.SectionNumber = int32(sectionNumber) } if priceFen, ok := rawReq["price_fen"].(float64); ok { req.PriceFen = int32(priceFen) } // require_previous_section 已废弃,不再从请求中读取,创建时固定为 false if timeIntervalTypeStr, ok := rawReq["time_interval_type"].(string); ok { req.TimeIntervalType = camp.TimeIntervalType(timeIntervalTypeStr) } if timeIntervalValue, ok := rawReq["time_interval_value"].(float64); ok { req.TimeIntervalValue = int32(timeIntervalValue) } // 验证必填字段 if req.CampID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": "打卡营ID不能为空", }) } if req.Title == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": "小节标题不能为空", }) } // 验证 TimeIntervalType if req.TimeIntervalType != camp.TimeIntervalTypeNone && req.TimeIntervalType != camp.TimeIntervalTypeHour && req.TimeIntervalType != camp.TimeIntervalTypeNaturalDay && req.TimeIntervalType != camp.TimeIntervalTypePaid { req.TimeIntervalType = camp.TimeIntervalTypeNone } resp, err := h.sectionService.CreateSection(&req) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "success": false, "message": "创建小节失败: " + err.Error(), }) } if !resp.Success { return c.Status(fiber.StatusBadRequest).JSON(resp) } return c.Status(fiber.StatusOK).JSON(resp) } // GetSection 获取小节 func (h *SectionHandler) GetSection(c *fiber.Ctx) error { // 优先从查询参数获取,如果没有则从路径参数获取 id := c.Query("id") if id == "" { id = c.Params("id") } if id == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": "小节ID不能为空", }) } resp, err := h.sectionService.GetSection(id) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "success": false, "message": "获取小节失败: " + err.Error(), }) } if !resp.Success { return c.Status(fiber.StatusNotFound).JSON(resp) } return c.Status(fiber.StatusOK).JSON(resp) } // UpdateSection 更新小节 func (h *SectionHandler) UpdateSection(c *fiber.Ctx) error { // 先解析为 map 以处理 time_interval_type 的数字到字符串转换 var rawReq map[string]interface{} if err := c.BodyParser(&rawReq); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": "请求参数解析失败: " + err.Error(), }) } // 转换 time_interval_type:如果前端传的是数字,转换为字符串 if timeIntervalTypeVal, ok := rawReq["time_interval_type"]; ok { switch v := timeIntervalTypeVal.(type) { case float64: // JSON 解析数字为 float64 switch int(v) { case 0: rawReq["time_interval_type"] = "none" case 1: rawReq["time_interval_type"] = "hour" case 2: rawReq["time_interval_type"] = "natural_day" case 3: rawReq["time_interval_type"] = "paid" default: rawReq["time_interval_type"] = "none" } case int: switch v { case 0: rawReq["time_interval_type"] = "none" case 1: rawReq["time_interval_type"] = "hour" case 2: rawReq["time_interval_type"] = "natural_day" case 3: rawReq["time_interval_type"] = "paid" default: rawReq["time_interval_type"] = "none" } } } // 将转换后的 map 转换为 UpdateSectionRequest var req camp.UpdateSectionRequest if id, ok := rawReq["id"].(string); ok { req.ID = id } if campID, ok := rawReq["camp_id"].(string); ok { req.CampID = campID } if title, ok := rawReq["title"].(string); ok { req.Title = title } if sectionNumber, ok := rawReq["section_number"].(float64); ok { req.SectionNumber = int32(sectionNumber) } if priceFen, ok := rawReq["price_fen"].(float64); ok { req.PriceFen = int32(priceFen) } // require_previous_section 已废弃,不再从请求中读取,更新时保留原值 if timeIntervalTypeStr, ok := rawReq["time_interval_type"].(string); ok { req.TimeIntervalType = camp.TimeIntervalType(timeIntervalTypeStr) } if timeIntervalValue, ok := rawReq["time_interval_value"].(float64); ok { req.TimeIntervalValue = int32(timeIntervalValue) } // 从 URL 参数获取 ID(如果请求体中没有) if req.ID == "" { req.ID = c.Query("id") if req.ID == "" { req.ID = c.Params("id") } } if req.ID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": "小节ID不能为空", }) } // 验证 TimeIntervalType if req.TimeIntervalType != camp.TimeIntervalTypeNone && req.TimeIntervalType != camp.TimeIntervalTypeHour && req.TimeIntervalType != camp.TimeIntervalTypeNaturalDay && req.TimeIntervalType != camp.TimeIntervalTypePaid { req.TimeIntervalType = camp.TimeIntervalTypeNone } resp, err := h.sectionService.UpdateSection(&req) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "success": false, "message": "更新小节失败: " + err.Error(), }) } if !resp.Success { return c.Status(fiber.StatusBadRequest).JSON(resp) } return c.Status(fiber.StatusOK).JSON(resp) } // DeleteSection 删除小节 func (h *SectionHandler) DeleteSection(c *fiber.Ctx) error { // 优先从查询参数获取,如果没有则从路径参数获取 id := c.Query("id") if id == "" { id = c.Params("id") } if id == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": "小节ID不能为空", }) } resp, err := h.sectionService.DeleteSection(id) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "success": false, "message": "删除小节失败: " + err.Error(), }) } if !resp.Success { return c.Status(fiber.StatusNotFound).JSON(resp) } return c.Status(fiber.StatusOK).JSON(resp) } // ListSections 列出小节 func (h *SectionHandler) ListSections(c *fiber.Ctx) error { var req camp.ListSectionsRequest if err := c.QueryParser(&req); err != nil { req.Page = 1 req.PageSize = 10 } // user_id 是可选参数,如果前端传递了则使用,否则为空字符串 // 这样 service 层可以根据 user_id 获取用户相关的状态信息 resp, err := h.sectionService.ListSections(&req) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "success": false, "message": "获取小节列表失败: " + err.Error(), }) } return c.Status(fiber.StatusOK).JSON(resp) } // PurchaseSection 购买小节(用于开启打卡营后的第一小节等场景) func (h *SectionHandler) PurchaseSection(c *fiber.Ctx) error { var req camp.PurchaseSectionRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": "请求参数解析失败: " + err.Error(), }) } // 验证必填字段 if req.UserID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": "用户ID不能为空", }) } if req.CampID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": "打卡营ID不能为空", }) } if req.SectionID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": "小节ID不能为空", }) } // 检查订单服务是否已设置 if h.orderService == nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "success": false, "message": "订单服务未初始化", }) } // 获取小节信息,检查价格 sectionResp, err := h.sectionService.GetSection(req.SectionID) if err != nil || !sectionResp.Success { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": "获取小节信息失败", }) } section := sectionResp.Section if section == nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": "小节不存在", }) } // 验证小节是否属于指定的打卡营 if section.CampID != req.CampID { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": "小节不属于指定的打卡营", }) } // 检查是否已拥有该小节(参考 gRPC 版本的逻辑) if h.orderDAO != nil { alreadyOwned, err := h.orderDAO.CheckUserHasSection(req.UserID, req.SectionID) if err == nil && alreadyOwned { // 已拥有:幂等返回,同时将当前小节更新为该节,保证 is_current 只有最新开启的小节为 true if h.userCampDAO != nil { _ = h.userCampDAO.UpdateCurrentSection(req.UserID, req.CampID, req.SectionID) } return c.Status(fiber.StatusOK).JSON(camp.PurchaseSectionResponse{ Success: true, Message: "已拥有", AlreadyOwned: true, }) } } // 无论免费还是付费,都创建订单(参考 gRPC 版本的逻辑) // 如果小节价格为0(免费),直接创建已支付的订单 if section.PriceFen == 0 { // 创建免费订单 orderReq := &order.CreateOrderRequest{ UserID: req.UserID, CampID: req.CampID, SectionID: req.SectionID, PaymentMethod: order.PaymentMethodUnknown, // 免费订单不需要支付方式 } orderResp, err := h.orderService.CreateOrder(c.Context(), orderReq) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "success": false, "message": "创建订单失败: " + err.Error(), }) } if !orderResp.Success { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": orderResp.Message, }) } return c.Status(fiber.StatusOK).JSON(camp.PurchaseSectionResponse{ Success: true, Message: "购买成功(免费)", OrderID: orderResp.OrderID, OrderStatus: string(orderResp.OrderStatus), ActualAmount: orderResp.ActualAmount, IsFree: true, }) } // 如果需要付费,也创建订单(参考 gRPC 版本的逻辑) // 创建待支付订单 orderReq := &order.CreateOrderRequest{ UserID: req.UserID, CampID: req.CampID, SectionID: req.SectionID, PaymentMethod: order.PaymentMethodUnknown, // 待支付订单,后续需要调用支付接口 } orderResp, err := h.orderService.CreateOrder(c.Context(), orderReq) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "success": false, "message": "创建订单失败: " + err.Error(), }) } if !orderResp.Success { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": orderResp.Message, }) } return c.Status(fiber.StatusOK).JSON(camp.PurchaseSectionResponse{ Success: true, Message: "订单创建成功,请完成支付", OrderID: orderResp.OrderID, OrderStatus: string(orderResp.OrderStatus), ActualAmount: orderResp.ActualAmount, IsFree: false, }) }