diff --git a/backend/app/middlewares/middlewares.go b/backend/app/middlewares/middlewares.go
index 6da38f0..78b243e 100644
--- a/backend/app/middlewares/middlewares.go
+++ b/backend/app/middlewares/middlewares.go
@@ -64,9 +64,6 @@ func (m *Middlewares) authenticate(ctx fiber.Ctx, requireToken bool) error {
if user.Status == consts.UserStatusBanned {
return errorx.ErrAccountDisabled
}
- if user.Status == consts.UserStatusBanned {
- return errorx.ErrAccountDisabled
- }
// Set Context
ctx.Locals(consts.CtxKeyUser, user)
@@ -104,6 +101,9 @@ func (m *Middlewares) SuperAuth(ctx fiber.Ctx) error {
if err != nil {
return errorx.ErrUnauthorized.WithCause(err).WithMsg("UserNotFound")
}
+ if user.Status == consts.UserStatusBanned {
+ return errorx.ErrAccountDisabled
+ }
if !hasRole(user.Roles, consts.RoleSuperAdmin) {
return errorx.ErrForbidden.WithMsg("无权限访问")
diff --git a/backend/app/services/super.go b/backend/app/services/super.go
index be018f3..ff1db34 100644
--- a/backend/app/services/super.go
+++ b/backend/app/services/super.go
@@ -27,12 +27,22 @@ type super struct {
}
func (s *super) Login(ctx context.Context, form *super_dto.LoginForm) (*super_dto.LoginResponse, error) {
- tbl, q := models.UserQuery.QueryContext(ctx)
- u, err := q.Where(tbl.Username.Eq(form.Username)).First()
- if err != nil {
- return nil, errorx.ErrInvalidCredentials.WithMsg("账号或密码错误")
+ if form == nil {
+ return nil, errorx.ErrInvalidParameter.WithMsg("登录参数不能为空")
}
- if u.Password != form.Password {
+ username := strings.TrimSpace(form.Username)
+ password := strings.TrimSpace(form.Password)
+ if username == "" || password == "" {
+ return nil, errorx.ErrInvalidParameter.WithMsg("账号或密码不能为空")
+ }
+
+ // 校验账号与权限。
+ tbl, q := models.UserQuery.QueryContext(ctx)
+ u, err := q.Where(tbl.Username.Eq(username)).First()
+ if err != nil {
+ return nil, errorx.ErrInvalidCredentials.WithCause(err).WithMsg("账号或密码错误")
+ }
+ if u.Password != password {
return nil, errorx.ErrInvalidCredentials.WithMsg("账号或密码错误")
}
if u.Status == consts.UserStatusBanned {
@@ -42,11 +52,12 @@ func (s *super) Login(ctx context.Context, form *super_dto.LoginForm) (*super_dt
return nil, errorx.ErrForbidden.WithMsg("无权限访问")
}
+ // 生成登录令牌。
token, err := s.jwt.CreateToken(s.jwt.CreateClaims(jwt_provider.BaseClaims{
UserID: u.ID,
}))
if err != nil {
- return nil, errorx.ErrInternalError.WithMsg("生成令牌失败")
+ return nil, errorx.ErrInternalError.WithCause(err).WithMsg("生成令牌失败")
}
return &super_dto.LoginResponse{
@@ -68,7 +79,10 @@ func (s *super) CheckToken(ctx context.Context, token string) (*super_dto.LoginR
tbl, q := models.UserQuery.QueryContext(ctx)
u, err := q.Where(tbl.ID.Eq(claims.UserID)).First()
if err != nil {
- return nil, errorx.ErrUnauthorized.WithMsg("UserNotFound")
+ return nil, errorx.ErrUnauthorized.WithCause(err).WithMsg("UserNotFound")
+ }
+ if u.Status == consts.UserStatusBanned {
+ return nil, errorx.ErrAccountDisabled
}
if !hasRole(u.Roles, consts.RoleSuperAdmin) {
return nil, errorx.ErrForbidden.WithMsg("无权限访问")
@@ -78,7 +92,7 @@ func (s *super) CheckToken(ctx context.Context, token string) (*super_dto.LoginR
UserID: u.ID,
}))
if err != nil {
- return nil, errorx.ErrInternalError.WithMsg("生成令牌失败")
+ return nil, errorx.ErrInternalError.WithCause(err).WithMsg("生成令牌失败")
}
return &super_dto.LoginResponse{
diff --git a/backend/app/services/super_test.go b/backend/app/services/super_test.go
index 4762761..20bb667 100644
--- a/backend/app/services/super_test.go
+++ b/backend/app/services/super_test.go
@@ -16,6 +16,7 @@ import (
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/suite"
"go.ipao.vip/atom/contracts"
+ "go.ipao.vip/gen/types"
"go.uber.org/dig"
)
@@ -74,6 +75,60 @@ func (s *SuperTestSuite) Test_ListUsers() {
})
}
+func (s *SuperTestSuite) Test_LoginAndCheckToken() {
+ Convey("Login and CheckToken", s.T(), func() {
+ ctx := s.T().Context()
+ database.Truncate(ctx, s.DB, models.TableNameUser)
+
+ admin := &models.User{
+ Username: "super_admin",
+ Password: "pass123",
+ Roles: types.Array[consts.Role]{consts.RoleSuperAdmin},
+ Status: consts.UserStatusVerified,
+ }
+ normal := &models.User{
+ Username: "normal_user",
+ Password: "pass123",
+ Status: consts.UserStatusVerified,
+ }
+ models.UserQuery.WithContext(ctx).Create(admin, normal)
+
+ Convey("should login as super admin", func() {
+ res, err := Super.Login(ctx, &super_dto.LoginForm{
+ Username: admin.Username,
+ Password: admin.Password,
+ })
+ So(err, ShouldBeNil)
+ So(res, ShouldNotBeNil)
+ So(res.Token, ShouldNotBeBlank)
+ So(res.User.ID, ShouldEqual, admin.ID)
+ })
+
+ Convey("should reject non-super admin", func() {
+ _, err := Super.Login(ctx, &super_dto.LoginForm{
+ Username: normal.Username,
+ Password: normal.Password,
+ })
+ So(err, ShouldNotBeNil)
+ })
+
+ Convey("should refresh token", func() {
+ loginRes, err := Super.Login(ctx, &super_dto.LoginForm{
+ Username: admin.Username,
+ Password: admin.Password,
+ })
+ So(err, ShouldBeNil)
+
+ token := "Bearer " + loginRes.Token
+ checkRes, err := Super.CheckToken(ctx, token)
+ So(err, ShouldBeNil)
+ So(checkRes, ShouldNotBeNil)
+ So(checkRes.Token, ShouldNotBeBlank)
+ So(checkRes.User.ID, ShouldEqual, admin.ID)
+ })
+ })
+}
+
func (s *SuperTestSuite) Test_CreateTenant() {
Convey("CreateTenant", s.T(), func() {
ctx := s.T().Context()
diff --git a/backend/docs/docs.go b/backend/docs/docs.go
index 96173b2..aca6f7b 100644
--- a/backend/docs/docs.go
+++ b/backend/docs/docs.go
@@ -1708,6 +1708,208 @@ const docTemplate = `{
}
}
},
+ "/t/{tenantCode}/v1/creator/coupons": {
+ "get": {
+ "description": "List coupon templates",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "CreatorCenter"
+ ],
+ "summary": "List coupons",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "Page",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Limit",
+ "name": "limit",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Type (fix_amount/discount)",
+ "name": "type",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Status (active/expired)",
+ "name": "status",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Keyword",
+ "name": "keyword",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/requests.Pager"
+ }
+ }
+ }
+ },
+ "post": {
+ "description": "Create coupon template",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "CreatorCenter"
+ ],
+ "summary": "Create coupon",
+ "parameters": [
+ {
+ "description": "Coupon form",
+ "name": "form",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/dto.CouponCreateForm"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/dto.CouponItem"
+ }
+ }
+ }
+ }
+ },
+ "/t/{tenantCode}/v1/creator/coupons/{id}": {
+ "get": {
+ "description": "Get coupon template detail",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "CreatorCenter"
+ ],
+ "summary": "Get coupon",
+ "parameters": [
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "Coupon ID",
+ "name": "id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/dto.CouponItem"
+ }
+ }
+ }
+ },
+ "put": {
+ "description": "Update coupon template",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "CreatorCenter"
+ ],
+ "summary": "Update coupon",
+ "parameters": [
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "Coupon ID",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Coupon form",
+ "name": "form",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/dto.CouponUpdateForm"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/dto.CouponItem"
+ }
+ }
+ }
+ }
+ },
+ "/t/{tenantCode}/v1/creator/coupons/{id}/grant": {
+ "post": {
+ "description": "Grant coupon to users",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "CreatorCenter"
+ ],
+ "summary": "Grant coupon",
+ "parameters": [
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "Coupon ID",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Grant form",
+ "name": "form",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/dto.CouponGrantForm"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Granted",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
"/t/{tenantCode}/v1/creator/dashboard": {
"get": {
"description": "Get creator dashboard stats",
@@ -2278,6 +2480,76 @@ const docTemplate = `{
}
}
},
+ "/t/{tenantCode}/v1/me/coupons/available": {
+ "get": {
+ "description": "List coupons available for the given order amount",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "UserCenter"
+ ],
+ "summary": "List available coupons",
+ "parameters": [
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "Order amount (cents)",
+ "name": "amount",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/dto.UserCouponItem"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/t/{tenantCode}/v1/me/coupons/receive": {
+ "post": {
+ "description": "Receive a coupon by coupon_id",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "UserCenter"
+ ],
+ "summary": "Receive coupon",
+ "parameters": [
+ {
+ "description": "Receive form",
+ "name": "form",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/dto.CouponReceiveForm"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/dto.UserCouponItem"
+ }
+ }
+ }
+ }
+ },
"/t/{tenantCode}/v1/me/favorites": {
"get": {
"description": "Get favorites",
@@ -4187,6 +4459,166 @@ const docTemplate = `{
}
}
},
+ "dto.CouponCreateForm": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "description": "Description 优惠券描述。",
+ "type": "string"
+ },
+ "end_at": {
+ "description": "EndAt 过期时间(RFC3339,可为空)。",
+ "type": "string"
+ },
+ "max_discount": {
+ "description": "MaxDiscount 折扣券最高抵扣金额(分)。",
+ "type": "integer"
+ },
+ "min_order_amount": {
+ "description": "MinOrderAmount 使用门槛金额(分)。",
+ "type": "integer"
+ },
+ "start_at": {
+ "description": "StartAt 生效时间(RFC3339,可为空)。",
+ "type": "string"
+ },
+ "title": {
+ "description": "Title 优惠券标题。",
+ "type": "string"
+ },
+ "total_quantity": {
+ "description": "TotalQuantity 发行总量(0 表示不限量)。",
+ "type": "integer"
+ },
+ "type": {
+ "description": "Type 优惠券类型(fix_amount/discount)。",
+ "type": "string"
+ },
+ "value": {
+ "description": "Value 优惠券面值(分/折扣百分比)。",
+ "type": "integer"
+ }
+ }
+ },
+ "dto.CouponGrantForm": {
+ "type": "object",
+ "properties": {
+ "user_ids": {
+ "description": "UserIDs 领取用户ID集合。",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "dto.CouponItem": {
+ "type": "object",
+ "properties": {
+ "created_at": {
+ "description": "CreatedAt 创建时间(RFC3339)。",
+ "type": "string"
+ },
+ "description": {
+ "description": "Description 优惠券描述。",
+ "type": "string"
+ },
+ "end_at": {
+ "description": "EndAt 过期时间(RFC3339)。",
+ "type": "string"
+ },
+ "id": {
+ "description": "ID 券模板ID。",
+ "type": "integer"
+ },
+ "max_discount": {
+ "description": "MaxDiscount 折扣券最高抵扣金额(分)。",
+ "type": "integer"
+ },
+ "min_order_amount": {
+ "description": "MinOrderAmount 使用门槛金额(分)。",
+ "type": "integer"
+ },
+ "start_at": {
+ "description": "StartAt 生效时间(RFC3339)。",
+ "type": "string"
+ },
+ "title": {
+ "description": "Title 优惠券标题。",
+ "type": "string"
+ },
+ "total_quantity": {
+ "description": "TotalQuantity 发行总量。",
+ "type": "integer"
+ },
+ "type": {
+ "description": "Type 优惠券类型(fix_amount/discount)。",
+ "type": "string"
+ },
+ "updated_at": {
+ "description": "UpdatedAt 更新时间(RFC3339)。",
+ "type": "string"
+ },
+ "used_quantity": {
+ "description": "UsedQuantity 已使用数量。",
+ "type": "integer"
+ },
+ "value": {
+ "description": "Value 优惠券面值(分/折扣百分比)。",
+ "type": "integer"
+ }
+ }
+ },
+ "dto.CouponReceiveForm": {
+ "type": "object",
+ "properties": {
+ "coupon_id": {
+ "description": "CouponID 券模板ID。",
+ "type": "integer"
+ }
+ }
+ },
+ "dto.CouponUpdateForm": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "description": "Description 优惠券描述(为空表示不修改)。",
+ "type": "string"
+ },
+ "end_at": {
+ "description": "EndAt 过期时间(RFC3339,可为空)。",
+ "type": "string"
+ },
+ "max_discount": {
+ "description": "MaxDiscount 折扣券最高抵扣金额(分)。",
+ "type": "integer"
+ },
+ "min_order_amount": {
+ "description": "MinOrderAmount 使用门槛金额(分)。",
+ "type": "integer"
+ },
+ "start_at": {
+ "description": "StartAt 生效时间(RFC3339,可为空)。",
+ "type": "string"
+ },
+ "title": {
+ "description": "Title 优惠券标题(为空表示不修改)。",
+ "type": "string"
+ },
+ "total_quantity": {
+ "description": "TotalQuantity 发行总量(0 表示不限量)。",
+ "type": "integer"
+ },
+ "type": {
+ "description": "Type 优惠券类型(fix_amount/discount)。",
+ "type": "string"
+ },
+ "value": {
+ "description": "Value 优惠券面值(分/折扣百分比)。",
+ "type": "integer"
+ }
+ }
+ },
"dto.DashboardStats": {
"type": "object",
"properties": {
diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json
index b3fed3a..eddaf88 100644
--- a/backend/docs/swagger.json
+++ b/backend/docs/swagger.json
@@ -1702,6 +1702,208 @@
}
}
},
+ "/t/{tenantCode}/v1/creator/coupons": {
+ "get": {
+ "description": "List coupon templates",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "CreatorCenter"
+ ],
+ "summary": "List coupons",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "Page",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "Limit",
+ "name": "limit",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Type (fix_amount/discount)",
+ "name": "type",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Status (active/expired)",
+ "name": "status",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Keyword",
+ "name": "keyword",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/requests.Pager"
+ }
+ }
+ }
+ },
+ "post": {
+ "description": "Create coupon template",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "CreatorCenter"
+ ],
+ "summary": "Create coupon",
+ "parameters": [
+ {
+ "description": "Coupon form",
+ "name": "form",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/dto.CouponCreateForm"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/dto.CouponItem"
+ }
+ }
+ }
+ }
+ },
+ "/t/{tenantCode}/v1/creator/coupons/{id}": {
+ "get": {
+ "description": "Get coupon template detail",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "CreatorCenter"
+ ],
+ "summary": "Get coupon",
+ "parameters": [
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "Coupon ID",
+ "name": "id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/dto.CouponItem"
+ }
+ }
+ }
+ },
+ "put": {
+ "description": "Update coupon template",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "CreatorCenter"
+ ],
+ "summary": "Update coupon",
+ "parameters": [
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "Coupon ID",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Coupon form",
+ "name": "form",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/dto.CouponUpdateForm"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/dto.CouponItem"
+ }
+ }
+ }
+ }
+ },
+ "/t/{tenantCode}/v1/creator/coupons/{id}/grant": {
+ "post": {
+ "description": "Grant coupon to users",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "CreatorCenter"
+ ],
+ "summary": "Grant coupon",
+ "parameters": [
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "Coupon ID",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Grant form",
+ "name": "form",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/dto.CouponGrantForm"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Granted",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
"/t/{tenantCode}/v1/creator/dashboard": {
"get": {
"description": "Get creator dashboard stats",
@@ -2272,6 +2474,76 @@
}
}
},
+ "/t/{tenantCode}/v1/me/coupons/available": {
+ "get": {
+ "description": "List coupons available for the given order amount",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "UserCenter"
+ ],
+ "summary": "List available coupons",
+ "parameters": [
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "Order amount (cents)",
+ "name": "amount",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/dto.UserCouponItem"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/t/{tenantCode}/v1/me/coupons/receive": {
+ "post": {
+ "description": "Receive a coupon by coupon_id",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "UserCenter"
+ ],
+ "summary": "Receive coupon",
+ "parameters": [
+ {
+ "description": "Receive form",
+ "name": "form",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/dto.CouponReceiveForm"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/dto.UserCouponItem"
+ }
+ }
+ }
+ }
+ },
"/t/{tenantCode}/v1/me/favorites": {
"get": {
"description": "Get favorites",
@@ -4181,6 +4453,166 @@
}
}
},
+ "dto.CouponCreateForm": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "description": "Description 优惠券描述。",
+ "type": "string"
+ },
+ "end_at": {
+ "description": "EndAt 过期时间(RFC3339,可为空)。",
+ "type": "string"
+ },
+ "max_discount": {
+ "description": "MaxDiscount 折扣券最高抵扣金额(分)。",
+ "type": "integer"
+ },
+ "min_order_amount": {
+ "description": "MinOrderAmount 使用门槛金额(分)。",
+ "type": "integer"
+ },
+ "start_at": {
+ "description": "StartAt 生效时间(RFC3339,可为空)。",
+ "type": "string"
+ },
+ "title": {
+ "description": "Title 优惠券标题。",
+ "type": "string"
+ },
+ "total_quantity": {
+ "description": "TotalQuantity 发行总量(0 表示不限量)。",
+ "type": "integer"
+ },
+ "type": {
+ "description": "Type 优惠券类型(fix_amount/discount)。",
+ "type": "string"
+ },
+ "value": {
+ "description": "Value 优惠券面值(分/折扣百分比)。",
+ "type": "integer"
+ }
+ }
+ },
+ "dto.CouponGrantForm": {
+ "type": "object",
+ "properties": {
+ "user_ids": {
+ "description": "UserIDs 领取用户ID集合。",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "dto.CouponItem": {
+ "type": "object",
+ "properties": {
+ "created_at": {
+ "description": "CreatedAt 创建时间(RFC3339)。",
+ "type": "string"
+ },
+ "description": {
+ "description": "Description 优惠券描述。",
+ "type": "string"
+ },
+ "end_at": {
+ "description": "EndAt 过期时间(RFC3339)。",
+ "type": "string"
+ },
+ "id": {
+ "description": "ID 券模板ID。",
+ "type": "integer"
+ },
+ "max_discount": {
+ "description": "MaxDiscount 折扣券最高抵扣金额(分)。",
+ "type": "integer"
+ },
+ "min_order_amount": {
+ "description": "MinOrderAmount 使用门槛金额(分)。",
+ "type": "integer"
+ },
+ "start_at": {
+ "description": "StartAt 生效时间(RFC3339)。",
+ "type": "string"
+ },
+ "title": {
+ "description": "Title 优惠券标题。",
+ "type": "string"
+ },
+ "total_quantity": {
+ "description": "TotalQuantity 发行总量。",
+ "type": "integer"
+ },
+ "type": {
+ "description": "Type 优惠券类型(fix_amount/discount)。",
+ "type": "string"
+ },
+ "updated_at": {
+ "description": "UpdatedAt 更新时间(RFC3339)。",
+ "type": "string"
+ },
+ "used_quantity": {
+ "description": "UsedQuantity 已使用数量。",
+ "type": "integer"
+ },
+ "value": {
+ "description": "Value 优惠券面值(分/折扣百分比)。",
+ "type": "integer"
+ }
+ }
+ },
+ "dto.CouponReceiveForm": {
+ "type": "object",
+ "properties": {
+ "coupon_id": {
+ "description": "CouponID 券模板ID。",
+ "type": "integer"
+ }
+ }
+ },
+ "dto.CouponUpdateForm": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "description": "Description 优惠券描述(为空表示不修改)。",
+ "type": "string"
+ },
+ "end_at": {
+ "description": "EndAt 过期时间(RFC3339,可为空)。",
+ "type": "string"
+ },
+ "max_discount": {
+ "description": "MaxDiscount 折扣券最高抵扣金额(分)。",
+ "type": "integer"
+ },
+ "min_order_amount": {
+ "description": "MinOrderAmount 使用门槛金额(分)。",
+ "type": "integer"
+ },
+ "start_at": {
+ "description": "StartAt 生效时间(RFC3339,可为空)。",
+ "type": "string"
+ },
+ "title": {
+ "description": "Title 优惠券标题(为空表示不修改)。",
+ "type": "string"
+ },
+ "total_quantity": {
+ "description": "TotalQuantity 发行总量(0 表示不限量)。",
+ "type": "integer"
+ },
+ "type": {
+ "description": "Type 优惠券类型(fix_amount/discount)。",
+ "type": "string"
+ },
+ "value": {
+ "description": "Value 优惠券面值(分/折扣百分比)。",
+ "type": "integer"
+ }
+ }
+ },
"dto.DashboardStats": {
"type": "object",
"properties": {
diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml
index 75e54a6..f4c87a6 100644
--- a/backend/docs/swagger.yaml
+++ b/backend/docs/swagger.yaml
@@ -472,6 +472,122 @@ definitions:
description: Title 内容标题(为空表示不修改)。
type: string
type: object
+ dto.CouponCreateForm:
+ properties:
+ description:
+ description: Description 优惠券描述。
+ type: string
+ end_at:
+ description: EndAt 过期时间(RFC3339,可为空)。
+ type: string
+ max_discount:
+ description: MaxDiscount 折扣券最高抵扣金额(分)。
+ type: integer
+ min_order_amount:
+ description: MinOrderAmount 使用门槛金额(分)。
+ type: integer
+ start_at:
+ description: StartAt 生效时间(RFC3339,可为空)。
+ type: string
+ title:
+ description: Title 优惠券标题。
+ type: string
+ total_quantity:
+ description: TotalQuantity 发行总量(0 表示不限量)。
+ type: integer
+ type:
+ description: Type 优惠券类型(fix_amount/discount)。
+ type: string
+ value:
+ description: Value 优惠券面值(分/折扣百分比)。
+ type: integer
+ type: object
+ dto.CouponGrantForm:
+ properties:
+ user_ids:
+ description: UserIDs 领取用户ID集合。
+ items:
+ type: integer
+ type: array
+ type: object
+ dto.CouponItem:
+ properties:
+ created_at:
+ description: CreatedAt 创建时间(RFC3339)。
+ type: string
+ description:
+ description: Description 优惠券描述。
+ type: string
+ end_at:
+ description: EndAt 过期时间(RFC3339)。
+ type: string
+ id:
+ description: ID 券模板ID。
+ type: integer
+ max_discount:
+ description: MaxDiscount 折扣券最高抵扣金额(分)。
+ type: integer
+ min_order_amount:
+ description: MinOrderAmount 使用门槛金额(分)。
+ type: integer
+ start_at:
+ description: StartAt 生效时间(RFC3339)。
+ type: string
+ title:
+ description: Title 优惠券标题。
+ type: string
+ total_quantity:
+ description: TotalQuantity 发行总量。
+ type: integer
+ type:
+ description: Type 优惠券类型(fix_amount/discount)。
+ type: string
+ updated_at:
+ description: UpdatedAt 更新时间(RFC3339)。
+ type: string
+ used_quantity:
+ description: UsedQuantity 已使用数量。
+ type: integer
+ value:
+ description: Value 优惠券面值(分/折扣百分比)。
+ type: integer
+ type: object
+ dto.CouponReceiveForm:
+ properties:
+ coupon_id:
+ description: CouponID 券模板ID。
+ type: integer
+ type: object
+ dto.CouponUpdateForm:
+ properties:
+ description:
+ description: Description 优惠券描述(为空表示不修改)。
+ type: string
+ end_at:
+ description: EndAt 过期时间(RFC3339,可为空)。
+ type: string
+ max_discount:
+ description: MaxDiscount 折扣券最高抵扣金额(分)。
+ type: integer
+ min_order_amount:
+ description: MinOrderAmount 使用门槛金额(分)。
+ type: integer
+ start_at:
+ description: StartAt 生效时间(RFC3339,可为空)。
+ type: string
+ title:
+ description: Title 优惠券标题(为空表示不修改)。
+ type: string
+ total_quantity:
+ description: TotalQuantity 发行总量(0 表示不限量)。
+ type: integer
+ type:
+ description: Type 优惠券类型(fix_amount/discount)。
+ type: string
+ value:
+ description: Value 优惠券面值(分/折扣百分比)。
+ type: integer
+ type: object
dto.DashboardStats:
properties:
new_messages:
@@ -2916,6 +3032,140 @@ paths:
summary: Update content
tags:
- CreatorCenter
+ /t/{tenantCode}/v1/creator/coupons:
+ get:
+ consumes:
+ - application/json
+ description: List coupon templates
+ parameters:
+ - description: Page
+ in: query
+ name: page
+ type: integer
+ - description: Limit
+ in: query
+ name: limit
+ type: integer
+ - description: Type (fix_amount/discount)
+ in: query
+ name: type
+ type: string
+ - description: Status (active/expired)
+ in: query
+ name: status
+ type: string
+ - description: Keyword
+ in: query
+ name: keyword
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/requests.Pager'
+ summary: List coupons
+ tags:
+ - CreatorCenter
+ post:
+ consumes:
+ - application/json
+ description: Create coupon template
+ parameters:
+ - description: Coupon form
+ in: body
+ name: form
+ required: true
+ schema:
+ $ref: '#/definitions/dto.CouponCreateForm'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/dto.CouponItem'
+ summary: Create coupon
+ tags:
+ - CreatorCenter
+ /t/{tenantCode}/v1/creator/coupons/{id}:
+ get:
+ consumes:
+ - application/json
+ description: Get coupon template detail
+ parameters:
+ - description: Coupon ID
+ format: int64
+ in: path
+ name: id
+ required: true
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/dto.CouponItem'
+ summary: Get coupon
+ tags:
+ - CreatorCenter
+ put:
+ consumes:
+ - application/json
+ description: Update coupon template
+ parameters:
+ - description: Coupon ID
+ format: int64
+ in: path
+ name: id
+ required: true
+ type: integer
+ - description: Coupon form
+ in: body
+ name: form
+ required: true
+ schema:
+ $ref: '#/definitions/dto.CouponUpdateForm'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/dto.CouponItem'
+ summary: Update coupon
+ tags:
+ - CreatorCenter
+ /t/{tenantCode}/v1/creator/coupons/{id}/grant:
+ post:
+ consumes:
+ - application/json
+ description: Grant coupon to users
+ parameters:
+ - description: Coupon ID
+ format: int64
+ in: path
+ name: id
+ required: true
+ type: integer
+ - description: Grant form
+ in: body
+ name: form
+ required: true
+ schema:
+ $ref: '#/definitions/dto.CouponGrantForm'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Granted
+ schema:
+ type: string
+ summary: Grant coupon
+ tags:
+ - CreatorCenter
/t/{tenantCode}/v1/creator/dashboard:
get:
consumes:
@@ -3290,6 +3540,52 @@ paths:
summary: List coupons
tags:
- UserCenter
+ /t/{tenantCode}/v1/me/coupons/available:
+ get:
+ consumes:
+ - application/json
+ description: List coupons available for the given order amount
+ parameters:
+ - description: Order amount (cents)
+ format: int64
+ in: query
+ name: amount
+ required: true
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/dto.UserCouponItem'
+ type: array
+ summary: List available coupons
+ tags:
+ - UserCenter
+ /t/{tenantCode}/v1/me/coupons/receive:
+ post:
+ consumes:
+ - application/json
+ description: Receive a coupon by coupon_id
+ parameters:
+ - description: Receive form
+ in: body
+ name: form
+ required: true
+ schema:
+ $ref: '#/definitions/dto.CouponReceiveForm'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/dto.UserCouponItem'
+ summary: Receive coupon
+ tags:
+ - UserCenter
/t/{tenantCode}/v1/me/favorites:
get:
consumes:
diff --git a/docs/todo_list.md b/docs/todo_list.md
index 9a22cc9..4178d0d 100644
--- a/docs/todo_list.md
+++ b/docs/todo_list.md
@@ -15,7 +15,7 @@
## P0(必须先做)
-### 1) 租户成员体系(加入/邀请/审核)
+### 1) 租户成员体系(加入/邀请/审核)(已完成)
**需求目标**
- 完成租户成员生命周期:申请加入、审核通过/拒绝、邀请加入。
- tenant_only 内容在“成员审核通过”后可访问;未审核需前置引导。
@@ -39,7 +39,7 @@
- 审核通过后,tenant_only 可访问;未通过不可访问。
- 邀请链接过期/重复使用处理。
-### 2) 鉴权与权限收口(必需)
+### 2) 鉴权与权限收口(必需)(已完成)
**需求目标**
- 受保护接口强制鉴权,超管接口增加 `super_admin` 角色校验。
- 补齐 `Super.Login / CheckToken` 逻辑。
@@ -191,6 +191,8 @@
---
## 已完成
+- 租户成员体系(加入/邀请/审核)。
+- 鉴权与权限收口(AuthOptional/AuthRequired、super_admin 校验、Super.Login/CheckToken)。
- 内容可见性与 tenant_only 访问控制。
- OTP 登录流程与租户成员校验(未加入拒绝登录)。
- ID 类型已统一为 int64(仅保留 upload_id/external_id/uuid 等非数字标识)。
diff --git a/frontend/portal/src/App.vue b/frontend/portal/src/App.vue
index 87ed26e..98240ae 100644
--- a/frontend/portal/src/App.vue
+++ b/frontend/portal/src/App.vue
@@ -1,3 +1,3 @@
© 2025 Quyun Platform. All rights reserved.
-ICP 备 88888888 号-1 | 公安网备 11010102000000 号
++ ICP 备 88888888 号-1 | 公安网备 11010102000000 号 +
{{ topics[0].desc }}
-{{ topic.desc }}
- - -+ {{ topics[0].desc }} +
++ {{ topic.desc }} +
+ + +专业的租户管理与内容交付平台,连接创作者与用户。
++ 专业的租户管理与内容交付平台,连接创作者与用户。 +
+未注册的手机号验证后将自动创建账号
++ 未注册的手机号验证后将自动创建账号 +
验证码已发送至 +86 {{ phone }}
+ ++ 验证码已发送至 +86 {{ phone }} +
-购买后可观看完整内容 (包含高清视频 + 完整剧本)
-支持微信 / 支付宝
-{{ comment.content }}
-+ 购买后可观看完整内容 (包含高清视频 + 完整剧本) +
+支持微信 / 支付宝
++ {{ comment.content }} +
+加入我们,获得专属频道主页,通过优质戏曲内容获取收益,与百万戏迷互动。
-灵活设置付费阅读与会员专栏,收益直接结算。
-拥有独立的品牌展示空间,积累私域粉丝。
-全方位的作品数据分析,助您优化创作方向。
-建议使用清晰的个人照或 Logo
-您的入驻申请已成功提交,平台将在 1-3 个工作日内完成审核。审核结果将通过短信和系统通知发送给您。
- -+ 加入我们,获得专属频道主页,通过优质戏曲内容获取收益,与百万戏迷互动。 +
++ 灵活设置付费阅读与会员专栏,收益直接结算。 +
++ 拥有独立的品牌展示空间,积累私域粉丝。 +
++ 全方位的作品数据分析,助您优化创作方向。 +
+建议使用清晰的个人照或 Logo
++ 您的入驻申请已成功提交,平台将在 1-3 + 个工作日内完成审核。审核结果将通过短信和系统通知发送给您。 +
+ +暂无收款账户,请添加
-暂无收款账户,请添加
+抱歉,您访问的页面走丢了。
-抱歉,您访问的页面走丢了。
+订单号: {{ order.id }}
-订单编号: {{ order.id }}
创建时间: {{ order.create_time }}
-付款时间: {{ order.pay_time }}
-张三 138****8888
-北京市 朝阳区 建国路88号
-订单号: {{ order.id }}
+
+ 订单编号:
+ {{ order.id }}
+
创建时间: {{ order.create_time }}
+付款时间: {{ order.pay_time }}
+张三 138****8888
+北京市 朝阳区 建国路88号
+订单提交成功,请尽快支付
-- 使用 {{ paymentMethodName }} 扫一扫 -
-二维码有效时间 2小时
-订单提交成功,请尽快支付
++ 使用 {{ paymentMethodName }} 扫一扫 +
+二维码有效时间 2小时
+{{ coupon.description }}
-{{ coupon.description }}
+暂无相关优惠券
+暂无相关优惠券
暂无收藏内容
-暂无收藏内容
+暂无已购内容
-暂无已购内容
+暂无点赞内容
-暂无点赞内容
+暂无相关订单
-暂无相关订单
+支持 JPG, PNG 格式,文件小于 5MB
-修改后需通过审核方可生效
-支持 JPG, PNG 格式,文件小于 5MB
+修改后需通过审核方可生效
+