package console import ( "strings" "time" "yunyan/comm" "yunyan/lego/sys/postgres" "yunyan/pb" "github.com/gin-gonic/gin" ) // ==================== 应用发版与入口控制 (app_release) ==================== // // 后台「服务配置 → 版本控制」与「服务配置 → 游客显隐控制」两个页面的后端。 // 模型见 comm/apprelease.go:一行 = 一个应用(app_name='' 为全局默认),**不分渠道**。 // // ⚠️ 两个页面共用同一行,各自只写自己那组字段。所以保存一律是 // 「先读出整行 → 只覆盖本组字段 → 整行写回」,绝不能直接把请求体当整行存 —— // 那样在「版本控制」页点一次保存就会把游客登录配置清空(反之亦然)。 // getAppRelease 取某作用域的发版配置。没配过时返回一行空值,前端不必区分「没有」和「空」。 func (this *serverComp) getAppRelease(c *gin.Context) { var req struct { AppName string `json:"app_name"` } _ = c.ShouldBindJSON(&req) row, err := this.loadAppRelease(req.AppName) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, gin.H{"item": row}) } // loadAppRelease 读某作用域的整行;没配过返回只带 AppName 的空行(RowId=0,保存时走插入)。 // // 走主库(FindOnePrimary)而非只读副本:两个页面共用同一行、保存是「读整行→改一组→写回」, // 读到副本的延迟旧行就会把另一组字段回滚成旧值 —— 刚在游客页保存完再去版本页保存, // 游客配置可能被打回上一版。未注册副本时它等同 FindOne,没有额外代价。 // // ⚠️ 「没查到」和「查不了」必须分开:连不上库时若也当成空行,接下来的保存会走插入分支、 // 撞上 app_name 唯一索引,用户看到的是一句莫名其妙的约束冲突而不是「数据库连不上」。 func (this *serverComp) loadAppRelease(appName string) (*comm.AppRelease, error) { row := &comm.AppRelease{} err := postgres.FindOnePrimary(comm.TableAppRelease, row, "app_name=?", appName) if err == nil { return row, nil } if err == postgres.ErrNoDocuments { return &comm.AppRelease{AppName: appName}, nil } return nil, err } // saveAppRelease 整行落库(存在则更新,不存在则插入)。调用方负责先做读改写。 func (this *serverComp) saveAppRelease(c *gin.Context, m *comm.AppRelease) { now := time.Now().UnixMilli() m.Updatetime = now var err error if m.RowId == 0 { m.Createtime = now err = postgres.Insert(comm.TableAppRelease, m) } else { err = postgres.Save(comm.TableAppRelease, m) } if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } broadcastConfigChanged(comm.ConfigKindAppRelease, "update", 0) writeOK(c, m) } // saveAppVersion 保存「版本控制」那一组:线上版本 / 最低支持版本 / 强更 / 更新日志 / // 默认下载地址 / 送审版本。不触碰游客登录字段。 func (this *serverComp) saveAppVersion(c *gin.Context) { var req struct { AppName string `json:"app_name"` Version string `json:"version"` MinVersion string `json:"min_version"` ForceUpdate bool `json:"force_update"` Changelog string `json:"changelog"` Address string `json:"address"` ReviewVersion string `json:"review_version"` } if err := c.ShouldBindJSON(&req); err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, err.Error()) return } req.Version = strings.TrimSpace(req.Version) req.MinVersion = strings.TrimSpace(req.MinVersion) req.ReviewVersion = strings.TrimSpace(req.ReviewVersion) // 强更必须有最低支持版本,否则「开了强更却挡不住任何人」,是配置期就能发现的错。 if req.ForceUpdate && req.MinVersion == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "开启强制更新时必须填「最低支持版本」") return } // 最低支持版本不该高于线上版本,否则线上用户全被判定为需强更却又没有更高版本可更。 if req.MinVersion != "" && req.Version != "" && comm.CompareVersion(req.MinVersion, req.Version) > 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "「最低支持版本」不能高于「当前线上版本」") return } m, err := this.loadAppRelease(req.AppName) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } m.AppName = req.AppName m.Version = req.Version m.MinVersion = req.MinVersion m.ForceUpdate = req.ForceUpdate m.Changelog = req.Changelog m.Address = strings.TrimSpace(req.Address) m.ReviewVersion = req.ReviewVersion this.saveAppRelease(c, m) } // saveAppTourists 保存「游客显隐控制」那一组:游客登录开关 + 生效版本。不触碰版本字段。 func (this *serverComp) saveAppTourists(c *gin.Context) { var req struct { AppName string `json:"app_name"` Tourists bool `json:"tourists"` TouristsVersion string `json:"touristsversion"` } if err := c.ShouldBindJSON(&req); err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, err.Error()) return } req.TouristsVersion = strings.TrimSpace(req.TouristsVersion) // 白名单语义:只有被点名的那一版可见。勾了开关却不填版本 = 谁都看不到,等于没开 —— // 这是配置期就能发现的错,别让运营以为「已经开了」再回头去查客户端。 // 要全部隐藏就直接取消勾选,不要靠留空版本号来表达。 if req.Tourists && req.TouristsVersion == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "开启游客登录时必须填「生效版本」——留空等于所有版本都看不到入口;要全部隐藏请直接取消勾选") return } if req.TouristsVersion != "" && !req.Tourists { writeErr(c, pb.ErrorCode_ReqParameterError, "填了「游客登录生效版本」就必须同时勾选「允许游客登录」,否则该版本仍然看不到入口") return } m, err := this.loadAppRelease(req.AppName) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } m.AppName = req.AppName m.Tourists = req.Tourists m.TouristsVersion = req.TouristsVersion this.saveAppRelease(c, m) }