first commit

This commit is contained in:
2026-09-11 01:41:52 +08:00
commit 9f22e6ccae
263 changed files with 28935 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
package v1
import (
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/middleware"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/service"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/app"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode"
"github.com/gin-gonic/gin"
)
// AdRewardHandler 广告激励额度接口。
type AdRewardHandler struct {
svc *service.AdRewardService
}
// NewAdRewardHandler 创建广告激励 handler。
func NewAdRewardHandler() *AdRewardHandler {
return &AdRewardHandler{svc: service.NewAdRewardService()}
}
// Grant 发放广告激励额度(当天有效,幂等防重复领取)。
func (h *AdRewardHandler) Grant(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.AdRewardGrantRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.Grant(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
+61
View File
@@ -0,0 +1,61 @@
package v1
import (
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/middleware"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/service"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/app"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode"
"github.com/gin-gonic/gin"
)
// AIRecommendHandler AI 穿搭推荐相关接口。
type AIRecommendHandler struct {
svc *service.AIRecommendService
}
// NewAIRecommendHandler 创建 AI 推荐 handler。
func NewAIRecommendHandler() *AIRecommendHandler {
return &AIRecommendHandler{svc: service.NewAIRecommendService()}
}
// Create 发起 AI 穿搭推荐。
func (h *AIRecommendHandler) Create(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.AIRecommendCreateRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.Create(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// Save 保存 AI 推荐穿搭到我的搭配。
func (h *AIRecommendHandler) Save(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.AIRecommendSaveRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
if e := h.svc.Save(c.Request.Context(), userID, &request); e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(nil)
}
+125
View File
@@ -0,0 +1,125 @@
package v1
import (
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/middleware"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/service"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/app"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode"
"github.com/gin-gonic/gin"
)
// ClosetShareHandler 衣柜分享相关接口。
type ClosetShareHandler struct {
svc *service.ClosetShareService
}
// NewClosetShareHandler 创建衣柜分享 handler。
func NewClosetShareHandler() *ClosetShareHandler {
return &ClosetShareHandler{svc: service.NewClosetShareService()}
}
// Create 创建衣柜分享。
func (h *ClosetShareHandler) Create(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.ClosetShareCreateRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.Create(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// List 分页查询我的分享。
func (h *ClosetShareHandler) List(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.ClosetShareListRequest
if err := c.ShouldBindQuery(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.List(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// ReceivedList 分页查询分享给我的分享。
func (h *ClosetShareHandler) ReceivedList(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.ClosetShareListRequest
if err := c.ShouldBindQuery(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.ListReceived(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// Detail 查看分享详情(uuid 来自路径参数)。
func (h *ClosetShareHandler) Detail(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
shareUUID := c.Param("uuid")
if shareUUID == "" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
out, e := h.svc.Detail(c.Request.Context(), userID, shareUUID)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// Cancel 取消分享(uuid 来自路径参数)。
func (h *ClosetShareHandler) Cancel(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
request := dto.ClosetShareCancelRequest{ShareUUID: c.Param("uuid")}
if request.ShareUUID == "" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
out, e := h.svc.Cancel(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
+158
View File
@@ -0,0 +1,158 @@
package v1
import (
"net/http"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/middleware"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/service"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/app"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode"
"github.com/gin-gonic/gin"
)
// ClothesHandler 衣物相关接口。
type ClothesHandler struct {
svc *service.ClothesService
}
// NewClothesHandler 创建衣物 handler。
func NewClothesHandler() *ClothesHandler {
return &ClothesHandler{svc: service.NewClothesService()}
}
// Upload 上传衣物图片(multipart,字段名 file)。
func (h *ClothesHandler) Upload(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
// 解析 multipart 前限制请求体大小(MaxSizeMB + 1MB 的 multipart 边界开销),
// 防止超大 body 在 FormFile 解析时耗尽内存/磁盘。
maxBytes := int64(global.UploadSetting.MaxSizeMB)*1024*1024 + 1024*1024
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxBytes)
fileHeader, err := c.FormFile("file")
if err != nil {
response.ToErrorResponse(errcode.UploadFileEmpty.WithDetails(err.Error()))
return
}
out, e := h.svc.Upload(c.Request.Context(), userID, fileHeader)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// List 分页查询当前用户衣物。
func (h *ClothesHandler) List(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.ClothesListRequest
if err := c.ShouldBindQuery(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.List(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// Get 获取衣物详情(uuid 来自路径参数)。
func (h *ClothesHandler) Get(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
request := dto.ClothesGetByUUIDRequest{ClothesUUID: c.Param("uuid")}
if request.ClothesUUID == "" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
out, e := h.svc.GetByUUID(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// Status 查询衣物图片处理状态(uuid 来自路径参数)。
func (h *ClothesHandler) Status(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
request := dto.ClothesProcessStatusGetByUUIDRequest{ClothesUUID: c.Param("uuid")}
if request.ClothesUUID == "" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
out, e := h.svc.ProcessStatusGetByUUID(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// Update 修改衣物属性(uuid 来自路径参数)。
func (h *ClothesHandler) Update(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.ClothesUpdateByUUIDRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
request.ClothesUUID = c.Param("uuid")
if request.ClothesUUID == "" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
out, e := h.svc.UpdateByUUID(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// Delete 删除衣物(uuid 来自路径参数)。
func (h *ClothesHandler) Delete(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
request := dto.ClothesDeleteByUUIDRequest{ClothesUUID: c.Param("uuid")}
if request.ClothesUUID == "" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
out, e := h.svc.DeleteByUUID(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
+83
View File
@@ -0,0 +1,83 @@
package v1
import (
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/middleware"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/service"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/app"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode"
"github.com/gin-gonic/gin"
)
// CollectHandler 收藏相关接口。
type CollectHandler struct {
svc *service.CollectService
}
// NewCollectHandler 创建收藏 handler。
func NewCollectHandler() *CollectHandler {
return &CollectHandler{svc: service.NewCollectService()}
}
// Collect 收藏/取消收藏社区穿搭。
func (h *CollectHandler) Collect(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.CollectRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.Collect(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// List 分页查询我收藏的穿搭。
func (h *CollectHandler) List(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.CollectListRequest
if err := c.ShouldBindQuery(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.List(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// GetByTarget 查询穿搭收藏状态与数量(targetUUID 来自路径参数)。
func (h *CollectHandler) GetByTarget(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
request := dto.CollectGetByTargetRequest{TargetUUID: c.Param("targetUUID")}
if request.TargetUUID == "" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
out, e := h.svc.GetByTarget(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
+51
View File
@@ -0,0 +1,51 @@
package v1
import (
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/service"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/app"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode"
"github.com/gin-gonic/gin"
)
// GoodsHandler 商品相关接口(公开)。
type GoodsHandler struct {
svc *service.GoodsService
}
// NewGoodsHandler 创建商品 handler。
func NewGoodsHandler() *GoodsHandler {
return &GoodsHandler{svc: service.NewGoodsService()}
}
// GetByCode 按商品编码查询商品(编码来自路径参数)。
func (h *GoodsHandler) GetByCode(c *gin.Context) {
response := app.NewResponse(c)
request := dto.GoodsGetByCodeRequest{GoodsCode: c.Param("code")}
if request.GoodsCode == "" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
out, e := h.svc.GetByCode(c.Request.Context(), &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// List 分页查询上架商品。
func (h *GoodsHandler) List(c *gin.Context) {
response := app.NewResponse(c)
var request dto.GoodsListRequest
if err := c.ShouldBindQuery(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.List(c.Request.Context(), &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
+35
View File
@@ -0,0 +1,35 @@
package v1
import (
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/middleware"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/service"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/app"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode"
"github.com/gin-gonic/gin"
)
// HoroscopeHandler 今日运势接口。
type HoroscopeHandler struct {
svc *service.HoroscopeService
}
// NewHoroscopeHandler 创建运势 handler。
func NewHoroscopeHandler() *HoroscopeHandler {
return &HoroscopeHandler{svc: service.NewHoroscopeService()}
}
// Today 获取当前用户今日运势。
func (h *HoroscopeHandler) Today(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
out, e := h.svc.GetToday(c.Request.Context(), userID)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
+62
View File
@@ -0,0 +1,62 @@
package v1
import (
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/middleware"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/service"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/app"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode"
"github.com/gin-gonic/gin"
)
// LikeHandler 点赞相关接口。
type LikeHandler struct {
svc *service.LikeService
}
// NewLikeHandler 创建点赞 handler。
func NewLikeHandler() *LikeHandler {
return &LikeHandler{svc: service.NewLikeService()}
}
// Like 点赞/取消点赞社区穿搭。
func (h *LikeHandler) Like(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.LikeCreateRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.LikeCreate(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// GetByTarget 查询穿搭点赞状态与数量(targetUUID 来自路径参数)。
func (h *LikeHandler) GetByTarget(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
request := dto.LikeGetByTargetIDRequest{TargetUUID: c.Param("targetUUID")}
if request.TargetUUID == "" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
out, e := h.svc.GetByTargetID(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
+99
View File
@@ -0,0 +1,99 @@
package v1
import (
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/middleware"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/service"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/app"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode"
"github.com/gin-gonic/gin"
)
// NotificationHandler 通知相关接口。
type NotificationHandler struct {
svc *service.NotificationService
}
// NewNotificationHandler 创建通知 handler。
func NewNotificationHandler() *NotificationHandler {
return &NotificationHandler{svc: service.NewNotificationService()}
}
// List 分页查询当前用户通知。
func (h *NotificationHandler) List(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.NotificationListRequest
if err := c.ShouldBindQuery(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.List(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// Unread 获取当前用户未读数。
func (h *NotificationHandler) Unread(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
out, e := h.svc.Unread(c.Request.Context(), userID)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// Get 获取通知详情(uuid 来自路径参数)。
func (h *NotificationHandler) Get(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
request := dto.NotificationGetByUUIDRequest{NotificationUUID: c.Param("uuid")}
if request.NotificationUUID == "" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
out, e := h.svc.GetByUUID(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// Read 标记通知已读(uuid 来自路径参数)。
func (h *NotificationHandler) Read(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
request := dto.NotificationReadRequest{NotificationUUID: c.Param("uuid")}
if request.NotificationUUID == "" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
out, e := h.svc.Read(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
+181
View File
@@ -0,0 +1,181 @@
package v1
import (
"io"
"net/http"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/middleware"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/service"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/app"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/wechat"
"github.com/gin-gonic/gin"
)
// OrderHandler 订单与退款相关接口。
type OrderHandler struct {
svc *service.OrderService
}
// NewOrderHandler 创建订单 handler。
func NewOrderHandler() *OrderHandler {
return &OrderHandler{svc: service.NewOrderService()}
}
// Create 创建订单并发起微信下单。
func (h *OrderHandler) Create(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.OrderCreateRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.Create(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// VirtualCreate 创建虚拟支付订单(小程序虚拟支付,覆盖 iOS/Android 全终端)。
func (h *OrderHandler) VirtualCreate(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.VirtualOrderCreateRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.VirtualOrderCreate(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// List 分页查询我的订单。
func (h *OrderHandler) List(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.OrderListRequest
if err := c.ShouldBindQuery(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.List(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// Cancel 取消订单(orderNo 来自路径参数)。
func (h *OrderHandler) Cancel(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
request := dto.OrderCancelRequest{OrderNo: c.Param("orderNo")}
if request.OrderNo == "" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
out, e := h.svc.Cancel(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// RefundApply 申请退款。
func (h *OrderHandler) RefundApply(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.RefundApplyRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.RefundApply(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// PayNotify 微信支付结果回调(公开,按微信规范返回受理结果)。
func (h *OrderHandler) PayNotify(c *gin.Context) {
if e := h.svc.PayNotify(c.Request.Context(), c.Request); e != nil {
wechatCallbackFail(c, e.Msg())
return
}
wechatCallbackSuccess(c)
}
// RefundNotify 微信退款结果回调(公开,按微信规范返回受理结果)。
func (h *OrderHandler) RefundNotify(c *gin.Context) {
if e := h.svc.RefundNotify(c.Request.Context(), c.Request); e != nil {
wechatCallbackFail(c, e.Msg())
return
}
wechatCallbackSuccess(c)
}
// XPayVerify 虚拟支付消息推送 URL 配置验证(GET):校验签名并原样回显 echostr。
func (h *OrderHandler) XPayVerify(c *gin.Context) {
timestamp := c.Query("timestamp")
nonce := c.Query("nonce")
signature := c.Query("signature")
echostr := c.Query("echostr")
if wechat.VerifyNotifyURL(timestamp, nonce, signature) {
c.String(http.StatusOK, echostr)
return
}
c.String(http.StatusUnauthorized, "invalid signature")
}
// XPayNotify 虚拟支付消息推送回调(POST,公开,按虚拟支付规范返回 ErrCode 应答)。
func (h *OrderHandler) XPayNotify(c *gin.Context) {
raw, err := io.ReadAll(c.Request.Body)
if err != nil {
c.Data(http.StatusOK, "application/json; charset=utf-8", wechat.BuildNotifyResponse(false))
return
}
resp := h.svc.XPayNotify(c.Request.Context(), raw)
c.Data(http.StatusOK, "application/json; charset=utf-8", resp)
}
// wechatCallbackSuccess 按微信规范返回受理成功,避免微信重复回调。
func wechatCallbackSuccess(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"})
}
// wechatCallbackFail 按微信规范返回受理失败,微信将按策略重试。
func wechatCallbackFail(c *gin.Context, message string) {
c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL", "message": message})
}
+156
View File
@@ -0,0 +1,156 @@
package v1
import (
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/middleware"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/service"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/app"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode"
"github.com/gin-gonic/gin"
)
// OutfitHandler 穿搭相关接口。
type OutfitHandler struct {
svc *service.OutfitService
}
// NewOutfitHandler 创建穿搭 handler。
func NewOutfitHandler() *OutfitHandler {
return &OutfitHandler{svc: service.NewOutfitService()}
}
// Create 创建穿搭。
func (h *OutfitHandler) Create(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.OutfitCreateRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.Create(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// List 分页查询我的穿搭。
func (h *OutfitHandler) List(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.OutfitListRequest
if err := c.ShouldBindQuery(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.List(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// Get 获取穿搭详情(uuid 来自路径参数)。
func (h *OutfitHandler) Get(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
request := dto.OutfitGetByUUIDRequest{OutfitUUID: c.Param("uuid")}
if request.OutfitUUID == "" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
out, e := h.svc.GetByUUID(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// Update 修改穿搭(uuid 来自路径参数)。
func (h *OutfitHandler) Update(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.OutfitUpdateByUUIDRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
request.OutfitUUID = c.Param("uuid")
if request.OutfitUUID == "" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
out, e := h.svc.UpdateByUUID(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// Delete 删除穿搭(uuid 来自路径参数)。
func (h *OutfitHandler) Delete(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
request := dto.OutfitDeleteRequest{OutfitUUID: c.Param("uuid")}
if request.OutfitUUID == "" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
out, e := h.svc.Delete(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// Public 设置穿搭公开状态(uuid 来自路径参数)。
func (h *OutfitHandler) Public(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.OutfitPublicRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
request.OutfitUUID = c.Param("uuid")
if request.OutfitUUID == "" {
response.ToErrorResponse(errcode.InvalidParams)
return
}
out, e := h.svc.Public(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
+57
View File
@@ -0,0 +1,57 @@
package v1
import (
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/middleware"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/service"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/app"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode"
"github.com/gin-gonic/gin"
)
// QuotaHandler 额度相关接口。
type QuotaHandler struct {
svc *service.QuotaService
}
// NewQuotaHandler 创建额度 handler。
func NewQuotaHandler() *QuotaHandler {
return &QuotaHandler{svc: service.NewQuotaService()}
}
// Info 获取当前用户额度概览。
func (h *QuotaHandler) Info(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
out, e := h.svc.InfoGetBySelf(c.Request.Context(), userID)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// FlowList 分页查询当前用户额度流水。
func (h *QuotaHandler) FlowList(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.QuotaFlowListRequest
if err := c.ShouldBindQuery(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.FlowList(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
+119
View File
@@ -0,0 +1,119 @@
// Package v1 提供 /api/v1 版本的 HTTP handler,负责请求绑定、鉴权透传与统一响应,
// 业务逻辑与归属校验均在 internal/service 层完成。
package v1
import (
"net/http"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/middleware"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/service"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/app"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode"
"github.com/gin-gonic/gin"
)
// UserHandler 用户相关接口。
type UserHandler struct {
svc *service.UserService
}
// NewUserHandler 创建用户 handler。
func NewUserHandler() *UserHandler {
return &UserHandler{svc: service.NewUserService()}
}
// WxLogin 微信登录(公开)。
func (h *UserHandler) WxLogin(c *gin.Context) {
response := app.NewResponse(c)
var request dto.UserWxLoginRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.WxLogin(c.Request.Context(), &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// RefreshToken 刷新访问令牌(公开)。
func (h *UserHandler) RefreshToken(c *gin.Context) {
response := app.NewResponse(c)
var request dto.UserRefreshTokenRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.RefreshToken(c.Request.Context(), &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// InfoGetBySelf 获取当前用户信息。
func (h *UserHandler) InfoGetBySelf(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
out, e := h.svc.InfoGetBySelf(c.Request.Context(), userID)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// ProfileUpdate 修改当前用户资料。
func (h *UserHandler) ProfileUpdate(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
var request dto.UserProfileUpdateBySelfRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
return
}
out, e := h.svc.ProfileUpdate(c.Request.Context(), userID, &request)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
// AvatarUpload 上传用户头像(multipart,字段名 file)。
func (h *UserHandler) AvatarUpload(c *gin.Context) {
response := app.NewResponse(c)
userID, ok := middleware.GetUserID(c.Request.Context())
if !ok {
response.ToErrorResponse(errcode.Unauthorized)
return
}
// 解析 multipart 前限制请求体大小(MaxSizeMB + 1MB 的 multipart 边界开销),
// 防止超大 body 在 FormFile 解析时耗尽内存/磁盘。
maxBytes := int64(global.UploadSetting.MaxSizeMB)*1024*1024 + 1024*1024
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxBytes)
fileHeader, err := c.FormFile("file")
if err != nil {
response.ToErrorResponse(errcode.UploadFileEmpty.WithDetails(err.Error()))
return
}
out, e := h.svc.AvatarUpload(c.Request.Context(), userID, fileHeader)
if e != nil {
response.ToErrorResponse(e)
return
}
response.ToResponse(out)
}
+133
View File
@@ -0,0 +1,133 @@
package routers
import (
"net/http"
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/middleware"
v1 "git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/routers/api/v1"
"github.com/gin-gonic/gin"
)
// NewRouter 构建 gin 引擎:注册中间件链与 /api/v1 路由。
func NewRouter() *gin.Engine {
r := gin.New()
// 中间件链:Recovery -> RequestID -> OTel -> Slog -> CORS
r.Use(
gin.Recovery(),
middleware.RequestID(),
middleware.GinOTelMiddleware(),
middleware.GinSlogMiddleware(),
middleware.CORS(),
)
// 健康检查
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong"})
})
// handler 实例
userHandler := v1.NewUserHandler()
quotaHandler := v1.NewQuotaHandler()
horoscopeHandler := v1.NewHoroscopeHandler()
clothesHandler := v1.NewClothesHandler()
outfitHandler := v1.NewOutfitHandler()
closetHandler := v1.NewClosetShareHandler()
collectHandler := v1.NewCollectHandler()
likeHandler := v1.NewLikeHandler()
notificationHandler := v1.NewNotificationHandler()
goodsHandler := v1.NewGoodsHandler()
orderHandler := v1.NewOrderHandler()
aiHandler := v1.NewAIRecommendHandler()
adRewardHandler := v1.NewAdRewardHandler()
apiV1 := r.Group("/api/v1")
// ===== 公开路由(无需登录)=====
{
apiV1.POST("/user/wxlogin", userHandler.WxLogin)
apiV1.POST("/user/refresh-token", userHandler.RefreshToken)
apiV1.GET("/goods", goodsHandler.List)
apiV1.GET("/goods/:code", goodsHandler.GetByCode)
// 微信支付/退款回调
apiV1.POST("/order/pay/notify", orderHandler.PayNotify)
apiV1.POST("/order/refund/notify", orderHandler.RefundNotify)
// 虚拟支付消息推送(GET 为 URL 配置验证,POST 为业务回调)
apiV1.GET("/xpay/notify", orderHandler.XPayVerify)
apiV1.POST("/xpay/notify", orderHandler.XPayNotify)
}
// ===== 鉴权路由(需登录)=====
auth := apiV1.Group("")
auth.Use(middleware.JWTAuth())
{
// 用户
auth.GET("/user/info", userHandler.InfoGetBySelf)
auth.PUT("/user/profile", userHandler.ProfileUpdate)
auth.POST("/user/avatar/upload", userHandler.AvatarUpload)
// 额度
auth.GET("/quota/info", quotaHandler.Info)
auth.GET("/quota/flow", quotaHandler.FlowList)
// 运势
auth.GET("/horoscope/today", horoscopeHandler.Today)
// 衣物
auth.POST("/clothes/upload", clothesHandler.Upload)
auth.GET("/clothes", clothesHandler.List)
auth.GET("/clothes/:uuid", clothesHandler.Get)
auth.GET("/clothes/:uuid/status", clothesHandler.Status)
auth.PUT("/clothes/:uuid", clothesHandler.Update)
auth.DELETE("/clothes/:uuid", clothesHandler.Delete)
// 穿搭
auth.POST("/outfit", outfitHandler.Create)
auth.GET("/outfit", outfitHandler.List)
auth.GET("/outfit/:uuid", outfitHandler.Get)
auth.PUT("/outfit/:uuid", outfitHandler.Update)
auth.DELETE("/outfit/:uuid", outfitHandler.Delete)
auth.PUT("/outfit/:uuid/public", outfitHandler.Public)
// 衣柜分享
auth.POST("/closet-share", closetHandler.Create)
auth.GET("/closet-share", closetHandler.List)
auth.GET("/closet-share/received", closetHandler.ReceivedList)
auth.GET("/closet-share/:uuid", closetHandler.Detail)
auth.POST("/closet-share/:uuid/cancel", closetHandler.Cancel)
// 收藏
auth.POST("/collect", collectHandler.Collect)
auth.GET("/collect", collectHandler.List)
auth.GET("/collect/:targetUUID", collectHandler.GetByTarget)
// 点赞
auth.POST("/like", likeHandler.Like)
auth.GET("/like/:targetUUID", likeHandler.GetByTarget)
// 通知
auth.GET("/notification", notificationHandler.List)
auth.GET("/notification/unread", notificationHandler.Unread)
auth.GET("/notification/:uuid", notificationHandler.Get)
auth.POST("/notification/:uuid/read", notificationHandler.Read)
// 订单
auth.POST("/order", orderHandler.Create)
auth.POST("/order/virtual", orderHandler.VirtualCreate)
auth.GET("/order", orderHandler.List)
auth.POST("/order/:orderNo/cancel", orderHandler.Cancel)
auth.POST("/order/refund", orderHandler.RefundApply)
// 广告激励额度
auth.POST("/ad-reward/grant", adRewardHandler.Grant)
// AI 推荐
auth.POST("/ai-recommend", aiHandler.Create)
auth.POST("/ai-recommend/save", aiHandler.Save)
}
return r
}