diff --git a/backend_v1/app/http/admin/posts.go b/backend_v1/app/http/admin/posts.go index c7bebc9..5b875d8 100644 --- a/backend_v1/app/http/admin/posts.go +++ b/backend_v1/app/http/admin/posts.go @@ -195,6 +195,21 @@ func (ctl *posts) Show(ctx fiber.Ctx, post *models.Post) (*PostItem, error) { }, nil } +// Buyers +// +// @Summary 作品购买人列表 +// @Tags Admin Posts +// @Produce json +// @Param id path int64 true "作品 ID" +// @Param pagination query requests.Pagination false "分页参数" +// @Success 200 {object} requests.Pager{items=dto.PostBuyerItem} "成功" +// @Router /admin/v1/posts/:id/buyers [get] +// @Bind post path key(id) model(id) +// @Bind pagination query +func (ctl *posts) Buyers(ctx fiber.Ctx, post *models.Post, pagination *requests.Pagination) (*requests.Pager, error) { + return services.Posts.Buyers(ctx, post.ID, pagination) +} + // SendTo // // @Summary 赠送作品给用户 diff --git a/backend_v1/app/http/admin/routes.gen.go b/backend_v1/app/http/admin/routes.gen.go index a23b0c2..da83f38 100644 --- a/backend_v1/app/http/admin/routes.gen.go +++ b/backend_v1/app/http/admin/routes.gen.go @@ -114,6 +114,15 @@ func (r *Routes) Register(router fiber.Router) { return models.PostQuery.WithContext(ctx).Where(field.NewUnsafeFieldRaw("id = ?", v)).First() }, )) + r.log.Debugf("Registering route: Get /admin/v1/posts/:id/buyers -> posts.Buyers") + router.Get("/admin/v1/posts/:id/buyers"[len(r.Path()):], DataFunc2( + r.posts.Buyers, + func(ctx fiber.Ctx) (*models.Post, error) { + v := fiber.Params[int](ctx, "id") + return models.PostQuery.WithContext(ctx).Where(field.NewUnsafeFieldRaw("id = ?", v)).First() + }, + Query[requests.Pagination]("pagination"), + )) r.log.Debugf("Registering route: Post /admin/v1/posts -> posts.Create") router.Post("/admin/v1/posts"[len(r.Path()):], Func1( r.posts.Create, diff --git a/backend_v1/app/http/dto/post_buyer.go b/backend_v1/app/http/dto/post_buyer.go new file mode 100644 index 0000000..4d908c1 --- /dev/null +++ b/backend_v1/app/http/dto/post_buyer.go @@ -0,0 +1,17 @@ +package dto + +import "time" + +type PostBuyerItem struct { + UserID int64 `json:"user_id"` // 用户 ID(购买人唯一标识,用于管理端关联用户详情/后续操作) + + Username string `json:"username"` // 用户名(购买人展示名称;可能为空或默认值,前端需兼容) + + Avatar string `json:"avatar"` // 用户头像 URL(用于列表展示;可能为空,前端需提供占位图/降级展示) + + Phone string `json:"phone"` // 用户手机号(管理端可见;用于客服联系/核对身份,可能为空) + + BoughtAt time.Time `json:"bought_at"` // 购买时间(以 user_posts.created_at 为准;用于排序/审计) + + Price int64 `json:"price"` // 购买价格(单位:分;-1 表示管理员赠送/免费,非负为实际支付金额) +} diff --git a/backend_v1/app/services/posts.go b/backend_v1/app/services/posts.go index b188409..45b898b 100644 --- a/backend_v1/app/services/posts.go +++ b/backend_v1/app/services/posts.go @@ -127,6 +127,62 @@ func (m *posts) Bought(ctx context.Context, userId int64, pagination *requests.P }, nil } +// Buyers 获取某个作品的购买人列表(管理端使用) +func (m *posts) Buyers(ctx context.Context, postID int64, pagination *requests.Pagination) (*requests.Pager, error) { + pagination.Format() + + // 先分页查询购买记录,避免一次性拉取全量 user_posts 造成内存/延迟抖动 + tblUserPost, queryUserPost := models.UserPostQuery.QueryContext(ctx) + queryUserPost = queryUserPost. + Where(tblUserPost.PostID.Eq(postID)). + Order(tblUserPost.CreatedAt.Desc()) + + userPosts, cnt, err := queryUserPost.FindByPage(int(pagination.Offset()), int(pagination.Limit)) + if err != nil { + return nil, err + } + if len(userPosts) == 0 { + return &requests.Pager{ + Items: []dto.PostBuyerItem{}, + Total: cnt, + Pagination: *pagination, + }, nil + } + + // 批量回表查询用户信息,避免 N+1 + userIDs := lo.Uniq(lo.Map(userPosts, func(item *models.UserPost, _ int) int64 { + return item.UserID + })) + tblUser, queryUser := models.UserQuery.QueryContext(ctx) + users, err := queryUser.Where(tblUser.ID.In(userIDs...)).Find() + if err != nil { + return nil, err + } + userMap := lo.KeyBy(users, func(item *models.User) int64 { return item.ID }) + + items := make([]dto.PostBuyerItem, 0, len(userPosts)) + for _, item := range userPosts { + user, ok := userMap[item.UserID] + if !ok { + continue + } + items = append(items, dto.PostBuyerItem{ + UserID: user.ID, + Username: user.Username, + Avatar: user.Avatar, + Phone: user.Phone, + BoughtAt: item.CreatedAt, + Price: item.Price, + }) + } + + return &requests.Pager{ + Items: items, + Total: cnt, + Pagination: *pagination, + }, nil +} + // GetPostsMapByIDs func (m *posts) GetPostsMapByIDs(ctx context.Context, ids []int64) (map[int64]*models.Post, error) { tbl, query := models.PostQuery.QueryContext(ctx) diff --git a/backend_v1/app/services/provider.gen.go b/backend_v1/app/services/provider.gen.go index f131058..cbe8918 100755 --- a/backend_v1/app/services/provider.gen.go +++ b/backend_v1/app/services/provider.gen.go @@ -1,6 +1,8 @@ package services import ( + "quyun/v2/providers/ali" + "go.ipao.vip/atom" "go.ipao.vip/atom/container" "go.ipao.vip/atom/contracts" @@ -52,8 +54,12 @@ func Provide(opts ...opt.Option) error { }, atom.GroupInitial); err != nil { return err } - if err := container.Container.Provide(func() (*users, error) { - obj := &users{} + if err := container.Container.Provide(func( + smsNotifyClient *ali.SMSNotifyClient, + ) (*users, error) { + obj := &users{ + smsNotifyClient: smsNotifyClient, + } if err := obj.Prepare(); err != nil { return nil, err } diff --git a/backend_v1/providers/ali/sms_notify_client.go b/backend_v1/providers/ali/sms_notify_client.go index 4ad5e10..54d7135 100644 --- a/backend_v1/providers/ali/sms_notify_client.go +++ b/backend_v1/providers/ali/sms_notify_client.go @@ -40,7 +40,6 @@ func NewSMSNotifyClient(cfg *Config) (*SMSNotifyClient, error) { } func (c *SMSNotifyClient) SendTo(phone string) (string, error) { - req := &dypnsapi20170525.SendSmsVerifyCodeRequest{ SignName: tea.String("速通互联验证码"), TemplateCode: tea.String("100001"), diff --git a/frontend/admin/src/api/postService.js b/frontend/admin/src/api/postService.js index 5189253..ad2620b 100644 --- a/frontend/admin/src/api/postService.js +++ b/frontend/admin/src/api/postService.js @@ -26,4 +26,9 @@ export const postService = { sendTo(id, userId) { return httpClient.post(`/posts/${id}/send-to/${userId}`); }, -} \ No newline at end of file + getBuyers(id, { page = 1, limit = 10 } = {}) { + return httpClient.get(`/posts/${id}/buyers`, { + params: { page, limit } + }); + }, +} diff --git a/frontend/admin/src/pages/PostPage.vue b/frontend/admin/src/pages/PostPage.vue index 148e7aa..357ffa8 100644 --- a/frontend/admin/src/pages/PostPage.vue +++ b/frontend/admin/src/pages/PostPage.vue @@ -194,6 +194,15 @@ const sendDialogVisible = ref(false); const selectedPost = ref(null); const selectedUser = ref(null); +// Buyers dialog state (购买人列表) +const buyersDialogVisible = ref(false); +const buyersPost = ref(null); +const buyersItems = ref([]); +const buyersLoading = ref(false); +const buyersFirst = ref(0); +const buyersRows = ref(10); +const buyersTotal = ref(0); + // 修改用户列表相关变量 const users = ref({ items: [], @@ -288,6 +297,48 @@ const handleSendConfirm = async () => { } }; +const formatBuyerPrice = (priceCents) => { + if (priceCents < 0) return '赠送'; + return formatPrice(priceCents / 100); +}; + +const fetchBuyers = async () => { + if (!buyersPost.value) return; + buyersLoading.value = true; + try { + const currentPage = (buyersFirst.value / buyersRows.value) + 1; + const response = await postService.getBuyers(buyersPost.value.id, { + page: currentPage, + limit: buyersRows.value + }); + buyersItems.value = response.data.items || []; + buyersTotal.value = response.data.total || 0; + } catch (error) { + toast.add({ + severity: 'error', + summary: '错误', + detail: '加载购买人列表失败', + life: 3000 + }); + } finally { + buyersLoading.value = false; + } +}; + +const openBuyers = async (post) => { + if (!post || !post.bought_count || post.bought_count <= 0) return; + buyersPost.value = post; + buyersFirst.value = 0; + buyersDialogVisible.value = true; + await fetchBuyers(); +}; + +const onBuyersPage = (event) => { + buyersFirst.value = event.first; + buyersRows.value = event.rows; + fetchBuyers(); +}; + @@ -363,6 +414,64 @@ const handleSendConfirm = async () => { + + +