更新代码

This commit is contained in:
2026-09-15 10:44:31 +08:00
parent 9875832019
commit 943c3708b0
125 changed files with 2009 additions and 1728 deletions
+10 -78
View File
@@ -1,93 +1,25 @@
package models
import (
"sync"
"github.com/beego/beego/v2/client/orm"
)
var adminRoleTenantColOnce sync.Once
var adminRoleCustomColOnce sync.Once
// EnsureAdminRoleTenantColumn 为角色表补齐 tenant_id 列(历史库可能早于租户隔离特性建表)。
// 字段已存在时 MySQL 报 duplicate column,统一忽略,不阻断主流程。
func EnsureAdminRoleTenantColumn() {
if Orm == nil {
return
}
adminRoleTenantColOnce.Do(func() {
_, _ = Orm.Raw(`
ALTER TABLE yz_system_admin_role
ADD COLUMN tenant_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID 0-平台/全局 >0-具体租户'
`).Exec()
})
}
// EnsureAdminRoleCustomColumn 为角色表补齐 is_custom 列(历史库可能早于自定义角色特性建表)。
// 字段已存在时 MySQL 报 duplicate column,统一忽略,不阻断主流程。
func EnsureAdminRoleCustomColumn() {
if Orm == nil {
return
}
adminRoleCustomColOnce.Do(func() {
_, _ = Orm.Raw(`
ALTER TABLE yz_system_admin_role
ADD COLUMN is_custom tinyint unsigned NOT NULL DEFAULT 0 COMMENT '自定义角色 0-系统/平台预置 1-租户自建'
`).Exec()
})
}
// EnsureDefaultTenantRoles 保证指定租户(cid=2)的默认角色齐全:
// - 租户管理员:全权限(rights 为空,见 permission.go 空权限即放行)
// - 部门负责人
// - 员工
// GetTenantAdminRole 返回全局「租户管理员」角色(cid=2, tenant_id=0)的 ID。
//
// 默认角色按租户隔离:每个租户首次访问角色列表时补建自己的一套,互不影响。
// 缺失的自动插入;已存在(同名同 cid 同租户)的不动。任何失败均静默忽略不阻断主流程。
func EnsureDefaultTenantRoles(tid uint64) {
if Orm == nil || tid == 0 {
return
}
// 确保 tenant_id / is_custom 列存在(兼容老表)
EnsureAdminRoleTenantColumn()
EnsureAdminRoleCustomColumn()
defaults := []string{"租户管理员", "部门负责人", "员工"}
for _, name := range defaults {
cnt, err := Orm.QueryTable(new(AdminRole)).
Filter("cid", 2).
Filter("tenant_id", tid).
Filter("name", name).
Count()
if err != nil {
continue
}
if cnt > 0 {
continue
}
role := &AdminRole{
TenantID: tid,
Cid: 2,
Name: name,
Status: 1,
Rights: nil, // 空权限 = 全权限(permission.go)
}
_, _ = Orm.Insert(role)
}
}
// GetTenantAdminRole 返回指定租户(cid=2)的「租户管理员」角色 ID。
// 角色不存在时先调用 EnsureDefaultTenantRoles 补建再查;任何失败返回 (0, err)。
// 普通租户(租户第一个账号)与租户管理员实为同一概念,均绑定此角色以获得 backend 全权限。
func GetTenantAdminRole(tid uint64) (uint64, error) {
if Orm == nil || tid == 0 {
// 角色模型:租户端的「租户管理员 / 租户用户」为全局共用角色(tenant_id=0),
// 所有租户共享同一批记录,由平台端角色管理统一维护;
// 角色数据走 SQL(docs/sql/update_role_system.sql),代码不做自动补建。
//
// 租户的第一个账号即租户管理员,绑定此角色以获得 backend 全权限。
// 任何失败返回 (0, err)。
func GetTenantAdminRole() (uint64, error) {
if Orm == nil {
return 0, orm.ErrNoRows
}
EnsureDefaultTenantRoles(tid)
var role AdminRole
if err := Orm.QueryTable(new(AdminRole)).
Filter("cid", 2).
Filter("tenant_id", tid).
Filter("tenant_id", 0).
Filter("name", "租户管理员").
One(&role); err != nil {
return 0, err
-56
View File
@@ -1,7 +1,6 @@
package models
import (
"sync"
"time"
"github.com/beego/beego/v2/client/orm"
@@ -54,61 +53,6 @@ func (m *CmsArticle) TableName() string {
return "yz_cms_article"
}
var cmsArticleTablesOnce sync.Once
// EnsureCmsArticleTables 首次使用时自动建表(若不存在)。
func EnsureCmsArticleTables() error {
var err error
cmsArticleTablesOnce.Do(func() {
_, err = Orm.Raw(`
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`).Exec()
if err != nil {
return
}
_, err = Orm.Raw(`
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`).Exec()
})
return err
}
func CmsCategoryNameMap(tid uint64, ids []uint64) map[uint64]string {
out := make(map[uint64]string)
if len(ids) == 0 {
-44
View File
@@ -1,7 +1,6 @@
package models
import (
"sync"
"time"
)
@@ -43,47 +42,4 @@ func (m *CmsProductCategory) TableName() string {
return "yz_cms_product_category"
}
var cmsProductTablesOnce sync.Once
// EnsureCmsProductTables 首次使用时自动建表(若不存在)。
func EnsureCmsProductTables() error {
var err error
cmsProductTablesOnce.Do(func() {
_, err = Orm.Raw(`
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`).Exec()
if err != nil {
return
}
_, err = Orm.Raw(`
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`).Exec()
})
return err
}
-46
View File
@@ -1,7 +1,6 @@
package models
import (
"sync"
"time"
)
@@ -43,49 +42,4 @@ func (m *CmsSolutionCategory) TableName() string {
return "yz_cms_solution_category"
}
var cmsSolutionTablesOnce sync.Once
// EnsureCmsSolutionTables 首次使用时自动建表(若不存在)。
func EnsureCmsSolutionTables() error {
var err error
cmsSolutionTablesOnce.Do(func() {
_, err = Orm.Raw(`
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`).Exec()
if err != nil {
return
}
// 确保已有表补齐 content 字段
_, _ = Orm.Raw("ALTER TABLE yz_cms_solution ADD COLUMN content mediumtext").Exec()
_, err = Orm.Raw(`
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`).Exec()
})
return err
}
-77
View File
@@ -1,77 +0,0 @@
package models
import "time"
// FrontMenuDefault 全局默认前端导航项定义
type FrontMenuDefault struct {
Title string
Path string
Type int8 // 菜单类型:2 页面 / 4 单页
Sort int
}
// FrontMenuDefaults 所有租户共用的默认导航(不可删除)。
// 以 tenant_id=0 存放于同一张菜单表,表示全局共享:
// 租户端增删改接口都带 tenant_id 过滤,天然无法触碰这些行。
// 路径对应官网前台路由:新闻中心 /news,其余为单页 /page/:path
// (单页内容在 backend 端 - 单页管理 中按 path 维护)。
var FrontMenuDefaults = []FrontMenuDefault{
{Title: "新闻中心", Path: "/news", Type: 2, Sort: 1},
{Title: "产品展示", Path: "/page/products", Type: 4, Sort: 2},
{Title: "解决方案", Path: "/page/solutions", Type: 4, Sort: 3},
{Title: "关于我们", Path: "/page/about", Type: 4, Sort: 4},
{Title: "联系我们", Path: "/page/contact", Type: 4, Sort: 5},
}
// EnsureGlobalDefaultFrontMenus 保证全局默认导航(tenant_id=0)齐全:
// 缺失的自动新增;被误删(软删)的自动恢复;已存在(含改名)的不动。
// 以"在库生效数量 >= 默认项总数"作为短路条件,避免改名后重复插入。
// 供后台菜单列表与前台导航渲染前调用,任何失败均静默忽略不阻断主流程。
func EnsureGlobalDefaultFrontMenus() {
if Orm == nil {
return
}
qs := Orm.QueryTable(new(BackendMenuFront)).Filter("tenant_id", 0)
activeCnt, err := qs.Filter("delete_time__isnull", true).Count()
if err != nil || activeCnt >= int64(len(FrontMenuDefaults)) {
return
}
// 含软删记录一起取,用于恢复被误删的默认菜单
var rows []BackendMenuFront
if _, err := qs.All(&rows); err != nil {
return
}
exist := make(map[string]*BackendMenuFront, len(rows))
for i := range rows {
exist[rows[i].Title] = &rows[i]
}
now := time.Now()
for _, def := range FrontMenuDefaults {
if row, ok := exist[def.Title]; ok {
if row.DeleteTime != nil {
_, _ = Orm.QueryTable(new(BackendMenuFront)).
Filter("id", row.ID).
Update(map[string]interface{}{
"delete_time": nil,
"status": 1,
"is_visible": 1,
"update_time": now,
})
}
continue
}
_, _ = Orm.Insert(&BackendMenuFront{
TenantID: 0,
Pid: 0,
Title: def.Title,
Path: def.Path,
Sort: def.Sort,
Status: 1,
IsVisible: 1,
Type: def.Type,
})
}
}
+11 -271
View File
@@ -140,278 +140,18 @@ func Init(_ string) {
)
// 创建全局 Ormer
// 数据库约定(重要):
// 1. 表结构与全局预置数据变更,一律以 SQL 文件形式放在 docs/sql/ 下由人工执行,
// 代码中禁止自动建表 / 补列 / 补全局预置数据(本层不做任何此类操作);
// 2. 例外:租户级默认业务数据初始化保留在业务函数中(新租户首次使用时补建,SQL 无法预置),
// 搜索「【租户级初始化】」可定位全部位置:
// EnsureOaDocDefaultCategories 内置文档分类(项目文档)
// crmEnsureDefaultUnits 默认计量单位(套/台/个…)
// crmEnsureDefaultProductCategories 默认产品分类(未分类/软件开发/软件产品/硬件产品)
// 3. 角色体系:角色数据统一维护在 yz_system_admin_role
// (平台角色 cid=1;全局租户角色 cid=2 且 tenant_id=0:租户管理员/租户用户),
// 新建与调整走 SQL:docs/sql/update_role_system.sql,代码不做自动补建。
Orm = orm.NewOrm()
// 补齐历史库缺失的列(已存在时 MySQL 报 duplicate column,统一忽略)
EnsureAdminRoleTenantColumn()
EnsureAdminRoleCustomColumn()
EnsureTenantUserOrgColumn()
EnsureCrmCustomerPoolColumns()
EnsureCrmCreateUserColumn()
EnsureCrmProjectDocColumn()
EnsureCrmContractTable()
EnsureCrmClueAmountColumn()
EnsureCrmContractOurRoleColumn()
EnsureCrmProjectProductsColumn()
EnsureCrmPaybackTable()
EnsureCrmPaybackRecordTable()
EnsureCrmProductDevColumns()
EnsureCrmUnitTable()
EnsureErpCompanyContactMobilesColumn()
EnsurePlatformSiteSettingTable()
}
// EnsureCrmPaybackTable 回款计划表建表(CREATE TABLE IF NOT EXISTS,可重复执行;
// 建表失败(如表已存在但结构不一致)时静默忽略,可用 sql/yz_backend_crm_payback.sql 手动修复)。
func EnsureCrmPaybackTable() {
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回款计划表'`
if _, err := Orm.Raw(sql).Exec(); err != nil {
// 表已存在或执行失败时忽略(完整表结构见 sql/yz_backend_crm_payback.sql)
return
}
}
// EnsureCrmPaybackRecordTable 回款记录表建表(CREATE TABLE IF NOT EXISTS,可重复执行)。
// 回款记录 = 回款进度流水:每次「新建回款」追加一条,以回款计划(payback_id)为先导条件。
func EnsureCrmPaybackRecordTable() {
sql := `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回款记录表(回款进度流水)'`
if _, err := Orm.Raw(sql).Exec(); err != nil {
// 表已存在或执行失败时忽略
return
}
}
// EnsureCrmContractOurRoleColumn 补齐合同表的我方角色字段(存量表已建时新增;
// 旧版「合同性质 contract_nature」字段弃用但保留不动,表不存在或列已存在时忽略错误)。
func EnsureCrmContractOurRoleColumn() {
sql := "ALTER TABLE " + new(TenantCrmContract).TableName() +
" ADD COLUMN our_role tinyint NOT NULL DEFAULT 2 COMMENT '我方角色:1甲方/2乙方/3丙方/4丁方'"
_, _ = Orm.Raw(sql).Exec()
}
// EnsureCrmClueAmountColumn 补齐线索表的预计金额字段(线索阶段预估金额;
// 存量表已建时新增,表不存在或列已存在时忽略错误)。
func EnsureCrmClueAmountColumn() {
sql := "ALTER TABLE " + new(TenantCrmClue).TableName() +
" ADD COLUMN amount decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '预计金额'"
_, _ = Orm.Raw(sql).Exec()
}
// EnsureCrmProjectProductsColumn 补齐项目表的产品清单字段(存量表已建时新增;
// 表不存在或列已存在时忽略错误,可用 docs/sql/alter_backend_crm_project_products_column.sql 手动修复)。
func EnsureCrmProjectProductsColumn() {
sql := "ALTER TABLE " + new(TenantCrmProject).TableName() +
" ADD COLUMN products text COMMENT '项目产品清单JSON(写入产品管理时使用)'"
_, _ = Orm.Raw(sql).Exec()
}
// EnsureCrmContractTable 合同表建表(CREATE TABLE IF NOT EXISTS,可重复执行;
// 建表失败(如表已存在但结构不一致)时静默忽略,可用 sql/yz_backend_crm_contract.sql 手动修复)。
func EnsureCrmContractTable() {
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合同表'`
if _, err := Orm.Raw(sql).Exec(); err != nil {
// 表已存在或执行失败时忽略(完整表结构见 sql/yz_backend_crm_contract.sql)
return
}
}
// EnsureCrmProjectDocColumn 补齐项目表的文档库文件夹分类字段(项目文档绑定 OA 文档库)。
// 表不存在或列已存在时忽略错误。
func EnsureCrmProjectDocColumn() {
sql := "ALTER TABLE " + new(TenantCrmProject).TableName() +
" ADD COLUMN doc_category_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '关联OA文档库项目文件夹分类ID'"
_, _ = Orm.Raw(sql).Exec()
}
// EnsureCrmUnitTable 计量单位表建表(CREATE TABLE IF NOT EXISTS,可重复执行;
// 建表失败(如表已存在但结构不一致)时静默忽略,可用 docs/sql/create_backend_crm_unit.sql 手动修复)。
// 默认单位由控制器在租户首次访问单位列表时写入(需要 tenant_id,无法在启动时预置)。
func EnsureCrmUnitTable() {
sql := `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计量单位表'`
if _, err := Orm.Raw(sql).Exec(); err != nil {
// 表已存在或执行失败时忽略(完整表结构见 docs/sql/create_backend_crm_unit.sql)
return
}
}
// EnsureErpCompanyContactMobilesColumn 补齐公司联系人表(yz_backend_contact_company)的多手机号字段 mobiles。
// 该表为客户/供应商正式联系人的共用库(项目联系人选择、通讯录、客户-供应商转化等均读取),
// 早期多手机号升级遗漏了此表,缺列会导致查询报错(前端表现为「联系人不显示」)。
// 表不存在或列已存在时忽略错误,可用 docs/sql/alter_contact_mobiles.sql 手动修复。
func EnsureErpCompanyContactMobilesColumn() {
table := new(ErpCompanyContact).TableName()
sql := fmt.Sprintf("ALTER TABLE %s ADD COLUMN mobiles text NULL COMMENT '手机号 JSON 数组(最多5个),phone 保留座机语义'", table)
if _, err := Orm.Raw(sql).Exec(); err != nil {
// 列已存在或表不存在时忽略
return
}
// 历史数据回填:原 phone 中的号码作为第一个手机号(仅在 mobiles 为空时)
_, _ = Orm.Raw("UPDATE " + table +
" SET mobiles = JSON_ARRAY(phone) WHERE (mobiles IS NULL OR mobiles = '') AND phone IS NOT NULL AND phone <> ''").Exec()
}
// EnsureCrmProductDevColumns 补齐产品表的软件开发字段(line_type / dev_unit_price / dev_mode / dev_tree);
// 表不存在或列已存在时忽略错误,可用 docs/sql/alter_backend_crm_product_dev_columns.sql 手动修复。
func EnsureCrmProductDevColumns() {
table := new(TenantCrmProduct).TableName()
cols := []string{
"line_type varchar(20) NOT NULL DEFAULT 'product' COMMENT '行类型:product成品/dev软件开发'",
"dev_unit_price decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '软件开发统一人天单价'",
"dev_mode varchar(20) NOT NULL DEFAULT 'self' COMMENT '软件开发研发方式:self自行研发/outsource外协研发'",
"dev_tree text COMMENT '软件开发模块树JSON(叶子人天×统一人天单价)'",
}
for _, col := range cols {
sql := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s", table, col)
if _, err := Orm.Raw(sql).Exec(); err != nil {
// 列已存在或表不存在时忽略
continue
}
}
}
// EnsureCrmCreateUserColumn 补齐客户/供应商的创建人字段(用于删除权限校验)。
// 同样兼容历史表名差异,表不存在或列已存在时忽略错误。
func EnsureCrmCreateUserColumn() {
tables := []string{
new(TenantCrmCustomer).TableName(), "yz_tenant_crm_customer",
new(TenantCrmSupplier).TableName(), "yz_tenant_crm_supplier",
}
for _, table := range tables {
sql := "ALTER TABLE " + table +
" ADD COLUMN create_user_id varchar(64) NOT NULL DEFAULT '' COMMENT '创建人用户ID,用于删除权限校验'"
if _, err := Orm.Raw(sql).Exec(); err != nil {
continue
}
}
}
// EnsureCrmCustomerPoolColumns 补齐客户公海相关字段。
// 兼容历史表名差异:模型 TableName 为 yz_backend_erp_customer,初始化 SQL 中为 yz_tenant_crm_customer,
// 对两个候选表名都执行 ALTER(表不存在或列已存在时忽略错误)。
func EnsureCrmCustomerPoolColumns() {
candidates := []string{
new(TenantCrmCustomer).TableName(),
"yz_tenant_crm_customer",
}
cols := []string{
"in_pool tinyint NOT NULL DEFAULT 0 COMMENT '是否在公海:0-否 1-是'",
"owner_user_id varchar(64) NOT NULL DEFAULT '' COMMENT '负责人ID,公海中为空'",
"owner_user_name varchar(128) NOT NULL DEFAULT '' COMMENT '负责人姓名'",
"last_owner_name varchar(128) NOT NULL DEFAULT '' COMMENT '移入公海前的原负责人'",
"pool_in_time datetime DEFAULT NULL COMMENT '进入公海时间'",
"pool_reason varchar(200) NOT NULL DEFAULT '' COMMENT '移入公海原因'",
}
for _, table := range candidates {
for _, col := range cols {
sql := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s", table, col)
if _, err := Orm.Raw(sql).Exec(); err != nil {
// 列已存在或表不存在时忽略
continue
}
}
}
}
+2 -151
View File
@@ -209,161 +209,12 @@ func DocTypeByExt(ext string) int8 {
return DocTypeOther
}
var oaDocumentTableOnce sync.Once
// EnsureOaDocumentTables 首次访问文档接口时自动建表(若不存在)。
// 与通知公告/日程保持一致的运行期自愈策略,避免新功能上线还需手工执行建表脚本。
func EnsureOaDocumentTables() error {
if Orm == nil {
return nil
}
var err error
oaDocumentTableOnce.Do(func() {
_, err = Orm.Raw(`
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文档分类表'`).Exec()
if err != nil {
return
}
_, err = Orm.Raw(`
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文档表'`).Exec()
if err != nil {
return
}
_, err = Orm.Raw(`
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文档关联表'`).Exec()
if err != nil {
return
}
// 历史库补齐 visibility 列(已存在时 MySQL 报 duplicate column,忽略)
_, _ = Orm.Raw(`
ALTER TABLE yz_backend_oa_doc
ADD COLUMN visibility tinyint NOT NULL DEFAULT 0 COMMENT '可见性 0-租户公开 1-私密(仅创建者与被共享者)'
`).Exec()
// 历史库补齐在线文档编辑源列 content(在线制作功能上线前已建表)
_, _ = Orm.Raw(`
ALTER TABLE yz_backend_oa_doc
ADD COLUMN content longtext NULL COMMENT '在线制作文档的编辑源(富文本HTML),仅用于再次编辑'
`).Exec()
// 历史库补齐分类表的 is_system 列(内置分类特性上线前已建表)
_, _ = Orm.Raw(`
ALTER TABLE yz_backend_oa_doc_category
ADD COLUMN is_system tinyint NOT NULL DEFAULT 0 COMMENT '是否系统内置分类 0-否(可删除) 1-是(不可删除)'
`).Exec()
// 历史库补齐分类表的 scope 列:既有分类默认归入共享文档,保证升级后仍可见
_, _ = Orm.Raw(`
ALTER TABLE yz_backend_oa_doc_category
ADD COLUMN scope varchar(16) NOT NULL DEFAULT 'shared' COMMENT '所属范围 personal-个人文档 shared-共享文档'
`).Exec()
// 历史库补齐分类表的 user_id 列:既有分类默认归租户级(共享分类)
_, _ = Orm.Raw(`
ALTER TABLE yz_backend_oa_doc_category
ADD COLUMN user_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '归属用户ID 0-租户级(共享分类) >0-该用户的个人分类'
`).Exec()
_, err = Orm.Raw(`
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文档共享表'`).Exec()
if err != nil {
return
}
// 双空间模型:兼容存量数据,将不在 (shared, personal) 范围内的分类统一归为共享空间
// (user_id 置 0、scope 置 shared),避免历史脏数据导致分类不可见。
// 注意:不再把 personal 并入 shared,私密空间需保留各自的 user_id 隔离。
_, _ = Orm.Raw(`
UPDATE yz_backend_oa_doc_category
SET user_id = 0, scope = 'shared'
WHERE scope NOT IN ('shared', 'personal') AND is_deleted = 0
`).Exec()
})
return err
}
// oaDocDefaultCategoryOnce 记录已完成内置分类补建的租户,避免每次请求重复查库。
// key 为 tid(int),value 为 struct{}{};补建失败时不写缓存,下次请求继续重试。
var oaDocDefaultCategoryOnce sync.Map
// EnsureOaDocDefaultCategories 保证指定租户的系统内置分类齐全(当前为「项目文档」)。
// 【租户级初始化】新租户首次使用时补建默认业务数据(非表结构/全局预置数据;结构变更须走 docs/sql/ SQL 文件)。
//
// 租户隔离:内置分类是真实数据行,每个租户各自持有一份(tid 不同),
// 首次访问文档接口时自动补建,租户之间互不可见、互不影响。
@@ -374,7 +225,7 @@ var oaDocDefaultCategoryOnce sync.Map
// 「未分类」不落库:它是虚拟分类,文档 category_id = 0 即表示未分类,因此天然不可删除;
// 同时在 services 层把「未分类」「项目文档」列为保留名,禁止新建同名分类与之混淆。
//
// 任何失败均静默忽略,不阻断主流程(与 EnsureDefaultTenantRoles 保持一致)。
// 任何失败均静默忽略,不阻断主流程(与其它租户级初始化保持一致)。
func EnsureOaDocDefaultCategories(tid int) {
if Orm == nil || tid <= 0 {
return
-35
View File
@@ -1,7 +1,6 @@
package models
import (
"sync"
"time"
)
@@ -32,38 +31,4 @@ func (m *OaNotice) TableName() string {
return "yz_backend_oa_notice"
}
var oaNoticeTableOnce sync.Once
// EnsureOaNoticeTable 首次访问通知公告接口时自动建表(若不存在)。
// 与日程表保持一致的"运行时自愈"策略,避免新功能上线还要手工执行建表脚本。
func EnsureOaNoticeTable() error {
if Orm == nil {
return nil
}
var err error
oaNoticeTableOnce.Do(func() {
_, err = Orm.Raw(`
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通知公告表'`).Exec()
})
return err
}
-27
View File
@@ -1,7 +1,6 @@
package models
import (
"sync"
"time"
)
@@ -48,30 +47,4 @@ func (m *OaSchedule) TableName() string {
return "yz_backend_oa_schedule"
}
var oaScheduleCarryColumnsOnce sync.Once
// EnsureOaScheduleCarryColumns 为日程表补齐"顺延到下一天"与"相关图片"所需字段。
// 历史库可能早于该特性建表,这里在首次访问日程接口时自动补列,
// 字段已存在时 MySQL 会报 duplicate column,统一忽略,不阻断主流程。
func EnsureOaScheduleCarryColumns() error {
if Orm == nil {
return nil
}
var err error
oaScheduleCarryColumnsOnce.Do(func() {
alters := []string{
"ALTER TABLE yz_backend_oa_schedule ADD COLUMN source_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '顺延来源日程ID 0-原始日程'",
"ALTER TABLE yz_backend_oa_schedule ADD COLUMN carry_over tinyint NOT NULL DEFAULT 0 COMMENT '是否已顺延到下一天 0-否 1-是'",
"ALTER TABLE yz_backend_oa_schedule ADD COLUMN carry_count int NOT NULL DEFAULT 0 COMMENT '累计顺延次数'",
"ALTER TABLE yz_backend_oa_schedule ADD COLUMN carry_to_date varchar(10) NOT NULL DEFAULT '' COMMENT '顺延目标日期(YYYY-MM-DD)'",
"ALTER TABLE yz_backend_oa_schedule ADD COLUMN images text NULL COMMENT '相关图片JSON数组 [{id,url,name}]'",
"ALTER TABLE yz_backend_oa_schedule ADD COLUMN reminder_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '绑定的提醒任务ID(对应yz_backend_schedule.id) 0-无提醒'",
}
for _, sql := range alters {
if _, e := Orm.Raw(sql).Exec(); e != nil {
err = e
}
}
})
return err
}
-131
View File
@@ -155,135 +155,4 @@ func GetPlatformSiteSettingRows() []PlatformSiteSetting {
return rows
}
// EnsurePlatformSiteSettingDefaults 补齐已登记但未入库的设置项(缺失时插入空值行)
// 注意:须用库中实际行判断,不能用 GetPlatformSiteTagMap —— 它预置了所有默认 tag 的 key,
// 会导致「已存在」判断永远成立、一行都插不进去。
func EnsurePlatformSiteSettingDefaults() {
exist := map[string]bool{}
for _, r := range GetPlatformSiteSettingRows() {
if r.Tag != "" {
exist[r.Tag] = true
}
}
now := time.Now()
for _, d := range PlatformSiteSettingDefs {
if exist[d.Tag] {
continue
}
if _, err := Orm.Insert(&PlatformSiteSetting{
Title: d.Title,
Tag: d.Tag,
Value: "",
Remark: d.Remark,
UpdateTime: &now,
}); err != nil {
continue
}
exist[d.Tag] = true
}
}
// EnsurePlatformSiteSettingTable 建表(通用键值结构,CREATE TABLE IF NOT EXISTS,可重复执行)。
// 若检测到旧宽表结构(含 logo_dark 列),先把历史数据迁移为键值行,再补齐默认项。
func EnsurePlatformSiteSettingTable() {
table := new(PlatformSiteSetting).TableName()
sql := `CREATE TABLE IF NOT EXISTS ` + table + ` (
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='平台官网站点设置表(通用键值结构)'`
if _, err := Orm.Raw(sql).Exec(); err != nil {
return
}
// 旧宽表升级场景:CREATE TABLE IF NOT EXISTS 不会改已有表,需补齐通用结构列
ensurePlatformSiteSettingColumns(table)
if hasTableColumn(table, "logo_dark") {
migrateLegacyPlatformSiteSetting(table)
}
EnsurePlatformSiteSettingDefaults()
}
// ensurePlatformSiteSettingColumns 补齐通用结构列(列缺失时新增,已存在则忽略)
func ensurePlatformSiteSettingColumns(table string) {
cols := []struct{ name, def string }{
{"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 '备注/填写说明'"},
}
for _, c := range cols {
if hasTableColumn(table, c.name) {
continue
}
_, _ = Orm.Raw("ALTER TABLE " + table + " ADD COLUMN " + c.def).Exec()
}
}
// hasTableColumn 判断表是否存在指定列(表不存在或查询异常时返回 false)
func hasTableColumn(table, column string) bool {
var cnt int64
err := Orm.Raw(
"SELECT COUNT(1) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?",
table, column,
).QueryRow(&cnt)
return err == nil && cnt > 0
}
// migrateLegacyPlatformSiteSetting 旧宽表结构迁移:
// 旧列为 sitename/logo_dark/logo_light/icp/police_icp/copyright/privacy_policy/terms_of_service,
// 迁移时仅在目标标签行为空的情况下写入,避免覆盖新配置。
func migrateLegacyPlatformSiteSetting(table string) {
type legacyRow struct {
Sitename string
LogoDark string
LogoLight string
Icp string
PoliceIcp string
Copyright string
Privacy string
Terms string
Companyname string
}
var rows []legacyRow
if _, err := Orm.Raw(
"SELECT IFNULL(sitename,'') sitename, IFNULL(logo_dark,'') logo_dark, IFNULL(logo_light,'') logo_light," +
" IFNULL(icp,'') icp, IFNULL(police_icp,'') police_icp, IFNULL(copyright,'') copyright," +
" IFNULL(privacy_policy,'') privacy_policy, IFNULL(terms_of_service,'') terms_of_service" +
" FROM " + table + " WHERE delete_time IS NULL ORDER BY id LIMIT 1").QueryRows(&rows); err != nil {
return
}
if len(rows) == 0 {
return
}
old := rows[0]
current := GetPlatformSiteTagMap()
pending := map[string]string{
"sitename": old.Sitename,
"logo": old.LogoDark,
"logow": old.LogoLight,
"icp": old.Icp,
"policeicp": old.PoliceIcp,
"copyright": old.Copyright,
"privacy": old.Privacy,
"terms": old.Terms,
}
for tag, val := range pending {
if val == "" || strings.TrimSpace(current[tag]) != "" {
continue
}
current[tag] = val
}
_ = SavePlatformSiteSettings(pending)
}
-31
View File
@@ -1,7 +1,6 @@
package models
import (
"sync"
"time"
)
@@ -48,34 +47,4 @@ const (
FileScopeUser = "user" // 用户个人文件
)
var systemFileStorageColumnsOnce sync.Once
// EnsureSystemFileStorageColumns 为附件表补齐文件存储分层改造所需字段。
// 历史库可能早于该特性建表,字段/索引已存在时 MySQL 报 duplicate,统一忽略,不阻断主流程。
// 写入 yz_system_files 之前必须调用,否则 INSERT 会因 Unknown column 失败。
func EnsureSystemFileStorageColumns() {
if Orm == nil {
return
}
systemFileStorageColumnsOnce.Do(func() {
alters := []string{
"ALTER TABLE yz_system_files ADD COLUMN source varchar(16) NOT NULL DEFAULT 'backend' COMMENT '来源端: backend-租户后台 platform-平台端'",
"ALTER TABLE yz_system_files ADD COLUMN scope varchar(16) NOT NULL DEFAULT 'tenant' COMMENT '归属: tenant-租户共享 user-用户个人'",
"ALTER TABLE yz_system_files ADD COLUMN storage varchar(16) NOT NULL DEFAULT '' COMMENT '存储类型: local/qiniu'",
"ALTER TABLE yz_system_files ADD COLUMN object_key varchar(512) NOT NULL DEFAULT '' COMMENT '存储相对路径(不含域名)'",
}
for _, sql := range alters {
_, _ = Orm.Raw(sql).Exec()
}
// 索引:MD5 精确去重 + 归属列表过滤
// md5 在部分历史库中是 TEXT 类型,MySQL 不允许对 TEXT/BLOB 建整列索引,
// 故使用前缀长度 md5(32)(MD5 十六进制串固定 32 字符)。
indexes := []string{
"ALTER TABLE yz_system_files ADD KEY idx_file_dedup (source, scope, tid, tuid, md5(32))",
"ALTER TABLE yz_system_files ADD KEY idx_file_owner (source, tid, scope, tuid, delete_time)",
}
for _, sql := range indexes {
_, _ = Orm.Raw(sql).Exec()
}
})
}
+25 -16
View File
@@ -1,24 +1,33 @@
package models
import "time"
import (
"time"
)
// SystemReminderList 站内信消息列表表 yz_system_reminderlist
type SystemReminderList struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Title string `orm:"column(title);size(255)" json:"title"`
Content string `orm:"column(content);type(text)" json:"content"`
SenderID uint64 `orm:"column(sender_id);default(0)" json:"sender_id"`
SenderType string `orm:"column(sender_type);size(32);default('system')" json:"sender_type"` // system, platform, tenant
ReceiverID uint64 `orm:"column(receiver_id)" json:"receiver_id"`
ReceiverType string `orm:"column(receiver_type);size(32)" json:"receiver_type"` // platform, tenant
IsRead int8 `orm:"column(is_read);default(0)" json:"is_read"` // 0-未读, 1-已读
ReadTime *time.Time `orm:"column(read_time);type(datetime);null" json:"read_time"`
CreateTime *time.Time `orm:"column(create_time);type(datetime);null" json:"create_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
BatchID string `orm:"column(batch_id);size(64);default('')" json:"batch_id"`
TargetType string `orm:"column(target_type);size(32);default('')" json:"target_type"`
TargetRoleID uint64 `orm:"column(target_role_id);default(0)" json:"target_role_id"`
TargetTenantID uint64 `orm:"column(target_tenant_id);default(0)" json:"target_tenant_id"`
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Title string `orm:"column(title);size(255)" json:"title"`
Content string `orm:"column(content);type(text)" json:"content"`
SenderID uint64 `orm:"column(sender_id);default(0)" json:"sender_id"`
SenderType string `orm:"column(sender_type);size(32);default('system')" json:"sender_type"` // system, platform, tenant
ReceiverID uint64 `orm:"column(receiver_id)" json:"receiver_id"`
ReceiverType string `orm:"column(receiver_type);size(32)" json:"receiver_type"` // platform, tenant
IsRead int8 `orm:"column(is_read);default(0)" json:"is_read"` // 0-未读, 1-已读
ReadTime *time.Time `orm:"column(read_time);type(datetime);null" json:"read_time"`
CreateTime *time.Time `orm:"column(create_time);type(datetime);null" json:"create_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
BatchID string `orm:"column(batch_id);size(64);default('')" json:"batch_id"`
// TargetType 接收目标类型:
// all 全平台 / platform 平台端 / tenant_all 租户端(所有租户) /
// tenant_role 租户角色 / tenant 租户 / tenant_user 租户用户
// 兼容旧数据:role 平台角色
TargetType string `orm:"column(target_type);size(32);default('')" json:"target_type"`
// TargetIDs 多选接收目标ID列表(JSON 数组字符串),按 TargetType 解释:
// tenant_role=角色ID列表, tenant=租户ID列表, tenant_user=用户ID列表
TargetIDs *string `orm:"column(target_ids);type(text);null" json:"target_ids"`
TargetRoleID uint64 `orm:"column(target_role_id);default(0)" json:"target_role_id"` // 兼容旧数据:平台角色ID
TargetTenantID uint64 `orm:"column(target_tenant_id);default(0)" json:"target_tenant_id"` // 兼容旧数据:特定租户ID
}
func (m *SystemReminderList) TableName() string {
-31
View File
@@ -1,7 +1,6 @@
package models
import (
"sync"
"time"
)
@@ -32,34 +31,4 @@ func (m *SystemTenantUser) TableName() string {
return "yz_system_tenant_user"
}
var tenantUserGroupColOnce sync.Once
// EnsureTenantUserGroupColumn 为租户用户表补齐 group_id(角色)列。
// 历史库可能早于"用户-角色"特性建表,字段已存在时 MySQL 报 duplicate column,统一忽略。
func EnsureTenantUserGroupColumn() {
if Orm == nil {
return
}
tenantUserGroupColOnce.Do(func() {
_, _ = Orm.Raw(`
ALTER TABLE yz_system_tenant_user
ADD COLUMN group_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '角色ID 关联 yz_system_admin_role(cid=2)'
`).Exec()
})
}
var tenantUserOrgColOnce sync.Once
// EnsureTenantUserOrgColumn 为租户用户表补齐 org_id(所属部门)列。
// 与 group_id 同样的幂等策略:字段已存在时 MySQL 报 duplicate column,统一忽略。
func EnsureTenantUserOrgColumn() {
if Orm == nil {
return
}
tenantUserOrgColOnce.Do(func() {
_, _ = Orm.Raw(`
ALTER TABLE yz_system_tenant_user
ADD COLUMN org_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '所属组织ID 关联 yz_backend_organization(id),0-未分配'
`).Exec()
})
}