241 lines
8.1 KiB
Go
241 lines
8.1 KiB
Go
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(),
|
||
}
|
||
}
|