增加自定义绑定域名

This commit is contained in:
2026-09-02 15:11:32 +08:00
parent db519d9af2
commit f644c278c9
23 changed files with 2966 additions and 212 deletions
+35
View File
@@ -0,0 +1,35 @@
package controllers
import (
"strings"
"server/services"
beego "github.com/beego/beego/v2/server/web"
)
// AcmeChallengeController 处理 Let's Encrypt 的 HTTP-01 验证回访。
//
// 路由:GET /.well-known/acme-challenge/:token
//
// 该路由在 routers/router.go 中无条件注册,不受 APP_MODE 影响——证书颁发机构
// 什么时候来验证与本服务以哪种模式启动无关。Nginx 扑底站点必须把这个路径按
// 明文 HTTP 反代过来,不能 301 跳转到 HTTPS(首次签发时还没有证书)。
type AcmeChallengeController struct {
beego.Controller
}
// Serve 返回 token 对应的 keyAuthorization(纯文本)
func (c *AcmeChallengeController) Serve() {
token := strings.TrimSpace(c.Ctx.Input.Param(":token"))
keyAuth, ok := services.GetHTTP01Challenge(token)
if !ok {
c.Ctx.Output.SetStatus(404)
_ = c.Ctx.Output.Body([]byte("not found"))
return
}
c.Ctx.Output.Header("Content-Type", "text/plain; charset=utf-8")
// challenge 是一次性的,不能被任何中间层缓存
c.Ctx.Output.Header("Cache-Control", "no-store")
_ = c.Ctx.Output.Body([]byte(keyAuth))
}
+141 -31
View File
@@ -323,20 +323,31 @@ func (c *BackendTenantDomainController) Index() {
tid, _ := c.GetUint64("tid")
statusStr := strings.TrimSpace(c.GetString("status"))
subDomain := strings.TrimSpace(c.GetString("sub_domain"))
keyword := strings.TrimSpace(c.GetString("sub_domain"))
domainTypeStr := strings.TrimSpace(c.GetString("domain_type"))
qs := models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("delete_time__isnull", true)
// 条件一次性拼好:SetCond 会覆盖 Filter 累积的条件,不能混用
cond := orm.NewCondition().And("delete_time__isnull", true)
if tid > 0 {
qs = qs.Filter("tid", tid)
cond = cond.And("tid", tid)
}
if statusStr != "" {
if st, err := strconv.Atoi(statusStr); err == nil {
qs = qs.Filter("status", st)
cond = cond.And("status", st)
}
}
if subDomain != "" {
qs = qs.Filter("sub_domain__icontains", subDomain)
if domainTypeStr != "" {
if dt, err := strconv.Atoi(domainTypeStr); err == nil {
cond = cond.And("domain_type", dt)
}
}
// 关键字同时匹配二级前缀与完整域名:自有域名没有 sub_domain,只能靠 full_domain 命中
if keyword != "" {
cond = cond.AndCond(orm.NewCondition().
Or("sub_domain__icontains", keyword).
Or("full_domain__icontains", keyword))
}
qs := models.Orm.QueryTable(new(models.SystemTenantDomain)).SetCond(cond)
total, err := qs.Count()
if err != nil {
@@ -349,8 +360,7 @@ func (c *BackendTenantDomainController) Index() {
jsonErr(&c.Controller, 500, 500, "获取租户域名失败: "+err.Error())
return
}
list := make([]models.SystemTenantDomain, 0, len(rows))
list = append(list, rows...)
list := tenantDomainList(rows)
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
@@ -359,20 +369,21 @@ func (c *BackendTenantDomainController) Index() {
_ = c.ServeJSON()
}
// MyDomains GET /backend/domain/tenant/myDomains?tid=1
// MyDomains GET /backend/domain/tenant/myDomains
// 租户 ID 取登录态,忽略请求参数里的 tid,避免越权查看其他租户域名。
func (c *BackendTenantDomainController) MyDomains() {
if _, err := requireBackend(&c.Controller); err != nil {
claims, err := requireBackend(&c.Controller)
if err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
tid, _ := c.GetUint64("tid")
if tid == 0 {
jsonErr(&c.Controller, 400, 400, "租户ID不能为空")
if claims.TenantId <= 0 {
jsonErr(&c.Controller, 400, 400, "当前登录账号未归属租户")
return
}
var rows []models.SystemTenantDomain
_, err := models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("tid", tid).
_, err = models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("tid", claims.TenantId).
Filter("delete_time__isnull", true).
OrderBy("-id").
All(&rows)
@@ -380,23 +391,101 @@ func (c *BackendTenantDomainController) MyDomains() {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": rows}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": tenantDomainList(rows)}
_ = c.ServeJSON()
}
// Apply POST /backend/domain/tenant/apply body:{tid,sub_domain,main_domain}
func (c *BackendTenantDomainController) Apply() {
// BindCustom POST /backend/domain/tenant/bindCustom body:{domain}
// 绑定租户自有域名。租户 ID 取登录态。
func (c *BackendTenantDomainController) BindCustom() {
claims, err := requireBackend(&c.Controller)
if err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
if claims.TenantId <= 0 {
jsonErr(&c.Controller, 400, 400, "当前登录账号未归属租户")
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p customDomainPayload
if err := json.Unmarshal(raw, &p); err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
id, status, errMsg := bindCustomDomain(uint64(claims.TenantId), p.Domain)
if errMsg != "" {
jsonErr(&c.Controller, status, status, errMsg)
return
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "绑定成功,请按提示配置解析后点击「检测解析」",
"data": map[string]interface{}{"id": id},
}
_ = c.ServeJSON()
}
// CheckCustomDns POST /backend/domain/tenant/checkCustomDns body:{id}
// 检测自有域名解析,通过后异步申请 HTTPS 证书。
func (c *BackendTenantDomainController) CheckCustomDns() {
claims, err := requireBackend(&c.Controller)
if err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
if claims.TenantId <= 0 {
jsonErr(&c.Controller, 400, 400, "当前登录账号未归属租户")
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p domainIDPayload
if err := json.Unmarshal(raw, &p); err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
data, status, errMsg := checkCustomDomainDNS(p.ID, uint64(claims.TenantId))
if errMsg != "" {
jsonErr(&c.Controller, status, status, errMsg)
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
// CustomDomainGuide GET /backend/domain/tenant/customDomainGuide
// 返回租户该怎么配解析(CNAME 目标 / A 记录 IP),前端不硬编码。
func (c *BackendTenantDomainController) CustomDomainGuide() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": customDomainGuideData()}
_ = c.ServeJSON()
}
// Apply POST /backend/domain/tenant/apply body:{sub_domain,main_domain}
// 租户 ID 取登录态,不信请求体,避免替其他租户申请。
func (c *BackendTenantDomainController) Apply() {
claims, err := requireBackend(&c.Controller)
if err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p struct {
Tid uint64 `json:"tid"`
SubDomain string `json:"sub_domain"`
MainDomain string `json:"main_domain"`
}
@@ -404,10 +493,11 @@ func (c *BackendTenantDomainController) Apply() {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
if p.Tid == 0 {
jsonErr(&c.Controller, 400, 400, "租户ID不能为空")
if claims.TenantId <= 0 {
jsonErr(&c.Controller, 400, 400, "当前登录账号未归属租户")
return
}
tenantID := uint64(claims.TenantId)
sub := strings.TrimSpace(p.SubDomain)
main := strings.TrimSpace(p.MainDomain)
if sub == "" {
@@ -423,13 +513,14 @@ func (c *BackendTenantDomainController) Apply() {
return
}
// 该租户是否已有域名
// 该租户是否已申请过平台二级域名(自有域名单独计数,两者可以共存)
cnt, _ := models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("tid", p.Tid).
Filter("tid", tenantID).
Filter("domain_type", models.DomainTypePlatformSub).
Filter("delete_time__isnull", true).
Count()
if cnt > 0 {
jsonErr(&c.Controller, 400, 400, "该租户已有域名,请删除后再次申请")
jsonErr(&c.Controller, 400, 400, "该租户已有二级域名,请删除后再次申请")
return
}
@@ -457,9 +548,10 @@ func (c *BackendTenantDomainController) Apply() {
full := sub + "." + main
now := time.Now()
tid := p.Tid
tid := tenantID
row := &models.SystemTenantDomain{
Tid: &tid,
DomainType: models.DomainTypePlatformSub,
SubDomain: &sub,
MainDomain: &main,
FullDomain: &full,
@@ -500,6 +592,10 @@ func (c *BackendTenantDomainController) Audit() {
jsonErr(&c.Controller, 404, 404, "域名不存在")
return
}
if row.DomainType == models.DomainTypeCustom {
jsonErr(&c.Controller, 400, 400, "自有域名无需人工审核,解析检测与证书签发通过后自动生效")
return
}
if row.Status != 0 {
jsonErr(&c.Controller, 400, 400, "该域名已审核过了")
return
@@ -547,11 +643,20 @@ func (c *BackendTenantDomainController) ToggleStatus() {
return
}
if row.Status == 0 {
jsonErr(&c.Controller, 400, 400, "审核中不可操作")
if row.DomainType == models.DomainTypeCustom {
jsonErr(&c.Controller, 400, 400, "该域名尚未生效(解析或证书未就绪),无需禁用")
} else {
jsonErr(&c.Controller, 400, 400, "审核中不可操作")
}
return
}
newStatus := 2
if row.Status == 2 {
// 自有域名重新启用要求证书还在,否则启用后访问会报证书错误
if row.DomainType == models.DomainTypeCustom && row.SSLStatus != models.SSLStatusIssued {
jsonErr(&c.Controller, 400, 400, "该域名的 HTTPS 证书未就绪,请先检测解析并等待证书签发")
return
}
newStatus = 1
}
now := time.Now()
@@ -568,8 +673,10 @@ func (c *BackendTenantDomainController) ToggleStatus() {
}
// Delete DELETE /backend/domain/tenant/delete/:id
// 只能删除本租户的域名记录。
func (c *BackendTenantDomainController) Delete() {
if _, err := requireBackend(&c.Controller); err != nil {
claims, err := requireBackend(&c.Controller)
if err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
@@ -579,11 +686,14 @@ func (c *BackendTenantDomainController) Delete() {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.SystemTenantDomain)).
qs := models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now, "update_time": now})
Filter("delete_time__isnull", true)
if claims.TenantId > 0 {
qs = qs.Filter("tid", claims.TenantId)
}
now := time.Now()
n, err := qs.Update(map[string]interface{}{"delete_time": now, "update_time": now})
if err != nil {
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
+280
View File
@@ -2,6 +2,11 @@ package controllers
import (
"regexp"
"strings"
"time"
"server/models"
"server/services"
beego "github.com/beego/beego/v2/server/web"
)
@@ -19,3 +24,278 @@ type domainPoolPayload struct {
}
var subDomainRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$`)
// customDomainPayload 绑定自有域名的请求体。
// tid 仅平台端(运营代操作)使用;租户端一律以登录态里的租户 ID 为准。
type customDomainPayload struct {
Tid uint64 `json:"tid"`
Domain string `json:"domain"`
}
// domainIDPayload 仅带域名记录 ID 的请求体
type domainIDPayload struct {
ID uint64 `json:"id"`
}
// ===== 列表输出 =====
// tenantDomainDTO 把域名记录转成前端需要的结构。
// 直接把 ORM 结构体丢给前端会让时间字段变成 RFC3339,且拿不到租户名称,
// 因此列表统一走这里:时间格式化,租户名按 tid 批量补齐。
func tenantDomainDTO(row models.SystemTenantDomain, tenantNames map[uint64]string) map[string]interface{} {
item := map[string]interface{}{
"id": row.ID,
"tid": row.Tid,
"domain_type": row.DomainType,
"sub_domain": strPtrVal(row.SubDomain),
"main_domain": strPtrVal(row.MainDomain),
"full_domain": strPtrVal(row.FullDomain),
"status": row.Status,
"ssl_status": row.SSLStatus,
"ssl_error": strPtrVal(row.SSLError),
"verify_status": row.VerifyStatus,
"verify_msg": strPtrVal(row.VerifyMsg),
"tenant_name": "",
"ssl_expire_at": "",
"last_verify_at": "",
"create_time": row.CreateTime.Format("2006-01-02 15:04:05"),
}
if row.Tid != nil && tenantNames != nil {
item["tenant_name"] = tenantNames[*row.Tid]
}
if row.SSLExpireAt != nil {
item["ssl_expire_at"] = row.SSLExpireAt.Format("2006-01-02 15:04:05")
}
if row.LastVerifyAt != nil {
item["last_verify_at"] = row.LastVerifyAt.Format("2006-01-02 15:04:05")
}
return item
}
// tenantDomainList 批量转换,顺带一次性查出涉及的租户名称
func tenantDomainList(rows []models.SystemTenantDomain) []map[string]interface{} {
tenantNames := lookupTenantNames(rows)
list := make([]map[string]interface{}, 0, len(rows))
for i := range rows {
list = append(list, tenantDomainDTO(rows[i], tenantNames))
}
return list
}
// lookupTenantNames 按 tid 查租户名称,避免列表里逐行查库
func lookupTenantNames(rows []models.SystemTenantDomain) map[uint64]string {
ids := make([]uint64, 0, len(rows))
seen := make(map[uint64]bool, len(rows))
for i := range rows {
if rows[i].Tid == nil || *rows[i].Tid == 0 || seen[*rows[i].Tid] {
continue
}
seen[*rows[i].Tid] = true
ids = append(ids, *rows[i].Tid)
}
names := make(map[uint64]string, len(ids))
if len(ids) == 0 {
return names
}
var tenants []models.SystemTenant
if _, err := models.Orm.QueryTable(new(models.SystemTenant)).
Filter("id__in", ids).
All(&tenants, "ID", "TenantName", "TenantShortName"); err != nil {
return names
}
for i := range tenants {
name := strings.TrimSpace(tenants[i].TenantName)
if name == "" && tenants[i].TenantShortName != nil {
name = strings.TrimSpace(*tenants[i].TenantShortName)
}
names[tenants[i].ID] = name
}
return names
}
func strPtrVal(p *string) string {
if p == nil {
return ""
}
return *p
}
// ===== 以下逻辑被 backend / platform 两套控制器共用 =====
// bindCustomDomain 为租户绑定自有域名。
// 返回 (记录ID, HTTP状态码, 错误信息);错误信息为空表示成功。
func bindCustomDomain(tid uint64, rawDomain string) (uint64, int, string) {
if tid == 0 {
return 0, 400, "租户ID不能为空"
}
host := services.NormalizeHost(rawDomain)
if host == "" {
return 0, 400, "域名格式不正确,请填写完整域名(如 www.example.com)"
}
// 通配符只在平台域名池里有意义:自有域名一条记录对应一个精确 Host,
// 悄悄把 *.example.com 收成 example.com 会绑到用户没想绑的域名上。
if strings.HasPrefix(strings.TrimSpace(rawDomain), "*.") {
return 0, 400, "暂不支持通配符域名,请填写要绑定的具体域名(如 www.example.com)"
}
// 租户必须存在且未停用(平台端可以传任意 tid,这里兜住手误)
tenantOK, _ := models.Orm.QueryTable(new(models.SystemTenant)).
Filter("id", tid).
Filter("delete_time__isnull", true).
Count()
if tenantOK == 0 {
return 0, 400, "租户不存在"
}
// 不允许绑定平台自己的域名:平台主域及其任意子域都由域名池统一发放
msg, err := checkNotPlatformDomain(host)
if err != nil {
return 0, 500, "域名校验失败,请稍后重试"
}
if msg != "" {
return 0, 400, msg
}
// 全局唯一:同一个域名只能指向一个租户官网
used, _ := models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("full_domain", host).
Filter("delete_time__isnull", true).
Count()
if used > 0 {
return 0, 400, "该域名已被绑定"
}
// 每个租户一个自有域名(平台二级域名不受此限制,两者可以同时存在)
exists, _ := models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("tid", tid).
Filter("domain_type", models.DomainTypeCustom).
Filter("delete_time__isnull", true).
Count()
if exists > 0 {
return 0, 400, "该租户已绑定自有域名,请先删除后再绑定新域名"
}
now := time.Now()
tidCopy := tid
row := &models.SystemTenantDomain{
Tid: &tidCopy,
DomainType: models.DomainTypeCustom,
FullDomain: &host,
Status: 0, // DNS 检测通过且证书签发成功后自动置 1
SSLStatus: models.SSLStatusPending,
VerifyStatus: models.VerifyStatusNone,
CreateTime: now,
UpdateTime: &now,
}
id, err := models.Orm.Insert(row)
if err != nil {
return 0, 500, "绑定失败: " + err.Error()
}
return uint64(id), 200, ""
}
// checkNotPlatformDomain 校验域名不属于平台域名池(含子域)。
// 平台域名的解析与证书由平台统一维护,租户只能通过「申请二级域名」获得。
// 返回 (拒绝原因, 查询错误)。查询出错时必须由调用方拒绝,
// 否则一次数据库抖动就能让租户把平台主域绑走。
func checkNotPlatformDomain(host string) (string, error) {
var pools []models.SystemDomainPool
if _, err := models.Orm.QueryTable(new(models.SystemDomainPool)).
Filter("delete_time__isnull", true).
All(&pools); err != nil {
return "", err
}
for i := range pools {
main := services.NormalizeHost(pools[i].MainDomain)
if main == "" {
continue
}
if host == main || strings.HasSuffix(host, "."+main) {
return "该域名属于平台域名 " + main + ",请改用「申请二级域名」功能", nil
}
}
return "", nil
}
// checkCustomDomainDNS 检测自有域名解析,通过则异步触发证书签发。
// tid 传 0 表示不限制归属(平台端运营场景)。
// 返回 (给前端的数据, HTTP状态码, 错误信息)。
func checkCustomDomainDNS(id, tid uint64) (map[string]interface{}, int, string) {
if id == 0 {
return nil, 400, "参数错误"
}
qs := models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("id", id).
Filter("delete_time__isnull", true)
if tid > 0 {
qs = qs.Filter("tid", tid)
}
var row models.SystemTenantDomain
if err := qs.One(&row); err != nil {
return nil, 404, "域名不存在"
}
if row.DomainType != models.DomainTypeCustom {
return nil, 400, "平台二级域名无需检测解析"
}
if row.FullDomain == nil || *row.FullDomain == "" {
return nil, 400, "域名数据异常,请删除后重新绑定"
}
host := *row.FullDomain
passed, msg := services.VerifyCustomDomainDNS(host)
// verify_msg 字段 varchar(500),解析出的 IP 列表可能很长,截断避免写库失败
if len(msg) > 480 {
msg = msg[:480]
}
verifyStatus := models.VerifyStatusFailed
if passed {
verifyStatus = models.VerifyStatusPassed
}
now := time.Now()
if _, err := models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("id", id).
Update(map[string]interface{}{
"verify_status": verifyStatus,
"verify_msg": msg,
"last_verify_at": now,
"update_time": now,
}); err != nil {
return nil, 500, "保存检测结果失败: " + err.Error()
}
data := map[string]interface{}{
"verify_status": verifyStatus,
"verify_msg": msg,
"ssl_status": row.SSLStatus,
}
if !passed {
return data, 200, ""
}
// 解析已通过:证书没签好就发起签发
if row.SSLStatus == models.SSLStatusIssued {
data["ssl_message"] = "解析正常,证书已就绪"
return data, 200, ""
}
if ok, reason := services.IssueCertificateAsync(id, host); ok {
data["ssl_status"] = models.SSLStatusIssuing
data["ssl_message"] = "解析已通过,正在申请 HTTPS 证书,请稍后刷新查看"
} else {
data["ssl_message"] = reason
}
return data, 200, ""
}
// customDomainGuideData 返回租户配置解析所需的信息
func customDomainGuideData() map[string]interface{} {
guide := services.GetCustomDomainGuide()
return map[string]interface{}{
"cname_target": guide.CnameTarget,
"a_ips": guide.AIPs,
"configured": guide.CnameTarget != "" || len(guide.AIPs) > 0,
}
}
+104 -12
View File
@@ -323,20 +323,31 @@ func (c *PlatformTenantDomainController) Index() {
tid, _ := c.GetUint64("tid")
statusStr := strings.TrimSpace(c.GetString("status"))
subDomain := strings.TrimSpace(c.GetString("sub_domain"))
keyword := strings.TrimSpace(c.GetString("sub_domain"))
domainTypeStr := strings.TrimSpace(c.GetString("domain_type"))
qs := models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("delete_time__isnull", true)
// 条件一次性拼好:SetCond 会覆盖 Filter 累积的条件,不能混用
cond := orm.NewCondition().And("delete_time__isnull", true)
if tid > 0 {
qs = qs.Filter("tid", tid)
cond = cond.And("tid", tid)
}
if statusStr != "" {
if st, err := strconv.Atoi(statusStr); err == nil {
qs = qs.Filter("status", st)
cond = cond.And("status", st)
}
}
if subDomain != "" {
qs = qs.Filter("sub_domain__icontains", subDomain)
if domainTypeStr != "" {
if dt, err := strconv.Atoi(domainTypeStr); err == nil {
cond = cond.And("domain_type", dt)
}
}
// 关键字同时匹配二级前缀与完整域名:自有域名没有 sub_domain,只能靠 full_domain 命中
if keyword != "" {
cond = cond.AndCond(orm.NewCondition().
Or("sub_domain__icontains", keyword).
Or("full_domain__icontains", keyword))
}
qs := models.Orm.QueryTable(new(models.SystemTenantDomain)).SetCond(cond)
total, err := qs.Count()
if err != nil {
@@ -349,8 +360,7 @@ func (c *PlatformTenantDomainController) Index() {
jsonErr(&c.Controller, 500, 500, "获取租户域名失败: "+err.Error())
return
}
list := make([]models.SystemTenantDomain, 0, len(rows))
list = append(list, rows...)
list := tenantDomainList(rows)
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
@@ -360,6 +370,7 @@ func (c *PlatformTenantDomainController) Index() {
}
// MyDomains GET /platform/domain/tenant/myDomains?tid=1
// 平台端账号不归属租户,tid 必须显式传入(运营代租户查看)。
func (c *PlatformTenantDomainController) MyDomains() {
if _, err := requirePlatform(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
@@ -380,7 +391,73 @@ func (c *PlatformTenantDomainController) MyDomains() {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": rows}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": tenantDomainList(rows)}
_ = c.ServeJSON()
}
// BindCustom POST /platform/domain/tenant/bindCustom body:{tid,domain}
// 平台端代租户绑定自有域名,tid 必传。
func (c *PlatformTenantDomainController) BindCustom() {
if _, err := requirePlatform(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p customDomainPayload
if err := json.Unmarshal(raw, &p); err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
id, status, errMsg := bindCustomDomain(p.Tid, p.Domain)
if errMsg != "" {
jsonErr(&c.Controller, status, status, errMsg)
return
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "绑定成功,请按提示配置解析后点击「检测解析」",
"data": map[string]interface{}{"id": id},
}
_ = c.ServeJSON()
}
// CheckCustomDns POST /platform/domain/tenant/checkCustomDns body:{id}
// 平台端不限制域名归属,可为任意租户触发检测与签发。
func (c *PlatformTenantDomainController) CheckCustomDns() {
if _, err := requirePlatform(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p domainIDPayload
if err := json.Unmarshal(raw, &p); err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
data, status, errMsg := checkCustomDomainDNS(p.ID, 0)
if errMsg != "" {
jsonErr(&c.Controller, status, status, errMsg)
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
// CustomDomainGuide GET /platform/domain/tenant/customDomainGuide
func (c *PlatformTenantDomainController) CustomDomainGuide() {
if _, err := requirePlatform(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": customDomainGuideData()}
_ = c.ServeJSON()
}
@@ -423,13 +500,14 @@ func (c *PlatformTenantDomainController) Apply() {
return
}
// 该租户是否已有域名
// 该租户是否已申请过平台二级域名(自有域名单独计数,两者可以共存)
cnt, _ := models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("tid", p.Tid).
Filter("domain_type", models.DomainTypePlatformSub).
Filter("delete_time__isnull", true).
Count()
if cnt > 0 {
jsonErr(&c.Controller, 400, 400, "该租户已有域名,请删除后再次申请")
jsonErr(&c.Controller, 400, 400, "该租户已有二级域名,请删除后再次申请")
return
}
@@ -460,6 +538,7 @@ func (c *PlatformTenantDomainController) Apply() {
tid := p.Tid
row := &models.SystemTenantDomain{
Tid: &tid,
DomainType: models.DomainTypePlatformSub,
SubDomain: &sub,
MainDomain: &main,
FullDomain: &full,
@@ -500,6 +579,10 @@ func (c *PlatformTenantDomainController) Audit() {
jsonErr(&c.Controller, 404, 404, "域名不存在")
return
}
if row.DomainType == models.DomainTypeCustom {
jsonErr(&c.Controller, 400, 400, "自有域名无需人工审核,解析检测与证书签发通过后自动生效")
return
}
if row.Status != 0 {
jsonErr(&c.Controller, 400, 400, "该域名已审核过了")
return
@@ -547,11 +630,20 @@ func (c *PlatformTenantDomainController) ToggleStatus() {
return
}
if row.Status == 0 {
jsonErr(&c.Controller, 400, 400, "审核中不可操作")
if row.DomainType == models.DomainTypeCustom {
jsonErr(&c.Controller, 400, 400, "该域名尚未生效(解析或证书未就绪),无需禁用")
} else {
jsonErr(&c.Controller, 400, 400, "审核中不可操作")
}
return
}
newStatus := 2
if row.Status == 2 {
// 自有域名重新启用要求证书还在,否则启用后访问会报证书错误
if row.DomainType == models.DomainTypeCustom && row.SSLStatus != models.SSLStatusIssued {
jsonErr(&c.Controller, 400, 400, "该域名的 HTTPS 证书未就绪,请先检测解析并等待证书签发")
return
}
newStatus = 1
}
now := time.Now()