diff --git a/backend/app/http/v1/dto/content.go b/backend/app/http/v1/dto/content.go index 7028749..a5e407b 100644 --- a/backend/app/http/v1/dto/content.go +++ b/backend/app/http/v1/dto/content.go @@ -4,11 +4,12 @@ import "quyun/v2/app/requests" type ContentListFilter struct { requests.Pagination - Keyword *string `query:"keyword"` - Genre *string `query:"genre"` - TenantID *string `query:"tenantId"` - Sort *string `query:"sort"` - IsPinned *bool `query:"is_pinned"` + Keyword *string `query:"keyword"` + Genre *string `query:"genre"` + TenantID *string `query:"tenantId"` + Sort *string `query:"sort"` + IsPinned *bool `query:"is_pinned"` + PriceType *string `query:"price_type"` } type ContentItem struct { diff --git a/backend/app/http/v1/dto/tenant.go b/backend/app/http/v1/dto/tenant.go index fbfc794..f73ebee 100644 --- a/backend/app/http/v1/dto/tenant.go +++ b/backend/app/http/v1/dto/tenant.go @@ -5,6 +5,7 @@ import "quyun/v2/app/requests" type TenantListFilter struct { requests.Pagination Keyword *string `query:"keyword"` + Sort *string `query:"sort"` } type TenantProfile struct { diff --git a/backend/app/http/v1/dto/user.go b/backend/app/http/v1/dto/user.go index 7b2da6f..fb7d7f2 100644 --- a/backend/app/http/v1/dto/user.go +++ b/backend/app/http/v1/dto/user.go @@ -53,6 +53,10 @@ type Order struct { TenantID string `json:"tenant_id"` TenantName string `json:"tenant_name"` IsVirtual bool `json:"is_virtual"` + BuyerName string `json:"buyer_name"` + BuyerAvatar string `json:"buyer_avatar"` + Title string `json:"title"` + Cover string `json:"cover"` } type Notification struct { diff --git a/backend/app/http/v1/routes.gen.go b/backend/app/http/v1/routes.gen.go index 1a636c7..598e905 100644 --- a/backend/app/http/v1/routes.gen.go +++ b/backend/app/http/v1/routes.gen.go @@ -351,6 +351,17 @@ func (r *Routes) Register(router fiber.Router) { r.user.Likes, Local[*models.User]("__ctx_user"), )) + r.log.Debugf("Registering route: Post /v1/me/notifications/:id/read -> user.MarkNotificationRead") + router.Post("/v1/me/notifications/:id/read"[len(r.Path()):], Func2( + r.user.MarkNotificationRead, + Local[*models.User]("__ctx_user"), + PathParam[string]("id"), + )) + r.log.Debugf("Registering route: Post /v1/me/notifications/read-all -> user.MarkAllNotificationsRead") + router.Post("/v1/me/notifications/read-all"[len(r.Path()):], Func1( + r.user.MarkAllNotificationsRead, + Local[*models.User]("__ctx_user"), + )) r.log.Debugf("Registering route: Get /v1/me/notifications -> user.Notifications") router.Get("/v1/me/notifications"[len(r.Path()):], DataFunc3( r.user.Notifications, diff --git a/backend/app/http/v1/user.go b/backend/app/http/v1/user.go index bf0db18..285ea7d 100644 --- a/backend/app/http/v1/user.go +++ b/backend/app/http/v1/user.go @@ -260,6 +260,34 @@ func (u *User) Notifications(ctx fiber.Ctx, user *models.User, typeArg string, p return services.Notification.List(ctx, user.ID, page, typeArg) } +// Mark notification as read +// +// @Router /v1/me/notifications/:id/read [post] +// @Summary Mark as read +// @Tags UserCenter +// @Accept json +// @Produce json +// @Param id path string true "Notification ID" +// @Success 200 {string} string "OK" +// @Bind user local key(__ctx_user) +// @Bind id path +func (u *User) MarkNotificationRead(ctx fiber.Ctx, user *models.User, id string) error { + return services.Notification.MarkRead(ctx, user.ID, id) +} + +// Mark all notifications as read +// +// @Router /v1/me/notifications/read-all [post] +// @Summary Mark all as read +// @Tags UserCenter +// @Accept json +// @Produce json +// @Success 200 {string} string "OK" +// @Bind user local key(__ctx_user) +func (u *User) MarkAllNotificationsRead(ctx fiber.Ctx, user *models.User) error { + return services.Notification.MarkAllRead(ctx, user.ID) +} + // List my coupons // // @Router /v1/me/coupons [get] diff --git a/backend/app/services/content.go b/backend/app/services/content.go index 2b8df29..f980460 100644 --- a/backend/app/services/content.go +++ b/backend/app/services/content.go @@ -38,6 +38,37 @@ func (s *content) List(ctx context.Context, filter *content_dto.ContentListFilte q = q.Where(tbl.IsPinned.Is(*filter.IsPinned)) } + if filter.PriceType != nil && *filter.PriceType != "all" { + if *filter.PriceType == "member" { + q = q.Where(tbl.Visibility.Eq(consts.ContentVisibilityTenantOnly)) + } else { + pTbl, pQ := models.ContentPriceQuery.QueryContext(ctx) + + var shouldFilter bool + var prices []*models.ContentPrice + + if *filter.PriceType == "free" { + shouldFilter = true + prices, _ = pQ.Where(pTbl.PriceAmount.Eq(0)).Select(pTbl.ContentID).Find() + } else if *filter.PriceType == "paid" { + shouldFilter = true + prices, _ = pQ.Where(pTbl.PriceAmount.Gt(0)).Select(pTbl.ContentID).Find() + } + + if shouldFilter { + ids := make([]int64, len(prices)) + for i, p := range prices { + ids[i] = p.ContentID + } + if len(ids) > 0 { + q = q.Where(tbl.ID.In(ids...)) + } else { + q = q.Where(tbl.ID.Eq(-1)) + } + } + } + } + // Sort sort := "latest" if filter.Sort != nil && *filter.Sort != "" { diff --git a/backend/app/services/creator.go b/backend/app/services/creator.go index 9d85a12..d1ded5c 100644 --- a/backend/app/services/creator.go +++ b/backend/app/services/creator.go @@ -570,7 +570,25 @@ func (s *creator) ListOrders( if filter.Status != nil && *filter.Status != "" { q = q.Where(tbl.Status.Eq(consts.OrderStatus(*filter.Status))) } - // Keyword could match ID or other fields if needed + + if filter.Keyword != nil && *filter.Keyword != "" { + k := *filter.Keyword + if id, err := cast.ToInt64E(k); err == nil { + q = q.Where(tbl.ID.Eq(id)) + } else { + uTbl, uQ := models.UserQuery.QueryContext(ctx) + users, _ := uQ.Where(uTbl.Nickname.Like("%" + k + "%")).Find() + uids := make([]int64, len(users)) + for i, u := range users { + uids[i] = u.ID + } + if len(uids) > 0 { + q = q.Where(tbl.UserID.In(uids...)) + } else { + q = q.Where(tbl.ID.Eq(-1)) // Match nothing + } + } + } list, err := q.Order(tbl.CreatedAt.Desc()).Find() if err != nil { @@ -579,11 +597,46 @@ func (s *creator) ListOrders( var data []creator_dto.Order for _, o := range list { + // Fetch Buyer Info + u, _ := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(o.UserID)).First() + buyerName := "未知用户" + buyerAvatar := "" + if u != nil { + buyerName = u.Nickname + buyerAvatar = u.Avatar + } + + // Fetch Content Info + var title, cover string + item, _ := models.OrderItemQuery.WithContext(ctx).Where(models.OrderItemQuery.OrderID.Eq(o.ID)).First() + if item != nil { + var c models.Content + err := models.ContentQuery.WithContext(ctx). + Where(models.ContentQuery.ID.Eq(item.ContentID)). + UnderlyingDB(). + Preload("ContentAssets.Asset"). + First(&c).Error + + if err == nil { + title = c.Title + for _, ca := range c.ContentAssets { + if ca.Role == consts.ContentAssetRoleCover && ca.Asset != nil { + cover = Common.GetAssetURL(ca.Asset.ObjectKey) + break + } + } + } + } + data = append(data, creator_dto.Order{ - ID: cast.ToString(o.ID), - Status: string(o.Status), // Enum conversion - Amount: float64(o.AmountPaid) / 100.0, - CreateTime: o.CreatedAt.Format(time.RFC3339), + ID: cast.ToString(o.ID), + Status: string(o.Status), + Amount: float64(o.AmountPaid) / 100.0, + CreateTime: o.CreatedAt.Format(time.RFC3339), + BuyerName: buyerName, + BuyerAvatar: buyerAvatar, + Title: title, + Cover: cover, }) } return data, nil @@ -803,6 +856,15 @@ func (s *creator) Withdraw(ctx context.Context, userID int64, form *creator_dto. } uid := userID + // Validate User Real-name Status + user, err := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(uid)).First() + if err != nil { + return errorx.ErrDatabaseError.WithCause(err) + } + if !user.IsRealNameVerified { + return errorx.ErrPreconditionFailed.WithMsg("请先完成实名认证后再申请提现") + } + amount := int64(form.Amount * 100) if amount <= 0 { return errorx.ErrBadRequest.WithMsg("金额无效") diff --git a/backend/app/services/notification.go b/backend/app/services/notification.go index 2ab0c4c..abd672c 100644 --- a/backend/app/services/notification.go +++ b/backend/app/services/notification.go @@ -71,6 +71,16 @@ func (s *notification) MarkRead(ctx context.Context, userID int64, id string) er return nil } +func (s *notification) MarkAllRead(ctx context.Context, userID int64) error { + _, err := models.NotificationQuery.WithContext(ctx). + Where(models.NotificationQuery.UserID.Eq(userID), models.NotificationQuery.IsRead.Is(false)). + UpdateSimple(models.NotificationQuery.IsRead.Value(true)) + if err != nil { + return errorx.ErrDatabaseError.WithCause(err) + } + return nil +} + func (s *notification) Send(ctx context.Context, userID int64, typ, title, content string) error { arg := args.NotificationArgs{ UserID: userID, diff --git a/docs/review_report.md b/docs/review_report.md new file mode 100644 index 0000000..8c854d5 --- /dev/null +++ b/docs/review_report.md @@ -0,0 +1,120 @@ +# QuyUn v2 代码审查与优化建议(backend + frontend/portal) + +> 审查范围:`backend/`、`frontend/portal/`、`specs/`、`docs/`、`frontend/superadmin/SUPERADMIN_PAGES.md`。 + +## 一、关键问题(按严重程度) + +### P0 / 安全与数据隔离 + +1) 多租户路由与上下文缺失,无法满足 `/t/:tenant_code/v1` 规范,存在跨租户数据泄漏风险。 +- 后端路由基座仍为 `/v1`:`backend/app/http/v1/routes.manual.go`。 +- 前端路由与 API 基座未含 tenant_code:`frontend/portal/src/router/index.js`、`frontend/portal/src/utils/request.js`。 +- 多数服务查询未强制带 `tenant_id` 条件(仅依赖 query 传参):如 `backend/app/services/content.go`、`backend/app/services/order.go`、`backend/app/services/common.go`。 + +2) 鉴权为“可选”,导致受保护接口可能被匿名访问;超级管理员接口无角色校验。 +- `Auth` 中间件未携带 Authorization 时直接放行:`backend/app/middlewares/middlewares.go`。 +- `/super/v1/*` 复用普通 `Auth`,无 `super_admin` 权限检查:`backend/app/http/super/v1/routes.manual.go`。 + +3) 超级管理员登录与鉴权核心逻辑为空实现,接口可用性与安全性均不足。 +- `services.Super.Login / CheckToken` 返回空结构:`backend/app/services/super.go`。 + +### P1 / 功能缺失与接口不一致 + +4) 后端多个关键接口未实现或返回空结果,前端调用会失败或表现异常。 +- `services.Order.Status` 返回 `nil`:`backend/app/services/order.go`。 +- 超管统计/订单详情/退款等均为空:`backend/app/services/super.go`。 +- 超管内容/订单列表 DTO 映射 TODO:`backend/app/services/super.go`。 + +5) 认证方式与规格偏离:现实现 OTP 登录 + JWT;规格为 WeChat OAuth + Cookie。 +- 认证流程:`backend/app/http/v1/auth/auth.go`、`backend/app/services/user.go`。 +- 前端调用 OTP 登录:`frontend/portal/src/api/auth.js`。 + +6) API 调用参数与后端 `@Bind` 约定不一致,导致请求失效。 +- `contentId` 参数期望为 `contentId`,前端用 `content_id`:`frontend/portal/src/api/user.js` ↔ `backend/app/http/v1/user.go`。 + +### P2 / 代码规范与可维护性 + +7) ID 类型全链路使用 `string`,与 DB `BIGINT` 不一致,导致重复转换与潜在路由冲突。 +- controller/service/DTO 使用 `string`:`backend/app/http/v1/*`、`backend/app/http/v1/dto/*`、`backend/app/services/*`。 +- 路由注解未使用 `:id`:`backend/app/http/**`。 + +8) DTO 结构缺少字段级中文注释,业务注释大量为英文,违反 `backend/llm.txt` 规范。 +- DTO 示例:`backend/app/http/v1/dto/user.go`、`backend/app/http/v1/dto/content.go`。 + +9) Service 读取 `ctx` 获取用户信息,违反“上下文提取必须在 Controller 层”的规则。 +- `services.audit.Log` 直接读 `ctx.Value`:`backend/app/services/audit.go`。 + +10) 数据库迁移与规格不一致,初始迁移为空,实际表结构仅在 `specs/DB.sql`。 +- `backend/database/migrations/20251227112605_init.sql` 为空。 + +11) N+1 查询与聚合性能风险。 +- 订单列表逐条查询租户/内容/订单项:`backend/app/services/order.go`。 +- 租户列表逐条统计 followers/contents:`backend/app/services/tenant.go`。 + +12) 代码残留调试输出。 +- `fmt.Printf` 直接打印配置:`backend/app/services/tenant.go`。 + +## 二、架构与实现优化建议 + +### 1) 多租户“强隔离”落地 +- 路由:将 `/v1` 全部改为 `/t/:tenantCode/v1`(注意 `@Router` 的 `:tenantCode` 与 `` 规则)。 +- 中间件:新增 `TenantResolver`(解析 tenant_code → tenant_id → ctx locals)。 +- 服务层:所有查询必须带 `tenant_id`(禁止依赖 query 传参)。 +- 前端:Router base + API base 统一从 URL 中解析 tenant_code。 + +### 2) 鉴权与权限体系补全 +- 拆分 `AuthOptional` 与 `AuthRequired` 两种中间件,受保护接口必须硬校验。 +- 超管接口增加 `super_admin` 角色校验(JWT claims 中带 roles)。 +- 完成 `Super.Login` / `CheckToken` 逻辑,支持 token 续期与失效。 + +### 3) ID 类型统一(int64 / model 注入) +- 统一所有 path/query/body 里的 ID 为 `int64`。 +- 控制器签名支持 `@Bind id path` + `int64` 或 `@Bind tenant model(id)` 进行 model 注入。 +- swagger `@Router` 必须使用 `:id` / `:tenantID`。 + +### 4) 规格一致性修复 +- 统一认证模型(OTP vs WeChat):要么按规格实现 WeChat OAuth,要么更新规格文档。 +- 统一存储方案(OSS + tenant_uuid + md5 命名);当前本地存储仅作开发 fallback。 +- 将 `specs/DB.sql` 迁入真实迁移文件并补齐中文字段注释。 + +### 5) 性能与稳定性 +- 列表场景引入预加载或批量查询,规避 N+1。 +- 订单/租户统计使用聚合 SQL 一次性获取。 +- 关键写操作增加幂等与重试策略。 + +### 6) 规范化与可维护性 +- DTO 增加字段级中文注释(含业务语义/约束)。 +- Service 层中文注释补齐业务意图与边界条件。 +- `Audit` 改为显式传入 operatorID 参数。 + +## 三、ID 类型统一与 Model 注入的推荐步骤(遵循 backend/llm.txt) + +1) **定义目标规范** +- 所有业务 ID 使用 `int64`(JSON 输出为数值)。 +- 路由中数字 ID 使用 `:id`。 + +2) **先改 Controller/DTO** +- `backend/app/http/v1/**` 和 `backend/app/http/super/v1/**` 中 path/query 的 `string` 改为 `int64`。 +- DTO 中 `ID` 字段统一改为 `int64`;所有 DTO 增加中文字段注释。 +- `@Router` 的路径参数补全 `` 约束。 + +3) **再改 Service** +- 将 `id string` 改为 `id int64`,移除 `cast.ToInt64`。 +- 依赖 model 的场景改为 `@Bind xxx model(id)` + `*models.Xxx` 直接注入。 + +4) **同步前端调用** +- URL/query 参数统一为 `contentId` / `userId` / `tenantId`(与 `@Bind` 的 key 对齐)。 +- 处理 ID 显示与输入时的类型转换(数值 ↔ 字符串)。 + +5) **生成与验证** +- 运行 `atomctl gen route`、`atomctl gen provider`、`atomctl swag init`。 +- 回归测试:关键列表、详情、操作接口。 + +## 四、建议的修复优先级 + +1) 多租户路由 + tenant_id 强隔离 +2) 超管鉴权/角色校验 + Login/Token 实现 +3) ID 类型统一与 `:id` 路由规范 +4) 规格一致性(认证/存储/迁移) +5) 性能优化(N+1/聚合)与可维护性补齐 + diff --git a/docs/superadmin_plan.md b/docs/superadmin_plan.md new file mode 100644 index 0000000..4a3c03d --- /dev/null +++ b/docs/superadmin_plan.md @@ -0,0 +1,148 @@ +# 超级管理员后台功能规划(按页面拆解) + +> 目标:基于现有 `/super/v1/*` 能力,补齐平台级“管理 + 统计”闭环。以下按页面拆分,分别给出管理动作、统计指标与接口对照。 + +## 0) 全局约定 + +- **鉴权**:`Authorization: Bearer `;登录后本地持久化 token。 +- **路由基座**:`/super/`(前端),API 基座 `/super/v1`。 +- **分页**:统一 `page/limit`,响应为 `requests.Pager`。 +- **枚举**:优先取 `/super/v1/tenants/statuses`、`/super/v1/users/statuses`。 + +## 1) 登录 `/auth/login` + +- 管理功能:账号登录、token 写入、自动续期。 +- 统计功能:可选记录登录失败次数、IP、设备指纹(审计)。 +- 现有接口: + - `POST /super/v1/auth/login`(需补齐实现) + - `GET /super/v1/auth/token`(token 校验/续期) + +## 2) 概览 Dashboard `/` + +- 管理功能:快捷入口(租户/用户/订单/内容)。 +- 统计指标(建议): + - 租户总数/活跃数/过期数 + - 用户总数/活跃数(按状态拆分) + - 订单数/成交额/退款额(按日、按状态) + - 内容总数/新增内容/被封禁内容 +- 现有接口: + - `GET /super/v1/users/statistics`(需补齐实现) + - `GET /super/v1/orders/statistics`(需补齐实现) + - `GET /super/v1/tenants?limit=1&page=1`(可取 total) + - `GET /super/v1/contents?limit=1&page=1`(可取 total) + +## 3) 租户管理 `/superadmin/tenants` + +- 管理功能: + - 新建租户(绑定管理员) + - 更新租户状态(正常/禁用) + - 续期/变更过期时间 +- 统计指标: + - 状态分布(待审核/正常/禁用) + - 即将过期租户数(7/30 天) + - 租户 GMV Top N(需补接口) +- 现有接口: + - `POST /super/v1/tenants` + - `GET /super/v1/tenants` + - `PATCH /super/v1/tenants/{tenantID}/status` + - `PATCH /super/v1/tenants/{tenantID}`(续期) + - `GET /super/v1/tenants/statuses` + +## 4) 租户详情 `/superadmin/tenants/:tenantID` + +- 管理功能(建议): + - 基本信息/状态/过期时间编辑 + - 管理员与成员列表(角色管理) + - 内容列表、订单列表、资金汇总 +- 统计指标(建议): + - 租户用户数、内容数、订单数、GMV +- 现有接口: + - `GET /super/v1/tenants/{tenantID}`(已有) +- 建议补充接口: + - `GET /super/v1/tenants/{tenantID}/users` + - `GET /super/v1/tenants/{tenantID}/contents` + - `GET /super/v1/tenants/{tenantID}/orders` + - `GET /super/v1/tenants/{tenantID}/statistics` + +## 5) 用户管理 `/superadmin/users` + +- 管理功能: + - 用户列表筛选(用户名/状态/角色/所属租户) + - 状态变更、角色授予 +- 统计指标: + - 用户状态统计(已提供) +- 现有接口: + - `GET /super/v1/users` + - `PATCH /super/v1/users/{userID}/status` + - `PATCH /super/v1/users/{userID}/roles` + - `GET /super/v1/users/statistics` + - `GET /super/v1/users/statuses` + +## 6) 用户详情 `/superadmin/users/:userID` + +- 管理功能(建议): + - 用户资料、角色、状态 + - 用户所属/拥有租户列表 + - 用户订单与内容购买记录 +- 统计指标(建议): + - 用户消费总额、退款次数 +- 现有接口: + - `GET /super/v1/users/{userID}`(已有) +- 建议补充接口: + - `GET /super/v1/users/{userID}/tenants` + - `GET /super/v1/users/{userID}/orders` + - `GET /super/v1/users/{userID}/contents` + +## 7) 订单管理 `/superadmin/orders` + +- 管理功能: + - 订单列表(按租户/用户/状态/时间过滤) + - 退款操作(平台侧) +- 统计指标: + - 订单状态分布、GMV、退款额 +- 现有接口: + - `GET /super/v1/orders` + - `POST /super/v1/orders/{orderID}/refund`(需补齐实现) + - `GET /super/v1/orders/statistics`(需补齐实现) + +## 8) 订单详情 `/superadmin/orders/:orderID` + +- 管理功能: + - 查看订单快照、支付信息、退款信息 + - 退款/强制关闭 +- 现有接口: + - `GET /super/v1/orders/{orderID}`(需补齐实现) + +## 9) 内容管理 `/superadmin/contents` + +- 管理功能: + - 跨租户内容列表 + - 内容状态更新(封禁/下架) +- 统计指标: + - 内容状态分布、热门内容 Top N +- 现有接口: + - `GET /super/v1/contents` + - `PATCH /super/v1/tenants/{tenantID}/contents/{contentID}/status` + +## 10) 财务/提现(可选) + +- 管理功能: + - 提现订单审核(通过/驳回) + - 记录操作原因 +- 统计指标: + - 提现订单数、金额、失败率 +- 现有接口:无(服务层有 `ListWithdrawals/Approve/Reject`,需补 controller + route) + +## 11) 审计日志 / 操作记录(建议) + +- 管理功能: + - 展示后台操作日志(操作人、对象、动作、时间) + - 支持导出 +- 现有接口:无(可基于 `services.Audit` 扩展) + +## 12) 系统配置 / 平台策略(建议) + +- 管理功能: + - 平台佣金比例、内容审核策略、默认到期策略 +- 现有接口:无(需新增配置表与接口) + diff --git a/frontend/portal/src/api/user.js b/frontend/portal/src/api/user.js index a85dc3a..d6a4f97 100644 --- a/frontend/portal/src/api/user.js +++ b/frontend/portal/src/api/user.js @@ -16,6 +16,8 @@ export const userApi = { addLike: (contentId) => request(`/me/likes?content_id=${contentId}`, { method: 'POST' }), removeLike: (contentId) => request(`/me/likes/${contentId}`, { method: 'DELETE' }), getNotifications: (type, page) => request(`/me/notifications?type=${type || 'all'}&page=${page || 1}`), + markNotificationRead: (id) => request(`/me/notifications/${id}/read`, { method: 'POST' }), + markAllNotificationsRead: () => request('/me/notifications/read-all', { method: 'POST' }), getFollowing: () => request('/me/following'), getCoupons: (status) => request(`/me/coupons?status=${status || 'unused'}`), }; diff --git a/frontend/portal/src/layout/LayoutUser.vue b/frontend/portal/src/layout/LayoutUser.vue index d8a65bb..3cfac27 100644 --- a/frontend/portal/src/layout/LayoutUser.vue +++ b/frontend/portal/src/layout/LayoutUser.vue @@ -9,11 +9,11 @@
-
-
Felix Demo
-
ID: 9527330
+
{{ user.nickname || '用户' }}
+
ID: {{ user.id }}
@@ -35,6 +35,11 @@ 我的钱包 + + + 我的优惠券 + @@ -80,6 +85,9 @@ diff --git a/frontend/portal/src/router/index.js b/frontend/portal/src/router/index.js index a1684bf..508e6ca 100644 --- a/frontend/portal/src/router/index.js +++ b/frontend/portal/src/router/index.js @@ -83,6 +83,11 @@ const router = createRouter({ name: 'user-wallet', component: () => import('../views/user/WalletView.vue') }, + { + path: 'coupons', + name: 'user-coupons', + component: () => import('../views/user/CouponsView.vue') + }, { path: 'library', name: 'user-library', diff --git a/frontend/portal/src/views/ExploreView.vue b/frontend/portal/src/views/ExploreView.vue index d8c8665..62ab315 100644 --- a/frontend/portal/src/views/ExploreView.vue +++ b/frontend/portal/src/views/ExploreView.vue @@ -105,7 +105,8 @@ const fetchContents = async (append = false) => { const params = { page: page.value, limit: 12, - sort: sort.value + sort: sort.value, + price_type: selectedPrice.value }; if (selectedGenre.value !== '全部') params.genre = selectedGenre.value; if (keyword.value) params.keyword = keyword.value; @@ -127,7 +128,7 @@ const loadMore = () => { fetchContents(true); }; -watch([selectedGenre, sort], () => { +watch([selectedGenre, selectedPrice, sort], () => { page.value = 1; fetchContents(); }); diff --git a/frontend/portal/src/views/HomeView.vue b/frontend/portal/src/views/HomeView.vue index 6b6df5d..8ab2a18 100644 --- a/frontend/portal/src/views/HomeView.vue +++ b/frontend/portal/src/views/HomeView.vue @@ -1,29 +1,40 @@