2026-07-15 09:43:33 +08:00

276 lines
10 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 activitytemplate
import (
"context"
"strconv"
"strings"
"time"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/integration/activityclient"
"hyapp-admin-server/internal/integration/statisticsclient"
"hyapp-admin-server/internal/integration/userclient"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/response"
activityv1 "hyapp.local/api/proto/activity/v1"
"github.com/gin-gonic/gin"
"google.golang.org/grpc/codes"
grpcstatus "google.golang.org/grpc/status"
)
type Handler struct {
activity activityclient.Client
stats statisticsclient.ActivityTemplateDataClient
users UserBatchGetter
audit shared.OperationLogger
}
type UserBatchGetter interface {
BatchGetUsers(context.Context, userclient.BatchGetUsersRequest) (map[int64]*userclient.User, error)
}
func New(activity activityclient.Client, audit shared.OperationLogger, dataClients ...statisticsclient.ActivityTemplateDataClient) *Handler {
handler := &Handler{activity: activity, audit: audit}
if len(dataClients) > 0 {
handler.stats = dataClients[0]
}
return handler
}
func (h *Handler) BindUserBatchGetter(users UserBatchGetter) *Handler {
if h != nil {
h.users = users
}
return h
}
func (h *Handler) List(c *gin.Context) {
options := shared.ListOptions(c)
startMS, startOK := parseOptionalNonNegativeInt64Query(c, "start_ms")
endMS, endOK := parseOptionalNonNegativeInt64Query(c, "end_ms")
if !startOK || !endOK {
response.BadRequest(c, "start_ms 或 end_ms 参数不正确")
return
}
req := &activityv1.ListActivityTemplatesRequest{
Meta: h.meta(c),
Keyword: strings.TrimSpace(options.Keyword),
Status: strings.TrimSpace(options.Status),
StartMs: startMS,
EndMs: endMS,
Page: int32(options.Page),
PageSize: int32(options.PageSize),
}
if raw, exists := c.GetQuery("region_id"); exists {
regionID, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
if err != nil || regionID <= 0 {
response.BadRequest(c, "region_id 参数不正确")
return
}
req.RegionId = &regionID
}
if raw, exists := c.GetQuery("all_regions"); exists {
allRegions, err := strconv.ParseBool(strings.TrimSpace(raw))
if err != nil {
response.BadRequest(c, "all_regions 参数不正确")
return
}
req.AllRegions = &allRegions
}
resp, err := h.activity.ListActivityTemplates(c.Request.Context(), req)
if err != nil {
h.writeGRPCError(c, err, "获取活动模版失败")
return
}
items := make([]templateSummaryDTO, 0, len(resp.GetTemplates()))
for _, item := range resp.GetTemplates() {
items = append(items, summaryFromProto(item))
}
response.OK(c, pageDTO{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal(), ServerTimeMS: resp.GetServerTimeMs()})
}
func (h *Handler) Create(c *gin.Context) {
var req templateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "活动模版参数不正确")
return
}
resp, err := h.activity.CreateActivityTemplate(c.Request.Context(), &activityv1.CreateActivityTemplateRequest{
Meta: h.meta(c), Template: req.toProto(""), OperatorAdminId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
h.writeGRPCError(c, err, "创建活动模版失败")
return
}
item := templateFromProto(resp.GetTemplate())
shared.OperationLogWithResourceID(c, h.audit, "create-activity-template", "activity_templates", item.TemplateID, "success", "")
response.Created(c, item)
}
func (h *Handler) Get(c *gin.Context) {
templateID := strings.TrimSpace(c.Param("template_id"))
resp, err := h.activity.GetActivityTemplate(c.Request.Context(), &activityv1.GetActivityTemplateRequest{Meta: h.meta(c), TemplateId: templateID})
if err != nil {
h.writeGRPCError(c, err, "获取活动模版详情失败")
return
}
response.OK(c, templateFromProto(resp.GetTemplate()))
}
func (h *Handler) Update(c *gin.Context) {
var req templateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "活动模版参数不正确")
return
}
resp, err := h.activity.UpdateActivityTemplate(c.Request.Context(), &activityv1.UpdateActivityTemplateRequest{
Meta: h.meta(c), Template: req.toProto(strings.TrimSpace(c.Param("template_id"))),
ExpectedRevision: req.ExpectedRevision, OperatorAdminId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
h.writeGRPCError(c, err, "更新活动模版失败")
return
}
item := templateFromProto(resp.GetTemplate())
shared.OperationLogWithResourceID(c, h.audit, "update-activity-template", "activity_templates", item.TemplateID, "success", "")
response.OK(c, item)
}
func (h *Handler) SetStatus(c *gin.Context) {
var req statusRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "活动模版状态参数不正确")
return
}
templateID := strings.TrimSpace(c.Param("template_id"))
resp, err := h.activity.SetActivityTemplateStatus(c.Request.Context(), &activityv1.SetActivityTemplateStatusRequest{
Meta: h.meta(c), TemplateId: templateID, Status: strings.TrimSpace(req.Status),
ExpectedRevision: req.ExpectedRevision, OperatorAdminId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
h.writeGRPCError(c, err, "更新活动模版状态失败")
return
}
item := templateFromProto(resp.GetTemplate())
shared.OperationLogWithResourceID(c, h.audit, "set-activity-template-status", "activity_templates", item.TemplateID, "success", "")
response.OK(c, item)
}
func (h *Handler) Delete(c *gin.Context) {
expectedRevision := parseInt64Query(c, "revision")
if expectedRevision <= 0 {
response.BadRequest(c, "revision 参数不正确")
return
}
templateID := strings.TrimSpace(c.Param("template_id"))
resp, err := h.activity.DeleteActivityTemplate(c.Request.Context(), &activityv1.DeleteActivityTemplateRequest{
Meta: h.meta(c), TemplateId: templateID, ExpectedRevision: expectedRevision,
OperatorAdminId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
h.writeGRPCError(c, err, "删除活动模版失败")
return
}
item := templateFromProto(resp.GetTemplate())
shared.OperationLogWithResourceID(c, h.audit, "archive-activity-template", "activity_templates", item.TemplateID, "success", "")
response.OK(c, gin.H{"template": item, "archived": resp.GetArchived()})
}
func (h *Handler) Versions(c *gin.Context) {
options := shared.ListOptions(c)
resp, err := h.activity.ListActivityTemplateVersions(c.Request.Context(), &activityv1.ListActivityTemplateVersionsRequest{
Meta: h.meta(c), TemplateId: strings.TrimSpace(c.Param("template_id")), Page: int32(options.Page), PageSize: int32(options.PageSize),
})
if err != nil {
h.writeGRPCError(c, err, "获取活动模版版本失败")
return
}
items := make([]versionDTO, 0, len(resp.GetVersions()))
for _, version := range resp.GetVersions() {
items = append(items, versionDTO{
TemplateID: version.GetTemplateId(), VersionNo: version.GetVersionNo(), Snapshot: templateFromProto(version.GetSnapshot()),
PublishedByAdminID: version.GetPublishedByAdminId(), PublishedAtMS: version.GetPublishedAtMs(),
RuntimeFromMS: version.GetRuntimeFromMs(), RuntimeToMS: version.GetRuntimeToMs(),
})
}
response.OK(c, pageDTO{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
}
func (h *Handler) Clone(c *gin.Context) {
var req cloneRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "复制活动模版参数不正确")
return
}
resp, err := h.activity.CloneActivityTemplate(c.Request.Context(), &activityv1.CloneActivityTemplateRequest{
Meta: h.meta(c), SourceTemplateId: strings.TrimSpace(c.Param("template_id")), SourceVersion: req.SourceVersion,
TemplateCode: strings.TrimSpace(req.TemplateCode), Name: strings.TrimSpace(req.Name), OperatorAdminId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
h.writeGRPCError(c, err, "复制活动模版失败")
return
}
item := templateFromProto(resp.GetTemplate())
shared.OperationLogWithResourceID(c, h.audit, "clone-activity-template", "activity_templates", item.TemplateID, "success", "")
response.Created(c, item)
}
func (h *Handler) Validate(c *gin.Context) {
var req validateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "活动模版校验参数不正确")
return
}
resp, err := h.activity.ValidateActivityTemplate(c.Request.Context(), &activityv1.ValidateActivityTemplateRequest{
Meta: h.meta(c), Template: req.Template.toProto(""), ForPublish: req.ForPublish,
})
if err != nil {
h.writeGRPCError(c, err, "校验活动模版失败")
return
}
issues := make([]validationIssueDTO, 0, len(resp.GetIssues()))
for _, issue := range resp.GetIssues() {
issues = append(issues, validationIssueDTO{Field: issue.GetField(), Code: issue.GetCode(), Message: issue.GetMessage()})
}
response.OK(c, gin.H{"valid": resp.GetValid(), "issues": issues})
}
func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
return &activityv1.RequestMeta{
RequestId: middleware.CurrentRequestID(c), Caller: "admin-server", AppCode: appctx.FromContext(c.Request.Context()),
SentAtMs: time.Now().UTC().UnixMilli(),
}
}
// writeGRPCError 把 owner service 的并发和状态冲突稳定映射为 HTTP 409
// 不能把 revision 冲突降级成 400否则编辑器无法区分字段错误与“别人已经保存过”。
func (h *Handler) writeGRPCError(c *gin.Context, err error, fallback string) {
status := grpcstatus.Convert(err)
switch status.Code() {
case codes.InvalidArgument:
response.BadRequest(c, status.Message())
case codes.NotFound:
response.NotFound(c, status.Message())
case codes.AlreadyExists, codes.FailedPrecondition, codes.Aborted:
response.Conflict(c, status.Message())
default:
response.ServerError(c, fallback)
}
}
func parseInt64Query(c *gin.Context, key string) int64 {
value, _ := strconv.ParseInt(strings.TrimSpace(c.Query(key)), 10, 64)
return value
}
func parseOptionalNonNegativeInt64Query(c *gin.Context, key string) (int64, bool) {
raw, exists := c.GetQuery(key)
if !exists {
return 0, true
}
value, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
return value, err == nil && value >= 0
}