Files
qitongxue-api/internal/logic/goods.go
2026-09-30 17:01:18 +08:00

241 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package logic
import (
"context"
"strings"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
adminv1 "tool-api/api/admin/v1"
userv1 "tool-api/api/user/v1"
"tool-api/internal/consts"
)
// ============================================================================
// 软件开发服务(迭代-2026-09-29)
//
// 分类固定四类(网站/小程序/App/桌面),category 存 tinyint;content_md 只存
// Markdown 原文(管理端编辑预览、小程序端解析渲染,后端不做 HTML 转换)。
// 封面 cover_path 库内存相对路径,输出统一 absoluteURL 拼接(图片域名不落库)。
// ============================================================================
// goodsSortWhitelist 管理端列表排序白名单(防注入)
var goodsSortWhitelist = map[string]string{
"id": "id",
"category": "category",
"sort": "sort",
"price": "price_cents",
"is_enabled": "is_enabled",
"created_at": "created_at",
}
// GoodsList C 端服务列表(仅上架;sort 升序、id 降序)
func GoodsList(ctx context.Context, req *userv1.GoodsListReq) (*userv1.GoodsListRes, error) {
page, pageSize := normalizePage(req.Page, req.PageSize, 20)
m := g.Model(consts.TableGoods).Where("is_enabled", 1)
if req.Category > 0 {
m = m.Where("category", req.Category)
}
total, err := m.Count()
if err != nil {
return nil, err
}
records, err := m.Page(page, pageSize).Order("sort asc, id desc").All()
if err != nil {
return nil, err
}
list := make([]userv1.GoodsItem, 0, len(records))
for _, r := range records {
list = append(list, goodsItemOf(ctx, r))
}
return &userv1.GoodsListRes{List: list, Total: total}, nil
}
// GoodsDetail C 端服务详情(仅上架;下架/不存在同义报错,避免探测下架内容)
func GoodsDetail(ctx context.Context, id int64) (*userv1.GoodsDetailRes, error) {
r, err := g.Model(consts.TableGoods).Where("id", id).Where("is_enabled", 1).One()
if err != nil {
return nil, err
}
if r.IsEmpty() {
return nil, gerror.New("服务不存在或已下架")
}
return &userv1.GoodsDetailRes{
Id: r["id"].Int64(),
Category: r["category"].Int(),
CategoryName: consts.GoodsCategoryNameOf(r["category"].Int()),
Title: r["title"].String(),
Summary: r["summary"].String(),
ContentMd: r["content_md"].String(),
CoverUrl: absoluteURL(ctx, r["cover_path"].String()),
PriceCents: r["price_cents"].Int64(),
PriceNote: r["price_note"].String(),
UpdatedAt: timeStr(r["updated_at"].GTime()),
}, nil
}
// AdminGoodsList 管理端服务列表(含下架)
func AdminGoodsList(ctx context.Context, req *adminv1.AdminGoodsListReq) (*adminv1.AdminGoodsListRes, error) {
page, pageSize := normalizePage(req.Page, req.PageSize, 20)
m := g.Model(consts.TableGoods)
if kw := strings.TrimSpace(req.Keyword); kw != "" {
m = m.Where("title LIKE ?", "%"+kw+"%")
}
if req.Category > 0 {
m = m.Where("category", req.Category)
}
if req.IsEnabled != nil {
m = m.Where("is_enabled", *req.IsEnabled)
}
total, err := m.Count()
if err != nil {
return nil, err
}
records, err := applySort(m.Page(page, pageSize), req.SortBy, req.SortOrder, goodsSortWhitelist, "sort asc, id desc").All()
if err != nil {
return nil, err
}
list := make([]adminv1.AdminGoodsItem, 0, len(records))
for _, r := range records {
list = append(list, adminv1.AdminGoodsItem{
Id: r["id"].Int64(),
Category: r["category"].Int(),
CategoryName: consts.GoodsCategoryNameOf(r["category"].Int()),
Title: r["title"].String(),
Summary: r["summary"].String(),
CoverPath: r["cover_path"].String(),
CoverUrl: absoluteURL(ctx, r["cover_path"].String()),
PriceCents: r["price_cents"].Int64(),
PriceNote: r["price_note"].String(),
Sort: r["sort"].Int(),
IsEnabled: r["is_enabled"].Int(),
CreatedAt: timeStr(r["created_at"].GTime()),
UpdatedAt: timeStr(r["updated_at"].GTime()),
})
}
return &adminv1.AdminGoodsListRes{List: list, Total: total}, nil
}
// AdminGoodsDetail 管理端服务详情(编辑回填,含 content_md)
func AdminGoodsDetail(ctx context.Context, id int64) (*adminv1.AdminGoodsDetailRes, error) {
r, err := g.Model(consts.TableGoods).Where("id", id).One()
if err != nil {
return nil, err
}
if r.IsEmpty() {
return nil, gerror.New("服务不存在")
}
return &adminv1.AdminGoodsDetailRes{
Id: r["id"].Int64(),
Category: r["category"].Int(),
CategoryName: consts.GoodsCategoryNameOf(r["category"].Int()),
Title: r["title"].String(),
Summary: r["summary"].String(),
ContentMd: r["content_md"].String(),
CoverPath: r["cover_path"].String(),
CoverUrl: absoluteURL(ctx, r["cover_path"].String()),
PriceCents: r["price_cents"].Int64(),
PriceNote: r["price_note"].String(),
Sort: r["sort"].Int(),
IsEnabled: r["is_enabled"].Int(),
}, nil
}
// AdminGoodsSave 新增/编辑服务。封面写入前归一化为相对路径(图片域名不落库)。
// TZ-01:datetime 写库一律 gdb.Raw("NOW()")。
func AdminGoodsSave(ctx context.Context, req *adminv1.AdminGoodsSaveReq) (*adminv1.AdminGoodsSaveRes, error) {
title := strings.TrimSpace(req.Title)
if title == "" {
return nil, gerror.New("请填写服务名称")
}
data := g.Map{
"category": req.Category,
"title": title,
"summary": strings.TrimSpace(req.Summary),
"content_md": req.ContentMd,
"price_cents": req.PriceCents,
"price_note": strings.TrimSpace(req.PriceNote),
"cover_path": normalizeToRelativePath(req.CoverPath),
"sort": req.Sort,
"is_enabled": req.IsEnabled,
}
if req.Id > 0 {
exist, err := g.Model(consts.TableGoods).Where("id", req.Id).One()
if err != nil {
return nil, err
}
if exist.IsEmpty() {
return nil, gerror.New("服务不存在")
}
data["updated_at"] = gdb.Raw("NOW()")
if _, err = g.Model(consts.TableGoods).Where("id", req.Id).Data(data).Update(); err != nil {
return nil, err
}
WriteAudit(ctx, AuditEntry{
Action: "goods.save",
TargetType: "goods",
TargetId: auditId(req.Id),
Before: g.Map{"title": exist["title"].String(), "is_enabled": exist["is_enabled"].Int()},
After: g.Map{"title": title, "is_enabled": req.IsEnabled},
Result: consts.AuditResultSuccess,
})
return &adminv1.AdminGoodsSaveRes{Id: req.Id}, nil
}
data["created_at"] = gdb.Raw("NOW()")
data["updated_at"] = gdb.Raw("NOW()")
res, err := g.Model(consts.TableGoods).Data(data).Insert()
if err != nil {
return nil, err
}
id, _ := res.LastInsertId()
WriteAudit(ctx, AuditEntry{
Action: "goods.save",
TargetType: "goods",
TargetId: auditId(id),
After: g.Map{"title": title, "is_enabled": req.IsEnabled},
Result: consts.AuditResultSuccess,
})
return &adminv1.AdminGoodsSaveRes{Id: id}, nil
}
// AdminGoodsDelete 软删除服务(deleted_at 置时间;GoFrame 对带 deleted_at 的表自动过滤已删行)
func AdminGoodsDelete(ctx context.Context, id int64) (*adminv1.AdminGoodsDeleteRes, error) {
exist, err := g.Model(consts.TableGoods).Where("id", id).One()
if err != nil {
return nil, err
}
if exist.IsEmpty() {
return nil, gerror.New("服务不存在")
}
if _, err = g.Model(consts.TableGoods).Where("id", id).Data(g.Map{
"deleted_at": gdb.Raw("NOW()"),
"updated_at": gdb.Raw("NOW()"),
}).Update(); err != nil {
return nil, err
}
WriteAudit(ctx, AuditEntry{
Action: "goods.delete",
TargetType: "goods",
TargetId: auditId(id),
Before: g.Map{"title": exist["title"].String()},
Result: consts.AuditResultSuccess,
})
return &adminv1.AdminGoodsDeleteRes{}, nil
}
// goodsItemOf 列表行 → C 端输出
func goodsItemOf(ctx context.Context, r gdb.Record) userv1.GoodsItem {
return userv1.GoodsItem{
Id: r["id"].Int64(),
Category: r["category"].Int(),
CategoryName: consts.GoodsCategoryNameOf(r["category"].Int()),
Title: r["title"].String(),
Summary: r["summary"].String(),
CoverUrl: absoluteURL(ctx, r["cover_path"].String()),
PriceCents: r["price_cents"].Int64(),
PriceNote: r["price_note"].String(),
}
}