增加网站管理功能

This commit is contained in:
2026-09-17 23:00:06 +08:00
parent 1224735aa8
commit 67e819c629
98 changed files with 8711 additions and 2254 deletions
+392
View File
@@ -0,0 +1,392 @@
package controllers
import (
"strconv"
"strings"
"server/models"
beego "github.com/beego/beego/v2/server/web"
"github.com/beego/beego/v2/client/orm"
)
// =============================================================
// 官网前台只读接口:平台官网内容(yz_platform_website_*)
//
// 供 website SPA 调用,无需登录(与 /site/setting 同级)。
// 数据由 platform 端「官网管理」模块维护;仅返回「已发布」的内容。
//
// GET /site/news 新闻列表(分页 / 分类 / 关键词)
// GET /site/news/categories 新闻分类 + 各分类已发布数量
// GET /site/news/:id 新闻详情(正文为 HTML)
// GET /site/news/:id/related 相关推荐
// POST /site/news/:id/view 浏览量 +1
//
// 说明:列表与详情中返回的 image 形如 /uploads/xxx,由前端拼接接口域名。
// =============================================================
// 文章状态:与 platform 端「文章管理」保持一致
const (
articleStatusPublished int8 = 2
)
// 导航菜单类型:与 platform 端「导航菜单」保持一致
const (
navTypeExternal int8 = 3
)
type IndexWebsiteController struct {
beego.Controller
}
func (c *IndexWebsiteController) ok(data interface{}) {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
func (c *IndexWebsiteController) fail(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
// publishedArticleQuery 已发布文章的基础查询条件
func publishedArticleCond() *orm.Condition {
return orm.NewCondition().
And("delete_time__isnull", true).
And("status", articleStatusPublished)
}
// articleListItem 列表项(不含正文,减小响应体)
// image 与 category_image 分开返回,由前端决定兜底:文章没有封面时用所属分类的图片。
func articleListItem(row models.PlatformWebsiteArticle, cate models.PlatformWebsiteCategory, tags []string) map[string]interface{} {
return map[string]interface{}{
"id": row.ID,
"title": row.Title,
"summary": row.Desc,
"image": row.Image,
"author": row.Author,
"views": row.Views,
"top": row.Top,
"recommend": row.Recommend,
"date": row.PublishTime,
"category_id": row.CateID,
"category_name": cate.Name,
"category_desc": cate.Desc,
"category_image": cate.Image,
"tags": tags,
}
}
// navNode 导航菜单节点(递归结构,供页头渲染)
type navNode struct {
ID uint64 `json:"id"`
Pid uint64 `json:"pid"`
Title string `json:"title"`
Type int8 `json:"type"` // 1 目录 / 2 页面 / 3 外链 / 4 单页
Path string `json:"path"`
Icon string `json:"icon"`
External bool `json:"external"`
Children []*navNode `json:"children"`
}
// Nav GET /site/nav
// 官网页头导航:仅返回启用且显示中的菜单项,按 pid 组树。
// 父级被隐藏/禁用时其子项一并隐藏,避免出现无归属的顶层菜单。
func (c *IndexWebsiteController) Nav() {
var rows []models.PlatformWebsiteNav
if _, err := models.Orm.QueryTable(new(models.PlatformWebsiteNav)).
Filter("delete_time__isnull", true).
Filter("status", 1).
Filter("is_visible", 1).
OrderBy("sort", "id").
All(&rows, "ID", "Pid", "Title", "Type", "Path", "Icon"); err != nil {
c.fail(500, 500, "获取导航菜单失败: "+err.Error())
return
}
nodes := make(map[uint64]*navNode, len(rows))
order := make([]uint64, 0, len(rows))
for _, r := range rows {
nodes[r.ID] = &navNode{
ID: r.ID,
Pid: r.Pid,
Title: r.Title,
Type: r.Type,
Path: r.Path,
Icon: r.Icon,
External: r.Type == navTypeExternal || strings.HasPrefix(r.Path, "http://") || strings.HasPrefix(r.Path, "https://"),
Children: []*navNode{},
}
order = append(order, r.ID)
}
roots := make([]*navNode, 0, len(rows))
for _, id := range order {
node := nodes[id]
if node.Pid == 0 {
roots = append(roots, node)
continue
}
parent, ok := nodes[node.Pid]
if !ok {
continue
}
parent.Children = append(parent.Children, node)
}
c.ok(map[string]interface{}{"list": roots})
}
// News GET /site/news?page=&pageSize=&category=&keyword=
func (c *IndexWebsiteController) News() {
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 6)
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 6
}
if pageSize > 50 {
pageSize = 50
}
cond := publishedArticleCond()
if cateStr := strings.TrimSpace(c.GetString("category")); cateStr != "" {
if cid, err := strconv.ParseUint(cateStr, 10, 64); err == nil && cid > 0 {
cond = cond.And("cate_id", cid)
}
}
if kw := strings.TrimSpace(c.GetString("keyword")); kw != "" {
cond = cond.AndCond(orm.NewCondition().
Or("title__icontains", kw).
Or("desc__icontains", kw))
}
qs := models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).SetCond(cond)
total, err := qs.Count()
if err != nil {
c.fail(500, 500, "获取新闻列表失败: "+err.Error())
return
}
var rows []models.PlatformWebsiteArticle
if _, err = qs.OrderBy("-top", "-recommend", "-publish_time", "-id").
Limit(pageSize, (page-1)*pageSize).
All(&rows, "ID", "CateID", "Title", "Author", "Desc", "Image",
"Status", "Top", "Recommend", "Views", "PublishTime"); err != nil {
c.fail(500, 500, "获取新闻列表失败: "+err.Error())
return
}
articleIDs := make([]uint64, 0, len(rows))
cateIDs := make([]uint64, 0, len(rows))
for _, r := range rows {
articleIDs = append(articleIDs, r.ID)
if r.CateID > 0 {
cateIDs = append(cateIDs, r.CateID)
}
}
cateMap := models.PlatformWebsiteCategoryMap(cateIDs)
tagMap := models.PlatformWebsiteArticleTagNameMap(articleIDs)
list := make([]map[string]interface{}, 0, len(rows))
for _, r := range rows {
tags := tagMap[r.ID]
if tags == nil {
tags = []string{}
}
list = append(list, articleListItem(r, cateMap[r.CateID], tags))
}
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
if totalPages < 1 {
totalPages = 1
}
c.ok(map[string]interface{}{
"list": list,
"total": total,
"page": page,
"pageSize": pageSize,
"totalPages": totalPages,
})
}
// NewsCategories GET /site/news/categories
// 返回启用中的分类及各自已发布文章数量,供列表页筛选条展示。
func (c *IndexWebsiteController) NewsCategories() {
var cates []models.PlatformWebsiteCategory
if _, err := models.Orm.QueryTable(new(models.PlatformWebsiteCategory)).
Filter("delete_time__isnull", true).
OrderBy("sort", "id").
All(&cates, "ID", "Name", "Desc", "Image", "Sort"); err != nil {
c.fail(500, 500, "获取分类失败: "+err.Error())
return
}
list := make([]map[string]interface{}, 0, len(cates))
for _, item := range cates {
cnt, _ := models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
Filter("delete_time__isnull", true).
Filter("status", articleStatusPublished).
Filter("cate_id", item.ID).
Count()
list = append(list, map[string]interface{}{
"id": item.ID,
"name": item.Name,
"desc": item.Desc,
"image": item.Image,
"count": cnt,
})
}
total, _ := models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
SetCond(publishedArticleCond()).
Count()
c.ok(map[string]interface{}{"list": list, "total": total})
}
// NewsDetail GET /site/news/:id
func (c *IndexWebsiteController) NewsDetail() {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
c.fail(400, 400, "无效的内容ID")
return
}
var row models.PlatformWebsiteArticle
if err = models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
SetCond(publishedArticleCond()).
Filter("id", id).
One(&row); err != nil {
c.fail(404, 404, "内容不存在或已下线")
return
}
cate := models.PlatformWebsiteCategoryMap([]uint64{row.CateID})[row.CateID]
tags := models.PlatformWebsiteArticleTagNameMap([]uint64{row.ID})[row.ID]
if tags == nil {
tags = []string{}
}
c.ok(map[string]interface{}{
"id": row.ID,
"title": row.Title,
"summary": row.Desc,
"image": row.Image,
"content": row.Content,
"author": row.Author,
"views": row.Views,
"date": row.PublishTime,
"category_id": row.CateID,
"category_name": cate.Name,
"category_desc": cate.Desc,
"category_image": cate.Image,
"tags": tags,
})
}
// NewsRelated GET /site/news/:id/related?limit=3
// 同分类优先,不足时用其它分类补齐。
func (c *IndexWebsiteController) NewsRelated() {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
c.fail(400, 400, "无效的内容ID")
return
}
limit, _ := c.GetInt("limit", 3)
if limit < 1 || limit > 12 {
limit = 3
}
var current models.PlatformWebsiteArticle
if err = models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
SetCond(publishedArticleCond()).
Filter("id", id).
One(&current); err != nil {
// 当前内容不存在时返回空列表,由前端展示"暂无推荐"
c.ok(map[string]interface{}{"list": []interface{}{}})
return
}
pick := make([]models.PlatformWebsiteArticle, 0, limit)
seen := map[uint64]bool{id: true}
// 1. 同分类
if current.CateID > 0 {
var same []models.PlatformWebsiteArticle
_, _ = models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
SetCond(publishedArticleCond()).
Filter("cate_id", current.CateID).
Exclude("id", id).
OrderBy("-publish_time", "-id").
Limit(limit).
All(&same, "ID", "CateID", "Title", "Author", "Desc", "Image",
"Top", "Recommend", "Views", "PublishTime")
for _, r := range same {
if len(pick) >= limit {
break
}
pick = append(pick, r)
seen[r.ID] = true
}
}
// 2. 不足时用最新其它文章补齐
if len(pick) < limit {
ids := make([]uint64, 0, len(seen))
for k := range seen {
ids = append(ids, k)
}
var others []models.PlatformWebsiteArticle
_, _ = models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
SetCond(publishedArticleCond()).
Exclude("id__in", ids).
OrderBy("-publish_time", "-id").
Limit(limit - len(pick)).
All(&others, "ID", "CateID", "Title", "Author", "Desc", "Image",
"Top", "Recommend", "Views", "PublishTime")
pick = append(pick, others...)
}
articleIDs := make([]uint64, 0, len(pick))
cateIDs := make([]uint64, 0, len(pick))
for _, r := range pick {
articleIDs = append(articleIDs, r.ID)
if r.CateID > 0 {
cateIDs = append(cateIDs, r.CateID)
}
}
cateMap := models.PlatformWebsiteCategoryMap(cateIDs)
tagMap := models.PlatformWebsiteArticleTagNameMap(articleIDs)
list := make([]map[string]interface{}, 0, len(pick))
for _, r := range pick {
tags := tagMap[r.ID]
if tags == nil {
tags = []string{}
}
list = append(list, articleListItem(r, cateMap[r.CateID], tags))
}
c.ok(map[string]interface{}{"list": list})
}
// NewsView POST /site/news/:id/view — 浏览量 +1(详情页打开时调用)
func (c *IndexWebsiteController) NewsView() {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
c.fail(400, 400, "无效的内容ID")
return
}
if _, err = models.Orm.Raw(
"UPDATE yz_platform_website_article SET views = views + 1 WHERE id = ? AND status = ? AND delete_time IS NULL",
id, articleStatusPublished,
).Exec(); err != nil {
c.fail(500, 500, "更新浏览量失败")
return
}
c.ok(nil)
}
+59
View File
@@ -287,6 +287,34 @@ type platformRenameCateBody struct {
Name string `json:"name"`
}
// protectedFileCateNames 受保护的文件分组名称(平台端)。
//
// 这些分组由功能模块按「名称」定位,删除或改名都会导致模块找不到分组:
// - 删除:素材被分散到未分类,且模块会重新补建一个空分组;
// - 改名:模块按名称找不到旧分组,会新建一个同名分组,历史文件被遗留在旧分组里。
// 因此统一禁止删除/重命名,需要调整时走 SQL 人工处理。
//
// 官网素材 官网管理(platform/src/views/website)所有上传的归属分组
// (见 platform/src/utils/websiteUpload.js)
// appsupgrade 软件升级安装包分组(见 platform/src/views/platform/softwareupgrade)
// 图片 平台预留的图片分组,作为官网/后台配图的固定归档位
var protectedFileCateNames = []string{
"官网素材",
"appsupgrade",
"图片",
}
// isProtectedFileCate 判断分组是否为受保护的系统默认分组
func isProtectedFileCate(name string) bool {
name = strings.TrimSpace(name)
for _, n := range protectedFileCateNames {
if n == name {
return true
}
}
return false
}
// RenameFileCate POST /platform/renamefilecate/:id
func (c *PlatformFileController) RenameFileCate() {
claims, err := c.platformClaims()
@@ -316,6 +344,22 @@ func (c *PlatformFileController) RenameFileCate() {
c.jsonErr(400, 400, "分组名称不能为空")
return
}
// 受保护分组禁止改名:功能模块按名称定位该分组
var existCate models.SystemFilesCategory
if err = models.Orm.QueryTable(new(models.SystemFilesCategory)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
One(&existCate); err != nil {
c.jsonErr(404, 404, "分组不存在")
return
}
if isProtectedFileCate(existCate.Name) {
c.jsonErr(400, 400, fmt.Sprintf("「%s」为系统默认分组,不可重命名", existCate.Name))
return
}
n, err := models.Orm.QueryTable(new(models.SystemFilesCategory)).
Filter("id", id).
Filter("tid", tid).
@@ -347,6 +391,21 @@ func (c *PlatformFileController) DeleteFileCate() {
c.jsonErr(400, 400, "无效的分组ID")
return
}
// 受保护分组禁止删除(先取出分组,便于给出明确提示)
var existCate models.SystemFilesCategory
if err = models.Orm.QueryTable(new(models.SystemFilesCategory)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
One(&existCate); err != nil {
c.jsonErr(404, 404, "分组不存在")
return
}
if isProtectedFileCate(existCate.Name) {
c.jsonErr(400, 400, fmt.Sprintf("「%s」为系统默认分组,不可删除", existCate.Name))
return
}
cnt, err := models.Orm.QueryTable(new(models.SystemFile)).
Filter("cate", id).
Filter("tid", tid).
+832
View File
@@ -0,0 +1,832 @@
package controllers
import (
"strings"
"time"
"server/models"
beego "github.com/beego/beego/v2/server/web"
)
// =============================================================
// 平台官网 - 文章管理 /platform/website/article/*
// - 分类管理 /platform/website/category/*
// 表:yz_platform_website_article / yz_platform_website_category
// =============================================================
type PlatformWebsiteArticleController struct {
beego.Controller
}
type platformWebsiteArticlePayload struct {
Title *string `json:"title"`
Author *string `json:"author"`
Cate *uint64 `json:"cate"`
CateID *uint64 `json:"cate_id"`
Content *string `json:"content"`
Desc *string `json:"desc"`
Image *string `json:"image"`
IsTrans *int8 `json:"is_trans"`
TransURL *string `json:"transurl"`
Status *int8 `json:"status"`
Tags []uint64 `json:"tags"`
}
// articleCateID 兼容前端 cate / cate_id 两种传参
func (p *platformWebsiteArticlePayload) articleCateID() (uint64, bool) {
if p.Cate != nil {
return *p.Cate, true
}
if p.CateID != nil {
return *p.CateID, true
}
return 0, false
}
// List GET /platform/website/article/list
// query: keyword、cate、status、startDate、endDate、page、pageSize
func (c *PlatformWebsiteArticleController) List() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
page, pageSize := websitePaging(&c.Controller)
qs := models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
Filter("delete_time__isnull", true)
if kw := strings.TrimSpace(c.GetString("keyword")); kw != "" {
qs = qs.Filter("title__icontains", kw)
}
if cate, err := c.GetInt64("cate"); err == nil && cate > 0 {
qs = qs.Filter("cate_id", cate)
}
if st := websiteStatusParam(&c.Controller); st >= 0 {
qs = qs.Filter("status", st)
}
if start := strings.TrimSpace(c.GetString("startDate")); start != "" {
qs = qs.Filter("publish_time__gte", start+" 00:00:00")
}
if end := strings.TrimSpace(c.GetString("endDate")); end != "" {
qs = qs.Filter("publish_time__lte", end+" 23:59:59")
}
total, err := qs.Count()
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
var rows []models.PlatformWebsiteArticle
if _, err = qs.OrderBy("-top", "-recommend", "-id").
Limit(pageSize, (page-1)*pageSize).
All(&rows,
"ID", "CateID", "Title", "Author", "Desc", "Image",
"IsTrans", "Status", "Top", "Recommend", "Views", "Likes",
"PublishTime", "CreateTime", "UpdateTime"); err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
cateIDs := make([]uint64, 0, len(rows))
for _, r := range rows {
if r.CateID > 0 {
cateIDs = append(cateIDs, r.CateID)
}
}
cateMap := models.PlatformWebsiteCategoryNameMap(cateIDs)
list := make([]map[string]interface{}, 0, len(rows))
for _, r := range rows {
list = append(list, map[string]interface{}{
"id": r.ID,
"cate_id": r.CateID,
"cate": cateMap[r.CateID],
"title": r.Title,
"author": r.Author,
"desc": r.Desc,
"image": r.Image,
"is_trans": r.IsTrans,
"status": r.Status,
"top": r.Top,
"recommend": r.Recommend,
"views": r.Views,
"likes": r.Likes,
"publish_time": r.PublishTime,
"create_time": r.CreateTime,
"update_time": r.UpdateTime,
})
}
websiteOK(&c.Controller, map[string]interface{}{"list": list, "total": total})
}
// ListAll GET /platform/website/article/all — 不分页,供选择/关联使用
func (c *PlatformWebsiteArticleController) ListAll() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
qs := models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
Filter("delete_time__isnull", true)
if kw := strings.TrimSpace(c.GetString("keyword")); kw != "" {
qs = qs.Filter("title__icontains", kw)
}
if st := websiteStatusParam(&c.Controller); st >= 0 {
qs = qs.Filter("status", st)
}
var rows []models.PlatformWebsiteArticle
if _, err := qs.OrderBy("-id").All(&rows, "ID", "Title", "Status", "CateID", "PublishTime"); err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
websiteOK(&c.Controller, rows)
}
// Recycle GET /platform/website/article/recycle — 回收站(已软删除的文章)
func (c *PlatformWebsiteArticleController) Recycle() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
page, pageSize := websitePaging(&c.Controller)
qs := models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
Filter("delete_time__isnull", false)
if kw := strings.TrimSpace(c.GetString("keyword")); kw != "" {
qs = qs.Filter("title__icontains", kw)
}
total, err := qs.Count()
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
var rows []models.PlatformWebsiteArticle
if _, err = qs.OrderBy("-delete_time").
Limit(pageSize, (page-1)*pageSize).
All(&rows, "ID", "CateID", "Title", "Author", "Desc", "Image",
"Status", "Top", "Recommend", "Views", "Likes",
"PublishTime", "CreateTime", "UpdateTime", "DeleteTime"); err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
cateIDs := make([]uint64, 0, len(rows))
for _, r := range rows {
if r.CateID > 0 {
cateIDs = append(cateIDs, r.CateID)
}
}
cateMap := models.PlatformWebsiteCategoryNameMap(cateIDs)
list := make([]map[string]interface{}, 0, len(rows))
for _, r := range rows {
list = append(list, map[string]interface{}{
"id": r.ID,
"cate_id": r.CateID,
"cate": cateMap[r.CateID],
"title": r.Title,
"author": r.Author,
"desc": r.Desc,
"image": r.Image,
"status": r.Status,
"top": r.Top,
"recommend": r.Recommend,
"views": r.Views,
"likes": r.Likes,
"publish_time": r.PublishTime,
"create_time": r.CreateTime,
"update_time": r.UpdateTime,
"delete_time": r.DeleteTime,
})
}
websiteOK(&c.Controller, map[string]interface{}{"list": list, "total": total})
}
// Restore POST /platform/website/article/:id/restore — 从回收站恢复
func (c *PlatformWebsiteArticleController) Restore() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
// 清空 delete_time 属于「反软删除」,beego 的 Update(map) 不便表达,直接走 SQL
res, err := models.Orm.Raw(
"UPDATE yz_platform_website_article SET delete_time = NULL, update_time = ? WHERE id = ? AND delete_time IS NOT NULL",
time.Now(), id,
).Exec()
if err != nil {
jsonErr(&c.Controller, 500, 500, "恢复失败: "+err.Error())
return
}
if affected, _ := res.RowsAffected(); affected == 0 {
jsonErr(&c.Controller, 404, 404, "文章不存在或未被删除")
return
}
websiteOK(&c.Controller, nil)
}
// Purge DELETE /platform/website/article/:id/purge — 彻底删除(仅限回收站中的记录)
func (c *PlatformWebsiteArticleController) Purge() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
Filter("id", id).
Filter("delete_time__isnull", false).
Delete()
if err != nil {
jsonErr(&c.Controller, 500, 500, "彻底删除失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "仅可彻底删除回收站中的文章")
return
}
_ = models.ClearPlatformWebsiteArticleTags(id)
websiteOK(&c.Controller, nil)
}
// Get GET /platform/website/article/:id — 详情(含标签ID列表)
func (c *PlatformWebsiteArticleController) Get() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
var row models.PlatformWebsiteArticle
if err := models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
Filter("id", id).
Filter("delete_time__isnull", true).
One(&row); err != nil {
jsonErr(&c.Controller, 404, 404, "文章不存在")
return
}
cateName := ""
if row.CateID > 0 {
cateName = models.PlatformWebsiteCategoryNameMap([]uint64{row.CateID})[row.CateID]
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{
"id": row.ID,
"cate_id": row.CateID,
"cate": cateName,
"title": row.Title,
"author": row.Author,
"desc": row.Desc,
"image": row.Image,
"content": row.Content,
"is_trans": row.IsTrans,
"transurl": row.TransURL,
"status": row.Status,
"top": row.Top,
"recommend": row.Recommend,
"views": row.Views,
"likes": row.Likes,
"publisher_id": row.PublisherID,
"publish_time": row.PublishTime,
"create_time": row.CreateTime,
"update_time": row.UpdateTime,
"tags": models.PlatformWebsiteArticleTagIDs(row.ID),
},
}
_ = c.ServeJSON()
}
// Create POST /platform/website/article
func (c *PlatformWebsiteArticleController) Create() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
var p platformWebsiteArticlePayload
if !websiteBind(&c.Controller, &p) {
return
}
title := ""
if p.Title != nil {
title = strings.TrimSpace(*p.Title)
}
if title == "" {
jsonErr(&c.Controller, 400, 400, "文章标题不能为空")
return
}
row := models.PlatformWebsiteArticle{Title: title}
if p.Author != nil {
row.Author = strings.TrimSpace(*p.Author)
}
if cate, has := p.articleCateID(); has {
row.CateID = cate
}
if p.Content != nil {
row.Content = *p.Content
}
if p.Desc != nil {
row.Desc = *p.Desc
}
if p.Image != nil {
row.Image = strings.ReplaceAll(*p.Image, "\\", "/")
}
if p.IsTrans != nil {
row.IsTrans = *p.IsTrans
}
if row.IsTrans == 1 && p.TransURL != nil {
url := strings.TrimSpace(*p.TransURL)
row.TransURL = &url
}
if p.Status != nil {
row.Status = *p.Status
}
// 直接提交待审核时同步记录发布时间
if row.Status == 2 {
now := time.Now()
row.PublishTime = &now
}
id, err := models.Orm.Insert(&row)
if err != nil {
jsonErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
return
}
if err = models.SetPlatformWebsiteArticleTags(uint64(id), p.Tags); err != nil {
jsonErr(&c.Controller, 500, 500, "标签保存失败: "+err.Error())
return
}
websiteOK(&c.Controller, map[string]interface{}{"id": id})
}
// Update POST /platform/website/article/:id
func (c *PlatformWebsiteArticleController) Update() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
var p platformWebsiteArticlePayload
if !websiteBind(&c.Controller, &p) {
return
}
up := map[string]interface{}{}
if p.Title != nil {
title := strings.TrimSpace(*p.Title)
if title == "" {
jsonErr(&c.Controller, 400, 400, "文章标题不能为空")
return
}
up["title"] = title
}
if p.Author != nil {
up["author"] = strings.TrimSpace(*p.Author)
}
if cate, has := p.articleCateID(); has {
up["cate_id"] = cate
}
if p.Content != nil {
up["content"] = *p.Content
}
if p.Desc != nil {
up["desc"] = *p.Desc
}
if p.Image != nil {
up["image"] = strings.ReplaceAll(*p.Image, "\\", "/")
}
if p.IsTrans != nil {
up["is_trans"] = *p.IsTrans
}
if p.TransURL != nil {
url := strings.TrimSpace(*p.TransURL)
if url == "" {
up["transurl"] = nil
} else {
up["transurl"] = url
}
}
if p.Status != nil {
up["status"] = *p.Status
if *p.Status == 2 {
up["publish_time"] = time.Now()
}
}
up["update_time"] = time.Now()
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(up)
if err != nil {
jsonErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "文章不存在")
return
}
// tags 传了才覆盖,未传则保持原有关联
if p.Tags != nil {
if err = models.SetPlatformWebsiteArticleTags(id, p.Tags); err != nil {
jsonErr(&c.Controller, 500, 500, "标签保存失败: "+err.Error())
return
}
}
websiteOK(&c.Controller, nil)
}
// Delete DELETE /platform/website/article/:id
func (c *PlatformWebsiteArticleController) Delete() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now, "update_time": now})
if err != nil {
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "文章不存在")
return
}
_ = models.ClearPlatformWebsiteArticleTags(id)
websiteOK(&c.Controller, nil)
}
// Publish POST /platform/website/article/:id/publish
// body: { uid } 选填,用于记录发布人(平台管理员ID)
func (c *PlatformWebsiteArticleController) Publish() {
claims, ok := websiteAuth(&c.Controller)
if !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
var p struct {
UID *uint64 `json:"uid"`
}
// 发布人参数可选,缺失时回退为当前登录管理员
websiteBindOptional(&c.Controller, &p)
publisher := uint64(claims.UserID)
if p.UID != nil && *p.UID > 0 {
publisher = *p.UID
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{
"status": 2,
"publish_time": now,
"publisher_id": publisher,
"update_time": now,
})
if err != nil {
jsonErr(&c.Controller, 500, 500, "发布失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "文章不存在")
return
}
websiteOK(&c.Controller, nil)
}
// UnPublish POST /platform/website/article/:id/unpublish — 下架后状态置为「已隐藏」
func (c *PlatformWebsiteArticleController) UnPublish() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"status": 3, "update_time": time.Now()})
if err != nil {
jsonErr(&c.Controller, 500, 500, "下架失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "文章不存在")
return
}
websiteOK(&c.Controller, nil)
}
// updateArticleFlag 统一处理推荐/置顶标记
func (c *PlatformWebsiteArticleController) updateArticleFlag(field string, value int8, failMsg string) {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{field: value, "update_time": time.Now()})
if err != nil {
jsonErr(&c.Controller, 500, 500, failMsg+": "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "文章不存在")
return
}
websiteOK(&c.Controller, nil)
}
// Recommend POST /platform/website/article/:id/recommend
func (c *PlatformWebsiteArticleController) Recommend() {
c.updateArticleFlag("recommend", 1, "推荐失败")
}
// UnRecommend POST /platform/website/article/:id/unrecommend
func (c *PlatformWebsiteArticleController) UnRecommend() {
c.updateArticleFlag("recommend", 0, "取消推荐失败")
}
// Top POST /platform/website/article/:id/top
func (c *PlatformWebsiteArticleController) Top() {
c.updateArticleFlag("top", 1, "置顶失败")
}
// UnTop POST /platform/website/article/:id/untop
func (c *PlatformWebsiteArticleController) UnTop() {
c.updateArticleFlag("top", 0, "取消置顶失败")
}
// =============================================================
// 分类管理
// =============================================================
type PlatformWebsiteCategoryController struct {
beego.Controller
}
type platformWebsiteCategoryPayload struct {
Name *string `json:"name"`
CID *uint64 `json:"cid"`
Image *string `json:"image"`
Desc *string `json:"desc"`
Sort *int `json:"sort"`
Status *int8 `json:"status"`
}
// All GET /platform/website/category/all — 全量返回(前端自行组树),支持 keyword
func (c *PlatformWebsiteCategoryController) All() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
qs := models.Orm.QueryTable(new(models.PlatformWebsiteCategory)).
Filter("delete_time__isnull", true)
if kw := strings.TrimSpace(c.GetString("keyword")); kw != "" {
qs = qs.Filter("name__icontains", kw)
}
var rows []models.PlatformWebsiteCategory
if _, err := qs.OrderBy("sort", "id").All(&rows); err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
websiteOK(&c.Controller, rows)
}
// List GET /platform/website/category/list — 分页
func (c *PlatformWebsiteCategoryController) List() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
page, pageSize := websitePaging(&c.Controller)
qs := models.Orm.QueryTable(new(models.PlatformWebsiteCategory)).
Filter("delete_time__isnull", true)
if kw := strings.TrimSpace(c.GetString("keyword")); kw != "" {
qs = qs.Filter("name__icontains", kw)
}
total, err := qs.Count()
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
var rows []models.PlatformWebsiteCategory
if _, err = qs.OrderBy("sort", "id").Limit(pageSize, (page-1)*pageSize).All(&rows); err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
websiteOK(&c.Controller, map[string]interface{}{"list": rows, "total": total})
}
// Get GET /platform/website/category/:id
func (c *PlatformWebsiteCategoryController) Get() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
var row models.PlatformWebsiteCategory
if err := models.Orm.QueryTable(new(models.PlatformWebsiteCategory)).
Filter("id", id).
Filter("delete_time__isnull", true).
One(&row); err != nil {
jsonErr(&c.Controller, 404, 404, "分类不存在")
return
}
websiteOK(&c.Controller, row)
}
// Create POST /platform/website/category
func (c *PlatformWebsiteCategoryController) Create() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
var p platformWebsiteCategoryPayload
if !websiteBind(&c.Controller, &p) {
return
}
if p.Name == nil || strings.TrimSpace(*p.Name) == "" {
jsonErr(&c.Controller, 400, 400, "分类名称不能为空")
return
}
row := models.PlatformWebsiteCategory{
Name: strings.TrimSpace(*p.Name),
Status: 1,
}
if p.CID != nil {
row.Cid = *p.CID
}
if p.Image != nil {
row.Image = strings.ReplaceAll(*p.Image, "\\", "/")
}
if p.Desc != nil {
row.Desc = *p.Desc
}
if p.Sort != nil {
row.Sort = *p.Sort
}
if p.Status != nil {
row.Status = *p.Status
}
id, err := models.Orm.Insert(&row)
if err != nil {
jsonErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
return
}
websiteOK(&c.Controller, map[string]interface{}{"id": id})
}
// Update POST /platform/website/category/:id
func (c *PlatformWebsiteCategoryController) Update() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
var p platformWebsiteCategoryPayload
if !websiteBind(&c.Controller, &p) {
return
}
up := map[string]interface{}{}
if p.Name != nil {
name := strings.TrimSpace(*p.Name)
if name == "" {
jsonErr(&c.Controller, 400, 400, "分类名称不能为空")
return
}
up["name"] = name
}
if p.CID != nil {
if *p.CID == id {
jsonErr(&c.Controller, 400, 400, "父级分类不能是自己")
return
}
up["cid"] = *p.CID
}
if p.Image != nil {
up["image"] = strings.ReplaceAll(*p.Image, "\\", "/")
}
if p.Desc != nil {
up["desc"] = *p.Desc
}
if p.Sort != nil {
up["sort"] = *p.Sort
}
if p.Status != nil {
up["status"] = *p.Status
}
if len(up) == 0 {
jsonErr(&c.Controller, 400, 400, "无更新字段")
return
}
up["update_time"] = time.Now()
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteCategory)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(up)
if err != nil {
jsonErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "分类不存在")
return
}
websiteOK(&c.Controller, nil)
}
// UpdateStatus PATCH /platform/website/category/:id/status — body { status }
func (c *PlatformWebsiteCategoryController) UpdateStatus() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
var p struct {
Status *int8 `json:"status"`
}
if !websiteBind(&c.Controller, &p) || p.Status == nil {
jsonErr(&c.Controller, 400, 400, "状态参数错误")
return
}
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteCategory)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"status": *p.Status, "update_time": time.Now()})
if err != nil {
jsonErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "分类不存在")
return
}
websiteOK(&c.Controller, nil)
}
// Delete DELETE /platform/website/category/:id — 存在子分类时禁止删除
func (c *PlatformWebsiteCategoryController) Delete() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
child, err := models.Orm.QueryTable(new(models.PlatformWebsiteCategory)).
Filter("cid", id).
Filter("delete_time__isnull", true).
Count()
if err != nil {
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
}
if child > 0 {
jsonErr(&c.Controller, 400, 400, "该分类下存在子分类,请先删除子分类")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteCategory)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now, "update_time": now})
if err != nil {
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "分类不存在")
return
}
websiteOK(&c.Controller, nil)
}
+99
View File
@@ -0,0 +1,99 @@
package controllers
import (
"bytes"
"encoding/json"
"io"
"strconv"
"strings"
"server/pkg/jwtutil"
beego "github.com/beego/beego/v2/server/web"
)
// =============================================================
// 平台官网管理(/platform/website/*)公共工具
// 数据表前缀 yz_platform_website_,建表见 docs/sql/create_platform_website.sql
// =============================================================
// websiteAuth 平台管理员鉴权,失败时已写出错误响应,返回 ok=false
func websiteAuth(c *beego.Controller) (*jwtutil.Claims, bool) {
claims, err := requirePlatform(c)
if err != nil {
jsonErr(c, 401, 401, err.Error())
return nil, false
}
return claims, true
}
// websiteOK 统一成功响应:{ code:200, msg:"success", data:... }
func websiteOK(c *beego.Controller, data interface{}) {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
// websitePaging 解析分页参数:page 默认 1,pageSize 默认 10(兼容 limit),上限 200
func websitePaging(c *beego.Controller) (int, int) {
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 0)
if pageSize <= 0 {
pageSize, _ = c.GetInt("limit", 0)
}
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
if pageSize > 200 {
pageSize = 200
}
return page, pageSize
}
// websiteID 解析路径参数 :id
func websiteID(c *beego.Controller) (uint64, bool) {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
jsonErr(c, 400, 400, "无效ID")
return 0, false
}
return id, true
}
// websiteStatusParam 解析 status 查询参数,未传时返回 -1(表示不过滤)
func websiteStatusParam(c *beego.Controller) int {
raw := strings.TrimSpace(c.GetString("status"))
if raw == "" {
return -1
}
v, err := strconv.Atoi(raw)
if err != nil {
return -1
}
return v
}
// websiteBind 解析 JSON 请求体到 target,失败时已写出 400
func websiteBind(c *beego.Controller, target interface{}) bool {
body, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(c, 400, 400, "参数错误")
return false
}
if err = json.Unmarshal(body, target); err != nil {
jsonErr(c, 400, 400, "参数错误")
return false
}
return true
}
// websiteBindOptional 宽松解析请求体:body 为空或非法时不作响应,调用方按零值处理
func websiteBindOptional(c *beego.Controller, target interface{}) {
body, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil || len(bytes.TrimSpace(body)) == 0 {
return
}
_ = json.Unmarshal(body, target)
}
+461
View File
@@ -0,0 +1,461 @@
package controllers
import (
"strings"
"time"
"server/models"
beego "github.com/beego/beego/v2/server/web"
"github.com/beego/beego/v2/client/orm"
)
// =============================================================
// 平台官网 - 标签管理 /platform/website/tag/*
// - 评论管理 /platform/website/comment/*
// 表:yz_platform_website_tag / yz_platform_website_comment
// =============================================================
type PlatformWebsiteTagController struct {
beego.Controller
}
type platformWebsiteTagPayload struct {
Name *string `json:"name"`
Sort *int `json:"sort"`
Status *int8 `json:"status"`
}
// List GET /platform/website/tag/list — 分页
func (c *PlatformWebsiteTagController) List() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
page, pageSize := websitePaging(&c.Controller)
qs := models.Orm.QueryTable(new(models.PlatformWebsiteTag)).
Filter("delete_time__isnull", true)
if kw := strings.TrimSpace(c.GetString("keyword")); kw != "" {
qs = qs.Filter("name__icontains", kw)
}
if st := websiteStatusParam(&c.Controller); st >= 0 {
qs = qs.Filter("status", st)
}
total, err := qs.Count()
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
var rows []models.PlatformWebsiteTag
if _, err = qs.OrderBy("sort", "id").Limit(pageSize, (page-1)*pageSize).All(&rows); err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
websiteOK(&c.Controller, map[string]interface{}{"list": rows, "total": total})
}
// All GET /platform/website/tag/all — 全量返回,供文章编辑选择标签
func (c *PlatformWebsiteTagController) All() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
var rows []models.PlatformWebsiteTag
if _, err := models.Orm.QueryTable(new(models.PlatformWebsiteTag)).
Filter("delete_time__isnull", true).
OrderBy("sort", "id").
All(&rows); err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
websiteOK(&c.Controller, rows)
}
// Get GET /platform/website/tag/:id
func (c *PlatformWebsiteTagController) Get() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
var row models.PlatformWebsiteTag
if err := models.Orm.QueryTable(new(models.PlatformWebsiteTag)).
Filter("id", id).
Filter("delete_time__isnull", true).
One(&row); err != nil {
jsonErr(&c.Controller, 404, 404, "标签不存在")
return
}
websiteOK(&c.Controller, row)
}
// Create POST /platform/website/tag
func (c *PlatformWebsiteTagController) Create() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
var p platformWebsiteTagPayload
if !websiteBind(&c.Controller, &p) {
return
}
if p.Name == nil || strings.TrimSpace(*p.Name) == "" {
jsonErr(&c.Controller, 400, 400, "标签名称不能为空")
return
}
name := strings.TrimSpace(*p.Name)
// 同名标签去重,避免前台聚合出现重复项
exist, err := models.Orm.QueryTable(new(models.PlatformWebsiteTag)).
Filter("name", name).
Filter("delete_time__isnull", true).
Count()
if err != nil {
jsonErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
return
}
if exist > 0 {
jsonErr(&c.Controller, 400, 400, "标签名称已存在")
return
}
row := models.PlatformWebsiteTag{Name: name, Status: 1}
if p.Sort != nil {
row.Sort = *p.Sort
}
if p.Status != nil {
row.Status = *p.Status
}
id, err := models.Orm.Insert(&row)
if err != nil {
jsonErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
return
}
websiteOK(&c.Controller, map[string]interface{}{"id": id})
}
// Update POST /platform/website/tag/:id
func (c *PlatformWebsiteTagController) Update() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
var p platformWebsiteTagPayload
if !websiteBind(&c.Controller, &p) {
return
}
up := map[string]interface{}{}
if p.Name != nil {
name := strings.TrimSpace(*p.Name)
if name == "" {
jsonErr(&c.Controller, 400, 400, "标签名称不能为空")
return
}
dup, err := models.Orm.QueryTable(new(models.PlatformWebsiteTag)).
Filter("name", name).
Filter("delete_time__isnull", true).
Exclude("id", id).
Count()
if err != nil {
jsonErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
return
}
if dup > 0 {
jsonErr(&c.Controller, 400, 400, "标签名称已存在")
return
}
up["name"] = name
}
if p.Sort != nil {
up["sort"] = *p.Sort
}
if p.Status != nil {
up["status"] = *p.Status
}
if len(up) == 0 {
jsonErr(&c.Controller, 400, 400, "无更新字段")
return
}
up["update_time"] = time.Now()
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteTag)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(up)
if err != nil {
jsonErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "标签不存在")
return
}
websiteOK(&c.Controller, nil)
}
// UpdateStatus PATCH /platform/website/tag/:id/status — body { status }
func (c *PlatformWebsiteTagController) UpdateStatus() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
var p struct {
Status *int8 `json:"status"`
}
if !websiteBind(&c.Controller, &p) || p.Status == nil {
jsonErr(&c.Controller, 400, 400, "状态参数错误")
return
}
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteTag)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"status": *p.Status, "update_time": time.Now()})
if err != nil {
jsonErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "标签不存在")
return
}
websiteOK(&c.Controller, nil)
}
// Delete DELETE /platform/website/tag/:id
func (c *PlatformWebsiteTagController) Delete() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteTag)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now, "update_time": now})
if err != nil {
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "标签不存在")
return
}
// 同步清理文章关联,避免前台聚合到已删除标签
_, _ = models.Orm.QueryTable(new(models.PlatformWebsiteArticleTag)).
Filter("tag_id", id).
Delete()
websiteOK(&c.Controller, nil)
}
// =============================================================
// 评论管理
// =============================================================
type PlatformWebsiteCommentController struct {
beego.Controller
}
// List GET /platform/website/comment/list
// query: keyword(内容/昵称)、status、article_id、page、pageSize
func (c *PlatformWebsiteCommentController) List() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
page, pageSize := websitePaging(&c.Controller)
cond := orm.NewCondition().And("delete_time__isnull", true)
if kw := strings.TrimSpace(c.GetString("keyword")); kw != "" {
cond = cond.AndCond(orm.NewCondition().
Or("content__icontains", kw).
Or("nickname__icontains", kw))
}
if st := websiteStatusParam(&c.Controller); st >= 0 {
cond = cond.And("status", st)
}
if articleID, err := c.GetInt64("article_id"); err == nil && articleID > 0 {
cond = cond.And("article_id", articleID)
}
qs := models.Orm.QueryTable(new(models.PlatformWebsiteComment)).SetCond(cond)
total, err := qs.Count()
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
var rows []models.PlatformWebsiteComment
if _, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows); err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
articleIDs := make([]uint64, 0, len(rows))
for _, r := range rows {
if r.ArticleID > 0 {
articleIDs = append(articleIDs, r.ArticleID)
}
}
titleMap := models.PlatformWebsiteArticleTitleMap(articleIDs)
list := make([]map[string]interface{}, 0, len(rows))
for _, r := range rows {
list = append(list, map[string]interface{}{
"id": r.ID,
"article_id": r.ArticleID,
"article_title": titleMap[r.ArticleID],
"nickname": r.Nickname,
"email": r.Email,
"content": r.Content,
"reply": r.Reply,
"status": r.Status,
"ip": r.IP,
"create_time": r.CreateTime,
"update_time": r.UpdateTime,
})
}
websiteOK(&c.Controller, map[string]interface{}{"list": list, "total": total})
}
// Get GET /platform/website/comment/:id
func (c *PlatformWebsiteCommentController) Get() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
var row models.PlatformWebsiteComment
if err := models.Orm.QueryTable(new(models.PlatformWebsiteComment)).
Filter("id", id).
Filter("delete_time__isnull", true).
One(&row); err != nil {
jsonErr(&c.Controller, 404, 404, "评论不存在")
return
}
websiteOK(&c.Controller, row)
}
// Audit POST /platform/website/comment/:id/audit — body { status: 1 通过 / 2 拒绝 }
func (c *PlatformWebsiteCommentController) Audit() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
var p struct {
Status *int8 `json:"status"`
}
if !websiteBind(&c.Controller, &p) || p.Status == nil {
jsonErr(&c.Controller, 400, 400, "状态参数错误")
return
}
if *p.Status != 1 && *p.Status != 2 {
jsonErr(&c.Controller, 400, 400, "仅支持通过(1)或拒绝(2)")
return
}
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteComment)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"status": *p.Status, "update_time": time.Now()})
if err != nil {
jsonErr(&c.Controller, 500, 500, "审核失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "评论不存在")
return
}
websiteOK(&c.Controller, nil)
}
// Reply POST /platform/website/comment/:id/reply — body { reply }
func (c *PlatformWebsiteCommentController) Reply() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
var p struct {
Reply *string `json:"reply"`
}
if !websiteBind(&c.Controller, &p) || p.Reply == nil || strings.TrimSpace(*p.Reply) == "" {
jsonErr(&c.Controller, 400, 400, "回复内容不能为空")
return
}
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteComment)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{
"reply": strings.TrimSpace(*p.Reply),
"update_time": time.Now(),
})
if err != nil {
jsonErr(&c.Controller, 500, 500, "回复失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "评论不存在")
return
}
websiteOK(&c.Controller, nil)
}
// Delete DELETE /platform/website/comment/:id
func (c *PlatformWebsiteCommentController) Delete() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteComment)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now, "update_time": now})
if err != nil {
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "评论不存在")
return
}
websiteOK(&c.Controller, nil)
}
// BatchDelete POST /platform/website/comment/batchdelete — body { ids: [] }
func (c *PlatformWebsiteCommentController) BatchDelete() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
var raw struct {
IDs []uint64 `json:"ids"`
}
if !websiteBind(&c.Controller, &raw) || len(raw.IDs) == 0 {
jsonErr(&c.Controller, 400, 400, "请选择要删除的评论")
return
}
now := time.Now()
if _, err := models.Orm.QueryTable(new(models.PlatformWebsiteComment)).
Filter("id__in", raw.IDs).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now, "update_time": now}); err != nil {
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
}
websiteOK(&c.Controller, nil)
}
+707
View File
@@ -0,0 +1,707 @@
package controllers
import (
"strings"
"time"
"server/models"
beego "github.com/beego/beego/v2/server/web"
)
// =============================================================
// 平台官网 - 导航菜单 /platform/website/nav/*
// - 友情链接 /platform/website/link/*
// - 单页管理 /platform/website/page/*
// 表:yz_platform_website_nav / yz_platform_website_link / yz_platform_website_page
// =============================================================
type PlatformWebsiteNavController struct {
beego.Controller
}
type platformWebsiteNavPayload struct {
Pid *uint64 `json:"pid"`
Title *string `json:"title"`
Type *int8 `json:"type"`
Path *string `json:"path"`
ComponentPath *string `json:"component_path"`
Icon *string `json:"icon"`
Sort *int `json:"sort"`
Status *int8 `json:"status"`
IsVisible *int8 `json:"is_visible"`
Permission *string `json:"permission"`
}
// normalize 按菜单类型规范化字段,并返回校验错误信息
func (p *platformWebsiteNavPayload) normalize() (path, componentPath string, errMsg string) {
navType := int8(1)
if p.Type != nil {
navType = *p.Type
}
if navType < 1 || navType > 4 {
return "", "", "菜单类型不正确"
}
if p.Path != nil {
path = strings.TrimSpace(*p.Path)
}
if p.ComponentPath != nil {
componentPath = strings.TrimSpace(*p.ComponentPath)
}
switch navType {
case 3: // 外链
if path == "" {
return "", "", "外链地址不能为空"
}
if !strings.HasPrefix(path, "http://") && !strings.HasPrefix(path, "https://") {
return "", "", "外链地址需以 http:// 或 https:// 开头"
}
componentPath = ""
case 2: // 页面
if path == "" {
return "", "", "路由地址不能为空"
}
if componentPath == "" {
return "", "", "页面类型需填写组件路径"
}
default: // 目录 / 单页
if path == "" {
return "", "", "路由地址不能为空"
}
componentPath = ""
}
return path, componentPath, ""
}
// List GET /platform/website/nav/list — 全量平铺返回,前端自行组树
func (c *PlatformWebsiteNavController) List() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
var rows []models.PlatformWebsiteNav
if _, err := models.Orm.QueryTable(new(models.PlatformWebsiteNav)).
Filter("delete_time__isnull", true).
OrderBy("sort", "id").
All(&rows); err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
websiteOK(&c.Controller, rows)
}
// Create POST /platform/website/nav
func (c *PlatformWebsiteNavController) Create() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
var p platformWebsiteNavPayload
if !websiteBind(&c.Controller, &p) {
return
}
if p.Title == nil || strings.TrimSpace(*p.Title) == "" {
jsonErr(&c.Controller, 400, 400, "菜单名称不能为空")
return
}
path, componentPath, errMsg := p.normalize()
if errMsg != "" {
jsonErr(&c.Controller, 400, 400, errMsg)
return
}
row := models.PlatformWebsiteNav{
Title: strings.TrimSpace(*p.Title),
Path: path,
ComponentPath: componentPath,
Status: 1,
IsVisible: 1,
Type: 1,
}
if p.Pid != nil {
row.Pid = *p.Pid
}
if p.Type != nil {
row.Type = *p.Type
}
if p.Icon != nil {
row.Icon = strings.TrimSpace(*p.Icon)
}
if p.Sort != nil {
row.Sort = *p.Sort
}
if p.Status != nil {
row.Status = *p.Status
}
if p.IsVisible != nil {
row.IsVisible = *p.IsVisible
}
if p.Permission != nil {
row.Permission = strings.TrimSpace(*p.Permission)
}
id, err := models.Orm.Insert(&row)
if err != nil {
jsonErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
return
}
websiteOK(&c.Controller, map[string]interface{}{"id": id})
}
// Update POST /platform/website/nav/:id
func (c *PlatformWebsiteNavController) Update() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
var p platformWebsiteNavPayload
if !websiteBind(&c.Controller, &p) {
return
}
// 类型/路由/组件路径需要联合校验,先取当前记录作为基线
var current models.PlatformWebsiteNav
if err := models.Orm.QueryTable(new(models.PlatformWebsiteNav)).
Filter("id", id).
Filter("delete_time__isnull", true).
One(&current); err != nil {
jsonErr(&c.Controller, 404, 404, "菜单不存在")
return
}
merged := platformWebsiteNavPayload{
Type: p.Type,
Path: p.Path,
ComponentPath: p.ComponentPath,
}
if merged.Type == nil {
t := current.Type
merged.Type = &t
}
if merged.Path == nil {
v := current.Path
merged.Path = &v
}
if merged.ComponentPath == nil {
v := current.ComponentPath
merged.ComponentPath = &v
}
path, componentPath, errMsg := merged.normalize()
if errMsg != "" {
jsonErr(&c.Controller, 400, 400, errMsg)
return
}
up := map[string]interface{}{
"path": path,
"component_path": componentPath,
"type": *merged.Type,
}
if p.Pid != nil {
if *p.Pid == id {
jsonErr(&c.Controller, 400, 400, "父级菜单不能是自己")
return
}
up["pid"] = *p.Pid
}
if p.Title != nil {
title := strings.TrimSpace(*p.Title)
if title == "" {
jsonErr(&c.Controller, 400, 400, "菜单名称不能为空")
return
}
up["title"] = title
}
if p.Icon != nil {
up["icon"] = strings.TrimSpace(*p.Icon)
}
if p.Sort != nil {
up["sort"] = *p.Sort
}
if p.Status != nil {
up["status"] = *p.Status
}
if p.IsVisible != nil {
up["is_visible"] = *p.IsVisible
}
if p.Permission != nil {
up["permission"] = strings.TrimSpace(*p.Permission)
}
up["update_time"] = time.Now()
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteNav)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(up)
if err != nil {
jsonErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "菜单不存在")
return
}
websiteOK(&c.Controller, nil)
}
// Delete DELETE /platform/website/nav/:id — 有子菜单时禁止删除
func (c *PlatformWebsiteNavController) Delete() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
child, err := models.Orm.QueryTable(new(models.PlatformWebsiteNav)).
Filter("pid", id).
Filter("delete_time__isnull", true).
Count()
if err != nil {
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
}
if child > 0 {
jsonErr(&c.Controller, 400, 400, "该菜单下存在子菜单,请先删除子菜单")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteNav)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now, "update_time": now})
if err != nil {
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "菜单不存在")
return
}
websiteOK(&c.Controller, nil)
}
// =============================================================
// 友情链接
// =============================================================
type PlatformWebsiteLinkController struct {
beego.Controller
}
type platformWebsiteLinkPayload struct {
LinkName *string `json:"link_name"`
LinkURL *string `json:"link_url"`
LinkLogo *string `json:"link_logo"`
Description *string `json:"description"`
Sort *int `json:"sort"`
Status *int8 `json:"status"`
}
// List GET /platform/website/link/list?page&pageSize(limit)&keyword&status
func (c *PlatformWebsiteLinkController) List() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
page, pageSize := websitePaging(&c.Controller)
qs := models.Orm.QueryTable(new(models.PlatformWebsiteLink)).
Filter("delete_time__isnull", true)
if kw := strings.TrimSpace(c.GetString("keyword")); kw != "" {
qs = qs.Filter("link_name__icontains", kw)
}
if st := websiteStatusParam(&c.Controller); st >= 0 {
qs = qs.Filter("status", st)
}
total, err := qs.Count()
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
var rows []models.PlatformWebsiteLink
if _, err = qs.OrderBy("sort", "-id").Limit(pageSize, (page-1)*pageSize).All(&rows); err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
websiteOK(&c.Controller, map[string]interface{}{"list": rows, "total": total})
}
// All GET /platform/website/link/all — 全量(仅启用),供官网/下拉使用
func (c *PlatformWebsiteLinkController) All() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
var rows []models.PlatformWebsiteLink
if _, err := models.Orm.QueryTable(new(models.PlatformWebsiteLink)).
Filter("delete_time__isnull", true).
Filter("status", 1).
OrderBy("sort", "-id").
All(&rows); err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
websiteOK(&c.Controller, rows)
}
// Create POST /platform/website/link
func (c *PlatformWebsiteLinkController) Create() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
var p platformWebsiteLinkPayload
if !websiteBind(&c.Controller, &p) {
return
}
if p.LinkName == nil || strings.TrimSpace(*p.LinkName) == "" {
jsonErr(&c.Controller, 400, 400, "链接名称不能为空")
return
}
if p.LinkURL == nil || strings.TrimSpace(*p.LinkURL) == "" {
jsonErr(&c.Controller, 400, 400, "链接地址不能为空")
return
}
row := models.PlatformWebsiteLink{
LinkName: strings.TrimSpace(*p.LinkName),
LinkURL: strings.TrimSpace(*p.LinkURL),
Status: 1,
}
if p.LinkLogo != nil {
row.LinkLogo = strings.ReplaceAll(*p.LinkLogo, "\\", "/")
}
if p.Description != nil {
row.Description = *p.Description
}
if p.Sort != nil {
row.Sort = *p.Sort
}
if p.Status != nil {
row.Status = *p.Status
}
id, err := models.Orm.Insert(&row)
if err != nil {
jsonErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
return
}
websiteOK(&c.Controller, map[string]interface{}{"id": id})
}
// Update PUT /platform/website/link/:id — 也支持只传部分字段(如仅 status)
func (c *PlatformWebsiteLinkController) Update() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
var p platformWebsiteLinkPayload
if !websiteBind(&c.Controller, &p) {
return
}
up := map[string]interface{}{}
if p.LinkName != nil {
name := strings.TrimSpace(*p.LinkName)
if name == "" {
jsonErr(&c.Controller, 400, 400, "链接名称不能为空")
return
}
up["link_name"] = name
}
if p.LinkURL != nil {
url := strings.TrimSpace(*p.LinkURL)
if url == "" {
jsonErr(&c.Controller, 400, 400, "链接地址不能为空")
return
}
up["link_url"] = url
}
if p.LinkLogo != nil {
up["link_logo"] = strings.ReplaceAll(*p.LinkLogo, "\\", "/")
}
if p.Description != nil {
up["description"] = *p.Description
}
if p.Sort != nil {
up["sort"] = *p.Sort
}
if p.Status != nil {
up["status"] = *p.Status
}
if len(up) == 0 {
jsonErr(&c.Controller, 400, 400, "无更新字段")
return
}
up["update_time"] = time.Now()
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteLink)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(up)
if err != nil {
jsonErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "链接不存在")
return
}
websiteOK(&c.Controller, nil)
}
// Delete DELETE /platform/website/link/:id
func (c *PlatformWebsiteLinkController) Delete() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.PlatformWebsiteLink)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now, "update_time": now})
if err != nil {
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "链接不存在")
return
}
websiteOK(&c.Controller, nil)
}
// BatchDelete POST /platform/website/link/batchdelete — body { ids: [] }
func (c *PlatformWebsiteLinkController) BatchDelete() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
var raw struct {
IDs []uint64 `json:"ids"`
}
if !websiteBind(&c.Controller, &raw) || len(raw.IDs) == 0 {
jsonErr(&c.Controller, 400, 400, "请选择要删除的链接")
return
}
now := time.Now()
if _, err := models.Orm.QueryTable(new(models.PlatformWebsiteLink)).
Filter("id__in", raw.IDs).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now, "update_time": now}); err != nil {
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
}
websiteOK(&c.Controller, nil)
}
// =============================================================
// 单页管理
// =============================================================
type PlatformWebsitePageController struct {
beego.Controller
}
type platformWebsitePagePayload struct {
Title *string `json:"title"`
Path *string `json:"path"`
Content *string `json:"content"`
Sort *int `json:"sort"`
Status *int8 `json:"status"`
}
// normalizePath 统一去掉前导斜杠,与前端提交约定保持一致
func normalizePath(raw string) string {
return strings.Trim(strings.TrimSpace(raw), "/")
}
// All GET /platform/website/page/all — 全量返回
func (c *PlatformWebsitePageController) All() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
var rows []models.PlatformWebsitePage
if _, err := models.Orm.QueryTable(new(models.PlatformWebsitePage)).
Filter("delete_time__isnull", true).
OrderBy("sort", "id").
All(&rows); err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
websiteOK(&c.Controller, rows)
}
// Get GET /platform/website/page/:id
func (c *PlatformWebsitePageController) Get() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
var row models.PlatformWebsitePage
if err := models.Orm.QueryTable(new(models.PlatformWebsitePage)).
Filter("id", id).
Filter("delete_time__isnull", true).
One(&row); err != nil {
jsonErr(&c.Controller, 404, 404, "单页不存在")
return
}
websiteOK(&c.Controller, row)
}
// Create POST /platform/website/page
func (c *PlatformWebsitePageController) Create() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
var p platformWebsitePagePayload
if !websiteBind(&c.Controller, &p) {
return
}
if p.Title == nil || strings.TrimSpace(*p.Title) == "" {
jsonErr(&c.Controller, 400, 400, "单页标题不能为空")
return
}
path := ""
if p.Path != nil {
path = normalizePath(*p.Path)
}
if path == "" {
jsonErr(&c.Controller, 400, 400, "路由不能为空")
return
}
exist, err := models.Orm.QueryTable(new(models.PlatformWebsitePage)).
Filter("path", path).
Filter("delete_time__isnull", true).
Count()
if err != nil {
jsonErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
return
}
if exist > 0 {
jsonErr(&c.Controller, 400, 400, "该路由已存在")
return
}
row := models.PlatformWebsitePage{
Title: strings.TrimSpace(*p.Title),
Path: path,
Status: 1,
}
if p.Content != nil {
row.Content = *p.Content
}
if p.Sort != nil {
row.Sort = *p.Sort
}
if p.Status != nil {
row.Status = *p.Status
}
id, err := models.Orm.Insert(&row)
if err != nil {
jsonErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
return
}
websiteOK(&c.Controller, map[string]interface{}{"id": id})
}
// Update POST /platform/website/page/:id
func (c *PlatformWebsitePageController) Update() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
var p platformWebsitePagePayload
if !websiteBind(&c.Controller, &p) {
return
}
up := map[string]interface{}{}
if p.Title != nil {
title := strings.TrimSpace(*p.Title)
if title == "" {
jsonErr(&c.Controller, 400, 400, "单页标题不能为空")
return
}
up["title"] = title
}
if p.Path != nil {
path := normalizePath(*p.Path)
if path == "" {
jsonErr(&c.Controller, 400, 400, "路由不能为空")
return
}
dup, err := models.Orm.QueryTable(new(models.PlatformWebsitePage)).
Filter("path", path).
Filter("delete_time__isnull", true).
Exclude("id", id).
Count()
if err != nil {
jsonErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
return
}
if dup > 0 {
jsonErr(&c.Controller, 400, 400, "该路由已存在")
return
}
up["path"] = path
}
if p.Content != nil {
up["content"] = *p.Content
}
if p.Sort != nil {
up["sort"] = *p.Sort
}
if p.Status != nil {
up["status"] = *p.Status
}
if len(up) == 0 {
jsonErr(&c.Controller, 400, 400, "无更新字段")
return
}
up["update_time"] = time.Now()
n, err := models.Orm.QueryTable(new(models.PlatformWebsitePage)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(up)
if err != nil {
jsonErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "单页不存在")
return
}
websiteOK(&c.Controller, nil)
}
// Delete DELETE /platform/website/page/:id
func (c *PlatformWebsitePageController) Delete() {
if _, ok := websiteAuth(&c.Controller); !ok {
return
}
id, ok := websiteID(&c.Controller)
if !ok {
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.PlatformWebsitePage)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now, "update_time": now})
if err != nil {
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "单页不存在")
return
}
websiteOK(&c.Controller, nil)
}
-144
View File
@@ -1,144 +0,0 @@
-- =============================================================
-- CRM 各表现有表补齐列(历史库升级用)
-- 覆盖:
-- 1) yz_backend_crm_contract 我方角色 our_role
-- 2) yz_backend_crm_clue 预计金额 amount
-- 3) yz_backend_crm_project 产品清单 products / 文档库分类 doc_category_id
-- 4) 客户/供应商 create_user_id(历史表名差异:yz_backend_erp_customer 与 yz_tenant_crm_customer,
-- 模型 TableName 为前者,初始化 SQL 中为后者,两个候选表名都执行)
-- 5) 客户公海字段(in_pool / owner_user_id / owner_user_name /
-- last_owner_name / pool_in_time / pool_reason)
-- 6) yz_backend_crm_product 软件开发字段(line_type / dev_unit_price /
-- dev_mode / dev_tree;dev_mode 另有单独文件 alter_backend_crm_product_dev_mode.sql)
-- 本脚本可重复执行:表或列不存在时才执行 ALTER,已存在自动跳过(不再报 1060/1146)。
-- 运行:mysql -h<host> -P<port> -u<user> -p <db> < alter_backend_crm_columns.sql
-- =============================================================
-- 1) 合同表:我方角色
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_contract');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_contract' AND COLUMN_NAME = 'our_role');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_contract` ADD COLUMN `our_role` tinyint NOT NULL DEFAULT 2 COMMENT ''我方角色:1甲方/2乙方/3丙方/4丁方''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 2) 线索表:预计金额
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_clue');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_clue' AND COLUMN_NAME = 'amount');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_clue` ADD COLUMN `amount` decimal(14,2) NOT NULL DEFAULT ''0.00'' COMMENT ''预计金额''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 3) 项目表:产品清单
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_project');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_project' AND COLUMN_NAME = 'products');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_project` ADD COLUMN `products` text COMMENT ''项目产品清单JSON(写入产品管理时使用)''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 3) 项目表:文档库文件夹分类
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_project');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_project' AND COLUMN_NAME = 'doc_category_id');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_project` ADD COLUMN `doc_category_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT ''关联OA文档库项目文件夹分类ID''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4) 客户:创建人(删除权限校验)
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_customer');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_customer' AND COLUMN_NAME = 'create_user_id');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_erp_customer` ADD COLUMN `create_user_id` varchar(64) NOT NULL DEFAULT '''' COMMENT ''创建人用户ID,用于删除权限校验''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_customer');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_customer' AND COLUMN_NAME = 'create_user_id');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_tenant_crm_customer` ADD COLUMN `create_user_id` varchar(64) NOT NULL DEFAULT '''' COMMENT ''创建人用户ID,用于删除权限校验''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4) 供应商:创建人(删除权限校验)
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_supplier');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_supplier' AND COLUMN_NAME = 'create_user_id');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_erp_supplier` ADD COLUMN `create_user_id` varchar(64) NOT NULL DEFAULT '''' COMMENT ''创建人用户ID,用于删除权限校验''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_supplier');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_supplier' AND COLUMN_NAME = 'create_user_id');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_tenant_crm_supplier` ADD COLUMN `create_user_id` varchar(64) NOT NULL DEFAULT '''' COMMENT ''创建人用户ID,用于删除权限校验''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 5) 客户公海字段:yz_backend_erp_customer
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_customer');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_customer' AND COLUMN_NAME = 'in_pool');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_erp_customer` ADD COLUMN `in_pool` tinyint NOT NULL DEFAULT 0 COMMENT ''是否在公海:0-否 1-是''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_customer');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_customer' AND COLUMN_NAME = 'owner_user_id');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_erp_customer` ADD COLUMN `owner_user_id` varchar(64) NOT NULL DEFAULT '''' COMMENT ''负责人ID,公海中为空''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_customer');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_customer' AND COLUMN_NAME = 'owner_user_name');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_erp_customer` ADD COLUMN `owner_user_name` varchar(128) NOT NULL DEFAULT '''' COMMENT ''负责人姓名''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_customer');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_customer' AND COLUMN_NAME = 'last_owner_name');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_erp_customer` ADD COLUMN `last_owner_name` varchar(128) NOT NULL DEFAULT '''' COMMENT ''移入公海前的原负责人''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_customer');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_customer' AND COLUMN_NAME = 'pool_in_time');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_erp_customer` ADD COLUMN `pool_in_time` datetime DEFAULT NULL COMMENT ''进入公海时间''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_customer');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_customer' AND COLUMN_NAME = 'pool_reason');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_erp_customer` ADD COLUMN `pool_reason` varchar(200) NOT NULL DEFAULT '''' COMMENT ''移入公海原因''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 5) 客户公海字段:yz_tenant_crm_customer(历史表名)
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_customer');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_customer' AND COLUMN_NAME = 'in_pool');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_tenant_crm_customer` ADD COLUMN `in_pool` tinyint NOT NULL DEFAULT 0 COMMENT ''是否在公海:0-否 1-是''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_customer');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_customer' AND COLUMN_NAME = 'owner_user_id');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_tenant_crm_customer` ADD COLUMN `owner_user_id` varchar(64) NOT NULL DEFAULT '''' COMMENT ''负责人ID,公海中为空''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_customer');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_customer' AND COLUMN_NAME = 'owner_user_name');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_tenant_crm_customer` ADD COLUMN `owner_user_name` varchar(128) NOT NULL DEFAULT '''' COMMENT ''负责人姓名''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_customer');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_customer' AND COLUMN_NAME = 'last_owner_name');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_tenant_crm_customer` ADD COLUMN `last_owner_name` varchar(128) NOT NULL DEFAULT '''' COMMENT ''移入公海前的原负责人''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_customer');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_customer' AND COLUMN_NAME = 'pool_in_time');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_tenant_crm_customer` ADD COLUMN `pool_in_time` datetime DEFAULT NULL COMMENT ''进入公海时间''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_customer');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_customer' AND COLUMN_NAME = 'pool_reason');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_tenant_crm_customer` ADD COLUMN `pool_reason` varchar(200) NOT NULL DEFAULT '''' COMMENT ''移入公海原因''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6) 产品表:软件开发字段
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_product');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_product' AND COLUMN_NAME = 'line_type');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_product` ADD COLUMN `line_type` varchar(20) NOT NULL DEFAULT ''product'' COMMENT ''行类型:product成品/dev软件开发''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_product');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_product' AND COLUMN_NAME = 'dev_unit_price');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_product` ADD COLUMN `dev_unit_price` decimal(14,2) NOT NULL DEFAULT ''0.00'' COMMENT ''软件开发统一人天单价''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_product');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_product' AND COLUMN_NAME = 'dev_mode');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_product` ADD COLUMN `dev_mode` varchar(20) NOT NULL DEFAULT ''self'' COMMENT ''软件开发研发方式:self自行研发/outsource外协研发''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_product');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_product' AND COLUMN_NAME = 'dev_tree');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_product` ADD COLUMN `dev_tree` text COMMENT ''软件开发模块树JSON(叶子人天×统一人天单价)''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-68
View File
@@ -1,68 +0,0 @@
-- =============================================================
-- CRM 逾期(overdue)判定与索引
-- 说明:
-- 逾期以「当前日期」实时判定,无需新增存储字段。
-- 前端在列表/详情中展示「逾期 N 天」标签,规则如下:
-- - 项目 : 结束日期(end_date) < 今天 且 状态 != 已完成(3)
-- - 合同 : 到期日期(expire_date) < 今天 且 状态 != 已完成(2) / 已作废(3)
-- - 回款明细: 计划回款日期(plan_date) < 今天 且 明细状态 != 已回款(3)
-- 本脚本仅补充判定所需的日期索引,并给出等价 SQL 供统计/定时任务使用。
-- =============================================================
-- 1) 项目:结束日期索引(逾期判定 + 按结束日期排序)
-- 可重复执行:索引不存在时才执行 ALTER,已存在自动跳过(不再报 1061 Duplicate key)。
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_project');
SET @idx := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_project' AND INDEX_NAME = 'idx_end_date');
SET @ddl := IF(@tbl > 0 AND @idx = 0, 'ALTER TABLE `yz_backend_crm_project` ADD INDEX `idx_end_date` (`end_date`)', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 2) 合同:到期日期索引(逾期判定 + 按到期日期排序)
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_contract');
SET @idx := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_contract' AND INDEX_NAME = 'idx_expire_date');
SET @ddl := IF(@tbl > 0 AND @idx = 0, 'ALTER TABLE `yz_backend_crm_contract` ADD INDEX `idx_expire_date` (`expire_date`)', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 3) 回款明细逾期:plan_date 内嵌于 items(JSON),无法建单列索引。
-- 如需按「逾期回款明细」做服务端统计,建议将回款明细拆为独立子表
-- (如 yz_backend_crm_payback_item,含 plan_date DATE 列),再建索引。
-- 当前版本逾期在应用层(前端/接口)实时计算,items 已含 plan_date。
-- =============================================================
-- 等价查询(统计「逾期」记录,供运营/定时任务参考)
-- 注意:DATE(end_date) < CURDATE() 表示「结束日期在今天之前」。
-- =============================================================
-- 逾期项目(结束日期已过且未完成)
SELECT id, project_name, end_date, status
FROM `yz_backend_crm_project`
WHERE `delete_time` IS NULL
AND `end_date` IS NOT NULL
AND DATE(`end_date`) < CURDATE()
AND `status` <> 3; -- 3=已完成
-- 逾期合同(到期日期已过且非已完成/已作废)
SELECT id, contract_name, expire_date, status
FROM `yz_backend_crm_contract`
WHERE `delete_time` IS NULL
AND `expire_date` IS NOT NULL
AND DATE(`expire_date`) < CURDATE()
AND `status` NOT IN (2, 3); -- 2=已完成, 3=已作废
-- 逾期回款明细(MySQL 5.7+ 可用 JSON 函数;低版本建议在应用层计算)
-- 列出「存在任一逾期明细」的回款计划
SELECT p.id, p.contract_name,
j.seq, j.name, j.plan_date, j.status
FROM `yz_backend_crm_payback` p,
JSON_TABLE(
p.items,
'$[*]' COLUMNS (
seq INT PATH '$.seq',
name VARCHAR(100) PATH '$.name',
plan_date DATE PATH '$.plan_date',
status INT PATH '$.status'
)
) j
WHERE p.`delete_time` IS NULL
AND j.plan_date IS NOT NULL
AND DATE(j.plan_date) < CURDATE()
AND j.status <> 3; -- 3=已回款
@@ -1,42 +0,0 @@
-- =============================================================
-- 回款计划表(yz_backend_crm_payback)建表 / 核对脚本
-- =============================================================
-- 说明:
-- 1. 计划明细 items 以 JSON 文本存储,plan_date(计划回款日期)已内置于
-- 各类 plan_type 的明细结构中,无需新增独立列,故无 ALTER TABLE 语句。
-- 2. 各 plan_type 的 items 明细结构:
-- 月度/季度/年度:{"seq":int,"name":"第1期","amount":0,"plan_date":"2026-10-01"}
-- 进度: {"seq":int,"name":"预付款","percent":30,"amount":0,"plan_date":"2026-10-01"}
-- 自定义: {"seq":int,"name":"首付款","plan_date":"2026-10-01","amount":0}
-- 每行另含 received_amount / received_date / status(回款进度登记用)。
-- 3. 执行下方建表脚本可创建或核对表结构(幂等,已存在则跳过)。
-- =============================================================
CREATE TABLE IF NOT EXISTS `yz_backend_crm_payback` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
`contract_id` BIGINT UNSIGNED NULL,
`contract_no` VARCHAR(50) NOT NULL DEFAULT '',
`contract_name` VARCHAR(100) NOT NULL DEFAULT '',
`customer_id` BIGINT UNSIGNED NULL,
`customer_name` VARCHAR(128) NOT NULL DEFAULT '',
`plan_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1月度/2季度/3年度/4进度/5自定义',
`pay_method` TINYINT NOT NULL DEFAULT 1 COMMENT '1对公/2网银/3现金/4支票/5支付宝/6微信/7其他',
`remind_days` INT NOT NULL DEFAULT 0 COMMENT '提前几天提醒',
`owner_user_id` VARCHAR(64) NOT NULL DEFAULT '',
`owner_user_name` VARCHAR(128) NOT NULL DEFAULT '',
`contract_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '合同总金额',
`total_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '计划回款总额',
`received_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '已回款金额',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1进行中/2已完成',
`items` TEXT NULL COMMENT '计划明细 JSON 数组(含 plan_date 计划回款日期)',
`remark` TEXT NULL,
`create_user_id` VARCHAR(64) NOT NULL DEFAULT '',
`create_time` DATETIME NOT NULL,
`update_time` DATETIME NOT NULL,
`delete_time` DATETIME NULL,
PRIMARY KEY (`id`),
KEY `idx_yz_backend_crm_payback_tenant` (`tenant_id`),
KEY `idx_yz_backend_crm_payback_contract` (`contract_id`),
KEY `idx_yz_backend_crm_payback_delete` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='回款计划表';
@@ -1,21 +0,0 @@
-- =============================================================
-- CRM 回款计划:流水回执(receipt)字段
-- 说明:
-- 回款登记支持上传「流水回执」(银行流水 / 转账凭证等),非必填。
-- 新增两列用于保存回执文件地址与文件名,随回款计划主表一并存取。
-- 文件本身经 /backend/uploadfile 上传到文件存储,此处仅存其可访问地址。
-- =============================================================
-- 回款计划表:流水回执地址 + 文件名(均允许为空)
-- 可重复执行:列不存在时才执行 ALTER,已存在自动跳过(不再报 1060 Duplicate column)。
-- receipt_url
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_payback');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_payback' AND COLUMN_NAME = 'receipt_url');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_payback` ADD COLUMN `receipt_url` VARCHAR(512) NULL COMMENT ''流水回执文件地址(选填)'' AFTER `remark`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- receipt_name
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_payback' AND COLUMN_NAME = 'receipt_name');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_payback` ADD COLUMN `receipt_name` VARCHAR(255) NULL COMMENT ''流水回执文件名(选填)'' AFTER `receipt_url`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
@@ -1,20 +0,0 @@
-- =============================================================
-- Product dev_mode: distinguish self-developed vs outsourced
-- (software development has two kinds: developed by ourselves,
-- or developed by others for us)
-- Table: yz_backend_crm_product
-- dev_mode varchar(20) NOT NULL DEFAULT 'self'
-- 'self' -> developed in-house (default)
-- 'outsource' -> outsourced / developed by others for us
-- Note: server-side auto-migration has been removed; run this file
-- manually. Re-running reports
-- "Duplicate column name", which is safe.
-- Run: mysql -h<host> -P<port> -u<user> -p <db> < alter_backend_crm_product_dev_mode.sql
-- NOTE: keep this file ASCII-only; piping through some shells mangles non-ASCII.
-- =============================================================
-- idempotent: ALTER only runs when the column is missing (no 1060 on re-run)
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_product');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_product' AND COLUMN_NAME = 'dev_mode');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_product` ADD COLUMN `dev_mode` VARCHAR(20) NOT NULL DEFAULT ''self'' COMMENT ''software dev mode: self in-house / outsource developed by others'' AFTER `dev_unit_price`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-55
View File
@@ -1,55 +0,0 @@
-- =============================================================
-- Contacts: support multiple mobile numbers (a person may have several)
-- Tables:
-- 1) yz_backend_crm_entity_contact : contacts of clue / business / project
-- 2) yz_backend_erp_contact : global address book contacts
-- 3) yz_backend_contact_company : company contacts (customers/suppliers),
-- shared by project contact picker / address book / customer-supplier
-- conversion. This one was missed before: without the column any SELECT
-- fails and contact lists show as empty. Run this script manually
-- (server-side auto-migration has been removed).
-- All tables get a new column `mobiles` (TEXT, JSON array, max 5).
-- The old `mobile` / `phone` column keeps the FIRST number for backward
-- compatibility (list display, phone-based dedupe on sync), and existing
-- single numbers are backfilled into `mobiles`.
-- Run: mysql -h<host> -P<port> -u<user> -p <db> < alter_contact_mobiles.sql
-- (idempotent: existing columns are skipped, safe to re-run)
-- NOTE: keep this file ASCII-only; piping through some shells mangles non-ASCII.
-- =============================================================
-- 1) clue / business / project contacts
-- idempotent: ALTER only runs when the column is missing (no 1060 on re-run)
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_entity_contact');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_entity_contact' AND COLUMN_NAME = 'mobiles');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_entity_contact` ADD COLUMN `mobiles` TEXT NULL COMMENT ''phones in JSON array, max 5'' AFTER `mobile`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- backfill existing single number as a one-element JSON array
UPDATE `yz_backend_crm_entity_contact`
SET `mobiles` = JSON_ARRAY(`mobile`)
WHERE (`mobiles` IS NULL OR `mobiles` = '')
AND `mobile` IS NOT NULL AND `mobile` <> '';
-- 2) global address book contacts
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_contact');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_contact' AND COLUMN_NAME = 'mobiles');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_erp_contact` ADD COLUMN `mobiles` TEXT NULL COMMENT ''phones in JSON array, max 5'' AFTER `phone`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
UPDATE `yz_backend_erp_contact`
SET `mobiles` = JSON_ARRAY(`phone`)
WHERE (`mobiles` IS NULL OR `mobiles` = '')
AND `phone` IS NOT NULL AND `phone` <> '';
-- 3) company contacts (customers / suppliers) - shared by project contact
-- picker / address book / customer-supplier conversion.
-- NOTE: run this script manually (server-side auto-migration has been removed).
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_contact_company');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_contact_company' AND COLUMN_NAME = 'mobiles');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_contact_company` ADD COLUMN `mobiles` TEXT NULL COMMENT ''phones in JSON array, max 5'' AFTER `phone`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
UPDATE `yz_backend_contact_company`
SET `mobiles` = JSON_ARRAY(`phone`)
WHERE (`mobiles` IS NULL OR `mobiles` = '')
AND `phone` IS NOT NULL AND `phone` <> '';
@@ -1,24 +0,0 @@
-- =============================================================
-- CRM product categories: system default category "未分类"
-- Table: yz_backend_crm_product_category
-- is_default = 1 -> system default category ("未分类"): kept by
-- the system, cannot be edited or deleted,
-- sort pinned to the top (-1)
-- is_default = 0 -> tenant custom category (full CRUD)
-- Existing "未分类" rows (if any) are marked as default below;
-- the backend also self-heals the flag and creates the record
-- on first use after upgrade.
-- Run: mysql --default-character-set=utf8mb4 -h<host> -P<port> -u<user> -p <db> < alter_crm_product_category_default.sql
-- (re-running only reports "Duplicate column name", which is safe)
-- =============================================================
-- 可重复执行:列不存在时才执行 ALTER,已存在自动跳过(不再报 1060 Duplicate column)。
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_product_category');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_product_category' AND COLUMN_NAME = 'is_default');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_product_category` ADD COLUMN `is_default` TINYINT NOT NULL DEFAULT 0 COMMENT ''1=system default category (read-only), 0=tenant custom'' AFTER `status`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- Mark existing default category (same name as backend default) and pin it to the top
UPDATE `yz_backend_crm_product_category`
SET `is_default` = 1, `sort` = -1
WHERE `name` = '未分类';
-22
View File
@@ -1,22 +0,0 @@
-- =============================================================
-- CRM units: mark system default units as not editable/deletable
-- Table: yz_backend_crm_unit
-- is_default = 1 -> system default unit (seeded by the system,
-- cannot be edited or deleted by tenants)
-- is_default = 0 -> tenant custom unit (full CRUD)
-- Existing seeded default units (matched by name) are marked below;
-- the backend also self-heals the flag on first use after upgrade.
-- Run: mysql --default-character-set=utf8mb4 -h<host> -P<port> -u<user> -p <db> < alter_crm_unit_default.sql
-- (re-running only reports "Duplicate column name", which is safe)
-- =============================================================
-- 可重复执行:列不存在时才执行 ALTER,已存在自动跳过(不再报 1060 Duplicate column)。
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_unit');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_unit' AND COLUMN_NAME = 'is_default');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_unit` ADD COLUMN `is_default` TINYINT NOT NULL DEFAULT 0 COMMENT ''1=system default unit (read-only), 0=tenant custom'' AFTER `status`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- Mark seeded default units (same list as backend crmDefaultUnits)
UPDATE `yz_backend_crm_unit`
SET `is_default` = 1
WHERE `name` IN ('套', '台', '个', '件', '批', '次', '项', '人月', '人天', '年', '月', '米', '平方米', '吨', '千克', '箱');
@@ -1,48 +0,0 @@
-- =============================================================
-- OA日程表补齐「顺延 / 相关图片 / 提醒绑定」字段:yz_backend_oa_schedule
-- 变更:
-- source_id bigint unsigned NOT NULL DEFAULT 0 顺延来源日程ID 0-原始日程
-- carry_over tinyint NOT NULL DEFAULT 0 是否已顺延到下一天 0-否 1-是
-- carry_count int NOT NULL DEFAULT 0 累计顺延次数
-- carry_to_date varchar(10) NOT NULL DEFAULT '' 顺延目标日期(YYYY-MM-DD)
-- images text NULL 相关图片JSON数组 [{"id":1,"url":"","name":""}]
-- reminder_id bigint unsigned NOT NULL DEFAULT 0 绑定的提醒任务ID(对应 yz_backend_schedule.id) 0-无提醒
-- 本脚本可重复执行:列不存在时才执行 ALTER,已存在自动跳过(不再报 1060 Duplicate column)。
-- 运行:mysql -h<host> -P<port> -u<user> -p <db> < alter_oa_schedule_carry_columns.sql
-- =============================================================
-- source_id:顺延来源日程ID
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_schedule');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_schedule' AND COLUMN_NAME = 'source_id');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_oa_schedule` ADD COLUMN `source_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT ''顺延来源日程ID 0-原始日程''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- carry_over:是否已顺延到下一天
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_schedule');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_schedule' AND COLUMN_NAME = 'carry_over');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_oa_schedule` ADD COLUMN `carry_over` tinyint NOT NULL DEFAULT 0 COMMENT ''是否已顺延到下一天 0-否 1-是''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- carry_count:累计顺延次数
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_schedule');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_schedule' AND COLUMN_NAME = 'carry_count');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_oa_schedule` ADD COLUMN `carry_count` int NOT NULL DEFAULT 0 COMMENT ''累计顺延次数''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- carry_to_date:顺延目标日期
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_schedule');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_schedule' AND COLUMN_NAME = 'carry_to_date');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_oa_schedule` ADD COLUMN `carry_to_date` varchar(10) NOT NULL DEFAULT '''' COMMENT ''顺延目标日期(YYYY-MM-DD)''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- images:相关图片
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_schedule');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_schedule' AND COLUMN_NAME = 'images');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_oa_schedule` ADD COLUMN `images` text NULL COMMENT ''相关图片JSON数组 [{id,url,name}]''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- reminder_id:绑定的提醒任务ID
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_schedule');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_schedule' AND COLUMN_NAME = 'reminder_id');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_oa_schedule` ADD COLUMN `reminder_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT ''绑定的提醒任务ID(对应yz_backend_schedule.id) 0-无提醒''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-23
View File
@@ -1,23 +0,0 @@
-- =============================================================
-- Payback records: mark which plan item (progress node / period)
-- a received payment belongs to, so payments can be registered
-- per progress node and shown as tree children under the plan.
-- Table: yz_backend_crm_payback_record
-- item_seq : seq of the plan items entry (1-based), 0 = not specified
-- item_name : node name snapshot (e.g. "预付款")
-- Run: mysql -h<host> -P<port> -u<user> -p <db> < alter_payback_record_item.sql
-- (re-running only reports "Duplicate column name", which is safe)
-- =============================================================
-- 可重复执行:列不存在时才执行 ALTER,已存在自动跳过(不再报 1060 Duplicate column)。
-- item_seq
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_payback_record');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_payback_record' AND COLUMN_NAME = 'item_seq');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_payback_record` ADD COLUMN `item_seq` INT NOT NULL DEFAULT 0 COMMENT ''plan item seq (1-based), 0 = not specified'' AFTER `payback_id`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- item_name
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_payback_record' AND COLUMN_NAME = 'item_name');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_payback_record` ADD COLUMN `item_name` VARCHAR(50) NULL COMMENT ''plan item name snapshot'' AFTER `item_seq`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
@@ -1,30 +0,0 @@
-- =============================================================
-- 支付单表补列:yz_platform_payment_order 增加推广方快照字段
-- 变更原因:佣金在「支付成功」事件上生成,需要知道该笔收款归属的推广方;
-- 推广方在下单时确定,随支付单落快照(后续改归属不影响历史佣金)。
-- 新增列:
-- promoter_id varchar(64) NOT NULL DEFAULT '' 推广方ID
-- promoter_name varchar(128) NOT NULL DEFAULT '' 推广方名称
-- promoter_type varchar(32) NOT NULL DEFAULT '' 推广方类型 channel/service/staff
-- 本脚本可重复执行:列不存在时才执行 ALTER,已存在自动跳过(不报 1060 Duplicate column)。
-- 运行:mysql -h<host> -P<port> -u<user> -p <db> < alter_platform_payment_order_promoter.sql
-- =============================================================
-- promoter_id
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_payment_order');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_payment_order' AND COLUMN_NAME = 'promoter_id');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_platform_payment_order` ADD COLUMN `promoter_id` varchar(64) NOT NULL DEFAULT '''' COMMENT ''推广方ID(下单时快照,支付成功后据此生成佣金)'' AFTER `order_synced`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- promoter_name
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_payment_order' AND COLUMN_NAME = 'promoter_name');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_platform_payment_order` ADD COLUMN `promoter_name` varchar(128) NOT NULL DEFAULT '''' COMMENT ''推广方名称快照'' AFTER `promoter_id`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- promoter_type
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_payment_order' AND COLUMN_NAME = 'promoter_type');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_platform_payment_order` ADD COLUMN `promoter_type` varchar(32) NOT NULL DEFAULT '''' COMMENT ''推广方类型快照:channel渠道伙伴/service服务商/staff内部员工'' AFTER `promoter_name`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 校验:
SHOW COLUMNS FROM yz_platform_payment_order LIKE 'promoter%';
-28
View File
@@ -1,28 +0,0 @@
-- =============================================================
-- Project contacts: pick contacts from the linked companies
-- (a project no longer shows ALL contacts of the customer)
-- Table: yz_backend_crm_project
-- 1) upstream_contact_ids : JSON array of contact IDs picked from
-- the customer's address book (upstream).
-- 2) downstream_contact_ids : JSON array of contact IDs picked from
-- the counterpart's address book
-- (downstream for type=2 / midstream for type=3).
-- Contact details always come from yz_backend_erp_contact in real time;
-- only the picked ID references are stored here. Empty array -> ''.
-- Run: mysql -h<host> -P<port> -u<user> -p <db> < alter_project_contacts.sql
-- (re-running only reports "Duplicate column name", which is safe)
-- NOTE: keep this file ASCII-only; piping through some shells mangles non-ASCII.
-- =============================================================
-- 可重复执行:列不存在时才执行 ALTER,已存在自动跳过(不再报 1060 Duplicate column)。
-- upstream_contact_ids
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_project');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_project' AND COLUMN_NAME = 'upstream_contact_ids');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_project` ADD COLUMN `upstream_contact_ids` TEXT NULL COMMENT ''picked contact IDs (JSON array) from customer address book'' AFTER `downstream_phone`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- downstream_contact_ids
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_project' AND COLUMN_NAME = 'downstream_contact_ids');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_crm_project` ADD COLUMN `downstream_contact_ids` TEXT NULL COMMENT ''picked contact IDs (JSON array) from counterpart address book (downstream/midstream)'' AFTER `upstream_contact_ids`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
@@ -1,21 +0,0 @@
-- =============================================================
-- 站内信消息表补齐「多选接收目标」字段:yz_system_reminderlist
-- 背景:接收目标改为 6 种,其中 3 种支持多选,多选ID统一存 target_ids:
-- target_type 取值:
-- all 全平台(平台端 + 租户端所有启用用户)
-- platform 平台端所有启用用户
-- tenant_all 租户端所有租户的启用用户
-- tenant_role 指定租户角色(多选)下的启用用户 -> target_ids = 角色ID列表
-- tenant 指定租户(多选)下的启用用户 -> target_ids = 租户ID列表
-- tenant_user 指定租户用户(多选) -> target_ids = 用户ID列表
-- target_ids text NULL JSON 数组字符串,如 [1,2,3];全体范围(all/platform/tenant_all)为 NULL
-- 兼容说明:
-- 旧字段 target_role_id(平台角色)/ target_tenant_id(特定租户)保留,旧数据不受影响。
-- 本脚本可重复执行:列不存在时才执行 ALTER,已存在自动跳过(不再报 1060 Duplicate column)。
-- 运行:mysql -h<host> -P<port> -u<user> -p <db> < alter_site_reminderlist_target_ids.sql
-- =============================================================
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_reminderlist');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_reminderlist' AND COLUMN_NAME = 'target_ids');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_system_reminderlist` ADD COLUMN `target_ids` text NULL COMMENT ''接收目标ID列表JSON数组(租户角色/租户/租户用户多选)''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
@@ -1,20 +0,0 @@
-- =============================================================
-- 角色表补齐列:yz_system_admin_role
-- 变更(历史库早于「租户隔离 / 自定义角色」特性建表时需要):
-- tenant_id bigint unsigned NOT NULL DEFAULT 0 租户ID 0-平台/全局 >0-具体租户
-- is_custom tinyint unsigned NOT NULL DEFAULT 0 自定义角色 0-系统/平台预置 1-租户自建
-- 本脚本可重复执行:列不存在时才执行 ALTER,已存在自动跳过(不再报 1060 Duplicate column)。
-- 运行:mysql -h<host> -P<port> -u<user> -p <db> < alter_system_admin_role_columns.sql
-- =============================================================
-- tenant_id:租户ID
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_admin_role');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_admin_role' AND COLUMN_NAME = 'tenant_id');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_system_admin_role` ADD COLUMN `tenant_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT ''租户ID 0-平台/全局 >0-具体租户''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- is_custom:是否租户自建角色
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_admin_role');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_admin_role' AND COLUMN_NAME = 'is_custom');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_system_admin_role` ADD COLUMN `is_custom` tinyint unsigned NOT NULL DEFAULT 0 COMMENT ''自定义角色 0-系统/平台预置 1-租户自建''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
@@ -1,51 +0,0 @@
-- =============================================================
-- 附件表补齐「文件存储分层」改造字段与索引:yz_system_files
-- 变更:
-- source varchar(16) NOT NULL DEFAULT 'backend' 来源端 backend-租户后台 platform-平台端
-- scope varchar(16) NOT NULL DEFAULT 'tenant' 归属 tenant-租户共享 user-用户个人
-- storage varchar(16) NOT NULL DEFAULT '' 存储类型 local/qiniu
-- object_key varchar(512) NOT NULL DEFAULT '' 存储相对路径(不含域名)
-- idx_file_dedup (source, scope, tid, tuid, md5(32)) MD5 精确去重
-- idx_file_owner (source, tid, scope, tuid, delete_time) 归属列表过滤
-- 注意:
-- 1. md5 在部分历史库中是 TEXT 类型,MySQL 不允许对 TEXT/BLOB 建整列索引,
-- 故索引使用前缀长度 md5(32)(MD5 十六进制串固定 32 字符)。
-- 2. 本脚本可重复执行:列/索引不存在时才执行 ALTER,已存在自动跳过(不再报 1060/1061)。
-- 运行:mysql -h<host> -P<port> -u<user> -p <db> < alter_system_files_storage_columns.sql
-- =============================================================
-- source:来源端
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_files');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_files' AND COLUMN_NAME = 'source');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_system_files` ADD COLUMN `source` varchar(16) NOT NULL DEFAULT ''backend'' COMMENT ''来源端: backend-租户后台 platform-平台端''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- scope:归属
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_files');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_files' AND COLUMN_NAME = 'scope');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_system_files` ADD COLUMN `scope` varchar(16) NOT NULL DEFAULT ''tenant'' COMMENT ''归属: tenant-租户共享 user-用户个人''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- storage:存储类型
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_files');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_files' AND COLUMN_NAME = 'storage');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_system_files` ADD COLUMN `storage` varchar(16) NOT NULL DEFAULT '''' COMMENT ''存储类型: local/qiniu''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- object_key:存储相对路径
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_files');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_files' AND COLUMN_NAME = 'object_key');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_system_files` ADD COLUMN `object_key` varchar(512) NOT NULL DEFAULT '''' COMMENT ''存储相对路径(不含域名)''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- idx_file_dedup:MD5 精确去重索引
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_files');
SET @idx := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_files' AND INDEX_NAME = 'idx_file_dedup');
SET @ddl := IF(@tbl > 0 AND @idx = 0, 'ALTER TABLE `yz_system_files` ADD KEY `idx_file_dedup` (`source`, `scope`, `tid`, `tuid`, `md5`(32))', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- idx_file_owner:归属列表过滤索引
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_files');
SET @idx := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_files' AND INDEX_NAME = 'idx_file_owner');
SET @ddl := IF(@tbl > 0 AND @idx = 0, 'ALTER TABLE `yz_system_files` ADD KEY `idx_file_owner` (`source`, `tid`, `scope`, `tuid`, `delete_time`)', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
@@ -1,20 +0,0 @@
-- =============================================================
-- 租户用户表补齐列:yz_system_tenant_user
-- 变更:
-- group_id bigint unsigned NOT NULL DEFAULT 0 角色ID 关联 yz_system_admin_role(id, cid=2),0-未分配(视为全权限)
-- org_id bigint unsigned NOT NULL DEFAULT 0 所属组织(部门)ID 关联 yz_backend_organization(id),0-未分配
-- 本脚本可重复执行:列不存在时才执行 ALTER,已存在自动跳过(不再报 1060 Duplicate column)。
-- 运行:mysql -h<host> -P<port> -u<user> -p <db> < alter_system_tenant_user_columns.sql
-- =============================================================
-- group_id:角色ID
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant_user');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant_user' AND COLUMN_NAME = 'group_id');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_system_tenant_user` ADD COLUMN `group_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT ''角色ID 关联 yz_system_admin_role(cid=2)''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- org_id:所属组织(部门)ID
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant_user');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant_user' AND COLUMN_NAME = 'org_id');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_system_tenant_user` ADD COLUMN `org_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT ''所属组织ID 关联 yz_backend_organization(id),0-未分配''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
@@ -1,14 +0,0 @@
-- 租户套餐按年计费(元/年)+ 到期自动切换基础套餐
-- 1) 套餐表增加有效时长(天),365 = 按年计费;2) 租户表增加套餐到期时间
-- yz_system_tenant_package 增加 duration_days
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant_package');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant_package' AND COLUMN_NAME = 'duration_days');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_system_tenant_package` ADD COLUMN `duration_days` int(11) NOT NULL DEFAULT 365 COMMENT ''套餐有效时长(天),365=按年计费;到期后租户自动切换基础套餐''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- yz_system_tenant 增加 package_expire_time
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant' AND COLUMN_NAME = 'package_expire_time');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_system_tenant` ADD COLUMN `package_expire_time` datetime NULL COMMENT ''套餐到期时间(NULL-永久有效);超过时效自动切换基础套餐''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
@@ -1,47 +0,0 @@
-- =============================================================
-- CRM合同表:yz_backend_crm_contract
-- 说明:
-- 含「我方角色 our_role」「合同形式 party_count」等字段;
-- 产品清单 / 各方签约主体以 JSON 文本存储。
-- 可重复执行(CREATE TABLE IF NOT EXISTS)。
-- 运行:mysql -h<host> -P<port> -u<user> -p <db> < create_backend_crm_contract.sql
-- =============================================================
CREATE TABLE IF NOT EXISTS `yz_backend_crm_contract` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`tenant_id` varchar(64) NOT NULL COMMENT '租户ID',
`contract_no` varchar(50) NOT NULL DEFAULT '' COMMENT '合同编号',
`contract_name` varchar(100) NOT NULL COMMENT '合同名称',
`contract_category` varchar(20) NOT NULL DEFAULT '' COMMENT '合同分类:1开发/2服务/3销售/4租赁/5采购/6运维/7咨询/8其他',
`our_role` tinyint(4) NOT NULL DEFAULT '2' COMMENT '我方角色:1甲方/2乙方/3丙方/4丁方',
`party_count` tinyint(4) NOT NULL DEFAULT '2' COMMENT '合同形式:2/3/4方',
`project_id` bigint(20) DEFAULT NULL COMMENT '关联项目ID,空=无头合同',
`project_name` varchar(100) NOT NULL DEFAULT '' COMMENT '项目名称',
`owner_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '项目负责人用户ID',
`owner_user_name` varchar(128) NOT NULL DEFAULT '' COMMENT '项目负责人姓名',
`sign_date` date DEFAULT NULL COMMENT '签订日期',
`effective_date` date DEFAULT NULL COMMENT '生效日期',
`expire_date` date DEFAULT NULL COMMENT '结束日期',
`parties` text COMMENT '各方签约主体JSON',
`products` text COMMENT '产品清单JSON',
`hardware_amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '硬件部分金额',
`software_amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '软件部分金额',
`other_amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '其他部分金额',
`total_amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '合同总金额',
`total_cost` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '产品总成本',
`total_profit` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '合同总利润',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1草稿/2已完成/3已作废/4履约中/5异常',
`step` tinyint(4) NOT NULL DEFAULT '1' COMMENT '进度步骤:1合同信息/2产品清单',
`remark` text COMMENT '备注',
`create_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人用户ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_contract_name` (`tenant_id`,`contract_name`),
KEY `idx_contract_no` (`tenant_id`,`contract_no`),
KEY `idx_project` (`tenant_id`,`project_id`),
KEY `idx_status` (`tenant_id`,`status`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户CRM合同表';
@@ -1,62 +0,0 @@
-- =============================================================
-- CRM回款相关表:回款计划 / 回款记录(回款进度流水)
-- yz_backend_crm_payback 回款计划
-- yz_backend_crm_payback_record 回款记录(每次「新建回款」追加一条,
-- 以回款计划 payback_id 为先导条件)
-- 可重复执行(CREATE TABLE IF NOT EXISTS)。
-- 运行:mysql -h<host> -P<port> -u<user> -p <db> < create_backend_crm_payback.sql
-- =============================================================
CREATE TABLE IF NOT EXISTS `yz_backend_crm_payback` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`tenant_id` varchar(64) NOT NULL COMMENT '租户ID',
`contract_id` bigint(20) DEFAULT NULL COMMENT '关联合同ID(yz_backend_crm_contract.id)',
`contract_no` varchar(50) NOT NULL DEFAULT '' COMMENT '合同编号',
`contract_name` varchar(100) NOT NULL DEFAULT '' COMMENT '合同名称',
`customer_id` bigint(20) DEFAULT NULL COMMENT '客户ID',
`customer_name` varchar(128) NOT NULL DEFAULT '' COMMENT '客户名称',
`plan_type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '回款周期:1月度/2季度/3年度/4进度/5自定义',
`pay_method` tinyint(4) NOT NULL DEFAULT '1' COMMENT '回款方式:1对公转账/2网银转账/3现金/4支票/5支付宝/6微信/7其他',
`remind_days` int(11) NOT NULL DEFAULT '0' COMMENT '提前几天提醒',
`owner_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '负责人用户ID',
`owner_user_name` varchar(128) NOT NULL DEFAULT '' COMMENT '负责人姓名',
`contract_amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '合同总金额',
`total_amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '计划回款总额',
`received_amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '已回款金额',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1进行中/2已完成',
`items` text COMMENT '计划明细JSON数组',
`remark` text COMMENT '备注',
`create_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人用户ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_contract` (`tenant_id`,`contract_id`),
KEY `idx_customer` (`tenant_id`,`customer_id`),
KEY `idx_status` (`tenant_id`,`status`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户CRM回款计划表';
CREATE TABLE IF NOT EXISTS `yz_backend_crm_payback_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`tenant_id` varchar(64) NOT NULL COMMENT '租户ID',
`contract_id` bigint(20) DEFAULT NULL COMMENT '关联合同ID(yz_backend_crm_contract.id)',
`payback_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '关联回款计划ID(yz_backend_crm_payback.id)',
`amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '本次回款金额',
`received_date` varchar(20) NOT NULL DEFAULT '' COMMENT '回款日期 YYYY-MM-DD',
`pay_method` tinyint(4) NOT NULL DEFAULT '1' COMMENT '回款方式:1对公/2网银/3现金/4支票/5支付宝/6微信/7其他',
`receipt_url` varchar(512) DEFAULT NULL COMMENT '流水回执文件地址',
`receipt_name` varchar(255) DEFAULT NULL COMMENT '流水回执文件名',
`remark` text COMMENT '备注',
`owner_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '负责人用户ID',
`owner_user_name` varchar(128) NOT NULL DEFAULT '' COMMENT '负责人姓名',
`create_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人用户ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
KEY `idx_tenant_contract` (`tenant_id`,`contract_id`),
KEY `idx_payback` (`tenant_id`,`payback_id`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户CRM回款记录表(回款进度流水)';
-28
View File
@@ -1,28 +0,0 @@
-- =============================================================
-- CRM 基本设置:计量单位表 yz_backend_crm_unit
-- 说明:
-- 单位用于产品管理(产品「单位」字段)、合同/报价产品清单等业务;
-- 在「CRM -> 基本设置 -> 单位设置」中维护,产品表单的单位下拉取自本表。
-- tenant_id + name 唯一(同租户下单位名称不可重复)。
-- 默认单位(套/台/个/件/批/次/项/人月/人天/年/月/米/平方米/吨/千克/箱)
-- 由服务端在租户首次访问单位列表时自动写入,无需手工初始化。
-- =============================================================
CREATE TABLE IF NOT EXISTS `yz_backend_crm_unit` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`tenant_id` varchar(64) NOT NULL COMMENT '租户ID',
`name` varchar(20) NOT NULL COMMENT '单位名称,如:套 / 台 / 个',
`code` varchar(50) NOT NULL DEFAULT '' COMMENT '单位编码(选填),如:SET / PCS',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序(升序,小的在前)',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1启用 / 0停用',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`create_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人用户ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_name` (`tenant_id`,`name`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_status` (`tenant_id`,`status`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户CRM计量单位表';
-49
View File
@@ -1,49 +0,0 @@
-- =============================================================
-- CMS 文章相关表:文章分类 / 文章
-- yz_cms_article_category 文章分类
-- yz_cms_article 文章
-- 可重复执行(CREATE TABLE IF NOT EXISTS)。
-- 运行:mysql -h<host> -P<port> -u<user> -p <db> < create_cms_article.sql
-- =============================================================
CREATE TABLE IF NOT EXISTS `yz_cms_article_category` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tid` bigint unsigned NOT NULL DEFAULT 0,
`cid` bigint unsigned NOT NULL DEFAULT 0,
`name` varchar(100) NOT NULL DEFAULT '',
`image` varchar(500) NOT NULL DEFAULT '',
`desc` varchar(500) NOT NULL DEFAULT '',
`sort` int NOT NULL DEFAULT 0,
`status` tinyint NOT NULL DEFAULT 1,
`create_time` datetime NOT NULL,
`update_time` datetime DEFAULT NULL,
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_tid_cid` (`tid`,`cid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `yz_cms_article` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tid` bigint unsigned NOT NULL DEFAULT 0,
`title` varchar(255) NOT NULL DEFAULT '',
`author` varchar(100) NOT NULL DEFAULT '',
`cate_id` bigint unsigned NOT NULL DEFAULT 0,
`content` mediumtext,
`desc` varchar(500) NOT NULL DEFAULT '',
`image` varchar(500) NOT NULL DEFAULT '',
`is_trans` tinyint NOT NULL DEFAULT 0,
`transurl` varchar(500) DEFAULT NULL,
`status` tinyint NOT NULL DEFAULT 0,
`top` tinyint NOT NULL DEFAULT 0,
`recommend` tinyint NOT NULL DEFAULT 0,
`views` int NOT NULL DEFAULT 0,
`likes` int NOT NULL DEFAULT 0,
`publisher_id` bigint unsigned DEFAULT NULL,
`publish_time` datetime DEFAULT NULL,
`create_time` datetime NOT NULL,
`update_time` datetime DEFAULT NULL,
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_tid_status` (`tid`,`status`),
KEY `idx_cate_id` (`cate_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-39
View File
@@ -1,39 +0,0 @@
-- =============================================================
-- CMS 产品相关表:产品 / 产品分类
-- yz_cms_product 产品
-- yz_cms_product_category 产品分类
-- 可重复执行(CREATE TABLE IF NOT EXISTS)。
-- 运行:mysql -h<host> -P<port> -u<user> -p <db> < create_cms_product.sql
-- =============================================================
CREATE TABLE IF NOT EXISTS `yz_cms_product` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tid` bigint unsigned NOT NULL DEFAULT 0,
`title` varchar(100) NOT NULL DEFAULT '',
`thumb` varchar(500) NOT NULL DEFAULT '',
`desc` varchar(500) NOT NULL DEFAULT '',
`content` mediumtext,
`url` varchar(500) NOT NULL DEFAULT '',
`sort` int NOT NULL DEFAULT 0,
`status` tinyint NOT NULL DEFAULT 1,
`create_time` datetime NOT NULL,
`update_time` datetime DEFAULT NULL,
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_tid_status` (`tid`,`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `yz_cms_product_category` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tid` bigint unsigned NOT NULL DEFAULT 0,
`title` varchar(100) NOT NULL DEFAULT '',
`pid` bigint unsigned NOT NULL DEFAULT 0,
`desc` varchar(500) NOT NULL DEFAULT '',
`sort` int NOT NULL DEFAULT 0,
`status` tinyint NOT NULL DEFAULT 1,
`create_time` datetime NOT NULL,
`update_time` datetime DEFAULT NULL,
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_tid_pid` (`tid`,`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-46
View File
@@ -1,46 +0,0 @@
-- =============================================================
-- CMS 解决方案(特色服务)相关表:解决方案 / 解决方案分类
-- yz_cms_solution 解决方案
-- yz_cms_solution_category 解决方案分类
-- 说明:建表可重复执行;末尾的历史库补列重复执行报
-- "Duplicate column name" 属正常,跳过即可。
-- 运行:mysql -h<host> -P<port> -u<user> -p <db> < create_cms_solution.sql
-- =============================================================
CREATE TABLE IF NOT EXISTS `yz_cms_solution` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tid` bigint unsigned NOT NULL DEFAULT 0,
`title` varchar(100) NOT NULL DEFAULT '',
`thumb` varchar(500) NOT NULL DEFAULT '',
`desc` varchar(500) NOT NULL DEFAULT '',
`content` mediumtext,
`url` varchar(500) NOT NULL DEFAULT '',
`sort` int NOT NULL DEFAULT 0,
`status` tinyint NOT NULL DEFAULT 1,
`create_time` datetime NOT NULL,
`update_time` datetime DEFAULT NULL,
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_tid_status` (`tid`,`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `yz_cms_solution_category` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tid` bigint unsigned NOT NULL DEFAULT 0,
`title` varchar(100) NOT NULL DEFAULT '',
`pid` bigint unsigned NOT NULL DEFAULT 0,
`desc` varchar(500) NOT NULL DEFAULT '',
`sort` int NOT NULL DEFAULT 0,
`status` tinyint NOT NULL DEFAULT 1,
`create_time` datetime NOT NULL,
`update_time` datetime DEFAULT NULL,
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_tid_pid` (`tid`,`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 历史库补齐在线内容字段(表已存在且缺列时才执行 ALTER,可重复执行不报错)
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_cms_solution');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_cms_solution' AND COLUMN_NAME = 'content');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_cms_solution` ADD COLUMN `content` mediumtext', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-133
View File
@@ -1,133 +0,0 @@
-- =============================================================
-- OA文档库相关表:分类表 / 文档表 / 关联表 / 共享表
-- yz_backend_oa_doc_category 文档分类(含共享/私密双空间)
-- yz_backend_oa_doc 文档
-- yz_backend_oa_doc_link 文档关联(图谱)
-- yz_backend_oa_doc_share 文档共享(用户/部门)
-- 说明:建表可重复执行;末尾的历史库补列与数据修正在重复执行时
-- 报 "Duplicate column name" 属正常,跳过即可。
-- 运行:mysql -h<host> -P<port> -u<user> -p <db> < create_oa_document.sql
-- =============================================================
CREATE TABLE IF NOT EXISTS `yz_backend_oa_doc_category` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tid` int NOT NULL DEFAULT 0 COMMENT '租户ID',
`user_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '归属用户ID 0-租户级(共享分类) >0-该用户的个人分类',
`parent_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '父级ID,0为一级分类',
`name` varchar(128) NOT NULL DEFAULT '' COMMENT '分类名称',
`sort` int NOT NULL DEFAULT 0 COMMENT '排序,越小越靠前',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`is_system` tinyint NOT NULL DEFAULT 0 COMMENT '是否系统内置分类 0-否(可删除) 1-是(不可删除)',
`scope` varchar(16) NOT NULL DEFAULT 'shared' COMMENT '所属范围 personal-个人文档 shared-共享文档',
`is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`),
KEY `idx_tid_parent` (`tid`,`parent_id`),
KEY `idx_tid_scope_user` (`tid`,`scope`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档分类表';
CREATE TABLE IF NOT EXISTS `yz_backend_oa_doc` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tid` int NOT NULL DEFAULT 0 COMMENT '租户ID',
`category_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '所属分类ID,0为未分类',
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '文档标题',
`file_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '关联附件ID(yz_system_files)',
`file_url` varchar(512) NOT NULL DEFAULT '' COMMENT '附件访问地址',
`file_name` varchar(255) NOT NULL DEFAULT '' COMMENT '原始文件名',
`ext` varchar(16) NOT NULL DEFAULT '' COMMENT '扩展名',
`size` bigint unsigned NOT NULL DEFAULT 0 COMMENT '文件大小(字节)',
`doc_type` tinyint NOT NULL DEFAULT 0 COMMENT '类型 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频',
`tags` varchar(255) NOT NULL DEFAULT '' COMMENT '标签,逗号分隔',
`summary` varchar(1000) NOT NULL DEFAULT '' COMMENT '摘要/描述',
`content` longtext NULL COMMENT '在线制作文档的编辑源(富文本HTML),仅用于再次编辑',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '状态 0-草稿 1-已发布 2-已归档',
`version` int NOT NULL DEFAULT 1 COMMENT '版本号',
`is_star` tinyint NOT NULL DEFAULT 0 COMMENT '是否收藏 0-否 1-是',
`owner_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '负责人ID',
`owner_name` varchar(100) NOT NULL DEFAULT '' COMMENT '负责人姓名',
`view_count` int NOT NULL DEFAULT 0 COMMENT '查看次数',
`download_count` int NOT NULL DEFAULT 0 COMMENT '下载次数',
`creator_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建人ID',
`creator_name` varchar(100) NOT NULL DEFAULT '' COMMENT '创建人姓名',
`visibility` tinyint NOT NULL DEFAULT 0 COMMENT '可见性 0-租户公开 1-私密(仅创建者与被共享者)',
`is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`),
KEY `idx_tid_cate` (`tid`,`category_id`,`is_deleted`),
KEY `idx_tid_status` (`tid`,`status`,`is_deleted`),
KEY `idx_tid_title` (`tid`,`title`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档表';
CREATE TABLE IF NOT EXISTS `yz_backend_oa_doc_link` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tid` int NOT NULL DEFAULT 0 COMMENT '租户ID',
`source_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '源文档ID',
`target_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '目标文档ID',
`relation` varchar(32) NOT NULL DEFAULT 'related' COMMENT '关系 reference-引用 related-相关 version-版本 belong-从属',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '关系说明',
`creator_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建人ID',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_doc_pair` (`tid`,`source_id`,`target_id`,`relation`),
KEY `idx_tid_source` (`tid`,`source_id`),
KEY `idx_tid_target` (`tid`,`target_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档关联表';
CREATE TABLE IF NOT EXISTS `yz_backend_oa_doc_share` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tid` int NOT NULL DEFAULT 0 COMMENT '租户ID',
`doc_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '文档ID',
`share_type` tinyint NOT NULL DEFAULT 0 COMMENT '共享类型 0-用户(target_id=uid) 1-部门(target_id=org_id,含子部门)',
`target_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '共享目标ID',
`creator_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '授权人ID',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_doc_target` (`tid`,`doc_id`,`share_type`,`target_id`),
KEY `idx_tid_doc` (`tid`,`doc_id`),
KEY `idx_target` (`tid`,`share_type`,`target_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档共享表';
-- -------------------------------------------------------------
-- 历史库补齐列(表已存在且缺列时才执行 ALTER,可重复执行不报错)
-- -------------------------------------------------------------
-- yz_backend_oa_doc.visibility
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_doc');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_doc' AND COLUMN_NAME = 'visibility');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_oa_doc` ADD COLUMN `visibility` tinyint NOT NULL DEFAULT 0 COMMENT ''可见性 0-租户公开 1-私密(仅创建者与被共享者)''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- yz_backend_oa_doc.content
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_doc');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_doc' AND COLUMN_NAME = 'content');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_oa_doc` ADD COLUMN `content` longtext NULL COMMENT ''在线制作文档的编辑源(富文本HTML),仅用于再次编辑''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- yz_backend_oa_doc_category.is_system
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_doc_category');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_doc_category' AND COLUMN_NAME = 'is_system');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_oa_doc_category` ADD COLUMN `is_system` tinyint NOT NULL DEFAULT 0 COMMENT ''是否系统内置分类 0-否(可删除) 1-是(不可删除)''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- yz_backend_oa_doc_category.scope
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_doc_category');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_doc_category' AND COLUMN_NAME = 'scope');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_oa_doc_category` ADD COLUMN `scope` varchar(16) NOT NULL DEFAULT ''shared'' COMMENT ''所属范围 personal-个人文档 shared-共享文档''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- yz_backend_oa_doc_category.user_id
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_doc_category');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_oa_doc_category' AND COLUMN_NAME = 'user_id');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_backend_oa_doc_category` ADD COLUMN `user_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT ''归属用户ID 0-租户级(共享分类) >0-该用户的个人分类''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 双空间模型:兼容存量数据,将不在 (shared, personal) 范围内的分类统一归为共享空间
-- (user_id 置 0、scope 置 shared),避免历史脏数据导致分类不可见。
-- 注意:不再把 personal 并入 shared,私密空间需保留各自的 user_id 隔离。
UPDATE `yz_backend_oa_doc_category`
SET `user_id` = 0, `scope` = 'shared'
WHERE `scope` NOT IN ('shared', 'personal') AND `is_deleted` = 0;
-31
View File
@@ -1,31 +0,0 @@
-- =============================================================
-- OA通知公告表:yz_backend_oa_notice
-- 说明:
-- 按租户 tid 隔离:租户内发布,租户内所有后台用户可见。
-- status: 0-草稿 1-已发布 2-已下架;notice_type: 0-通知 1-公告 2-活动;
-- level: 0-普通 1-重要 2-紧急;is_top: 0-否 1-是(置顶优先展示)。
-- 可重复执行(CREATE TABLE IF NOT EXISTS)。
-- 运行:mysql -h<host> -P<port> -u<user> -p <db> < create_oa_notice.sql
-- =============================================================
CREATE TABLE IF NOT EXISTS `yz_backend_oa_notice` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tid` int NOT NULL DEFAULT 0 COMMENT '租户ID',
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '公告标题',
`content` text COMMENT '公告内容',
`notice_type` tinyint NOT NULL DEFAULT 0 COMMENT '类型 0-通知 1-公告 2-活动',
`level` tinyint NOT NULL DEFAULT 0 COMMENT '紧急程度 0-普通 1-重要 2-紧急',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '状态 0-草稿 1-已发布 2-已下架',
`is_top` tinyint NOT NULL DEFAULT 0 COMMENT '是否置顶 0-否 1-是',
`publish_time` datetime DEFAULT NULL COMMENT '发布时间',
`publisher_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '发布人ID',
`publisher_name` varchar(100) NOT NULL DEFAULT '' COMMENT '发布人姓名',
`read_count` int NOT NULL DEFAULT 0 COMMENT '阅读次数',
`is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`),
KEY `idx_tid_status` (`tid`,`status`),
KEY `idx_tid_top` (`tid`,`is_top`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA通知公告表';
@@ -1,73 +0,0 @@
-- =============================================================
-- 平台端推广佣金相关表
-- yz_platform_commission_rule 佣金规则(按比例 / 固定金额 / 阶梯)
-- yz_platform_commission_order 佣金单(台账,含发放登记)
--
-- 业务约定:
-- 1. 佣金由平台自有资金支出,与租户收款资金流解耦,不参与分账;
-- 2. 佣金单在「业务订单支付成功」事件上按启用中的规则生成,状态先为 payable(应付);
-- 3. 一笔支付单 + 同一推广方只应生成一张佣金单(uk_pay_promoter),
-- 但 MySQL 唯一索引对 NULL 不去重(delete_time),业务层仍需按 pay_no 查重后再插入;
-- 4. 金额一律以「分」为单位的 bigint 存储;
-- 5. 规则修改只影响之后生成的佣金单,历史佣金单保留规则快照字段(rule_name/calc_type)。
--
-- 可重复执行(CREATE TABLE IF NOT EXISTS)。
-- 运行:mysql --default-character-set=utf8mb4 -h<host> -P<port> -u<user> -p <db> < create_platform_commission.sql
-- =============================================================
-- -------------------------------------------------------------
-- 1. 佣金规则表
-- -------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `yz_platform_commission_rule` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`rule_name` varchar(128) NOT NULL DEFAULT '' COMMENT '规则名称,如「渠道推广-平台使用费 5%」',
`calc_type` varchar(20) NOT NULL DEFAULT 'percent' COMMENT '计算方式:percent按比例/fixed固定金额/tiered阶梯',
`value` bigint(20) NOT NULL DEFAULT '0' COMMENT '规则值:percent=百分比×100(5%存500,支持2.5%存250);fixed=固定金额(分);tiered=0且明细见 tier_json',
`tier_json` text COMMENT '阶梯配置JSON:[{"min":0,"max":1000000,"rate":300}];金额单位为分,max 为 null 表示不限,rate 同为百分比×100',
`order_type` varchar(32) NOT NULL DEFAULT 'platform_usage' COMMENT '适用订单类型:all全部/platform_usage平台使用费/module_shop模块购买/service_fee服务费',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1启用/0停用(停用后新佣金单不再匹配该规则,历史佣金单不受影响)',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`create_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人ID',
`create_user_name` varchar(128) NOT NULL DEFAULT '' COMMENT '创建人姓名',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
KEY `idx_status` (`status`,`order_type`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台推广佣金规则表';
-- -------------------------------------------------------------
-- 2. 佣金单表(台账)
-- -------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `yz_platform_commission_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`commission_no` varchar(40) NOT NULL DEFAULT '' COMMENT '佣金单号,形如 CM202609150001',
`pay_no` varchar(40) NOT NULL DEFAULT '' COMMENT '关联支付单号(yz_platform_payment_order.pay_no)',
`out_trade_no` varchar(64) NOT NULL DEFAULT '' COMMENT '关联业务订单号',
`rule_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '命中的佣金规则ID',
`rule_name` varchar(128) NOT NULL DEFAULT '' COMMENT '命中规则名称快照',
`calc_type` varchar(20) NOT NULL DEFAULT '' COMMENT '计算方式快照:percent/fixed/tiered',
`promoter_id` varchar(64) NOT NULL DEFAULT '' COMMENT '推广方ID(渠道伙伴 / 服务商 / 内部员工)',
`promoter_name` varchar(128) NOT NULL DEFAULT '' COMMENT '推广方名称',
`promoter_type` varchar(32) NOT NULL DEFAULT '' COMMENT '推广方类型:channel渠道伙伴/service服务商/staff内部员工',
`base_amount` bigint(20) NOT NULL DEFAULT '0' COMMENT '计佣基数(分)= 业务订单实付金额',
`amount` bigint(20) NOT NULL DEFAULT '0' COMMENT '佣金金额(分)',
`status` varchar(20) NOT NULL DEFAULT 'payable' COMMENT '佣金状态:payable应付/settled已付/tax_processing税务处理中/canceled已作废',
`pay_method` varchar(20) NOT NULL DEFAULT '' COMMENT '发放方式:bank银行转账/balance余额抵扣/manual人工发放/offset下期抵扣',
`pay_voucher` varchar(255) NOT NULL DEFAULT '' COMMENT '发放凭证 / 银行流水号',
`paid_at` datetime DEFAULT NULL COMMENT '发放时间',
`paid_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '登记发放人ID',
`paid_user_name` varchar(128) NOT NULL DEFAULT '' COMMENT '登记发放人姓名',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_commission_no` (`commission_no`,`delete_time`),
UNIQUE KEY `uk_pay_promoter` (`pay_no`,`promoter_id`,`delete_time`),
KEY `idx_out_trade_no` (`out_trade_no`),
KEY `idx_status` (`status`,`create_time`),
KEY `idx_promoter` (`promoter_id`,`create_time`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台推广佣金单表(台账)';
-223
View File
@@ -1,223 +0,0 @@
-- =============================================================
-- 平台端支付模块相关表(平台向租户收取平台使用费)
-- yz_platform_payment_channel 渠道配置(一渠道一行,敏感参数加密后存 JSON)
-- yz_platform_payment_order 支付单
-- yz_platform_payment_order_log 支付单状态流转流水
-- yz_platform_payment_callback_log 渠道回调日志(幂等去重 + 问题排查)
-- yz_platform_payment_channel_bill 渠道账单明细(对账源数据)
-- yz_platform_payment_reconcile_diff 对账差异
-- yz_platform_payment_refund 手动原路退回记录(兜底能力)
--
-- 设计约定(与业务约定一致):
-- 1. 资金流向:租户付款直接进平台对公账户,不涉及分账;仅保留后台「手动原路退回」兜底;
-- 2. 金额一律用「分」为单位的 bigint 存储,前端展示时 /100 转元;
-- 3. 状态类字段用 varchar 存英文枚举,前端 StatusTag 直接映射,避免数字魔法值;
-- 4. 渠道密钥 / 私钥 / ClientSecret 不入库明文,只存 config_json(写入前由后端加密);
-- 证书本体存文件(object_key),库内只留元信息(cert_json);
-- 5. 表结构与项目其他表一致:软删除 delete_time、create_time/update_time 自动维护;
-- 6. 唯一键带上 delete_time 是本项目约定(软删除后可重新插入同键数据);
-- 注意 MySQL 唯一索引对 NULL 不去重,因此「支付幂等/回调去重」仍必须在业务层
-- 用「按 pay_no 查重 + 条件更新(WHERE status <> 'paid')」兜底,不能只依赖唯一键。
-- 7. 业务订单字段(业务订单状态、订单摘要等)由业务模块持有,本模块只冗余订单号与金额,
-- 详情接口聚合返回,不在此重复落库。
--
-- 可重复执行(CREATE TABLE IF NOT EXISTS)。
-- 运行:mysql --default-character-set=utf8mb4 -h<host> -P<port> -u<user> -p <db> < create_platform_payment.sql
-- =============================================================
-- -------------------------------------------------------------
-- 1. 支付渠道配置表
-- -------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `yz_platform_payment_channel` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`channel` varchar(32) NOT NULL DEFAULT '' COMMENT '渠道标识:wechat/alipay/unionpay/cloudpay/paypal',
`name` varchar(64) NOT NULL DEFAULT '' COMMENT '渠道名称(展示用)',
`merchant_no` varchar(128) NOT NULL DEFAULT '' COMMENT '商户号(展示用,未配置时前端显示未配置)',
`config_json` text COMMENT '渠道参数JSON(含密钥/私钥/Secret,写入前加密;接口回显敏感字段时只回掩码)',
`cert_json` text COMMENT '证书元信息JSON:文件名、object_key、序列号、到期日(不含证书私钥内容)',
`callback_url` varchar(255) NOT NULL DEFAULT '' COMMENT '回调地址(只读):/api/payment/callback/{channel}',
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用:1启用/0停用(参数不完整时前端展示为未配置)',
`extra_json` text COMMENT '渠道扩展开关JSON:云闪付 show_logo/logo_positions、PayPal env 等',
`last_test_time` datetime DEFAULT NULL COMMENT '最近一次「测试连接」时间',
`last_test_result` varchar(255) NOT NULL DEFAULT '' COMMENT '最近一次测试连接结果说明',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_channel` (`channel`,`delete_time`),
KEY `idx_enabled` (`enabled`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台支付渠道配置表';
-- -------------------------------------------------------------
-- 2. 支付单表
-- -------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `yz_platform_payment_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`pay_no` varchar(40) NOT NULL DEFAULT '' COMMENT '支付单号(支付模块生成,形如 P202609150009)',
`out_trade_no` varchar(64) NOT NULL DEFAULT '' COMMENT '业务订单号(业务模块生成;同一订单可有多笔支付单)',
`order_type` varchar(32) NOT NULL DEFAULT 'platform_usage' COMMENT '订单类型:platform_usage平台使用费/module_shop模块购买/service_fee服务费',
`subject` varchar(255) NOT NULL DEFAULT '' COMMENT '订单标题(渠道侧展示文案)',
`tenant_id` varchar(64) NOT NULL DEFAULT '' COMMENT '租户ID',
`tenant_name` varchar(128) NOT NULL DEFAULT '' COMMENT '租户名称(冗余,便于列表展示)',
`amount` bigint(20) NOT NULL DEFAULT '0' COMMENT '实付金额(分)',
`order_amount` bigint(20) NOT NULL DEFAULT '0' COMMENT '业务订单金额(分)',
`currency` varchar(8) NOT NULL DEFAULT 'CNY' COMMENT '币种',
`channel` varchar(32) NOT NULL DEFAULT '' COMMENT '支付渠道标识',
`merchant_no` varchar(128) NOT NULL DEFAULT '' COMMENT '下单时商户号快照',
`channel_trade_no` varchar(64) NOT NULL DEFAULT '' COMMENT '渠道流水号',
`status` varchar(20) NOT NULL DEFAULT 'created' COMMENT '状态:created/pending/paying/paid/failed/closed/refunding/refunded',
`refund_amount` bigint(20) NOT NULL DEFAULT '0' COMMENT '已退回金额(分),仅兜底手动退回时累加',
`client_ip` varchar(64) NOT NULL DEFAULT '' COMMENT '下单客户端IP',
`return_url` varchar(255) NOT NULL DEFAULT '' COMMENT '支付完成同步跳回地址(由下单参数带入)',
`notify_url` varchar(255) NOT NULL DEFAULT '' COMMENT '渠道回调地址快照',
`expire_at` datetime DEFAULT NULL COMMENT '支付超时时间(过期由补偿任务置为 closed)',
`paid_at` datetime DEFAULT NULL COMMENT '支付完成时间',
`closed_at` datetime DEFAULT NULL COMMENT '关闭时间',
`notify_at` datetime DEFAULT NULL COMMENT '最近一次成功回调时间',
`last_query_at` datetime DEFAULT NULL COMMENT '最近一次主动查询渠道状态时间',
`order_synced` tinyint(1) NOT NULL DEFAULT '0' COMMENT '业务订单状态是否已同步:1已同步/0待同步(补偿任务扫描用)',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pay_no` (`pay_no`,`delete_time`),
KEY `idx_out_trade_no` (`out_trade_no`),
KEY `idx_tenant` (`tenant_id`,`create_time`),
KEY `idx_status` (`status`,`create_time`),
KEY `idx_channel_trade` (`channel`,`channel_trade_no`),
KEY `idx_order_synced` (`order_synced`,`status`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台支付单表';
-- -------------------------------------------------------------
-- 3. 支付单状态流转流水(详情页时间轴数据源)
-- -------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `yz_platform_payment_order_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`pay_no` varchar(40) NOT NULL DEFAULT '' COMMENT '支付单号(yz_platform_payment_order.pay_no)',
`from_status` varchar(20) NOT NULL DEFAULT '' COMMENT '变更前状态(创建时为空)',
`to_status` varchar(20) NOT NULL DEFAULT '' COMMENT '变更后状态',
`operator` varchar(64) NOT NULL DEFAULT '' COMMENT '操作来源:租户端下单页/渠道回调/平台管理员/定时补偿',
`remark` varchar(500) NOT NULL DEFAULT '' COMMENT '变更说明(如:支付成功,已同步业务订单为已支付)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
KEY `idx_pay_no` (`pay_no`,`create_time`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台支付单状态流转流水表';
-- -------------------------------------------------------------
-- 4. 渠道回调日志表(幂等去重与排查)
-- -------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `yz_platform_payment_callback_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`channel` varchar(32) NOT NULL DEFAULT '' COMMENT '渠道标识',
`event_type` varchar(64) NOT NULL DEFAULT '' COMMENT '事件类型:TRANSACTION.SUCCESS/trade.success/PAYMENT.CAPTURE.COMPLETED 等',
`event_id` varchar(128) NOT NULL DEFAULT '' COMMENT '渠道事件ID(微信 event id / 支付宝 notify_id / PayPal event id)',
`pay_no` varchar(40) NOT NULL DEFAULT '' COMMENT '关联支付单号',
`out_trade_no` varchar(64) NOT NULL DEFAULT '' COMMENT '业务订单号',
`channel_trade_no` varchar(64) NOT NULL DEFAULT '' COMMENT '渠道流水号',
`trade_state` varchar(32) NOT NULL DEFAULT '' COMMENT '渠道侧交易状态原文',
`amount` bigint(20) NOT NULL DEFAULT '0' COMMENT '渠道通知金额(分)',
`verify_result` tinyint(1) NOT NULL DEFAULT '0' COMMENT '验签结果:1通过/0失败',
`is_duplicate` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否重复通知:1是(幂等命中,直接返回渠道要求的成功响应)/0否',
`handle_result` tinyint(1) NOT NULL DEFAULT '0' COMMENT '处理结果:1成功/0失败',
`handle_msg` varchar(255) NOT NULL DEFAULT '' COMMENT '处理说明或错误信息',
`raw_body` mediumtext COMMENT '渠道原始报文(脱敏后保留,详情页「渠道回调原文」数据源)',
`client_ip` varchar(64) NOT NULL DEFAULT '' COMMENT '渠道来源IP',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_channel_event` (`channel`,`channel_trade_no`,`event_type`,`delete_time`),
KEY `idx_pay_no` (`pay_no`),
KEY `idx_event_id` (`event_id`),
KEY `idx_create_time` (`create_time`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台支付渠道回调日志表(幂等去重)';
-- -------------------------------------------------------------
-- 5. 渠道账单明细表(对账源数据)
-- -------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `yz_platform_payment_channel_bill` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`channel` varchar(32) NOT NULL DEFAULT '' COMMENT '渠道标识',
`bill_date` date NOT NULL COMMENT '账单日期(渠道账单自然日)',
`channel_trade_no` varchar(64) NOT NULL DEFAULT '' COMMENT '渠道流水号',
`out_trade_no` varchar(64) NOT NULL DEFAULT '' COMMENT '渠道侧商户订单号(对应业务订单号)',
`amount` bigint(20) NOT NULL DEFAULT '0' COMMENT '渠道账单金额(分)',
`fee` bigint(20) NOT NULL DEFAULT '0' COMMENT '渠道手续费(分)',
`trade_state` varchar(32) NOT NULL DEFAULT '' COMMENT '渠道侧交易状态',
`trade_time` datetime DEFAULT NULL COMMENT '渠道交易时间',
`is_matched` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已与本地支付单匹配:1是/0否',
`matched_pay_no` varchar(40) NOT NULL DEFAULT '' COMMENT '匹配到的本地支付单号',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_channel_trade` (`channel`,`channel_trade_no`,`delete_time`),
KEY `idx_bill_date` (`channel`,`bill_date`,`is_matched`),
KEY `idx_out_trade_no` (`out_trade_no`,`bill_date`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台支付渠道账单明细表(对账源数据)';
-- -------------------------------------------------------------
-- 6. 对账差异表
-- -------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `yz_platform_payment_reconcile_diff` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`channel` varchar(32) NOT NULL DEFAULT '' COMMENT '渠道标识',
`bill_date` date NOT NULL COMMENT '对账日期(同渠道账单日)',
`batch_no` varchar(40) NOT NULL DEFAULT '' COMMENT '对账批次号(同渠道同日一次对账一个批次,便于追溯与重跑)',
`local_trade_no` varchar(64) NOT NULL DEFAULT '' COMMENT '本地支付单号(长款时为空串,不写 NULL 以便参与唯一键)',
`channel_trade_no` varchar(64) NOT NULL DEFAULT '' COMMENT '渠道流水号(短款时为空串)',
`local_amount` bigint(20) DEFAULT NULL COMMENT '本地金额(分),无本地记录时为 NULL',
`channel_amount` bigint(20) DEFAULT NULL COMMENT '渠道金额(分),无渠道记录时为 NULL',
`diff_amount` bigint(20) NOT NULL DEFAULT '0' COMMENT '差额(分)= 渠道金额 - 本地金额,负数表示短款',
`diff_type` varchar(20) NOT NULL DEFAULT '' COMMENT '差异类型:long长款(渠道有本地无)/short短款(本地有渠道无)/status_mismatch状态不一致',
`handle_status` varchar(20) NOT NULL DEFAULT 'unhandled' COMMENT '处理状态:unhandled待处理/handling处理中/handled已处理/ignored已忽略',
`handle_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '处理人ID',
`handle_user_name` varchar(128) NOT NULL DEFAULT '' COMMENT '处理人姓名',
`handle_time` datetime DEFAULT NULL COMMENT '处理时间',
`remark` varchar(500) NOT NULL DEFAULT '' COMMENT '处理说明(人工填写,便于日后追溯)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_diff` (`channel`,`bill_date`,`diff_type`,`channel_trade_no`,`local_trade_no`,`delete_time`),
KEY `idx_channel_date` (`channel`,`bill_date`),
KEY `idx_handle` (`handle_status`,`bill_date`),
KEY `idx_batch_no` (`batch_no`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台支付对账差异表';
-- -------------------------------------------------------------
-- 7. 手动原路退回记录表(兜底能力,业务上不提供退款入口)
-- -------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `yz_platform_payment_refund` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`refund_no` varchar(40) NOT NULL DEFAULT '' COMMENT '退回单号(支付模块生成)',
`pay_no` varchar(40) NOT NULL DEFAULT '' COMMENT '关联支付单号',
`out_trade_no` varchar(64) NOT NULL DEFAULT '' COMMENT '关联业务订单号',
`channel` varchar(32) NOT NULL DEFAULT '' COMMENT '渠道标识',
`channel_refund_no` varchar(64) NOT NULL DEFAULT '' COMMENT '渠道退回流水号',
`amount` bigint(20) NOT NULL DEFAULT '0' COMMENT '退回金额(分)',
`reason` varchar(500) NOT NULL DEFAULT '' COMMENT '退回原因(必填,人工填写,用于审计)',
`status` varchar(20) NOT NULL DEFAULT 'processing' COMMENT '退回状态:processing处理中/success成功/failed失败',
`fail_reason` varchar(500) NOT NULL DEFAULT '' COMMENT '失败原因',
`operator_id` varchar(64) NOT NULL DEFAULT '' COMMENT '操作人ID(平台管理员)',
`operator_name` varchar(128) NOT NULL DEFAULT '' COMMENT '操作人姓名',
`finished_at` datetime DEFAULT NULL COMMENT '退回完成时间',
`remark` varchar(500) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_refund_no` (`refund_no`,`delete_time`),
KEY `idx_pay_no` (`pay_no`),
KEY `idx_status` (`status`,`create_time`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台支付手动原路退回记录表(兜底能力)';
@@ -1,98 +0,0 @@
-- =============================================================
-- 产品定价(platform「基础设置 → 产品定价」)
--
-- 业务说明:
-- 1. 产品(yz_platform_product):平台对外售卖的功能产品,如「进销存 ERP」「CRM 客户管理」。
-- 仅维护:产品名称、产品编码、价格、关联功能(module_code)、上下架状态。
-- 2. 功能节点(yz_platform_product_feature):产品的功能树,支持父子层级(pid)。
-- 购买页按树形展示产品包含的功能清单。
-- 3. 购买订单(yz_platform_product_order):租户下单记录。
-- 在线支付走 /backend/payment/create;未配置支付渠道时可先下单,由平台在订单里手动开通。
--
-- 本脚本可重复执行:建表用 IF NOT EXISTS,补列做存在性判断,预置数据做存在性判断。
-- 运行:mysql --default-character-set=utf8mb4 -h<host> -P<port> -u<user> -p <db> < create_platform_product_pricing.sql
-- =============================================================
-- 1) 产品表
CREATE TABLE IF NOT EXISTS `yz_platform_product` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`code` varchar(64) NOT NULL DEFAULT '' COMMENT '产品编码(唯一,建议与功能模块 code 一致,如 erp/crm)',
`name` varchar(128) NOT NULL DEFAULT '' COMMENT '产品名称',
`price` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '产品价格(元)',
`module_code` varchar(64) NOT NULL DEFAULT '' COMMENT '关联功能模块编码 yz_system_modules(code)',
`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序号,越小越靠前',
`status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '状态:1上架 0下架',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT NULL,
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_code` (`code`, `delete_time`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品定价-产品表';
-- 2) 产品功能节点表(树形,pid=0 为顶级)
CREATE TABLE IF NOT EXISTS `yz_platform_product_feature` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`product_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '产品ID 关联 yz_platform_product(id)',
`pid` bigint(20) NOT NULL DEFAULT 0 COMMENT '上级节点ID,0=顶级节点',
`title` varchar(128) NOT NULL DEFAULT '' COMMENT '功能节点标题',
`description` varchar(500) NOT NULL DEFAULT '' COMMENT '功能节点说明',
`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序号,越小越靠前',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_product_id` (`product_id`),
KEY `idx_pid` (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品定价-产品功能节点表(树形)';
-- 3) 产品购买订单表
CREATE TABLE IF NOT EXISTS `yz_platform_product_order` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`order_no` varchar(40) NOT NULL DEFAULT '' COMMENT '业务订单号',
`tid` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户ID',
`tenant_name` varchar(128) NOT NULL DEFAULT '' COMMENT '租户名称(快照)',
`product_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '产品ID',
`product_code` varchar(64) NOT NULL DEFAULT '' COMMENT '产品编码(快照)',
`product_name` varchar(128) NOT NULL DEFAULT '' COMMENT '产品名称(快照)',
`amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单金额(元)',
`package_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '购买后绑定的套餐ID(快照)',
`pay_no` varchar(40) NOT NULL DEFAULT '' COMMENT '支付单号 yz_platform_payment_order(pay_no)',
`channel` varchar(32) NOT NULL DEFAULT '' COMMENT '支付渠道:wechat/alipay/... 空=线下',
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending待支付 paid已支付 activated已开通 closed已关闭',
`activate_time` datetime DEFAULT NULL COMMENT '开通时间',
`expire_time` datetime DEFAULT NULL COMMENT '有效期截止时间(单品功能默认开通后 1 年,到期权益自动失效,需续费)',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT NULL,
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_tid` (`tid`),
KEY `idx_status` (`status`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品定价-购买订单表';
-- 4) 兼容旧结构:历史库已建过产品表时补齐/清理字段
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_product');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_product' AND COLUMN_NAME = 'price');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_platform_product` ADD COLUMN `price` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT ''产品价格(元)'' AFTER `name`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_product_feature');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_product_feature' AND COLUMN_NAME = 'pid');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_platform_product_feature` ADD COLUMN `pid` bigint(20) NOT NULL DEFAULT 0 COMMENT ''上级节点ID,0=顶级节点'' AFTER `product_id`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 购买订单有效期字段(历史库补齐;单品功能默认开通后 1 年有效)
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_product_order');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_product_order' AND COLUMN_NAME = 'expire_time');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_platform_product_order` ADD COLUMN `expire_time` datetime DEFAULT NULL COMMENT ''有效期截止时间(单品功能默认开通后 1 年,到期权益自动失效,需续费)'' AFTER `activate_time`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 5) 平台端菜单:基础设置 → 产品定价(仅平台端可见)
-- pid=17 为「基础设置」菜单;若环境中 17 不是基础设置,请在平台端「菜单管理」里手工挂载本页面:
-- 组件路径 /basicSettings/pricing/index.vue,路径 /basicSettings/pricing
INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `permission`, `creater`, `remark`, `create_time`, `update_time`)
SELECT 17, '产品定价', '/basicSettings/pricing', '/basicSettings/pricing/index.vue', 'Goods', 5, 1, 1, '[1]', 2, '', 'system', '产品定价:产品与功能节点树', NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM (SELECT `id` FROM `yz_system_menu` WHERE `path` = '/basicSettings/pricing' AND `delete_time` IS NULL) t)
AND EXISTS (SELECT 1 FROM (SELECT `id` FROM `yz_system_menu` WHERE `id` = 17 AND `delete_time` IS NULL) t2);
@@ -1,29 +0,0 @@
-- 平台官网站点设置表 yz_platform_site_setting(通用键值结构)
-- 一行一个设置项:title 名称 / value 值 / tag 标签名 / remark 说明。
-- 模板里用 {yz:tag} 输出对应 value;新增设置项只需插一行,无需加字段。
-- 表结构需手动执行本文件(服务端已取消自动建表);
-- 建表后,已登记的默认设置项会由服务端启动时自动补齐(纯数据初始化)。
-- 已登记的默认设置项:
-- sitename 站点名称
-- subtitle 副标题
-- logo 深色 Logo(浅色背景)
-- logow 浅色 Logo(深色背景)
-- icp ICP 备案号
-- policeicp 公安备案号
-- copyright 版权信息
-- privacy 隐私政策链接
-- terms 使用条款链接
CREATE TABLE IF NOT EXISTS `yz_platform_site_setting` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`title` varchar(128) NOT NULL DEFAULT '' COMMENT '设置项名称',
`value` text COMMENT '设置项的值',
`tag` varchar(64) NOT NULL DEFAULT '' COMMENT '标签名,模板中写作 {yz:tag}',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注/填写说明',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tag` (`tag`, `delete_time`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台官网站点设置表';
-101
View File
@@ -1,101 +0,0 @@
-- =============================================================
-- 微信公众号(服务号)绑定与推送
--
-- 业务说明:
-- 1. 服务号配置(yz_platform_wechat_mp_config):AppID / AppSecret / Token / EncodingAESKey,
-- 其中 AppSecret 与 EncodingAESKey 以 AES-256-GCM 加密后存储
-- (密钥取 app.conf 的 wechat_mp_secret_key,代码内部 SHA-256 派生)。
-- 2. 关注用户(yz_platform_wechat_mp_follower):用户关注/取关事件沉淀,openid 维度唯一;
-- 通过「扫码关注 → 公众号回复验证码 → 平台输入验证码」流程绑定到
-- 平台用户(platform_user)或租户用户(tenant_user),认证后可定向推送。
-- 3. 验证码(yz_platform_wechat_mp_verify_code):一次绑定会话一条,scene 绑定二维码场景值,
-- 默认 5 分钟有效;状态:0待扫码 1已扫码待核销 2已核销 3已过期。
--
-- 未认证服务号限制:仅支持服务器配置、关注/扫码事件、被动回复、带参数二维码;
-- 模板消息/订阅通知/客服消息需认证后用。
--
-- 本脚本可重复执行:建表用 IF NOT EXISTS,旧表名(yz_wechat_*)做存在性改名兼容。
-- 运行:mysql --default-character-set=utf8mb4 -h<host> -P<port> -u<user> -p <db> < create_platform_wechat_mp.sql
-- =============================================================
-- 0) 兼容早期表名(yz_wechat_mp_config / yz_wechat_follower / yz_wechat_verify_code):
-- 旧表存在且新表不存在时直接改名,保留已配置数据。
SET @old := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_wechat_mp_config');
SET @new := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_wechat_mp_config');
SET @ddl := IF(@old > 0 AND @new = 0, 'RENAME TABLE `yz_wechat_mp_config` TO `yz_platform_wechat_mp_config`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @old := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_wechat_follower');
SET @new := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_wechat_mp_follower');
SET @ddl := IF(@old > 0 AND @new = 0, 'RENAME TABLE `yz_wechat_follower` TO `yz_platform_wechat_mp_follower`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @old := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_wechat_verify_code');
SET @new := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_platform_wechat_mp_verify_code');
SET @ddl := IF(@old > 0 AND @new = 0, 'RENAME TABLE `yz_wechat_verify_code` TO `yz_platform_wechat_mp_verify_code`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 1) 服务号配置(单行)
CREATE TABLE IF NOT EXISTS `yz_platform_wechat_mp_config` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`app_id` varchar(64) NOT NULL DEFAULT '' COMMENT '公众号 AppID',
`app_secret` varchar(512) NOT NULL DEFAULT '' COMMENT 'AppSecret(AES 加密存储)',
`token` varchar(128) NOT NULL DEFAULT '' COMMENT '服务器配置 Token(自定义,用于签名校验)',
`aes_key` varchar(512) NOT NULL DEFAULT '' COMMENT 'EncodingAESKey(43位,AES 加密存储)',
`encrypt_mode` varchar(16) NOT NULL DEFAULT 'plain' COMMENT '消息加解密方式:plain明文 compatible兼容 safe安全',
`template_id` varchar(128) NOT NULL DEFAULT '' COMMENT '模板消息ID(认证后在公众号后台申请后填入)',
`verified` tinyint(4) NOT NULL DEFAULT 0 COMMENT '公众号是否已认证:1是 0否(认证后才可主动推送)',
`enabled` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否启用:1启用 0停用',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信公众号(服务号)配置表';
-- 2) 关注用户(粉丝)
CREATE TABLE IF NOT EXISTS `yz_platform_wechat_mp_follower` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`openid` varchar(64) NOT NULL DEFAULT '' COMMENT '微信 openid(公众号维度用户标识)',
`unionid` varchar(64) NOT NULL DEFAULT '' COMMENT '微信 unionid(开放平台绑定后才有)',
`nickname` varchar(128) NOT NULL DEFAULT '' COMMENT '昵称(未认证公众号可能拉取不到,留空展示 openid 缩写)',
`avatar` varchar(512) NOT NULL DEFAULT '' COMMENT '头像地址',
`sex` tinyint(4) NOT NULL DEFAULT 0 COMMENT '性别:0未知 1男 2女',
`city` varchar(64) NOT NULL DEFAULT '' COMMENT '城市',
`province` varchar(64) NOT NULL DEFAULT '' COMMENT '省份',
`country` varchar(64) NOT NULL DEFAULT '' COMMENT '国家',
`subscribe` tinyint(4) NOT NULL DEFAULT 1 COMMENT '关注状态:1已关注 0已取关',
`subscribe_time` datetime DEFAULT NULL COMMENT '关注时间',
`unsubscribe_time` datetime DEFAULT NULL COMMENT '取关时间',
`bind_type` varchar(32) NOT NULL DEFAULT '' COMMENT '绑定对象:platform_user平台用户 tenant_user租户用户 空=未绑定',
`bind_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '绑定对象ID(平台用户ID / 租户用户ID)',
`bind_tid` bigint(20) NOT NULL DEFAULT 0 COMMENT '绑定租户ID(租户用户时记录)',
`bind_time` datetime DEFAULT NULL COMMENT '绑定时间',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注(平台可编辑)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_openid` (`openid`),
KEY `idx_bind` (`bind_type`,`bind_id`),
KEY `idx_subscribe` (`subscribe`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信公众号关注用户(粉丝)表';
-- 3) 关注验证码(绑定会话)
CREATE TABLE IF NOT EXISTS `yz_platform_wechat_mp_verify_code` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`scene` varchar(64) NOT NULL DEFAULT '' COMMENT '二维码场景值(随机串,唯一)',
`code` varchar(8) NOT NULL DEFAULT '' COMMENT '验证码(6位数字,公众号被动回复给用户)',
`openid` varchar(64) NOT NULL DEFAULT '' COMMENT '扫码用户 openid(扫码后写入)',
`status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态:0待扫码 1已扫码待核销 2已核销 3已过期',
`bind_type` varchar(32) NOT NULL DEFAULT '' COMMENT '发起绑定方:platform_user / tenant_user',
`bind_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '发起绑定方ID',
`bind_tid` bigint(20) NOT NULL DEFAULT 0 COMMENT '发起绑定租户ID',
`expire_at` datetime DEFAULT NULL COMMENT '有效期截止(默认5分钟)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_scene` (`scene`),
KEY `idx_openid` (`openid`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信公众号关注取码/绑定验证码表';
-228
View File
@@ -1,228 +0,0 @@
-- =============================================================
-- 租户套餐 + 用户数配额(平台端「租户套餐」功能)
--
-- 业务说明:
-- 1. 套餐(yz_system_tenant_package)决定租户能开通哪些功能模块
-- (模块取自 yz_system_modules,如 erp / oa / crm / cms),
-- 租户绑定套餐后,租户端菜单按套餐包含的模块过滤(未包含的整块菜单不展示)。
-- 2. 所有租户都有用户数上限(yz_system_tenant.user_quota,默认 20),
-- 租户用户数达到上限后不允许再新增账号,需要先增购用户数:
-- · 单个增购:按套餐的 extra_user_price(如 200 元/人)
-- · 套餐增购:按 yz_system_tenant_user_quota_package 配置的规格(如 5人/500元)
-- 每次增购写入 yz_system_tenant_quota_order 记录,并累加到租户的 user_quota。
--
-- 本脚本可重复执行:建表用 IF NOT EXISTS,补列/预置数据均做存在性判断。
-- 运行:mysql --default-character-set=utf8mb4 -h<host> -P<port> -u<user> -p <db> < create_tenant_package.sql
-- =============================================================
-- 1) 租户套餐表
CREATE TABLE IF NOT EXISTS `yz_system_tenant_package` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`name` varchar(64) NOT NULL DEFAULT '' COMMENT '套餐名称,如:ERP套餐',
`code` varchar(64) NOT NULL DEFAULT '' COMMENT '套餐编码(唯一,程序识别用)',
`description` varchar(500) NOT NULL DEFAULT '' COMMENT '套餐说明',
`price` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '套餐售价(元)',
`user_quota` int(11) NOT NULL DEFAULT 20 COMMENT '套餐包含的用户数(新租户绑定套餐后初始化的用户数上限)',
`extra_user_price` decimal(10,2) NOT NULL DEFAULT 200.00 COMMENT '单个用户增购价格(元/人)',
`is_default` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否默认套餐:1是 0否(新建租户未指定套餐时使用)',
`allow_repurchase` tinyint(4) NOT NULL DEFAULT 1 COMMENT '是否允许重复购买:1允许(默认) 0仅可购买一次(体验套餐)',
`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序号,越小越靠前',
`status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '状态:1启用 0禁用',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
KEY `idx_code` (`code`, `delete_time`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户套餐表(功能模块 + 用户数配额)';
-- 2) 套餐-功能模块关联表(一个套餐勾选多个模块,模块取自 yz_system_modules.code)
CREATE TABLE IF NOT EXISTS `yz_system_tenant_package_module` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`package_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '套餐ID 关联 yz_system_tenant_package(id)',
`module_code` varchar(64) NOT NULL DEFAULT '' COMMENT '功能模块编码 关联 yz_system_modules(code),如 erp/oa/crm/cms',
`module_name` varchar(64) NOT NULL DEFAULT '' COMMENT '功能模块名称(冗余,便于展示)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_package_id` (`package_id`),
KEY `idx_module_code` (`module_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户套餐-功能模块关联表';
-- 3) 用户数加购套餐表(平台可维护的「人/元」规格,如 5人 500元)
CREATE TABLE IF NOT EXISTS `yz_system_tenant_user_quota_package` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`name` varchar(64) NOT NULL DEFAULT '' COMMENT '规格名称,如:5人套餐',
`user_count` int(11) NOT NULL DEFAULT 0 COMMENT '包含用户数',
`price` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '售价(元)',
`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序号,越小越靠前',
`status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '状态:1启用 0禁用',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
PRIMARY KEY (`id`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户用户数加购套餐表';
-- 4) 租户用户数增购记录表
CREATE TABLE IF NOT EXISTS `yz_system_tenant_quota_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
`tid` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户ID 关联 yz_system_tenant(id)',
`type` tinyint(4) NOT NULL DEFAULT 1 COMMENT '增购方式:1单个增购 2套餐增购',
`quota_package_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '加购套餐ID(type=2 时有效)',
`package_name` varchar(64) NOT NULL DEFAULT '' COMMENT '加购规格名称(冗余)',
`user_count` int(11) NOT NULL DEFAULT 0 COMMENT '本次增购用户数',
`unit_price` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '单价(元/人,type=1 时使用)',
`amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '本次金额(元)',
`quota_before` int(11) NOT NULL DEFAULT 0 COMMENT '增购前用户数上限',
`quota_after` int(11) NOT NULL DEFAULT 0 COMMENT '增购后用户数上限',
`operator` varchar(64) NOT NULL DEFAULT '' COMMENT '操作人(平台账号)',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_tid` (`tid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户用户数增购记录表';
-- 4.5) 历史库补列:套餐是否允许重复购买(0=仅可购买一次,适用于体验套餐)
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant_package');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant_package' AND COLUMN_NAME = 'allow_repurchase');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_system_tenant_package` ADD COLUMN `allow_repurchase` tinyint(4) NOT NULL DEFAULT 1 COMMENT ''是否允许重复购买:1允许(默认) 0仅可购买一次(体验套餐)'' AFTER `is_default`', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 体验套餐(不允许重复购买)不涉及用户数增购:清理历史增购价
UPDATE `yz_system_tenant_package` SET `extra_user_price` = 0 WHERE `allow_repurchase` = 0;
-- 5) 租户表补列:套餐ID + 用户数上限
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant' AND COLUMN_NAME = 'package_id');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_system_tenant` ADD COLUMN `package_id` bigint(20) NOT NULL DEFAULT 0 COMMENT ''套餐ID 关联 yz_system_tenant_package(id),0-未绑定''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @tbl := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant');
SET @col := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_system_tenant' AND COLUMN_NAME = 'user_quota');
SET @ddl := IF(@tbl > 0 AND @col = 0, 'ALTER TABLE `yz_system_tenant` ADD COLUMN `user_quota` int(11) NOT NULL DEFAULT 20 COMMENT ''用户数上限(默认20,增购后累加)''', 'DO 0');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 存量租户兜底:用户数上限为 0 的按默认 20 初始化
UPDATE `yz_system_tenant` SET `user_quota` = 20 WHERE `user_quota` <= 0;
-- 存量租户兜底:用户数上限小于已开用户数时,按已开用户数抬高(避免历史租户立刻被锁死)
UPDATE `yz_system_tenant` t
SET t.`user_quota` = (
SELECT cnt FROM (SELECT COUNT(*) AS cnt FROM `yz_system_tenant_user` u WHERE u.`tid` = t.`id` AND u.`delete_time` IS NULL) c
)
WHERE t.`user_quota` < (
SELECT cnt FROM (SELECT COUNT(*) AS cnt FROM `yz_system_tenant_user` u WHERE u.`tid` = t.`id` AND u.`delete_time` IS NULL) c2
);
-- 6) 预置套餐(幂等:按 code 判断,不存在才插入)
INSERT INTO `yz_system_tenant_package` (`name`, `code`, `description`, `price`, `user_quota`, `extra_user_price`, `is_default`, `sort`, `status`, `remark`, `create_time`, `update_time`)
SELECT '基础版', 'basic', '基础功能:系统管理、基础设置、便捷工具', 0.00, 20, 200.00, 1, 1, 1, '默认套餐,新建租户自动绑定', NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM (SELECT `id` FROM `yz_system_tenant_package` WHERE `code` = 'basic' AND `delete_time` IS NULL) t);
INSERT INTO `yz_system_tenant_package` (`name`, `code`, `description`, `price`, `user_quota`, `extra_user_price`, `is_default`, `sort`, `status`, `remark`, `create_time`, `update_time`)
SELECT 'ERP套餐', 'erp', '基础功能 + 进销存(ERP)', 0.00, 20, 200.00, 0, 2, 1, '', NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM (SELECT `id` FROM `yz_system_tenant_package` WHERE `code` = 'erp' AND `delete_time` IS NULL) t);
INSERT INTO `yz_system_tenant_package` (`name`, `code`, `description`, `price`, `user_quota`, `extra_user_price`, `is_default`, `sort`, `status`, `remark`, `create_time`, `update_time`)
SELECT 'CRM套餐', 'crm', '基础功能 + 客户关系管理(CRM)', 0.00, 20, 200.00, 0, 3, 1, '', NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM (SELECT `id` FROM `yz_system_tenant_package` WHERE `code` = 'crm' AND `delete_time` IS NULL) t);
INSERT INTO `yz_system_tenant_package` (`name`, `code`, `description`, `price`, `user_quota`, `extra_user_price`, `is_default`, `sort`, `status`, `remark`, `create_time`, `update_time`)
SELECT '企业全功能版', 'all', '基础功能 + 进销存 + OA + CRM + 网站管理', 0.00, 20, 200.00, 0, 9, 1, '', NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM (SELECT `id` FROM `yz_system_tenant_package` WHERE `code` = 'all' AND `delete_time` IS NULL) t);
-- 7) 预置套餐功能模块(按套餐 code + 模块 code 幂等插入)
-- 注意:MySQL 的派生表不能引用外层字段,因此这里把「已存在的关联」整表materialize 成 ex,
-- 再在 WHERE 中与外层套餐/模块做关联,既避免了「不能 SELECT 目标表」,也保证可重复执行。
-- 基础模块:基础设置(basicSettings)、系统管理(systemManager)、便捷工具(bianjiegongju)
INSERT INTO `yz_system_tenant_package_module` (`package_id`, `module_code`, `module_name`, `create_time`)
SELECT p.`id`, m.`code`, m.`name`, NOW()
FROM `yz_system_tenant_package` p
JOIN `yz_system_modules` m ON m.`delete_time` IS NULL AND m.`code` IN ('basicSettings', 'systemManager', 'bianjiegongju')
WHERE p.`code` IN ('basic', 'erp', 'crm', 'all') AND p.`delete_time` IS NULL
AND NOT EXISTS (
SELECT 1 FROM (SELECT `package_id`, `module_code` FROM `yz_system_tenant_package_module`) ex
WHERE ex.`package_id` = p.`id` AND ex.`module_code` = m.`code`
);
-- ERP套餐:附加 erp 模块
INSERT INTO `yz_system_tenant_package_module` (`package_id`, `module_code`, `module_name`, `create_time`)
SELECT p.`id`, m.`code`, m.`name`, NOW()
FROM `yz_system_tenant_package` p
JOIN `yz_system_modules` m ON m.`delete_time` IS NULL AND m.`code` = 'erp'
WHERE p.`code` = 'erp' AND p.`delete_time` IS NULL
AND NOT EXISTS (
SELECT 1 FROM (SELECT `package_id`, `module_code` FROM `yz_system_tenant_package_module`) ex
WHERE ex.`package_id` = p.`id` AND ex.`module_code` = m.`code`
);
-- CRM套餐:附加 crm 模块
INSERT INTO `yz_system_tenant_package_module` (`package_id`, `module_code`, `module_name`, `create_time`)
SELECT p.`id`, m.`code`, m.`name`, NOW()
FROM `yz_system_tenant_package` p
JOIN `yz_system_modules` m ON m.`delete_time` IS NULL AND m.`code` = 'crm'
WHERE p.`code` = 'crm' AND p.`delete_time` IS NULL
AND NOT EXISTS (
SELECT 1 FROM (SELECT `package_id`, `module_code` FROM `yz_system_tenant_package_module`) ex
WHERE ex.`package_id` = p.`id` AND ex.`module_code` = m.`code`
);
-- 企业全功能版:附加 erp / oa / crm / cms
INSERT INTO `yz_system_tenant_package_module` (`package_id`, `module_code`, `module_name`, `create_time`)
SELECT p.`id`, m.`code`, m.`name`, NOW()
FROM `yz_system_tenant_package` p
JOIN `yz_system_modules` m ON m.`delete_time` IS NULL AND m.`code` IN ('erp', 'oa', 'crm', 'cms')
WHERE p.`code` = 'all' AND p.`delete_time` IS NULL
AND NOT EXISTS (
SELECT 1 FROM (SELECT `package_id`, `module_code` FROM `yz_system_tenant_package_module`) ex
WHERE ex.`package_id` = p.`id` AND ex.`module_code` = m.`code`
);
-- 8) 预置用户数加购套餐(5人 500元;可按需在平台端维护更多规格)
INSERT INTO `yz_system_tenant_user_quota_package` (`name`, `user_count`, `price`, `sort`, `status`, `remark`, `create_time`, `update_time`)
SELECT '5人套餐', 5, 500.00, 1, 1, '一次性增购 5 个用户数', NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM (SELECT `id` FROM `yz_system_tenant_user_quota_package` WHERE `name` = '5人套餐' AND `delete_time` IS NULL) t);
INSERT INTO `yz_system_tenant_user_quota_package` (`name`, `user_count`, `price`, `sort`, `status`, `remark`, `create_time`, `update_time`)
SELECT '10人套餐', 10, 900.00, 2, 1, '一次性增购 10 个用户数', NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM (SELECT `id` FROM `yz_system_tenant_user_quota_package` WHERE `name` = '10人套餐' AND `delete_time` IS NULL) t);
-- 9) 存量租户兜底:未绑定套餐的存量租户绑定「企业全功能版」,
-- 避免上线后老租户因套餐未配置而看不到原有功能菜单;
-- 之后可在「租户管理 → 编辑 / 套餐与用户数」中按实际购买情况改为 ERP/CRM 等套餐。
-- (若没有 all 套餐,则退回绑定默认套餐)
UPDATE `yz_system_tenant` t
SET t.`package_id` = (SELECT p.`id` FROM (SELECT `id` FROM `yz_system_tenant_package` WHERE `code` = 'all' AND `delete_time` IS NULL ORDER BY `sort`, `id` LIMIT 1) p)
WHERE (t.`package_id` IS NULL OR t.`package_id` = 0)
AND EXISTS (SELECT 1 FROM (SELECT `id` FROM `yz_system_tenant_package` WHERE `code` = 'all' AND `delete_time` IS NULL) p2);
UPDATE `yz_system_tenant` t
SET t.`package_id` = (SELECT p.`id` FROM (SELECT `id` FROM `yz_system_tenant_package` WHERE `is_default` = 1 AND `delete_time` IS NULL ORDER BY `sort`, `id` LIMIT 1) p)
WHERE (t.`package_id` IS NULL OR t.`package_id` = 0)
AND EXISTS (SELECT 1 FROM (SELECT `id` FROM `yz_system_tenant_package` WHERE `is_default` = 1 AND `delete_time` IS NULL) p2);
-- 10) 平台端菜单:基础设置 → 租户套餐(仅平台端可见)
-- pid=17 为「基础设置」菜单;若环境中 17 不是基础设置,请在平台端「菜单管理」里手工挂载本页面:
-- 组件路径 /basicSettings/tenantpackage/index.vue,路径 /basicSettings/tenantpackage
INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `permission`, `creater`, `remark`, `create_time`, `update_time`)
SELECT 17, '租户套餐', '/basicSettings/tenantpackage', '/basicSettings/tenantpackage/index.vue', 'Box', 4, 1, 1, '[1]', 2, '', 'system', '租户功能套餐与用户数加购规格', NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM (SELECT `id` FROM `yz_system_menu` WHERE `path` = '/basicSettings/tenantpackage' AND `delete_time` IS NULL) t)
AND EXISTS (SELECT 1 FROM (SELECT `id` FROM `yz_system_menu` WHERE `id` = 17 AND `delete_time` IS NULL) t2);
-- 11) 【按需执行】存量租户套餐调整
-- 本脚本第 9 步只处理「package_id 为空/0」的租户;若存量租户已被绑定为「基础版」,
-- 上线菜单按套餐过滤后,这些租户将看不到 进销存/OA/CRM/网站管理 等菜单。
-- 如需要让老租户保留原有全部功能,按下面的语句把它们改为「企业全功能版」
-- (或改成各自实际购买的套餐;也可直接在平台端「租户管理 → 编辑」里选择套餐)。
--
-- UPDATE `yz_system_tenant`
-- SET `package_id` = (SELECT p.`id` FROM (SELECT `id` FROM `yz_system_tenant_package` WHERE `code` = 'all' AND `delete_time` IS NULL ORDER BY `sort`, `id` LIMIT 1) p)
-- WHERE `id` IN (1, 2); -- ← 改成你的租户ID,或去掉 WHERE 批量处理
-- 校验:套餐与模块
-- SELECT p.id, p.name, p.code, p.user_quota, p.extra_user_price, GROUP_CONCAT(pm.module_code) AS modules
-- FROM yz_system_tenant_package p LEFT JOIN yz_system_tenant_package_module pm ON pm.package_id = p.id
-- WHERE p.delete_time IS NULL GROUP BY p.id;
-- SELECT id, tenant_name, package_id, user_quota FROM yz_system_tenant;
@@ -1,20 +0,0 @@
-- =============================================================
-- 租户站点扩展设置表:yz_system_tenant_setting_items
-- 说明:租户后台「站点设置」的扩展键值项(tid + setting_key 唯一)。
-- 可重复执行(CREATE TABLE IF NOT EXISTS)。
-- 运行:mysql -h<host> -P<port> -u<user> -p <db> < create_tenant_setting_items.sql
-- =============================================================
CREATE TABLE IF NOT EXISTS `yz_system_tenant_setting_items` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`tid` BIGINT UNSIGNED NOT NULL DEFAULT 0,
`setting_key` VARCHAR(64) NOT NULL DEFAULT '',
`setting_value` LONGTEXT NULL,
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`delete_time` DATETIME NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tid_key` (`tid`,`setting_key`),
KEY `idx_tid` (`tid`),
KEY `idx_delete_time` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户站点扩展设置';
-29
View File
@@ -1,29 +0,0 @@
-- =============================================================
-- 官网前端导航:全局默认菜单(tenant_id=0,全租户共享、不可删改)
-- 表:yz_backend_menu_front
-- 说明:
-- 默认 5 个一级导航:新闻中心 / 产品展示 / 解决方案 / 关于我们 / 联系我们。
-- 以 tenant_id=0 存放,租户端接口带 tenant_id 过滤,天然无法触碰这些行。
-- 路径对应官网前台路由:新闻中心 /news,其余为单页 /page/:path
-- (单页内容在 backend 端「单页管理」中按 path 维护)。
-- 本脚本幂等:同 tenant_id=0 下存在同名(含已软删)记录则跳过,可重复执行。
-- 如需恢复被误删的默认菜单,手动将其 delete_time 置 NULL 即可。
-- 运行:mysql --default-character-set=utf8mb4 -h<host> -P<port> -u<user> -p <db> < init_front_menu_defaults.sql
-- =============================================================
INSERT INTO `yz_backend_menu_front`
(`tenant_id`, `pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `type`, `permission`, `create_time`, `update_time`)
SELECT v.`tenant_id`, v.`pid`, v.`title`, v.`path`, '', '', v.`sort`, 1, 1, v.`type`, '', NOW(), NOW()
FROM (
SELECT 0 AS `tenant_id`, 0 AS `pid`, '新闻中心' AS `title`, '/news' AS `path`, 1 AS `sort`, 2 AS `type`
UNION ALL SELECT 0, 0, '产品展示', '/page/products', 2, 4
UNION ALL SELECT 0, 0, '解决方案', '/page/solutions', 3, 4
UNION ALL SELECT 0, 0, '关于我们', '/page/about', 4, 4
UNION ALL SELECT 0, 0, '联系我们', '/page/contact', 5, 4
) v
WHERE NOT EXISTS (
SELECT 1 FROM `yz_backend_menu_front` m
WHERE m.`tenant_id` = 0 AND m.`title` = v.`title`
);
-- 校验:SELECT id, title, path, sort, type, delete_time FROM yz_backend_menu_front WHERE tenant_id = 0 ORDER BY sort;
@@ -1,39 +0,0 @@
-- =============================================================
-- 平台支付渠道:初始化五个渠道配置行
-- 表:yz_platform_payment_channel
-- 说明:
-- 五个渠道(微信支付 / 支付宝 / 银联 / 云闪付 / PayPal)与前端
-- platform/src/views/system/payment/channels/ 一一对应。
-- 这里只插入「渠道骨架」:名称、回调地址、默认开关,商户号与密钥留空,
-- 由平台管理员在渠道配置页填写(敏感参数写入 config_json 前由后端加密)。
-- 初始 enabled 一律为 0(停用),配置完成并通过「测试连接」后再由页面启用,
-- 前端会按「enabled=1 → 已启用;enabled=0 且无参数 → 未配置;enabled=0 且有参数 → 已停用」展示状态。
-- 云闪付复用银联商户参数,仅存收银台标识开关(extra_json.show_logo / logo_positions);
-- PayPal 默认沙箱环境(extra_json.env=sandbox),当前业务不跨境,保持停用。
-- 本脚本幂等:已存在同名渠道(含已软删)则跳过,可重复执行。
-- 依赖:先执行 create_platform_payment.sql 建表。
-- 运行:mysql --default-character-set=utf8mb4 -h<host> -P<port> -u<user> -p <db> < init_platform_payment_channel.sql
-- =============================================================
INSERT INTO `yz_platform_payment_channel`
(`channel`, `name`, `merchant_no`, `config_json`, `cert_json`, `callback_url`, `enabled`, `extra_json`, `remark`, `create_time`, `update_time`)
SELECT v.`channel`, v.`name`, '', NULL, NULL,
CONCAT('/api/payment/callback/', v.`channel`), 0, v.`extra_json`, v.`remark`, NOW(), NOW()
FROM (
SELECT 'wechat' AS `channel`, '微信支付' AS `name`, '' AS `extra_json`,
'主渠道:JSAPI / Native 扫码收款,需商户号、APPID、APIv3 密钥与商户证书' AS `remark`
UNION ALL SELECT 'alipay', '支付宝', '',
'电脑网站支付 / 手机网站支付,RSA2 签名,异步通知无需登记'
UNION ALL SELECT 'unionpay', '银联', '',
'全渠道收单,商户号与证书由收单机构下发,回调地址需向收单机构报备'
UNION ALL SELECT 'cloudpay', '云闪付', '{"show_logo":1,"logo_positions":["pc","h5"]}',
'复用银联商户参数,仅控制收银台是否展示云闪付标识'
UNION ALL SELECT 'paypal', 'PayPal', '{"env":"sandbox"}',
'跨境预留渠道,当前业务不跨境,默认停用'
) v
WHERE NOT EXISTS (
SELECT 1 FROM `yz_platform_payment_channel` c WHERE c.`channel` = v.`channel`
);
-- 校验:SELECT id, channel, name, merchant_no, enabled, callback_url, delete_time
-- FROM yz_platform_payment_channel ORDER BY id;
@@ -1,67 +0,0 @@
-- 平台官网站点设置 - 初始化/调整数据(yz_platform_site_setting,通用键值结构)
--
-- 前提:表已按 create_platform_site_setting.sql 建成通用结构(title/value/tag/remark)。
--
-- remark 可选标记(用 | 分隔,未识别片段作为说明文字展示):
-- group:分组名 决定 platform 端界面落在哪个 tab(缺省「基本设置」)
-- type:控件类型 text(默认) / textarea / image(图片上传)
--
-- 当前分组:基本设置 / 公司信息 / 备案信息 / 政策条款
-- value 为建议初始值,请按实际情况修改(备案号请替换为真实备案号)。
--
-- ============================================================
-- PART 1:全新环境(表中无数据)—— 直接执行
-- ============================================================
INSERT INTO `yz_platform_site_setting`
(`title`, `value`, `tag`, `remark`, `create_time`, `update_time`)
VALUES
-- 基本设置
('站点名称', '云泽科技', 'sitename', '常用于 <title> 和页头 | group:基本设置', NOW(), NOW()),
('副标题', 'YUNZER TECH', 'subtitle', '站点名称下方的补充说明,常用于页头/首页 | group:基本设置', NOW(), NOW()),
('Logo(深色)', '', 'logo', '深色 Logo,用于浅色背景(如页头) | group:基本设置 | type:image', NOW(), NOW()),
('Logo(浅色)', '', 'logow', '浅色 Logo,用于深色背景(如页脚) | group:基本设置 | type:image', NOW(), NOW()),
-- 公司信息
('公司名称', '云泽科技有限公司', 'companyname', '公司全称,常用于页脚与「关于我们」 | group:公司信息', NOW(), NOW()),
('联系电话', '400-000-0000', 'phone', '服务热线,常用于页头与页脚 | group:公司信息', NOW(), NOW()),
('电子邮箱', 'contact@yunzer.com', 'email', '商务合作邮箱,可配合 mailto: 使用 | group:公司信息', NOW(), NOW()),
('公司地址', '北京市海淀区中关村软件园 25 号楼', 'address', '公司所在地址,展示于页脚/联系页面 | group:公司信息', NOW(), NOW()),
('工作时间', '工作日 9:00 - 18:00', 'worktime', '如:工作日 9:00 - 18:00 | group:公司信息', NOW(), NOW()),
-- 备案信息
('ICP 备案号', '京ICP备00000000号-1', 'icp', '请替换为真实备案号,常用于页脚 | group:备案信息', NOW(), NOW()),
('公安备案', '京公网安备11010800000000号', 'policeicp', '请替换为真实公安备案号,常用于页脚 | group:备案信息', NOW(), NOW()),
('版权信息', 'COPYRIGHT (C) 2026 YUNZER TECHNOLOGY CO., LTD. ALL RIGHTS RESERVED.', 'copyright', '常用于页脚 | group:备案信息', NOW(), NOW()),
-- 政策条款
('隐私政策', '/policy/privacy', 'privacy', '隐私政策页面链接 | group:政策条款', NOW(), NOW()),
('使用条款', '/policy/terms', 'terms', '使用条款页面链接 | group:政策条款', NOW(), NOW());
-- ============================================================
-- PART 2:已有数据(表里有旧的 9 项)—— 只做增量调整,可重复执行
-- ============================================================
-- 2.1 隐私政策 / 使用条款 归入「政策条款」tab
UPDATE `yz_platform_site_setting`
SET `remark` = '隐私政策页面链接 | group:政策条款', `update_time` = NOW()
WHERE `tag` = 'privacy' AND `delete_time` IS NULL;
UPDATE `yz_platform_site_setting`
SET `remark` = '使用条款页面链接 | group:政策条款', `update_time` = NOW()
WHERE `tag` = 'terms' AND `delete_time` IS NULL;
-- 2.2 新增「公司信息」分组的 5 个设置项(已存在同 tag 的记录不会重复插入)
INSERT INTO `yz_platform_site_setting`
(`title`, `value`, `tag`, `remark`, `create_time`, `update_time`)
SELECT v.`title`, v.`value`, v.`tag`, v.`remark`, NOW(), NOW()
FROM (
SELECT '公司名称' AS `title`, '云泽科技有限公司' AS `value`, 'companyname' AS `tag`,
'公司全称,常用于页脚与「关于我们」 | group:公司信息' AS `remark`
UNION ALL SELECT '联系电话', '400-000-0000', 'phone', '服务热线,常用于页头与页脚 | group:公司信息'
UNION ALL SELECT '电子邮箱', 'contact@yunzer.com', 'email', '商务合作邮箱,可配合 mailto: 使用 | group:公司信息'
UNION ALL SELECT '公司地址', '北京市海淀区中关村软件园 25 号楼', 'address', '公司所在地址,展示于页脚/联系页面 | group:公司信息'
UNION ALL SELECT '工作时间', '工作日 9:00 - 18:00', 'worktime', '如:工作日 9:00 - 18:00 | group:公司信息'
) v
WHERE NOT EXISTS (
SELECT 1 FROM `yz_platform_site_setting` s
WHERE s.`tag` = v.`tag` AND s.`delete_time` IS NULL
);
-- 校验:SELECT tag, title, value, remark FROM yz_platform_site_setting WHERE delete_time IS NULL ORDER BY id;
@@ -0,0 +1,22 @@
-- =============================================================
-- 平台官网管理专用上传分组:官网素材
--
-- 官网管理(platform/src/views/website)下所有图片/文件上传统一归入该分组,
-- 便于在「系统管理 → 文件管理」(/system/fileManager) 中集中查找与复用。
--
-- 归属:tid = 0(平台端),uid = 1(平台管理员),与既有 appsupgrade、图片 分组一致。
-- 幂等:已存在同名未删除分组时不会重复插入。
-- 运行:mysql -h<host> -P<port> -u<user> -p <db> < init_platform_website_file_cate.sql
--
-- 备注:前端 utils/websiteUpload.js 也会在上传时按名称兜底查找/补建,
-- 因此即使本文件未执行,首次上传后该分组同样会出现。
-- =============================================================
INSERT INTO `yz_system_files_category` (`tid`, `uid`, `name`, `create_time`)
SELECT * FROM (
SELECT 0 AS tid, 1 AS uid, '官网素材' AS name, NOW() AS create_time
) AS seed
WHERE NOT EXISTS (
SELECT 1 FROM `yz_system_files_category`
WHERE `tid` = 0 AND `name` = '官网素材' AND `delete_time` IS NULL
);
-67
View File
@@ -1,67 +0,0 @@
-- =============================================================
-- 角色体系整理:命名规范 + 全局角色统一
-- 表:yz_system_admin_role
--
-- 整理后的角色模型:
-- 平台端(cid=1, tenant_id=0):
-- 总管理员 平台端最高权限(rights 为空 = 全权限),管理所有租户
-- 平台管理员 平台端运营人员(由第 4 段创建)
-- 租户端(cid=2, tenant_id=0,全局共用,所有租户都能看到/使用):
-- 租户管理员 租户内的管理员(rights 为空 = 全权限)
-- 租户用户 租户内的普通用户(原「普通租户」改名,避免被误解为"租户实体")
-- 租户自建角色(cid=2, tenant_id=租户ID, is_custom=1):租户端自行创建
--
-- 说明:本脚本幂等(按「id/旧名」匹配,重复执行不会重复改、不会误改)。
-- 运行:mysql --default-character-set=utf8mb4 -h<host> -P<port> -u<user> -p <db> < update_role_system.sql
-- =============================================================
-- 1) 平台端最高角色:管理员 → 总管理员;并修正 cid / tenant_id / is_custom
UPDATE `yz_system_admin_role`
SET `name` = '总管理员', `cid` = 1, `tenant_id` = 0, `is_custom` = 0, `update_time` = NOW()
WHERE `id` = 1 AND `name` = '管理员';
-- 2) 「租户管理员」由租户专属改为全局共用(tenant_id=0):
-- 迁移后所有租户共用同一条记录,由平台端统一维护;
-- 若全局已存在同名角色则不重复迁移(避免出现两条全局「租户管理员」)。
UPDATE `yz_system_admin_role`
SET `tenant_id` = 0, `is_custom` = 0, `update_time` = NOW()
WHERE `name` = '租户管理员' AND `tenant_id` > 0
AND NOT EXISTS (
SELECT 1 FROM (
SELECT `id` FROM `yz_system_admin_role`
WHERE `name` = '租户管理员' AND `tenant_id` = 0
) t
);
-- 3) 「租户用户」(租户内的普通用户):原「普通租户」更名为「租户用户」
-- 3.1 改名(若全局已存在「租户用户」则不重复改名)
UPDATE `yz_system_admin_role`
SET `name` = '租户用户', `update_time` = NOW()
WHERE `name` = '普通租户' AND `cid` = 2
AND NOT EXISTS (
SELECT 1 FROM (
SELECT `id` FROM `yz_system_admin_role`
WHERE `name` = '租户用户' AND `cid` = 2 AND `tenant_id` = 0
) t
);
-- 3.2 确保「租户用户」为全局角色(幂等保险,已是全局时不生效)
UPDATE `yz_system_admin_role`
SET `cid` = 2, `tenant_id` = 0, `is_custom` = 0, `update_time` = NOW()
WHERE `name` = '租户用户' AND `cid` = 2
AND (`tenant_id` <> 0 OR `is_custom` <> 0);
-- 4) 平台端第二级角色「平台管理员」(幂等:已存在则跳过;默认未分配用户,按需在平台端用户管理中绑定)
INSERT INTO `yz_system_admin_role` (`tenant_id`, `cid`, `is_custom`, `name`, `status`, `create_time`, `update_time`)
SELECT 0, 1, 0, '平台管理员', 1, NOW(), NOW()
WHERE NOT EXISTS (
SELECT 1 FROM (SELECT `id` FROM `yz_system_admin_role` WHERE `cid` = 1 AND `tenant_id` = 0 AND `name` = '平台管理员') t
);
-- 5) 清理租户旧的预置角色「部门负责人」「员工」(仅删租户级预置角色,无用户绑定则删除无影响)
-- 本环境已执行过;其它环境如仍在业务中使用请勿执行,租户需要时可在租户端自建自定义角色。
DELETE FROM `yz_system_admin_role`
WHERE `name` IN ('部门负责人', '员工') AND `tenant_id` > 0 AND `is_custom` = 0;
-- 校验:整理后的角色清单
SELECT id, cid, tenant_id, is_custom, name, status FROM yz_system_admin_role ORDER BY cid, tenant_id, id;
+10
View File
@@ -120,6 +120,16 @@ func Init(_ string) {
new(CmsFrontendOnepage),
new(BackendMenuFront),
// 平台官网管理(docs/sql/create_platform_website.sql)
new(PlatformWebsiteCategory),
new(PlatformWebsiteArticle),
new(PlatformWebsiteTag),
new(PlatformWebsiteArticleTag),
new(PlatformWebsiteComment),
new(PlatformWebsiteNav),
new(PlatformWebsiteLink),
new(PlatformWebsitePage),
new(SystemReminderList),
new(SystemNormalSetting),
+307
View File
@@ -0,0 +1,307 @@
package models
import (
"time"
)
// =============================================================
// 平台官网管理模型(表前缀 yz_platform_website_)
//
// 对应 platform 端「官网管理」模块,仅服务平台主站官网,
// 与租户站点内容表(yz_cms_*)互相独立。
// 建表 SQL:docs/sql/create_platform_website.sql
// =============================================================
// PlatformWebsiteCategory 平台官网-文章分类 yz_platform_website_category
type PlatformWebsiteCategory struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Cid uint64 `orm:"column(cid);default(0)" json:"cid"` // 父级分类ID,0 为顶级
Name string `orm:"column(name);size(100);default('')" json:"name"`
Image string `orm:"column(image);size(500);default('')" json:"image"`
Desc string `orm:"column(desc);size(500);default('')" json:"desc"`
Sort int `orm:"column(sort);default(0)" json:"sort"`
Status int8 `orm:"column(status);default(1)" json:"status"`
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *PlatformWebsiteCategory) TableName() string {
return "yz_platform_website_category"
}
// PlatformWebsiteArticle 平台官网-文章 yz_platform_website_article
type PlatformWebsiteArticle struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
CateID uint64 `orm:"column(cate_id);default(0)" json:"cate_id"`
Title string `orm:"column(title);size(255);default('')" json:"title"`
Author string `orm:"column(author);size(100);default('')" json:"author"`
Desc string `orm:"column(desc);size(500);default('')" json:"desc"`
Image string `orm:"column(image);size(500);default('')" json:"image"`
Content string `orm:"column(content);type(mediumtext);null" json:"content"`
IsTrans int8 `orm:"column(is_trans);default(0)" json:"is_trans"`
TransURL *string `orm:"column(transurl);size(500);null" json:"transurl"`
Status int8 `orm:"column(status);default(0)" json:"status"` // 0 草稿 1 待审核 2 已发布 3 已隐藏
Top int8 `orm:"column(top);default(0)" json:"top"`
Recommend int8 `orm:"column(recommend);default(0)" json:"recommend"`
Views int `orm:"column(views);default(0)" json:"views"`
Likes int `orm:"column(likes);default(0)" json:"likes"`
PublisherID *uint64 `orm:"column(publisher_id);null" json:"publisher_id"`
PublishTime *time.Time `orm:"column(publish_time);type(datetime);null" json:"publish_time"`
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *PlatformWebsiteArticle) TableName() string {
return "yz_platform_website_article"
}
// PlatformWebsiteTag 平台官网-标签 yz_platform_website_tag
type PlatformWebsiteTag struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Name string `orm:"column(name);size(50);default('')" json:"name"`
Sort int `orm:"column(sort);default(0)" json:"sort"`
Status int8 `orm:"column(status);default(1)" json:"status"`
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *PlatformWebsiteTag) TableName() string {
return "yz_platform_website_tag"
}
// PlatformWebsiteArticleTag 平台官网-文章标签关联 yz_platform_website_article_tag
type PlatformWebsiteArticleTag struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
ArticleID uint64 `orm:"column(article_id);default(0)" json:"article_id"`
TagID uint64 `orm:"column(tag_id);default(0)" json:"tag_id"`
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
}
func (m *PlatformWebsiteArticleTag) TableName() string {
return "yz_platform_website_article_tag"
}
// PlatformWebsiteComment 平台官网-评论 yz_platform_website_comment
type PlatformWebsiteComment struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
ArticleID uint64 `orm:"column(article_id);default(0)" json:"article_id"`
Nickname string `orm:"column(nickname);size(100);default('')" json:"nickname"`
Email string `orm:"column(email);size(150);default('')" json:"email"`
Content string `orm:"column(content);type(text);null" json:"content"`
Reply string `orm:"column(reply);type(text);null" json:"reply"`
Status int8 `orm:"column(status);default(0)" json:"status"` // 0 待审核 1 已通过 2 已拒绝
IP string `orm:"column(ip);size(64);default('')" json:"ip"`
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *PlatformWebsiteComment) TableName() string {
return "yz_platform_website_comment"
}
// PlatformWebsiteNav 平台官网-导航菜单 yz_platform_website_nav
type PlatformWebsiteNav struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Pid uint64 `orm:"column(pid);default(0)" json:"pid"`
Title string `orm:"column(title);size(100);default('')" json:"title"`
Type int8 `orm:"column(type);default(1)" json:"type"` // 1 目录 2 页面 3 外链 4 单页
Path string `orm:"column(path);size(255);default('')" json:"path"`
ComponentPath string `orm:"column(component_path);size(255);default('')" json:"component_path"`
Icon string `orm:"column(icon);size(100);default('')" json:"icon"`
Sort int `orm:"column(sort);default(0)" json:"sort"`
Status int8 `orm:"column(status);default(1)" json:"status"`
IsVisible int8 `orm:"column(is_visible);default(1)" json:"is_visible"`
Permission string `orm:"column(permission);size(100);default('')" json:"permission"`
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *PlatformWebsiteNav) TableName() string {
return "yz_platform_website_nav"
}
// PlatformWebsiteLink 平台官网-友情链接 yz_platform_website_link
type PlatformWebsiteLink struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
LinkName string `orm:"column(link_name);size(100);default('')" json:"link_name"`
LinkURL string `orm:"column(link_url);size(500);default('')" json:"link_url"`
LinkLogo string `orm:"column(link_logo);size(500);default('')" json:"link_logo"`
Description string `orm:"column(description);size(255);default('')" json:"description"`
Sort int `orm:"column(sort);default(0)" json:"sort"`
Status int8 `orm:"column(status);default(1)" json:"status"`
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *PlatformWebsiteLink) TableName() string {
return "yz_platform_website_link"
}
// PlatformWebsitePage 平台官网-单页 yz_platform_website_page
type PlatformWebsitePage struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Title string `orm:"column(title);size(255);default('')" json:"title"`
Path string `orm:"column(path);size(200);default('')" json:"path"`
Content string `orm:"column(content);type(longtext);null" json:"content"`
Sort int `orm:"column(sort);default(0)" json:"sort"`
Status int8 `orm:"column(status);default(1)" json:"status"`
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *PlatformWebsitePage) TableName() string {
return "yz_platform_website_page"
}
// ===================== 公共查询helper =====================
// PlatformWebsiteCategoryNameMap 分类ID -> 分类名称
func PlatformWebsiteCategoryNameMap(ids []uint64) map[uint64]string {
out := make(map[uint64]string)
if len(ids) == 0 {
return out
}
var rows []PlatformWebsiteCategory
if _, err := Orm.QueryTable(new(PlatformWebsiteCategory)).
Filter("id__in", ids).
All(&rows, "ID", "Name"); err != nil {
return out
}
for _, r := range rows {
out[r.ID] = r.Name
}
return out
}
// PlatformWebsiteCategoryMap 分类ID -> 分类记录
// 官网前台用:展示分类名与描述,并在文章没有封面图时用分类图片兜底。
func PlatformWebsiteCategoryMap(ids []uint64) map[uint64]PlatformWebsiteCategory {
out := make(map[uint64]PlatformWebsiteCategory)
if len(ids) == 0 {
return out
}
var rows []PlatformWebsiteCategory
if _, err := Orm.QueryTable(new(PlatformWebsiteCategory)).
Filter("id__in", ids).
All(&rows, "ID", "Name", "Desc", "Image"); err != nil {
return out
}
for _, r := range rows {
out[r.ID] = r
}
return out
}
// PlatformWebsiteArticleTitleMap 文章ID -> 标题(评论列表展示所属文章用)
func PlatformWebsiteArticleTitleMap(ids []uint64) map[uint64]string {
out := make(map[uint64]string)
if len(ids) == 0 {
return out
}
var rows []PlatformWebsiteArticle
if _, err := Orm.QueryTable(new(PlatformWebsiteArticle)).
Filter("id__in", ids).
All(&rows, "ID", "Title"); err != nil {
return out
}
for _, r := range rows {
out[r.ID] = r.Title
}
return out
}
// PlatformWebsiteArticleTagNameMap 文章ID -> 标签名称列表(官网前台展示用)
func PlatformWebsiteArticleTagNameMap(articleIDs []uint64) map[uint64][]string {
out := make(map[uint64][]string)
if len(articleIDs) == 0 {
return out
}
var links []PlatformWebsiteArticleTag
if _, err := Orm.QueryTable(new(PlatformWebsiteArticleTag)).
Filter("article_id__in", articleIDs).
All(&links, "ID", "ArticleID", "TagID"); err != nil {
return out
}
if len(links) == 0 {
return out
}
tagIDs := make([]uint64, 0, len(links))
for _, l := range links {
tagIDs = append(tagIDs, l.TagID)
}
var tags []PlatformWebsiteTag
if _, err := Orm.QueryTable(new(PlatformWebsiteTag)).
Filter("id__in", tagIDs).
Filter("delete_time__isnull", true).
All(&tags, "ID", "Name"); err != nil {
return out
}
nameMap := make(map[uint64]string, len(tags))
for _, t := range tags {
nameMap[t.ID] = t.Name
}
for _, l := range links {
if name, ok := nameMap[l.TagID]; ok && name != "" {
out[l.ArticleID] = append(out[l.ArticleID], name)
}
}
return out
}
// PlatformWebsiteArticleTagIDs 取某文章绑定的标签ID列表
func PlatformWebsiteArticleTagIDs(articleID uint64) []uint64 {
out := make([]uint64, 0)
if articleID == 0 {
return out
}
var rows []PlatformWebsiteArticleTag
if _, err := Orm.QueryTable(new(PlatformWebsiteArticleTag)).
Filter("article_id", articleID).
All(&rows, "ID", "TagID"); err != nil {
return out
}
for _, r := range rows {
out = append(out, r.TagID)
}
return out
}
// SetPlatformWebsiteArticleTags 覆盖式写入文章标签(先清空再插入)
func SetPlatformWebsiteArticleTags(articleID uint64, tagIDs []uint64) error {
if articleID == 0 {
return nil
}
if _, err := Orm.QueryTable(new(PlatformWebsiteArticleTag)).
Filter("article_id", articleID).
Delete(); err != nil {
return err
}
seen := make(map[uint64]bool, len(tagIDs))
for _, tid := range tagIDs {
if tid == 0 || seen[tid] {
continue
}
seen[tid] = true
if _, err := Orm.Insert(&PlatformWebsiteArticleTag{ArticleID: articleID, TagID: tid}); err != nil {
return err
}
}
return nil
}
// ClearPlatformWebsiteArticleTags 删除文章时清理标签关联
func ClearPlatformWebsiteArticleTags(articleID uint64) error {
_, err := Orm.QueryTable(new(PlatformWebsiteArticleTag)).
Filter("article_id", articleID).
Delete()
return err
}
+11
View File
@@ -17,6 +17,17 @@ func Register() {
// 官网前台站点设置(公开接口,供 SPA 官网读取,字段与模板标签 {yz:xxx} 同源)
beego.Router("/site/setting", &controllers.TenantSiteController{}, "get:SiteSetting")
// 官网页头导航菜单(平台「官网管理 - 导航菜单」配置,仅返回启用且显示的项)
beego.Router("/site/nav", &controllers.IndexWebsiteController{}, "get:Nav")
// SPA 官网内容接口(读取平台官网管理 yz_platform_website_*,仅返回已发布内容)
// 注意:静态段必须在 :id 之前注册,保证 /site/news/categories 不被当作详情
beego.Router("/site/news", &controllers.IndexWebsiteController{}, "get:News")
beego.Router("/site/news/categories", &controllers.IndexWebsiteController{}, "get:NewsCategories")
beego.Router("/site/news/:id/related", &controllers.IndexWebsiteController{}, "get:NewsRelated")
beego.Router("/site/news/:id/view", &controllers.IndexWebsiteController{}, "post:NewsView")
beego.Router("/site/news/:id", &controllers.IndexWebsiteController{}, "get:NewsDetail")
beego.Router("/index/headmenu", &controllers.IndexPortalController{}, "get:GetHeadMenu")
beego.Router("/index/footerdata", &controllers.IndexPortalController{}, "get:GetFooterData")
beego.Router("/index/newscentertop4", &controllers.IndexPortalController{}, "get:GetNewsCenterTop4")
+54
View File
@@ -348,4 +348,58 @@ func Register() {
// 平台官网站点设置(yz_platform_site_setting)
beego.Router("/platform/website/setting/info", &controllers.PlatformWebsiteSettingController{}, "get:Info")
beego.Router("/platform/website/setting/save", &controllers.PlatformWebsiteSettingController{}, "post:Save")
// ===== 平台官网管理(docs/sql/create_platform_website.sql)=====
// 文章管理 yz_platform_website_article
beego.Router("/platform/website/article/list", &controllers.PlatformWebsiteArticleController{}, "get:List")
beego.Router("/platform/website/article/all", &controllers.PlatformWebsiteArticleController{}, "get:ListAll")
beego.Router("/platform/website/article/recycle", &controllers.PlatformWebsiteArticleController{}, "get:Recycle")
beego.Router("/platform/website/article", &controllers.PlatformWebsiteArticleController{}, "post:Create")
beego.Router("/platform/website/article/:id/restore", &controllers.PlatformWebsiteArticleController{}, "post:Restore")
beego.Router("/platform/website/article/:id/purge", &controllers.PlatformWebsiteArticleController{}, "delete:Purge")
beego.Router("/platform/website/article/:id/publish", &controllers.PlatformWebsiteArticleController{}, "post:Publish")
beego.Router("/platform/website/article/:id/unpublish", &controllers.PlatformWebsiteArticleController{}, "post:UnPublish")
beego.Router("/platform/website/article/:id/recommend", &controllers.PlatformWebsiteArticleController{}, "post:Recommend")
beego.Router("/platform/website/article/:id/unrecommend", &controllers.PlatformWebsiteArticleController{}, "post:UnRecommend")
beego.Router("/platform/website/article/:id/top", &controllers.PlatformWebsiteArticleController{}, "post:Top")
beego.Router("/platform/website/article/:id/untop", &controllers.PlatformWebsiteArticleController{}, "post:UnTop")
beego.Router("/platform/website/article/:id", &controllers.PlatformWebsiteArticleController{}, "get:Get;post:Update;delete:Delete")
// 分类管理 yz_platform_website_category
beego.Router("/platform/website/category/all", &controllers.PlatformWebsiteCategoryController{}, "get:All")
beego.Router("/platform/website/category/list", &controllers.PlatformWebsiteCategoryController{}, "get:List")
beego.Router("/platform/website/category", &controllers.PlatformWebsiteCategoryController{}, "post:Create")
beego.Router("/platform/website/category/:id/status", &controllers.PlatformWebsiteCategoryController{}, "patch:UpdateStatus")
beego.Router("/platform/website/category/:id", &controllers.PlatformWebsiteCategoryController{}, "get:Get;post:Update;delete:Delete")
// 标签管理 yz_platform_website_tag
beego.Router("/platform/website/tag/all", &controllers.PlatformWebsiteTagController{}, "get:All")
beego.Router("/platform/website/tag/list", &controllers.PlatformWebsiteTagController{}, "get:List")
beego.Router("/platform/website/tag", &controllers.PlatformWebsiteTagController{}, "post:Create")
beego.Router("/platform/website/tag/:id/status", &controllers.PlatformWebsiteTagController{}, "patch:UpdateStatus")
beego.Router("/platform/website/tag/:id", &controllers.PlatformWebsiteTagController{}, "get:Get;post:Update;delete:Delete")
// 评论管理 yz_platform_website_comment
beego.Router("/platform/website/comment/list", &controllers.PlatformWebsiteCommentController{}, "get:List")
beego.Router("/platform/website/comment/batchdelete", &controllers.PlatformWebsiteCommentController{}, "post:BatchDelete")
beego.Router("/platform/website/comment/:id/audit", &controllers.PlatformWebsiteCommentController{}, "post:Audit")
beego.Router("/platform/website/comment/:id/reply", &controllers.PlatformWebsiteCommentController{}, "post:Reply")
beego.Router("/platform/website/comment/:id", &controllers.PlatformWebsiteCommentController{}, "get:Get;delete:Delete")
// 导航菜单 yz_platform_website_nav
beego.Router("/platform/website/nav/list", &controllers.PlatformWebsiteNavController{}, "get:List")
beego.Router("/platform/website/nav", &controllers.PlatformWebsiteNavController{}, "post:Create")
beego.Router("/platform/website/nav/:id", &controllers.PlatformWebsiteNavController{}, "post:Update;delete:Delete")
// 友情链接 yz_platform_website_link
beego.Router("/platform/website/link/list", &controllers.PlatformWebsiteLinkController{}, "get:List")
beego.Router("/platform/website/link/all", &controllers.PlatformWebsiteLinkController{}, "get:All")
beego.Router("/platform/website/link/batchdelete", &controllers.PlatformWebsiteLinkController{}, "post:BatchDelete")
beego.Router("/platform/website/link", &controllers.PlatformWebsiteLinkController{}, "post:Create")
beego.Router("/platform/website/link/:id", &controllers.PlatformWebsiteLinkController{}, "put:Update;delete:Delete")
// 单页管理 yz_platform_website_page
beego.Router("/platform/website/page/all", &controllers.PlatformWebsitePageController{}, "get:All")
beego.Router("/platform/website/page", &controllers.PlatformWebsitePageController{}, "post:Create")
beego.Router("/platform/website/page/:id", &controllers.PlatformWebsitePageController{}, "get:Get;post:Update;delete:Delete")
}