From 2c633dac0f160e52b18f878713257b9fae2062e6 Mon Sep 17 00:00:00 2001 From: Rogee Date: Tue, 30 Dec 2025 17:53:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BC=98=E6=83=A0=E5=88=B8=E5=88=97=E8=A1=A8=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=8F=8A=E7=9B=B8=E5=85=B3=E6=95=B0=E6=8D=AE=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api-spec.yaml | 124 ++++++++++++++++++++++++++++ backend/app/http/v1/dto/coupon.go | 14 ++++ backend/app/http/v1/routes.gen.go | 5 ++ backend/app/http/v1/user.go | 15 ++++ backend/app/services/coupon.go | 49 +++++++++++ backend/app/services/coupon_test.go | 2 +- backend/app/services/order.go | 1 - 7 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 backend/app/http/v1/dto/coupon.go diff --git a/api-spec.yaml b/api-spec.yaml index 3a21e42..036b3fb 100644 --- a/api-spec.yaml +++ b/api-spec.yaml @@ -286,6 +286,32 @@ components: realname: type: string + UserCouponItem: + type: object + properties: + id: + type: string + coupon_id: + type: string + title: + type: string + description: + type: string + type: + type: string + enum: [fix_amount, discount] + value: + type: integer + min_order_amount: + type: integer + start_at: + type: string + end_at: + type: string + status: + type: string + enum: [unused, used, expired] + # --- Upload --- UploadResult: type: object @@ -787,6 +813,84 @@ paths: time: type: string + /me/coupons: + get: + summary: List user coupons + parameters: + - name: status + in: query + schema: + type: string + enum: [unused, used, expired] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UserCouponItem' + + # ============================ + # Storage (Presigned) + # ============================ + /storage/{key}: + put: + summary: Upload file (Presigned) + tags: [Storage] + parameters: + - name: key + in: path + required: true + schema: + type: string + - name: expires + in: query + required: true + schema: + type: string + - name: sign + in: query + required: true + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '200': + description: Upload successful + get: + summary: Download file (Presigned) + tags: [Storage] + parameters: + - name: key + in: path + required: true + schema: + type: string + - name: expires + in: query + required: true + schema: + type: string + - name: sign + in: query + required: true + schema: + type: string + responses: + '200': + description: Download file + content: + application/octet-stream: + schema: + type: string + format: binary + # ============================ # Transaction # ============================ @@ -806,6 +910,8 @@ paths: quantity: type: integer default: 1 + user_coupon_id: + type: string responses: '200': content: @@ -864,6 +970,24 @@ paths: type: string enum: [unpaid, paid, completed] + /webhook/payment/notify: + post: + summary: Payment Webhook + tags: [Transaction] + requestBody: + content: + application/json: + schema: + type: object + properties: + order_id: + type: string + external_id: + type: string + responses: + '200': + description: Success + # ============================ # Creator Center # ============================ diff --git a/backend/app/http/v1/dto/coupon.go b/backend/app/http/v1/dto/coupon.go new file mode 100644 index 0000000..7873cc2 --- /dev/null +++ b/backend/app/http/v1/dto/coupon.go @@ -0,0 +1,14 @@ +package dto + +type UserCouponItem struct { + ID string `json:"id"` + CouponID string `json:"coupon_id"` + Title string `json:"title"` + Description string `json:"description"` + Type string `json:"type"` + Value int64 `json:"value"` + MinOrderAmount int64 `json:"min_order_amount"` + StartAt string `json:"start_at"` + EndAt string `json:"end_at"` + Status string `json:"status"` +} diff --git a/backend/app/http/v1/routes.gen.go b/backend/app/http/v1/routes.gen.go index 214ab61..971eafe 100644 --- a/backend/app/http/v1/routes.gen.go +++ b/backend/app/http/v1/routes.gen.go @@ -237,6 +237,11 @@ func (r *Routes) Register(router fiber.Router) { router.Get("/v1/me"[len(r.Path()):], DataFunc0( r.user.Me, )) + r.log.Debugf("Registering route: Get /v1/me/coupons -> user.MyCoupons") + router.Get("/v1/me/coupons"[len(r.Path()):], DataFunc1( + r.user.MyCoupons, + QueryParam[string]("status"), + )) r.log.Debugf("Registering route: Get /v1/me/favorites -> user.Favorites") router.Get("/v1/me/favorites"[len(r.Path()):], DataFunc0( r.user.Favorites, diff --git a/backend/app/http/v1/user.go b/backend/app/http/v1/user.go index b306723..e74b9e7 100644 --- a/backend/app/http/v1/user.go +++ b/backend/app/http/v1/user.go @@ -241,3 +241,18 @@ func (u *User) Following(ctx fiber.Ctx) ([]dto.TenantProfile, error) { func (u *User) Notifications(ctx fiber.Ctx, typeArg string, page int) (*requests.Pager, error) { return services.Notification.List(ctx.Context(), page, typeArg) } + +// List my coupons +// +// @Router /v1/me/coupons [get] +// @Summary List coupons +// @Description List my coupons +// @Tags UserCenter +// @Accept json +// @Produce json +// @Param status query string false "Status (unused, used, expired)" +// @Success 200 {array} dto.UserCouponItem +// @Bind status query +func (u *User) MyCoupons(ctx fiber.Ctx, status string) ([]dto.UserCouponItem, error) { + return services.Coupon.ListUserCoupons(ctx.Context(), status) +} diff --git a/backend/app/services/coupon.go b/backend/app/services/coupon.go index b3ef7b4..44d5d05 100644 --- a/backend/app/services/coupon.go +++ b/backend/app/services/coupon.go @@ -5,12 +5,61 @@ import ( "time" "quyun/v2/app/errorx" + coupon_dto "quyun/v2/app/http/v1/dto" "quyun/v2/database/models" + "quyun/v2/pkg/consts" + + "github.com/spf13/cast" ) // @provider type coupon struct{} +func (s *coupon) ListUserCoupons(ctx context.Context, status string) ([]coupon_dto.UserCouponItem, error) { + userID := ctx.Value(consts.CtxKeyUser) + if userID == nil { + return nil, errorx.ErrUnauthorized + } + uid := cast.ToInt64(userID) + + tbl, q := models.UserCouponQuery.QueryContext(ctx) + q = q.Where(tbl.UserID.Eq(uid)) + if status != "" { + q = q.Where(tbl.Status.Eq(status)) + } + + list, err := q.Order(tbl.CreatedAt.Desc()).Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + var res []coupon_dto.UserCouponItem + for _, v := range list { + c, _ := models.CouponQuery.WithContext(ctx).Where(models.CouponQuery.ID.Eq(v.CouponID)).First() + + item := coupon_dto.UserCouponItem{ + ID: cast.ToString(v.ID), + CouponID: cast.ToString(v.CouponID), + Status: v.Status, + } + if c != nil { + item.Title = c.Title + item.Description = c.Description + item.Type = c.Type + item.Value = c.Value + item.MinOrderAmount = c.MinOrderAmount + if !c.StartAt.IsZero() { + item.StartAt = c.StartAt.Format(time.RFC3339) + } + if !c.EndAt.IsZero() { + item.EndAt = c.EndAt.Format(time.RFC3339) + } + } + res = append(res, item) + } + return res, nil +} + // Validate checks if a coupon can be used for an order and returns the discount amount func (s *coupon) Validate(ctx context.Context, userID, userCouponID, amount int64) (int64, error) { uc, err := models.UserCouponQuery.WithContext(ctx).Where(models.UserCouponQuery.ID.Eq(userCouponID)).First() diff --git a/backend/app/services/coupon_test.go b/backend/app/services/coupon_test.go index 1adc572..72952a5 100644 --- a/backend/app/services/coupon_test.go +++ b/backend/app/services/coupon_test.go @@ -108,4 +108,4 @@ func (s *CouponTestSuite) Test_CouponFlow() { So(ucReload.OrderID, ShouldEqual, oid) }) }) -} \ No newline at end of file +} diff --git a/backend/app/services/order.go b/backend/app/services/order.go index e978e2d..232dd78 100644 --- a/backend/app/services/order.go +++ b/backend/app/services/order.go @@ -157,7 +157,6 @@ func (s *order) Create(ctx context.Context, form *transaction_dto.OrderCreateFor return nil }) - if err != nil { if _, ok := err.(*errorx.AppError); ok { return nil, err