package sys import ( "context" "encoding/json" "fmt" "net/url" "strings" "time" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gctx" "hotgo/internal/dao" "hotgo/internal/library/contexts" "hotgo/internal/library/imadmin" "hotgo/internal/library/imtenant" "hotgo/internal/library/location" "hotgo/internal/model/entity" "hotgo/internal/model/input/sysin" "hotgo/internal/service" ) type sSysCultivateFlower struct{} func NewSysCultivateFlower() *sSysCultivateFlower { return &sSysCultivateFlower{} } func init() { service.RegisterSysCultivateFlower(NewSysCultivateFlower()) } func cultivateTenantID(ctx context.Context) int64 { id := contexts.GetTenantId(ctx) if id <= 0 { id = g.Cfg().MustGet(ctx, "cultivate.defaultTenantId", 1).Int64() } return id } func (s *sSysCultivateFlower) List(ctx context.Context, in *sysin.CultivateFlowerListInp) (list []*sysin.CultivateFlowerListModel, totalCount int, err error) { c := dao.FlowerCatalog.Columns() mod := dao.FlowerCatalog.Ctx(ctx).Fields("cv_flower_catalog.*, COUNT(cv_flower_stage_rule.id) AS stage_count").LeftJoin(dao.FlowerStageRule.Table(), "cv_flower_stage_rule.flower_catalog_id=cv_flower_catalog.id AND cv_flower_stage_rule.tenant_id=cv_flower_catalog.tenant_id").Where("cv_flower_catalog."+c.TenantId, cultivateTenantID(ctx)).Group("cv_flower_catalog." + c.Id) if in.FlowerCode != "" { mod = mod.WhereLike("cv_flower_catalog."+c.FlowerCode, "%"+strings.TrimSpace(in.FlowerCode)+"%") } if in.FlowerName != "" { mod = mod.WhereLike("cv_flower_catalog."+c.FlowerName, "%"+strings.TrimSpace(in.FlowerName)+"%") } if in.Status != "" { mod = mod.Where("cv_flower_catalog."+c.Status, in.Status) } if in.ConfigVersion != "" { mod = mod.Where("cv_flower_catalog."+c.ConfigVersion, in.ConfigVersion) } if len(in.CreatedAt) == 2 { mod = mod.WhereBetween("cv_flower_catalog."+c.CreatedAt, in.CreatedAt[0], in.CreatedAt[1]) } mod = mod.Page(in.Page, in.PerPage).OrderAsc("cv_flower_catalog." + c.SortNo).OrderDesc("cv_flower_catalog." + c.Id) if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { err = gerror.Wrap(err, "获取花种列表失败") } return } func (s *sSysCultivateFlower) View(ctx context.Context, in *sysin.CultivateFlowerViewInp) (res *sysin.CultivateFlowerViewModel, err error) { c := dao.FlowerCatalog.Columns() var catalog entity.FlowerCatalog if err = dao.FlowerCatalog.Ctx(ctx).Where(g.Map{c.Id: in.Id, c.TenantId: cultivateTenantID(ctx)}).Scan(&catalog); err != nil { return } if catalog.Id == 0 { return nil, gerror.New("花种不存在") } res = &sysin.CultivateFlowerViewModel{CultivateFlowerEditInp: sysin.CultivateFlowerEditInp{Id: catalog.Id, FlowerCode: catalog.FlowerCode, FlowerName: catalog.FlowerName, FlowerDesc: catalog.FlowerDesc, IconFileId: catalog.IconFileId, Status: catalog.Status, DailyWaterLimit: catalog.DailyWaterLimit, DailyFertilizeLimit: catalog.DailyFertilizeLimit, ShareConfig: catalog.ShareConfig, SortNo: catalog.SortNo}, ConfigVersion: catalog.ConfigVersion, CreatedAt: catalog.CreatedAt, UpdatedAt: catalog.UpdatedAt} rc := dao.FlowerStageRule.Columns() var rules []*entity.FlowerStageRule if err = dao.FlowerStageRule.Ctx(ctx).Where(g.Map{rc.TenantId: cultivateTenantID(ctx), rc.FlowerCatalogId: catalog.Id}).OrderAsc(rc.SortNo).OrderAsc(rc.Id).Scan(&rules); err != nil { return } for _, rule := range rules { actions := []string{} if rule.AllowedActions != nil { _ = json.Unmarshal(rule.AllowedActions.MustToJson(), &actions) } res.StageRules = append(res.StageRules, &sysin.CultivateStageRuleEditInp{Id: rule.Id, Stage: rule.Stage, NextStage: rule.NextStage, RequiredPoints: rule.RequiredPoints, WaterPoints: rule.WaterPoints, FertilizePoints: rule.FertilizePoints, AllowedActions: actions, ImageFileId: rule.ImageFileId, AnimationFileId: rule.AnimationFileId, ProgressTextTemplate: rule.ProgressTextTemplate, CompletionText: rule.CompletionText, SortNo: rule.SortNo}) } return } func (s *sSysCultivateFlower) Edit(ctx context.Context, in *sysin.CultivateFlowerEditInp) (err error) { tenantID := cultivateTenantID(ctx) if err = validateCultivateAssets(ctx, tenantID, in); err != nil { return err } operatorID := contexts.GetUserId(ctx) now := time.Now().UTC() version := "cat_" + now.Format("20060102_150405") return dao.FlowerCatalog.Transaction(ctx, func(txCtx context.Context, tx gdb.TX) error { c := dao.FlowerCatalog.Columns() var before entity.FlowerCatalog if in.Id > 0 { if e := tx.Model(dao.FlowerCatalog.Table()).Where(g.Map{c.Id: in.Id, c.TenantId: tenantID}).LockUpdate().Scan(&before); e != nil { return e } if before.Id == 0 { return gerror.New("花种不存在") } if before.FlowerCode != in.FlowerCode { return gerror.New("花种编码创建后不可修改") } } shareJSON := "{}" if in.ShareConfig != nil { shareJSON = in.ShareConfig.MustToJsonString() } data := g.Map{c.FlowerName: in.FlowerName, c.FlowerDesc: in.FlowerDesc, c.IconFileId: in.IconFileId, c.Status: in.Status, c.ConfigVersion: version, c.DailyWaterLimit: in.DailyWaterLimit, c.DailyFertilizeLimit: in.DailyFertilizeLimit, c.ShareConfig: shareJSON, c.SortNo: in.SortNo, c.UpdatedAt: now} if in.Status == "online" { data[c.OnlineAt] = now data[c.OfflineAt] = nil } else if in.Status == "offline" { data[c.OfflineAt] = now } if in.Id == 0 { id, e := imtenant.NextSnowflakeID(txCtx) if e != nil { return e } in.Id = uint64(id) data[c.Id] = id data[c.TenantId] = tenantID data[c.FlowerCode] = in.FlowerCode data[c.CreatedAt] = now if _, e = tx.Model(dao.FlowerCatalog.Table()).Data(data).Insert(); e != nil { return gerror.Wrap(e, "新增花种失败") } } else { if _, e := tx.Model(dao.FlowerCatalog.Table()).Where(g.Map{c.Id: in.Id, c.TenantId: tenantID}).Data(data).Update(); e != nil { return gerror.Wrap(e, "修改花种失败") } } rc := dao.FlowerStageRule.Columns() if _, e := tx.Model(dao.FlowerStageRule.Table()).Where(g.Map{rc.TenantId: tenantID, rc.FlowerCatalogId: in.Id}).Delete(); e != nil { return e } for _, rule := range in.StageRules { id, e := imtenant.NextSnowflakeID(txCtx) if e != nil { return e } actions, _ := json.Marshal(rule.AllowedActions) _, e = tx.Model(dao.FlowerStageRule.Table()).Data(g.Map{rc.Id: id, rc.TenantId: tenantID, rc.FlowerCatalogId: in.Id, rc.Stage: rule.Stage, rc.NextStage: rule.NextStage, rc.RequiredPoints: rule.RequiredPoints, rc.WaterPoints: rule.WaterPoints, rc.FertilizePoints: rule.FertilizePoints, rc.AllowedActions: string(actions), rc.ImageFileId: rule.ImageFileId, rc.AnimationFileId: rule.AnimationFileId, rc.ProgressTextTemplate: rule.ProgressTextTemplate, rc.CompletionText: rule.CompletionText, rc.SortNo: rule.SortNo, rc.CreatedAt: now, rc.UpdatedAt: now}).Insert() if e != nil { return gerror.Wrap(e, "保存成长阶段失败") } } return writeCultivateOperation(txCtx, tx, tenantID, operatorID, map[bool]string{true: "flower.edit", false: "flower.create"}[before.Id > 0], "flower", fmt.Sprint(in.Id), before, in, "", now) }) } func validateCultivateAssets(ctx context.Context, tenantID int64, in *sysin.CultivateFlowerEditInp) error { seen := make(map[string]struct{}) fileIDs := make([]string, 0, 1+len(in.StageRules)*2) add := func(fileID string) { fileID = strings.TrimSpace(fileID) if fileID == "" { return } if _, ok := seen[fileID]; ok { return } seen[fileID] = struct{}{} fileIDs = append(fileIDs, fileID) } add(in.IconFileId) for _, rule := range in.StageRules { add(rule.ImageFileId) add(rule.AnimationFileId) } if len(fileIDs) == 0 { return nil } tenant, err := resolveImAdminTenant(ctx, tenantID) if err != nil { return err } path := "/internal/cultivate/tenants/" + url.PathEscape(tenant.Code) + "/files/validate" for offset := 0; offset < len(fileIDs); offset += 20 { end := offset + 20 if end > len(fileIDs) { end = len(fileIDs) } var out struct { Items []struct { FileId string `json:"file_id"` ReviewStatus string `json:"review_status"` } `json:"items"` } if err = imadmin.DefaultClient(ctx).InternalPost(ctx, path, map[string]any{"user_id": 0, "file_ids": fileIDs[offset:end]}, &out); err != nil { return gerror.Wrap(err, "校验花种素材失败") } for _, item := range out.Items { if item.ReviewStatus == "risky" { return gerror.Newf("素材 %s 审核未通过", item.FileId) } if item.ReviewStatus == "pending" { return gerror.Newf("素材 %s 尚在审核中", item.FileId) } } } return nil } func (s *sSysCultivateFlower) Status(ctx context.Context, in *sysin.CultivateFlowerStatusInp) error { tenantID := cultivateTenantID(ctx) operatorID := contexts.GetUserId(ctx) now := time.Now().UTC() return dao.FlowerCatalog.Transaction(ctx, func(txCtx context.Context, tx gdb.TX) error { c := dao.FlowerCatalog.Columns() var before entity.FlowerCatalog if e := tx.Model(dao.FlowerCatalog.Table()).Where(g.Map{c.Id: in.Id, c.TenantId: tenantID}).LockUpdate().Scan(&before); e != nil { return e } if before.Id == 0 { return gerror.New("花种不存在") } data := g.Map{c.Status: in.Status, c.ConfigVersion: "cat_" + now.Format("20060102_150405"), c.UpdatedAt: now} if in.Status == "online" { data[c.OnlineAt] = now data[c.OfflineAt] = nil } else if in.Status == "offline" { data[c.OfflineAt] = now } if _, e := tx.Model(dao.FlowerCatalog.Table()).Where(c.Id, in.Id).Data(data).Update(); e != nil { return e } return writeCultivateOperation(txCtx, tx, tenantID, operatorID, "flower.status", "flower", fmt.Sprint(in.Id), before, data, in.Reason, now) }) } func writeCultivateOperation(ctx context.Context, tx gdb.TX, tenantID, operatorID int64, operation, targetType, targetID string, before, after any, reason string, now time.Time) error { id, e := imtenant.NextSnowflakeID(ctx) if e != nil { return e } beforeJSON, _ := json.Marshal(before) afterJSON, _ := json.Marshal(after) ipAddress, userAgent := "", "" if request := ghttp.RequestFromCtx(ctx); request != nil { ipAddress = location.GetClientIp(request) userAgent = request.UserAgent() } c := dao.AdminOperationLog.Columns() _, e = tx.Model(dao.AdminOperationLog.Table()).Data(g.Map{c.Id: id, c.TenantId: tenantID, c.OperatorId: operatorID, c.OperationType: operation, c.TargetType: targetType, c.TargetId: targetID, c.RequestId: gctx.CtxId(ctx), c.BeforeSnapshot: gjson.New(beforeJSON).MustToJsonString(), c.AfterSnapshot: gjson.New(afterJSON).MustToJsonString(), c.Reason: reason, c.IpAddress: ipAddress, c.UserAgent: userAgent, c.CreatedAt: now}).Insert() return e }