package httpapi import ( "database/sql" "errors" "strings" "time" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/compress" "github.com/gofiber/fiber/v3/middleware/cors" "github.com/gofiber/fiber/v3/middleware/recover" "github.com/rogeecn/wxapp-kouqiang-guahao/backend/internal/config" "github.com/rogeecn/wxapp-kouqiang-guahao/backend/internal/service" "github.com/sirupsen/logrus" ) func New(cfg config.Config, svc *service.Service, log *logrus.Logger) *fiber.App { wechatPhone := newWeChatPhoneClient(cfg) app := fiber.New(fiber.Config{ AppName: "骨安通 API", BodyLimit: 8 * 1024 * 1024, ErrorHandler: errorHandler, }) app.Use(recover.New()) app.Use(compress.New()) app.Use(cors.New(cors.Config{ AllowOrigins: splitList(cfg.AllowOrigins), AllowHeaders: []string{ fiber.HeaderOrigin, fiber.HeaderContentType, fiber.HeaderAccept, fiber.HeaderAuthorization, }, AllowMethods: []string{ fiber.MethodGet, fiber.MethodPost, fiber.MethodPatch, fiber.MethodDelete, fiber.MethodOptions, }, })) app.Use(logRequests(log)) app.Get("/healthz", func(c fiber.Ctx) error { return c.JSON(fiber.Map{"ok": true, "time": time.Now().Format(time.RFC3339)}) }) api := app.Group("/api") api.Post("/auth/wechat/session", func(c fiber.Ctx) error { var req struct { Code string `json:"code"` } if err := bindBody(c, &req); err != nil { return err } if strings.TrimSpace(req.Code) == "" { return badRequest("login code is required") } openid, err := wechatPhone.LoginOpenID(c.Context(), req.Code) if err != nil { log.WithError(err).Error("wechat login exchange failed") var configErr missingWeChatConfigError if errors.As(err, &configErr) { return fiber.NewError(500, err.Error()) } return fiber.NewError(502, err.Error()) } session, err := svc.Login(c.Context(), openid) if err != nil { return err } return c.JSON(session) }) api.Post("/auth/wechat/phone", func(c fiber.Ctx) error { var req struct { OpenID string `json:"openid"` Phone string `json:"phone"` PhoneCode string `json:"phoneCode"` } if err := bindBody(c, &req); err != nil { return err } phone := strings.TrimSpace(req.Phone) phoneCode := strings.TrimSpace(req.PhoneCode) log.WithFields(logrus.Fields{ "openid": maskIdentifierForLog(req.OpenID), "has_openid": strings.TrimSpace(req.OpenID) != "", "has_phone": phone != "", "phone": maskPhoneForLog(phone), "has_phone_code": phoneCode != "", "phone_code_len": len(phoneCode), "ip": c.IP(), }).Info("wechat phone bind request") if phone == "" { if phoneCode != "" { exchangedPhone, err := wechatPhone.PhoneNumber(c.Context(), phoneCode) if err != nil { log.WithFields(logrus.Fields{ "openid": maskIdentifierForLog(req.OpenID), "phone_code_len": len(phoneCode), }).WithError(err).Error("wechat phone exchange failed") var configErr missingWeChatConfigError if errors.As(err, &configErr) { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } return fiber.NewError(fiber.StatusBadGateway, err.Error()) } phone = exchangedPhone log.WithFields(logrus.Fields{ "openid": maskIdentifierForLog(req.OpenID), "phone_code_len": len(phoneCode), "phone": maskPhoneForLog(phone), }).Info("wechat phone exchange success") } } if phone == "" { log.WithField("openid", maskIdentifierForLog(req.OpenID)).Warn("wechat phone bind rejected: phone missing") return badRequest("phone is required") } user, err := svc.BindPhone(c.Context(), req.OpenID, phone) if err != nil { log.WithFields(logrus.Fields{ "openid": maskIdentifierForLog(req.OpenID), "phone": maskPhoneForLog(phone), }).WithError(err).Error("wechat phone bind failed") return err } log.WithFields(logrus.Fields{ "openid": maskIdentifierForLog(user.Openid), "user_id": user.ID, "phone": maskPhoneForLog(phone), }).Info("wechat phone bind success") return c.JSON(fiber.Map{"user": user}) }) api.Post("/price-inquiries", func(c fiber.Ctx) error { var req service.CreatePriceInquiryRequest if err := bindBody(c, &req); err != nil { return err } inquiry, err := svc.CreatePriceInquiry(c.Context(), req) if err != nil { if errors.Is(err, sql.ErrNoRows) { return badRequest("phone authorization is required") } if strings.Contains(err.Error(), "required") || strings.Contains(err.Error(), "unsupported") { return badRequest(err.Error()) } return err } log.WithFields(logrus.Fields{ "inquiry_id": inquiry.ID, "openid": maskIdentifierForLog(req.OpenID), "phone": maskPhoneForLog(inquiry.Phone), "province": inquiry.Province, "city": inquiry.City, "district": inquiry.District, "project_name": inquiry.ProjectName, "status": inquiry.Status, "created_at": inquiry.CreatedAt, "updated_at": inquiry.UpdatedAt, }).Info("price inquiry submitted") return c.Status(fiber.StatusCreated).JSON(fiber.Map{ "id": inquiry.ID, "status": inquiry.Status, "created_at": inquiry.CreatedAt, "updated_at": inquiry.UpdatedAt, }) }) adminUI := newAdminUI(cfg, svc) adminUI.registerPublic(app) admin := app.Group("/admin", adminUI.requireLogin) adminUI.registerPages(admin) return app } func errorHandler(c fiber.Ctx, err error) error { var e *fiber.Error if errors.As(err, &e) { return c.Status(e.Code).JSON(fiber.Map{"error": e.Message}) } return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } func badRequest(msg string) error { return fiber.NewError(fiber.StatusBadRequest, msg) } func bindBody(c fiber.Ctx, out any) error { if err := c.Bind().Body(out); err != nil { return badRequest(err.Error()) } return nil } func logRequests(log *logrus.Logger) fiber.Handler { return func(c fiber.Ctx) error { start := time.Now() err := c.Next() if err != nil { if handlerErr := c.App().Config().ErrorHandler(c, err); handlerErr != nil { return handlerErr } } entry := log.WithFields(logrus.Fields{ "method": c.Method(), "path": c.Path(), "status": c.Response().StatusCode(), "latency_ms": time.Since(start).Milliseconds(), }) if err != nil { entry.WithError(err).Error("request failed") } else { entry.Info("request") } return nil } } func maskPhoneForLog(phone string) string { phone = strings.TrimSpace(phone) if len(phone) < 7 { return phone } return phone[:3] + "****" + phone[len(phone)-4:] } func maskIdentifierForLog(value string) string { value = strings.TrimSpace(value) if len(value) <= 8 { return value } return value[:4] + "..." + value[len(value)-4:] } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" } func splitList(value string) []string { parts := strings.Split(value, ",") values := make([]string, 0, len(parts)) for _, part := range parts { if trimmed := strings.TrimSpace(part); trimmed != "" { values = append(values, trimmed) } } return values }