更新代码

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
+1 -6
View File
@@ -120,6 +120,7 @@ import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled, Loading, Me
import { ElMessage } from 'element-plus';
import { getMySiteReminders, readAllSiteReminders } from "@/api/sitereminder";
import MessageDetailDialog from "./MessageDetailDialog.vue";
import { formatTime } from "@/utils/datetime";
const router = useRouter();
const route = useRoute();
@@ -181,12 +182,6 @@ const loadingMessages = ref(false);
const detailVisible = ref(false);
const currentReminder = ref<any>({});
const formatTime = (timeStr: any) => {
if (!timeStr) return "-";
const date = new Date(timeStr);
return date.toLocaleString();
};
const fetchUnreadCount = async () => {
if (!authStore.token) return;
try {
@@ -32,6 +32,7 @@
import { computed, ref, watch } from "vue";
import { User, Calendar } from "@element-plus/icons-vue";
import { readSiteReminder } from "@/api/sitereminder";
import { formatTime } from "@/utils/datetime";
const props = defineProps({
modelValue: Boolean,
@@ -62,12 +63,6 @@ const getSenderName = (item: any) => {
return item.sender_type;
};
const formatTime = (timeStr: any) => {
if (!timeStr) return "-";
const date = new Date(timeStr);
return date.toLocaleString();
};
watch(() => props.modelValue, async (val) => {
if (val && props.reminder && !props.viewOnly && props.reminder.is_read === 0) {
loading.value = true;
+55
View File
@@ -0,0 +1,55 @@
/**
* 统一日期时间格式化工具(backend 端)
*
* 后端(Go)返回的时间字段为 RFC3339 / ISO 8601 字符串,
* 例如 "2026-03-31T23:34:07+08:00",直接渲染不友好。
* 页面上显示时间统一使用本工具,输出:
* formatDateTime / formatTime -> 2026-03-31 23:34:07(YYYY-MM-DD HH:mm:ss)
* formatDate / formatDateOnly -> 2026-03-31(YYYY-MM-DD)
*/
const pad = (n: number) => String(n).padStart(2, "0");
function toDate(value: any): Date | null {
if (value === null || value === undefined || value === "") return null;
if (value instanceof Date) return isNaN(value.getTime()) ? null : value;
// 兼容秒级 / 毫秒级时间戳
if (typeof value === "number") {
const d = new Date(value < 1e12 ? value * 1000 : value);
return isNaN(d.getTime()) ? null : d;
}
// "2026-03-31 23:34:07" 形式在部分浏览器解析异常,统一先替换为 ISO 形式
const str = String(value).trim().replace(" ", "T");
const d = new Date(str);
return isNaN(d.getTime()) ? null : d;
}
/** 2026-03-31 23:34:07(YYYY-MM-DD HH:mm:ss);非法值返回 "-" */
export function formatDateTime(value: any): string {
const d = toDate(value);
if (!d) return "-";
return (
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
);
}
/** 与 formatDateTime 相同(兼容旧调用名) */
export function formatTime(value: any): string {
return formatDateTime(value);
}
/** 2026-03-31(YYYY-MM-DD);非法值返回 "-";纯日期串不做时区换算 */
export function formatDate(value: any): string {
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value.trim())) {
return value.trim();
}
const d = toDate(value);
if (!d) return "-";
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** 与 formatDate 相同(兼容旧调用名) */
export function formatDateOnly(value: any): string {
return formatDate(value);
}
@@ -30,7 +30,7 @@
</span>
<span class="meta-item">
<i class="el-icon-view"></i>
创建日期:{{ model?.create_time }}
创建日期:{{ formatDateTime(model?.create_time) }}
</span>
</div>
</div>
@@ -52,6 +52,7 @@
<script lang="ts" setup>
import { ref, watch, onMounted } from "vue";
import { listCategories } from "@/api/article";
import { formatDateTime } from "@/utils/datetime";
import { ElMessage, ElMessageBox } from "element-plus";
const categoryOptions = ref([]);
@@ -117,8 +117,12 @@
width="100"
align="center"
/>
<el-table-column prop="publish_date" label="发布时间" width="160" />
<el-table-column prop="update_time" label="更新时间" width="160" />
<el-table-column prop="publish_date" label="发布时间" width="160">
<template #default="{ row }">{{ formatDate(row.publish_date) }}</template>
</el-table-column>
<el-table-column prop="update_time" label="更新时间" width="160">
<template #default="{ row }">{{ formatDateTime(row.update_time) }}</template>
</el-table-column>
<el-table-column prop="publisher" label="发布人" width="120" />
<el-table-column label="操作" width="260" fixed="right" align="center">
<template #default="{ row }">
@@ -215,6 +219,7 @@ import { ElMessage, ElMessageBox } from "element-plus";
import { Search } from "@element-plus/icons-vue";
import Edit from "./components/edit.vue";
import Preview from "./components/preview.vue";
import { formatDate, formatDateTime } from "@/utils/datetime";
import { useAuthStore } from "@/stores/auth";
import {
listArticles,
+4 -1
View File
@@ -63,7 +63,9 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="180" />
<el-table-column prop="create_time" label="创建时间" width="180">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)"
@@ -108,6 +110,7 @@ import { ref, reactive, onMounted } from "vue";
import { Plus, Refresh } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus";
import Edit from "./components/edit.vue";
import { formatDateTime } from "@/utils/datetime";
import { getDemandList, deleteDemand } from "@/api/demand";
// 状态管理
+4 -1
View File
@@ -76,7 +76,9 @@
<el-tag v-else :type="sslTagType(scope.row.ssl_status)">{{ sslText(scope.row.ssl_status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="申请时间" width="170" />
<el-table-column prop="create_time" label="申请时间" width="170">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="230" fixed="right" align="center">
<template #default="scope">
<!-- 自有域名无人工审核:解析与证书就绪后自动生效,这里只提供重新检测与禁用 -->
@@ -134,6 +136,7 @@
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import { formatDateTime } from '@/utils/datetime'
import {
getTenantDomainList,
auditTenantDomain,
+4 -1
View File
@@ -49,7 +49,9 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="180" />
<el-table-column prop="create_time" label="创建时间" width="180">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right" align="center">
<template #default="scope">
<el-button size="small" text @click="handleEdit(scope.row)">
@@ -107,6 +109,7 @@
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Edit, Delete, Refresh, Switch } from '@element-plus/icons-vue'
import { formatDateTime } from '@/utils/datetime'
import {
getDomainPoolList,
createDomainPool,
@@ -81,7 +81,9 @@
/>
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="180" align="center" />
<el-table-column prop="create_time" label="创建时间" width="180" align="center">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">
@@ -130,6 +132,7 @@ import {
deleteFriendlink
} from '@/api/friendlink'
import EditDialog from './components/edit.vue'
import { formatDateTime } from '@/utils/datetime'
// @ts-ignore
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
@@ -60,7 +60,9 @@
<el-table-column prop="desc" label="产品描述" min-width="150" align="center" />
<el-table-column prop="url" label="跳转地址" min-width="150" align="center" />
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column prop="create_time" label="创建时间" width="180" align="center" />
<el-table-column prop="create_time" label="创建时间" width="180" align="center">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">
@@ -103,6 +105,7 @@ import {
deleteProducts
} from '@/api/products'
import EditDialog from './components/edit.vue'
import { formatDateTime } from '@/utils/datetime'
// @ts-ignore
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
@@ -40,7 +40,9 @@
<el-table-column prop="desc" label="分类描述" min-width="200" align="center" />
<el-table-column prop="pid" label="父级ID" width="110" align="center" />
<el-table-column prop="sort" label="排序" width="90" align="center" />
<el-table-column prop="create_time" label="创建时间" min-width="180" align="center" />
<el-table-column prop="create_time" label="创建时间" min-width="180" align="center">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">
@@ -81,6 +83,7 @@
deleteProductsTypes
} from '@/api/products'
import EditDialog from './components/edit.vue'
import { formatDateTime } from '@/utils/datetime'
// 加载状态
const loading = ref(false)
@@ -65,7 +65,9 @@
<el-table-column prop="desc" label="方案描述" min-width="150" align="center" />
<el-table-column prop="url" label="跳转地址" min-width="150" align="center" />
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column prop="create_time" label="创建时间" width="180" align="center" />
<el-table-column prop="create_time" label="创建时间" width="180" align="center">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">
@@ -108,6 +110,7 @@ import {
deleteService
} from '@/api/services'
import EditDialog from './components/edit.vue'
import { formatDateTime } from '@/utils/datetime'
// @ts-ignore
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
@@ -40,7 +40,9 @@
<el-table-column prop="desc" label="分类描述" min-width="200" align="center" />
<el-table-column prop="pid" label="父级ID" width="110" align="center" />
<el-table-column prop="sort" label="排序" width="90" align="center" />
<el-table-column prop="create_time" label="创建时间" min-width="180" align="center" />
<el-table-column prop="create_time" label="创建时间" min-width="180" align="center">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">
@@ -81,6 +83,7 @@ import {
deleteServicesTypes
} from '@/api/services'
import EditDialog from './components/edit.vue'
import { formatDateTime } from '@/utils/datetime'
// 加载状态
const loading = ref(false)
@@ -91,7 +91,7 @@
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ contact.create_time || '-' }}
{{ contact.create_time ? formatDateTime(contact.create_time) : '-' }}
</el-descriptions-item>
</el-descriptions>
</div>
@@ -108,6 +108,7 @@
<script setup>
import { computed } from 'vue'
import { formatDateTime } from '@/utils/datetime'
const props = defineProps({
modelValue: { type: Boolean, default: false },
@@ -299,6 +299,8 @@ import {
import { finishSchedule, getScheduleList, getScheduleStats } from "@/api/oaSchedule";
import ScheduleDetail from "../schedule/components/detail.vue";
import SchedulePostpone from "../schedule/components/postponeDialog.vue";
// 时间显示统一为:YYYY-MM-DD HH:mm:ss
import { formatDateTime } from "@/utils/datetime";
const emptyStat = () => ({ total: 0, paid: 0, unpaid: 0, count: 0 });
const dashboard = reactive({
@@ -607,14 +609,7 @@ const noticeDetailItem = ref(null);
const noticeTypeLabel = (type) => ({ 1: "公告", 2: "活动" }[type] || "通知");
const noticeTypeTag = (type) => ({ 1: "primary", 2: "success" }[type] || "info");
const formatDateTime = (value) => {
if (!value) return "-";
const d = new Date(value);
if (Number.isNaN(d.getTime())) return String(value);
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(
d.getHours()
)}:${pad2(d.getMinutes())}`;
};
// 列表右侧时间:今天显示 HH:mm,本年显示 M/D,跨年显示 YYYY-M-D
const noticeTime = (item) => {
@@ -297,6 +297,8 @@ import {
summarizeReceiptAmountRows
} from '../utils/invoiceAmount'
import ItemForm from './itemForm.vue'
// 时间显示统一为:YYYY-MM-DD HH:mm:ss
import { formatDateTime } from '@/utils/datetime'
const props = defineProps({
modelValue: { type: Boolean, default: false },
@@ -913,7 +915,6 @@ const removeReceipt = async receipt => {
const actionLabel = value => ({ 1: '提交申请', 2: '审批通过', 3: '审批驳回', 4: '转审', 5: '撤回' })[value] || '审批操作'
const money = value => Number(value || 0).toFixed(2)
const formatDateTime = value => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '-'
</script>
<style lang="less" scoped>
@@ -169,6 +169,8 @@ import { Plus, Refresh, Search, Setting } from "@element-plus/icons-vue";
import ReimburseEdit from "./components/edit.vue";
import ReimburseDetail from "./components/detail.vue";
import ExpenseTypeDialog from "./components/expenseTypeDialog.vue";
// 日期/时间显示统一格式:YYYY-MM-DD 与 YYYY-MM-DD HH:mm:ss
import { formatDate as formatDateUtil, formatDateTime } from "@/utils/datetime";
import { exportReimbursementPdf } from "./utils/exportReimbursementPdf";
import {
createReimbursement,
@@ -351,15 +353,11 @@ const handleExport = async (row) => {
};
const money = (value) => Number(value || 0).toFixed(2);
const formatDate = (value) => {
// 日期显示统一为:YYYY-MM-DD
const formatDate = (value: any) => {
if (!value) return "-";
const text = String(value);
const match = text.match(/^(\d{4}-\d{2}-\d{2})/);
if (match) return match[1];
return text;
return formatDateUtil(value);
};
const formatDateTime = (value) =>
value ? new Date(value).toLocaleString("zh-CN", { hour12: false }) : "-";
const statusLabel = (value) =>
statusOptions.find((item) => item.value === value)?.label || "未知";
const statusType = (value) =>
@@ -38,10 +38,10 @@
</span>
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ roleDetail.create_time }}
{{ formatDateTime(roleDetail.create_time) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ roleDetail.update_time }}
{{ formatDateTime(roleDetail.update_time) }}
</el-descriptions-item>
</el-descriptions>
@@ -56,6 +56,7 @@ import { ref, watch, computed } from "vue";
import { ElMessage } from "element-plus";
import { getRoleById } from "@/api/role";
import { getAssignableMenus } from "@/api/menu";
import { formatDateTime } from "@/utils/datetime";
interface Props {
modelValue: boolean;
@@ -19,7 +19,7 @@
<div class="filter-bar">
<el-radio-group v-model="typeFilter">
<el-radio-button :value="'all'">全部</el-radio-button>
<el-radio-button :value="2">租户角色</el-radio-button>
<el-radio-button :value="'global'">全局角色</el-radio-button>
<el-radio-button :value="'custom'">自定义角色</el-radio-button>
</el-radio-group>
</div>
@@ -41,7 +41,9 @@
/>
<el-table-column prop="cid" label="类型" width="150" align="center">
<template #default="{ row }">
<el-tag v-if="row.cid === 1" type="danger" size="small">平台角色</el-tag>
<el-tag v-if="Number(row.tenant_id || 0) === 0" type="success" size="small">
全局角色
</el-tag>
<el-tag v-else-if="Number(row.is_custom) === 1" type="warning" size="small">
自定义角色
</el-tag>
@@ -73,6 +75,7 @@
详情
</el-button>
<el-button
v-if="Number(row.tenant_id || 0) > 0"
size="small"
type="primary"
link
@@ -124,8 +127,9 @@ const loading = ref(false);
const error = ref("");
const typeFilter = ref<string | number>("all");
const displayRoles = computed(() => {
// custom = 本租户自建的自定义角色;2 = 租户角色(含系统预置)
// global = 全局角色(所有租户共用,平台端维护);custom = 本租户自建的自定义角色
if (typeFilter.value === "all") return roles.value;
if (typeFilter.value === "global") return roles.value.filter((r) => Number(r.tenant_id || 0) === 0);
if (typeFilter.value === "custom") return roles.value.filter((r) => Number(r.is_custom) === 1);
return roles.value.filter((r) => Number(r.cid) === Number(typeFilter.value));
});
@@ -36,7 +36,7 @@
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ detailData.create_time || '-' }}
{{ detailData.create_time ? formatDateTime(detailData.create_time) : '-' }}
</el-descriptions-item>
</el-descriptions>
@@ -68,6 +68,7 @@
import { ref } from "vue";
import { getTenantDetail } from "@/api/tenant";
import TenantUsersTab from "./TenantUsersTab.vue";
import { formatDateTime } from "@/utils/datetime";
const visible = ref(false);
const loading = ref(false);
@@ -90,7 +90,9 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="申请时间" width="170" />
<el-table-column prop="create_time" label="申请时间" width="170">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="230" fixed="right" align="center">
<template #default="scope">
<el-button
@@ -238,6 +240,7 @@
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Link } from "@element-plus/icons-vue";
import { formatDateTime } from "@/utils/datetime";
import {
getMyDomains,
applyTenantDomain,
@@ -48,10 +48,10 @@
{{ user.login_count || 0 }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ user.create_time || "未知" }}
{{ user.create_time ? formatDateTime(user.create_time) : "未知" }}
</el-descriptions-item>
<el-descriptions-item label="更新时间" :span="2">
{{ user.update_time || "未知" }}
{{ user.update_time ? formatDateTime(user.update_time) : "未知" }}
</el-descriptions-item>
</el-descriptions>
</div>
@@ -66,6 +66,7 @@
import { ref, watch } from "vue";
import { UserFilled } from "@element-plus/icons-vue";
import { getAllRoles } from "@/api/role";
import { formatDateTime } from "@/utils/datetime";
interface User {
id: number;
@@ -71,7 +71,7 @@
</el-select>
</el-form-item>
<!-- 角色(关联 yz_system_admin_role, cid=2) -->
<!-- 角色(关联 yz_system_admin_role, cid=2):平台默认角色 + 租户自定义角色 -->
<el-form-item label="角色">
<el-select
v-model="form.group_id"
@@ -79,12 +79,22 @@
clearable
style="width: 100%"
>
<el-option-group v-if="globalRoles.length" label="平台默认角色">
<el-option
v-for="role in roles"
v-for="role in globalRoles"
:key="role.id"
:label="role.name"
:value="role.id"
/>
</el-option-group>
<el-option-group v-if="customRoles.length" label="租户自定义角色">
<el-option
v-for="role in customRoles"
:key="role.id"
:label="role.name"
:value="role.id"
/>
</el-option-group>
</el-select>
</el-form-item>
</el-form>
@@ -142,6 +152,7 @@ interface RoleOption {
id: number;
name: string;
status?: number;
tenant_id?: number;
}
const roles = ref<RoleOption[]>([]);
@@ -155,6 +166,15 @@ const fetchRoles = async () => {
}
};
// 平台规定的默认角色(全局角色,tenant_id=0,平台端统一维护)
const globalRoles = computed(() =>
roles.value.filter((r) => Number(r.tenant_id || 0) === 0)
);
// 租户自己建立的角色(tenant_id=当前租户)
const customRoles = computed(() =>
roles.value.filter((r) => Number(r.tenant_id || 0) > 0)
);
const dialogTitle = computed(() => {
return isAdd.value ? "添加用户" : "编辑用户";
});
@@ -100,6 +100,8 @@ import { Plus, Edit, Delete } from "@element-plus/icons-vue";
import type { FormInstance, FormRules } from "element-plus";
import { getModuleCategory,editModuleCategory } from "@/api/moduleCenter";
// 该页面展示精确到秒的时间:YYYY-MM-DD HH:mm:ss
import { formatDateTime as formatDate } from "@/utils/datetime";
interface Category {
id: number;
@@ -160,16 +162,7 @@ const getStatusType = (status: number) => {
return map[status] || "info";
};
const formatDate = (date: string) => {
if (!date) return "-";
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hours = String(d.getHours()).padStart(2, "0");
const minutes = String(d.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
const handleCategoryAdd = () => {
isEdit.value = false;
+3 -10
View File
@@ -146,6 +146,8 @@ import type { FormInstance, FormRules } from "element-plus";
import { getModules, getModuleCategory,editModules } from "@/api/moduleCenter";
import CreateModules from "../components/createModules.vue";
import { uploadFile } from '@/api/file.js';
// 该页面展示精确到秒的时间:YYYY-MM-DD HH:mm:ss
import { formatDateTime as formatDate } from "@/utils/datetime";
interface Module {
id: number;
@@ -266,16 +268,7 @@ function handlePictureCardPreview(file: any) {
const formatDate = (date: string) => {
if (!date) return "-";
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hours = String(d.getHours()).padStart(2, "0");
const minutes = String(d.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
const handleCreate = () => {
createDialogVisible.value = true;
+3 -10
View File
@@ -175,6 +175,8 @@ import { Plus, View, Delete, UploadFilled, Document } from "@element-plus/icons-
import type { FormInstance, FormRules, UploadFile } from "element-plus";
import { getModules, getModuleCategory } from "@/api/moduleCenter";
// 该页面展示精确到秒的时间:YYYY-MM-DD HH:mm:ss
import { formatDateTime as formatDate } from "@/utils/datetime";
interface Publish {
id: number;
@@ -245,16 +247,7 @@ const getStatusType = (status: number) => {
return map[status] || "info";
};
const formatDate = (date: string) => {
if (!date) return "-";
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hours = String(d.getHours()).padStart(2, "0");
const minutes = String(d.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
const handlePublish = () => {
publishDialogVisible.value = true;
@@ -139,11 +139,12 @@
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { ElMessage } from 'element-plus';
import { InfoFilled, Refresh, DocumentCopy } from '@element-plus/icons-vue';
import { formatDateTime } from '@/utils/datetime';
// 系统信息数据
const loading = ref(false);
const systemInfo = ref<any>({});
const currentTime = ref(new Date().toLocaleString('zh-CN'));
const currentTime = ref(formatDateTime(new Date()));
// API基础地址
const apiBaseUrl = computed(() => {
@@ -251,7 +252,7 @@ async function fetchSystemInfo() {
// 刷新信息
function refreshInfo() {
fetchSystemInfo();
currentTime.value = new Date().toLocaleString('zh-CN');
currentTime.value = formatDateTime(new Date());
ElMessage.success('信息已刷新');
}
@@ -285,7 +286,7 @@ function copySystemInfo() {
- 屏幕分辨率:${clientInfo.value.screenResolution}
- 设备类型:${clientInfo.value.isMobile ? '移动设备' : '桌面设备'}
生成时间:${new Date().toLocaleString('zh-CN')}`;
生成时间:${formatDateTime(new Date())}`;
if (navigator.clipboard) {
navigator.clipboard.writeText(infoText).then(() => {
@@ -315,7 +316,7 @@ onMounted(() => {
fetchSystemInfo();
// 每秒更新时间
timeInterval = setInterval(() => {
currentTime.value = new Date().toLocaleString('zh-CN');
currentTime.value = formatDateTime(new Date());
}, 1000);
});
@@ -216,21 +216,13 @@
{ immediate: true }
)
// 格式化日期
// 日期显示统一为:YYYY-MM-DD HH:mm:ss
function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return ''
try {
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
} catch {
return dateStr as string
}
const d = new Date(dateStr)
if (isNaN(d.getTime())) return String(dateStr)
const p = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
}
// 获取字典类型列表(用于下拉选择)
@@ -156,21 +156,13 @@
return result
})
// 格式化日期
// 日期显示统一为:YYYY-MM-DD HH:mm:ss
function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return ''
try {
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
} catch {
return dateStr as string
}
const d = new Date(dateStr)
if (isNaN(d.getTime())) return String(dateStr)
const p = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
}
// 获取字典类型列表
+3 -14
View File
@@ -149,6 +149,7 @@ import {
} from '@/api/dict'
import DictTypeEdit from './components/DictTypeEdit.vue'
import DictItemList from './components/DictItemList.vue'
import { formatDateTime } from '@/utils/datetime'
const currentView = ref<'types' | 'items'>('types')
const typeLoading = ref(false)
@@ -172,21 +173,9 @@ const selectedDictType = computed(() => {
return dictTypes.value.find((type) => type.id === selectedDictTypeId.value) || null
})
// 格式化日期
// 日期显示统一为:YYYY-MM-DD HH:mm:ss(utils/datetime)
function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return ''
try {
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
} catch {
return dateStr as string
}
return dateStr ? formatDateTime(dateStr) : ''
}
// 获取字典类型列表
@@ -496,6 +496,8 @@ import UploadFile from "./components/uploadFile.vue";
import MoveFile from "./components/moveFile.vue";
import RenameCategory from "./components/renameCategory.vue";
import CreateCategory from "./components/createCategory.vue";
// 日期显示统一为:YYYY-MM-DD
import { formatDate } from "@/utils/datetime";
// 分组相关
interface Group {
@@ -872,14 +874,7 @@ const formatFileSize = (bytes: number): string => {
};
// 格式化日期
const formatDate = (dateStr: string): string => {
const date = new Date(dateStr);
return date.toLocaleDateString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
};
// 文件操作
const handleFileClick = (file: FileItem) => {
+4 -6
View File
@@ -124,12 +124,9 @@
show-overflow-tooltip
/>
<el-table-column prop="ip" label="IP地址" width="130" align="center" />
<el-table-column
prop="create_time"
label="登录时间"
width="180"
align="center"
/>
<el-table-column prop="create_time" label="登录时间" width="180" align="center">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="scope">
<el-button
@@ -192,6 +189,7 @@ import {
batchDeleteLoginLogs,
} from "@/api/loginLog";
import DetailDialog from "./components/detail.vue";
import { formatDateTime } from "@/utils/datetime";
interface LoginLog {
id: number;
@@ -117,12 +117,9 @@
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="create_time"
label="操作时间"
width="180"
align="center"
/>
<el-table-column prop="create_time" label="操作时间" width="180" align="center">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="scope">
<el-button
@@ -170,6 +167,7 @@ import {
getOperationStatistics,
} from "@/api/operationLog";
import DetailDialog from "./components/detail.vue";
import { formatDateTime } from "@/utils/datetime";
interface OperationLog {
id: number;
+2 -5
View File
@@ -77,6 +77,7 @@ import { ElMessage, ElMessageBox } from 'element-plus';
import { Plus, Search, Delete, MoreFilled } from '@element-plus/icons-vue';
import NoteEditor from './components/edit.vue';
import { getNotebookList, deleteNotebook } from '@/api/notebook';
import { formatDate } from '@/utils/datetime';
const searchKeyword = ref('');
const currentNoteId = ref(null);
@@ -117,11 +118,7 @@ const formatTime = (dateStr) => {
} else if (diff < 7 * day) {
return `${Math.floor(diff / day)} 天前`;
} else {
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
return formatDate(date);
}
};
+3 -10
View File
@@ -61,6 +61,8 @@ import { useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import { getUserInfo } from "@/api/user";
import { useAuthStore } from "@/stores/auth";
// 该页面展示精确到秒的时间:YYYY-MM-DD HH:mm:ss
import { formatDateTime as formatDate } from "@/utils/datetime";
import ProfileCard from "./components/ProfileCard.vue";
import QuickActions from "./components/QuickActions.vue";
import DetailCard from "./components/DetailCard.vue";
@@ -79,16 +81,7 @@ const passwordDialogVisible = ref(false);
const bindPhoneDialogVisible = ref(false);
const bindEmailDialogVisible = ref(false);
const formatDate = (date) => {
if (!date) return "-";
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hours = String(d.getHours()).padStart(2, "0");
const minutes = String(d.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
const getSexText = (sex) => {
const map = {
-1
View File
@@ -40,7 +40,6 @@ func main() {
fmt.Println("数据库未初始化")
os.Exit(1)
}
models.EnsureSystemFileStorageColumns()
storageSvc, err := services.GetStorageService()
if err != nil {
+2 -5
View File
@@ -400,15 +400,12 @@ func (c *BackendAdminUserController) AddUser() {
// 关联角色(group_id):0 表示未分配,前端展示为"未分配/全权限"
if p.GroupID != nil && *p.GroupID > 0 {
models.EnsureTenantUserGroupColumn()
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("id", id).
Update(map[string]interface{}{"group_id": *p.GroupID})
} else if isDefault == 1 {
// 租户第一个账号(普通租户)即租户管理员:自动绑定「租户管理员」角色,
// 使"普通租户"与"租户管理员"合二为一(均可登录 backend 并拥有全权限)。
if adminRoleID, err := models.GetTenantAdminRole(p.Tid); err == nil && adminRoleID > 0 {
models.EnsureTenantUserGroupColumn()
// 租户的第一个账号即租户管理员:自动绑定全局「租户管理员」角色(拥有 backend 全权限)。
if adminRoleID, err := models.GetTenantAdminRole(); err == nil && adminRoleID > 0 {
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("id", id).
Update(map[string]interface{}{"group_id": adminRoleID})
-58
View File
@@ -81,16 +81,6 @@ func (c *BackendArticleCategoryController) cmsJSONErr(httpStatus, bizCode int, m
_ = c.ServeJSON()
}
func cmsEnsureTables(c *beego.Controller) bool {
if err := models.EnsureCmsArticleTables(); err != nil {
c.Ctx.Output.SetStatus(500)
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "初始化文章表失败: " + err.Error()}
_ = c.ServeJSON()
return false
}
return true
}
func cmsPortalTid(c *beego.Controller) uint64 {
if tid, err := c.GetUint64("tid"); err == nil && tid > 0 {
return tid
@@ -179,9 +169,6 @@ func cmsCategoryToMap(row models.CmsArticleCategory) map[string]interface{} {
// ContentStats GET /backend/contentstats
func (c *BackendArticleController) ContentStats() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -261,9 +248,6 @@ func (c *BackendArticleController) ContentStats() {
// List GET /backend/articlesList
func (c *BackendArticleController) List() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -348,9 +332,6 @@ func (c *BackendArticleController) ListAll() {
// Detail GET /backend/articles/:id
func (c *BackendArticleController) Detail() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -407,9 +388,6 @@ type cmsArticlePayload struct {
// Create POST /backend/createarticle
func (c *BackendArticleController) Create() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -479,9 +457,6 @@ func (c *BackendArticleController) Create() {
// Update POST /backend/editarticle/:id
func (c *BackendArticleController) Update() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -538,9 +513,6 @@ func (c *BackendArticleController) Update() {
// Delete DELETE /backend/deletearticle/:id
func (c *BackendArticleController) Delete() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -573,9 +545,6 @@ func (c *BackendArticleController) Delete() {
}
func (c *BackendArticleController) setArticleFlag(field string, value int8) {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -609,9 +578,6 @@ func (c *BackendArticleController) setArticleFlag(field string, value int8) {
}
func (c *BackendArticleController) Publish() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -663,9 +629,6 @@ func (c *BackendArticleController) Publish() {
}
func (c *BackendArticleController) Unpublish() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -704,9 +667,6 @@ func (c *BackendArticleController) Untop() { c.setArticleFlag("top", 0) }
// List GET /backend/categories
func (c *BackendArticleCategoryController) List() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -763,9 +723,6 @@ func (c *BackendArticleCategoryController) List() {
// ListAll GET /backend/allcategories
func (c *BackendArticleCategoryController) ListAll() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -804,9 +761,6 @@ func (c *BackendArticleCategoryController) ListAll() {
// Detail GET /backend/categories/:id
func (c *BackendArticleCategoryController) Detail() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -850,9 +804,6 @@ type cmsCategoryPayload struct {
// Create POST /backend/createCategory
func (c *BackendArticleCategoryController) Create() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -904,9 +855,6 @@ func (c *BackendArticleCategoryController) Create() {
// Update POST /backend/editCategory/:id
func (c *BackendArticleCategoryController) Update() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -973,9 +921,6 @@ func (c *BackendArticleCategoryController) Update() {
// Delete DELETE /backend/categories/:id
func (c *BackendArticleCategoryController) Delete() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -1042,9 +987,6 @@ func (c *BackendArticleCategoryController) Delete() {
// UpdateStatus PATCH /backend/categories/:id/status
func (c *BackendArticleCategoryController) UpdateStatus() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -68,6 +68,7 @@ func crmIsSystemCategory(row *models.TenantCrmProductCategory) bool {
}
// crmEnsureDefaultProductCategories 确保租户存在系统默认分类(未分类 / 软件开发 / 软件产品 / 硬件产品):
// 【租户级初始化】新租户首次使用时补建默认业务数据(非表结构/全局预置数据;结构变更须走 docs/sql/ SQL 文件)。
// - 不存在(含曾被软删除)时创建 / 恢复,保证其始终可用(复用软删记录,避免唯一键冲突);
// - 已存在同名分类(租户此前自建)时升级为系统默认,并修复默认标记、状态与内置排序(历史数据升级自修复)。
func crmEnsureDefaultProductCategories(tenantID string) {
+1
View File
@@ -62,6 +62,7 @@ var crmDefaultUnits = []struct {
}
// crmEnsureDefaultUnits 租户首次使用(从未有过任何单位记录,含已软删除的)时写入默认单位。
// 【租户级初始化】新租户首次使用时补建默认业务数据(非表结构/全局预置数据;结构变更须走 docs/sql/ SQL 文件)。
// 用户手动清空后不再自动补,避免越删越有;需要恢复时用「恢复默认单位」接口。
// 已有记录时顺带修复存量默认单位的 is_default 标记(历史数据升级)。
func crmEnsureDefaultUnits(tenantID string) {
-5
View File
@@ -79,11 +79,6 @@ func (c *BackendFileController) effectiveTid(claims *jwtutil.Claims) uint64 {
return 0
}
// Prepare 所有接口执行前补齐文件存储分层所需字段,避免历史库缺列导致查询/写入报错
func (c *BackendFileController) Prepare() {
models.EnsureSystemFileStorageColumns()
}
// resolveTuid 解析归属用户 ID,返回 0 表示租户共享文件。
//
// 取值顺序:
-3
View File
@@ -77,9 +77,6 @@ func (c *BackendMenuController) GetMenu() {
}
func (c *BackendMenuController) GetBackendMenu() {
// 确保租户用户 group_id 列存在(老库兼容)
models.EnsureTenantUserGroupColumn()
var menus []models.SystemMenu
_, err := models.Orm.QueryTable(new(models.SystemMenu)).Filter("status", 1).All(&menus)
if err != nil {
-3
View File
@@ -49,9 +49,6 @@ func (c *BackendMenuFrontController) List() {
return
}
// 保证全局默认导航(tenant_id=0)齐全,缺失自动补齐
models.EnsureGlobalDefaultFrontMenus()
// 全局默认菜单(tenant_id=0,不可删改)+ 租户自定义菜单
var menus []models.BackendMenuFront
_, err := models.Orm.QueryTable("yz_backend_menu_front").
-6
View File
@@ -7,7 +7,6 @@ import (
"strconv"
"strings"
"server/models"
"server/pkg/jwtutil"
"server/services"
@@ -27,11 +26,6 @@ type BackendOaDocumentController struct {
beego.Controller
}
// Prepare 所有接口执行前确保文档相关表存在。
func (c *BackendOaDocumentController) Prepare() {
_ = models.EnsureOaDocumentTables()
}
func (c *BackendOaDocumentController) oaDocClaims() (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
-5
View File
@@ -21,11 +21,6 @@ type BackendOaNoticeController struct {
beego.Controller
}
// Prepare 所有接口执行前确保通知公告表存在。
func (c *BackendOaNoticeController) Prepare() {
_ = models.EnsureOaNoticeTable()
}
func (c *BackendOaNoticeController) oaNoticeClaims() (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
-5
View File
@@ -53,11 +53,6 @@ func (c *BackendOaScheduleController) oasOk(data interface{}) {
_ = c.ServeJSON()
}
// Prepare 所有日程接口执行前补齐顺延所需字段,避免历史库缺列导致查询报错。
func (c *BackendOaScheduleController) Prepare() {
_ = models.EnsureOaScheduleCarryColumns()
}
var (
oasDatePattern = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
oasTimePattern = regexp.MustCompile(`^\d{2}:\d{2}$`)
-34
View File
@@ -43,16 +43,6 @@ func (c *BackendProductCategoryController) cmsJSONErr(httpStatus, bizCode int, m
_ = c.ServeJSON()
}
func cmsEnsureProductTables(c *beego.Controller) bool {
if err := models.EnsureCmsProductTables(); err != nil {
c.Ctx.Output.SetStatus(500)
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "初始化产品表失败: " + err.Error()}
_ = c.ServeJSON()
return false
}
return true
}
func cmsProductToMap(row models.CmsProduct) map[string]interface{} {
return map[string]interface{}{
"id": row.ID,
@@ -85,9 +75,6 @@ func cmsProductCateToMap(row models.CmsProductCategory) map[string]interface{} {
// List GET /backend/productsList
func (c *BackendProductController) List() {
if !cmsEnsureProductTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -153,9 +140,6 @@ type cmsProductPayload struct {
// Create POST /backend/addProducts
func (c *BackendProductController) Create() {
if !cmsEnsureProductTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -208,9 +192,6 @@ func (c *BackendProductController) Create() {
// Update PUT /backend/editProducts/:id
func (c *BackendProductController) Update() {
if !cmsEnsureProductTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -269,9 +250,6 @@ func (c *BackendProductController) Update() {
// Delete DELETE /backend/deleteProducts/:id
func (c *BackendProductController) Delete() {
if !cmsEnsureProductTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -305,9 +283,6 @@ func (c *BackendProductController) Delete() {
// List GET /backend/productsTypesList
func (c *BackendProductCategoryController) List() {
if !cmsEnsureProductTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -367,9 +342,6 @@ type cmsProductCategoryPayload struct {
// Create POST /backend/addProductsTypes
func (c *BackendProductCategoryController) Create() {
if !cmsEnsureProductTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -420,9 +392,6 @@ func (c *BackendProductCategoryController) Create() {
// Update PUT /backend/editProductsTypes/:id
func (c *BackendProductCategoryController) Update() {
if !cmsEnsureProductTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -482,9 +451,6 @@ func (c *BackendProductCategoryController) Update() {
// Delete DELETE /backend/deleteProductsTypes/:id
func (c *BackendProductCategoryController) Delete() {
if !cmsEnsureProductTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
+64 -7
View File
@@ -145,21 +145,20 @@ func (c *BackendRoleController) currentTenantID() (uint64, bool) {
return uint64(claims.TenantId), true
}
// GetAllRoles 获取当前租户的角色列表
// GetAllRoles 获取当前租户可见的角色列表
// GET /backend/allRoles
// 返回「全局角色(cid=2, tenant_id=0,所有租户共用)」+「本租户自建角色」;
// 全局角色由平台端统一维护,租户端只读。
func (c *BackendRoleController) GetAllRoles() {
tid, ok := c.currentTenantID()
if !ok {
return
}
// 懒加载补齐当前租户的默认角色(租户管理员/部门负责人/员工)
models.EnsureDefaultTenantRoles(tid)
var rows []models.AdminRole
_, err := models.Orm.QueryTable(new(models.AdminRole)).
Filter("cid", 2).
Filter("tenant_id", tid).
OrderBy("-id").
Filter("tenant_id__in", []uint64{0, tid}).
OrderBy("tenant_id", "is_custom", "id").
All(&rows)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "查询失败"}
@@ -188,7 +187,7 @@ func (c *BackendRoleController) GetRoleByID() {
if err := models.Orm.QueryTable(new(models.AdminRole)).
Filter("id", id).
Filter("cid", 2).
Filter("tenant_id", tid).
Filter("tenant_id__in", []uint64{0, tid}).
One(&role); err != nil {
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "角色不存在"}
_ = c.ServeJSON()
@@ -218,6 +217,22 @@ func (c *BackendRoleController) CreateRole() {
_ = c.ServeJSON()
return
}
// 不能与本租户可见的角色重名(含平台默认的全局角色),避免混淆
dup, derr := models.Orm.QueryTable(new(models.AdminRole)).
Filter("cid", 2).
Filter("tenant_id__in", []uint64{0, tid}).
Filter("name", p.Name).
Count()
if derr != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "创建失败"}
_ = c.ServeJSON()
return
}
if dup > 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "已存在同名角色(含平台默认角色),请更换名称"}
_ = c.ServeJSON()
return
}
status := uint8(1)
if p.Status != nil {
@@ -282,6 +297,37 @@ func (c *BackendRoleController) UpdateRole() {
return
}
// 全局角色(tenant_id=0)由平台端统一维护,租户端只读
var target models.AdminRole
if e := models.Orm.QueryTable(new(models.AdminRole)).
Filter("id", id).
Filter("cid", 2).
One(&target); e == nil && target.TenantID == 0 {
c.Data["json"] = map[string]interface{}{"code": 403, "msg": "全局角色由平台端统一维护,租户端不能修改"}
_ = c.ServeJSON()
return
}
// 改名时不能与可见角色重名(含平台默认的全局角色,排除自身)
if newName, ok := update["name"]; ok {
dup, derr := models.Orm.QueryTable(new(models.AdminRole)).
Filter("cid", 2).
Filter("tenant_id__in", []uint64{0, tid}).
Filter("name", newName).
Exclude("id", id).
Count()
if derr != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "更新失败"}
_ = c.ServeJSON()
return
}
if dup > 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "已存在同名角色(含平台默认角色),请更换名称"}
_ = c.ServeJSON()
return
}
}
// 仅允许更新本租户名下的角色,防止越权改到其他租户
cnt, err := models.Orm.QueryTable(new(models.AdminRole)).
Filter("id", id).
@@ -316,6 +362,17 @@ func (c *BackendRoleController) DeleteRole() {
_ = c.ServeJSON()
return
}
// 全局角色(tenant_id=0)由平台端统一维护,租户端不能删除
var target models.AdminRole
if e := models.Orm.QueryTable(new(models.AdminRole)).
Filter("id", id).
Filter("cid", 2).
One(&target); e == nil && target.TenantID == 0 {
c.Data["json"] = map[string]interface{}{"code": 403, "msg": "全局角色由平台端统一维护,租户端不能删除"}
_ = c.ServeJSON()
return
}
// 仅允许删除本租户名下的角色
cnt, err := models.Orm.QueryTable(new(models.AdminRole)).
Filter("id", id).
-26
View File
@@ -311,35 +311,12 @@ func (c *BackendSiteSettingsController) resolveBackendTenantID(claims *jwtutil.C
return tid
}
func (c *BackendSiteSettingsController) ensureBackendSettingItemsTable() error {
_, err := models.Orm.Raw(`
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='租户站点扩展设置';
`).Exec()
return err
}
func (c *BackendSiteSettingsController) getBackendSettingItems(tid uint64, keys []string) (map[string]string, error) {
out := make(map[string]string, len(keys))
for _, key := range keys {
out[key] = ""
}
if err := c.ensureBackendSettingItemsTable(); err != nil {
return out, err
}
type rowItem struct {
SettingKey string
SettingValue string
@@ -360,9 +337,6 @@ func (c *BackendSiteSettingsController) getBackendSettingItems(tid uint64, keys
}
func (c *BackendSiteSettingsController) saveBackendSettingItems(tid uint64, values map[string]string) error {
if err := c.ensureBackendSettingItemsTable(); err != nil {
return err
}
for key, value := range values {
_, err := models.Orm.Raw(`
INSERT INTO yz_system_tenant_setting_items (tid, setting_key, setting_value, create_time, update_time)
-34
View File
@@ -43,16 +43,6 @@ func (c *BackendSolutionCategoryController) cmsJSONErr(httpStatus, bizCode int,
_ = c.ServeJSON()
}
func cmsEnsureSolutionTables(c *beego.Controller) bool {
if err := models.EnsureCmsSolutionTables(); err != nil {
c.Ctx.Output.SetStatus(500)
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "初始化解决方案表失败: " + err.Error()}
_ = c.ServeJSON()
return false
}
return true
}
func cmsSolutionToMap(row models.CmsSolution) map[string]interface{} {
return map[string]interface{}{
"id": row.ID,
@@ -85,9 +75,6 @@ func cmsSolutionCateToMap(row models.CmsSolutionCategory) map[string]interface{}
// List GET /backend/servicesList
func (c *BackendSolutionController) List() {
if !cmsEnsureSolutionTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -153,9 +140,6 @@ type cmsSolutionPayload struct {
// Create POST /backend/addServices
func (c *BackendSolutionController) Create() {
if !cmsEnsureSolutionTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -208,9 +192,6 @@ func (c *BackendSolutionController) Create() {
// Update PUT /backend/editServices/:id
func (c *BackendSolutionController) Update() {
if !cmsEnsureSolutionTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -269,9 +250,6 @@ func (c *BackendSolutionController) Update() {
// Delete DELETE /backend/deleteServices/:id
func (c *BackendSolutionController) Delete() {
if !cmsEnsureSolutionTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -305,9 +283,6 @@ func (c *BackendSolutionController) Delete() {
// List GET /backend/servicesTypesList
func (c *BackendSolutionCategoryController) List() {
if !cmsEnsureSolutionTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -367,9 +342,6 @@ type cmsSolutionCategoryPayload struct {
// Create POST /backend/addServicesTypes
func (c *BackendSolutionCategoryController) Create() {
if !cmsEnsureSolutionTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -420,9 +392,6 @@ func (c *BackendSolutionCategoryController) Create() {
// Update PUT /backend/editServicesTypes/:id
func (c *BackendSolutionCategoryController) Update() {
if !cmsEnsureSolutionTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
@@ -482,9 +451,6 @@ func (c *BackendSolutionCategoryController) Update() {
// Delete DELETE /backend/deleteServicesTypes/:id
func (c *BackendSolutionCategoryController) Delete() {
if !cmsEnsureSolutionTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
-7
View File
@@ -265,7 +265,6 @@ func (c *IndexPortalController) GetFooterData() {
// GetNewsCenterTop4 GET /index/newscentertop4
func (c *IndexPortalController) GetNewsCenterTop4() {
tid := c.getTidByHost()
_ = models.EnsureCmsArticleTables()
var articles []models.CmsArticle
_, err := models.Orm.QueryTable(new(models.CmsArticle)).
@@ -293,7 +292,6 @@ func (c *IndexPortalController) GetNewsCenterTop4() {
// queryCmsArticles 统一的分页查询文章辅助方法
func (c *IndexPortalController) queryCmsArticles(categoryName string, categoryID int64) {
tid := c.getTidByHost()
_ = models.EnsureCmsArticleTables()
defaultLimit := templateNewsRow(tid)
@@ -366,7 +364,6 @@ func (c *IndexPortalController) GetTechnologyCenter() {
// GetTechnologyCategories GET /index/technologyCategories
func (c *IndexPortalController) GetTechnologyCategories() {
tid := c.getTidByHost()
_ = models.EnsureCmsArticleTables()
var categories []models.CmsArticleCategory
_, err := models.Orm.QueryTable(new(models.CmsArticleCategory)).
@@ -405,7 +402,6 @@ func (c *IndexPortalController) getArticleDetail() {
idStr := c.Ctx.Input.Param(":id")
id, _ := strconv.ParseUint(idStr, 10, 64)
tid := c.getTidByHost()
_ = models.EnsureCmsArticleTables()
var article models.CmsArticle
err := models.Orm.QueryTable(new(models.CmsArticle)).
@@ -433,7 +429,6 @@ func (c *IndexPortalController) AddLike() {
idStr := c.Ctx.Input.Param(":id")
id, _ := strconv.ParseUint(idStr, 10, 64)
tid := c.getTidByHost()
_ = models.EnsureCmsArticleTables()
_, _ = models.Orm.QueryTable(new(models.CmsArticle)).
Filter("id", id).
@@ -451,7 +446,6 @@ func (c *IndexPortalController) ReduceLike() {
idStr := c.Ctx.Input.Param(":id")
id, _ := strconv.ParseUint(idStr, 10, 64)
tid := c.getTidByHost()
_ = models.EnsureCmsArticleTables()
_, _ = models.Orm.QueryTable(new(models.CmsArticle)).
Filter("id", id).
@@ -470,7 +464,6 @@ func (c *IndexPortalController) AddViews() {
idStr := c.Ctx.Input.Param(":id")
id, _ := strconv.ParseUint(idStr, 10, 64)
tid := c.getTidByHost()
_ = models.EnsureCmsArticleTables()
_, _ = models.Orm.QueryTable(new(models.CmsArticle)).
Filter("id", id).
-5
View File
@@ -78,11 +78,6 @@ func (c *PlatformFileController) effectiveTid(claims *jwtutil.Claims) uint64 {
return 0
}
// Prepare 所有接口执行前补齐文件存储分层所需字段,避免历史库缺列导致查询/写入报错
func (c *PlatformFileController) Prepare() {
models.EnsureSystemFileStorageColumns()
}
func (c *PlatformFileController) jsonErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
+36 -8
View File
@@ -36,6 +36,30 @@ func (c *PlatformSiteReminderController) platformClaims() (*jwtutil.Claims, erro
return claims, nil
}
// mergeReminderTargetIDs 合并多选目标ID与旧版单值入参。
// 旧版:role → target_role_id,tenant → target_tenant_id;多选参数优先。
func mergeReminderTargetIDs(targetType string, targetIDs []uint64, legacyRoleID, legacyTenantID uint64) []uint64 {
ids := make([]uint64, 0, len(targetIDs)+1)
for _, id := range targetIDs {
if id > 0 {
ids = append(ids, id)
}
}
if len(ids) == 0 {
switch targetType {
case "role":
if legacyRoleID > 0 {
ids = append(ids, legacyRoleID)
}
case "tenant":
if legacyTenantID > 0 {
ids = append(ids, legacyTenantID)
}
}
}
return ids
}
func (c *PlatformSiteReminderController) jsonErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
@@ -99,9 +123,10 @@ func (c *PlatformSiteReminderController) Send() {
var p struct {
Title string `json:"title"`
Content string `json:"content"`
TargetType string `json:"target_type"` // platform, tenant_all, role, tenant
TargetRoleID uint64 `json:"target_role_id"`
TargetTenantID uint64 `json:"target_tenant_id"`
TargetType string `json:"target_type"` // all, platform, tenant_all, tenant_role, tenant, tenant_user
TargetIDs []uint64 `json:"target_ids"`
TargetRoleID uint64 `json:"target_role_id"` // 兼容旧入参
TargetTenantID uint64 `json:"target_tenant_id"` // 兼容旧入参
}
if err := json.Unmarshal(raw, &p); err != nil {
c.jsonErr(400, 400, "参数错误")
@@ -118,7 +143,8 @@ func (c *PlatformSiteReminderController) Send() {
return
}
err = services.SendSiteReminder(p.Title, p.Content, uint64(claims.UserID), "platform", p.TargetType, p.TargetRoleID, p.TargetTenantID)
targetIDs := mergeReminderTargetIDs(p.TargetType, p.TargetIDs, p.TargetRoleID, p.TargetTenantID)
err = services.SendSiteReminder(p.Title, p.Content, uint64(claims.UserID), "platform", p.TargetType, targetIDs)
if err != nil {
c.jsonErr(500, 500, "发送失败: "+err.Error())
return
@@ -278,9 +304,10 @@ func (c *PlatformSiteReminderController) UpdateSent() {
BatchID string `json:"batch_id"`
Title string `json:"title"`
Content string `json:"content"`
TargetType string `json:"target_type"`
TargetRoleID uint64 `json:"target_role_id"`
TargetTenantID uint64 `json:"target_tenant_id"`
TargetType string `json:"target_type"` // all, platform, tenant_all, tenant_role, tenant, tenant_user
TargetIDs []uint64 `json:"target_ids"`
TargetRoleID uint64 `json:"target_role_id"` // 兼容旧入参
TargetTenantID uint64 `json:"target_tenant_id"` // 兼容旧入参
}
if err := json.Unmarshal(raw, &p); err != nil {
c.jsonErr(400, 400, "参数错误")
@@ -297,7 +324,8 @@ func (c *PlatformSiteReminderController) UpdateSent() {
return
}
err = services.UpdateSentReminder(p.BatchID, p.Title, p.Content, p.TargetType, p.TargetRoleID, p.TargetTenantID)
targetIDs := mergeReminderTargetIDs(p.TargetType, p.TargetIDs, p.TargetRoleID, p.TargetTenantID)
err = services.UpdateSentReminder(p.BatchID, p.Title, p.Content, p.TargetType, targetIDs)
if err != nil {
c.jsonErr(500, 500, "修改失败: "+err.Error())
return
+2 -4
View File
@@ -208,10 +208,8 @@ func (c *PlatformTenantUserController) CreateTenantUser() {
}
if isDefault == 1 {
_ = services.SetDefaultTenant(p.Uid, p.Tid)
// 租户第一个账号(普通租户)即租户管理员:自动绑定「租户管理员」角色,
// 使"普通租户"与"租户管理员"合二为一(均可登录 backend 并拥有全权限)。
if adminRoleID, err := models.GetTenantAdminRole(p.Tid); err == nil && adminRoleID > 0 {
models.EnsureTenantUserGroupColumn()
// 租户的第一个账号即租户管理员:自动绑定全局「租户管理员」角色(拥有 backend 全权限)。
if adminRoleID, err := models.GetTenantAdminRole(); err == nil && adminRoleID > 0 {
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("id", id).
Update(map[string]interface{}{"group_id": adminRoleID})
+144
View File
@@ -0,0 +1,144 @@
-- =============================================================
-- 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;
+9 -4
View File
@@ -10,12 +10,17 @@
-- =============================================================
-- 1) 项目:结束日期索引(逾期判定 + 按结束日期排序)
ALTER TABLE `yz_backend_crm_project`
ADD INDEX `idx_end_date` (`end_date`);
-- 可重复执行:索引不存在时才执行 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) 合同:到期日期索引(逾期判定 + 按到期日期排序)
ALTER TABLE `yz_backend_crm_contract`
ADD INDEX `idx_expire_date` (`expire_date`);
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),无法建单列索引。
-- 如需按「逾期回款明细」做服务端统计,建议将回款明细拆为独立子表
@@ -7,6 +7,15 @@
-- =============================================================
-- 回款计划表:流水回执地址 + 文件名(均允许为空)
ALTER TABLE `yz_backend_crm_payback`
ADD COLUMN `receipt_url` VARCHAR(512) NULL COMMENT '流水回执文件地址(选填)' AFTER `remark`,
ADD COLUMN `receipt_name` VARCHAR(255) NULL COMMENT '流水回执文件名(选填)' AFTER `receipt_url`;
-- 可重复执行:列不存在时才执行 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;
@@ -6,15 +6,15 @@
-- dev_mode varchar(20) NOT NULL DEFAULT 'self'
-- 'self' -> developed in-house (default)
-- 'outsource' -> outsourced / developed by others for us
-- Note: the server also auto-adds this column on startup
-- (models.EnsureCrmProductDevColumns); this file is only
-- for manual repair. Re-running reports
-- 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.
-- =============================================================
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`;
-- 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;
+17 -10
View File
@@ -6,20 +6,23 @@
-- 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 (fixed by the server-side
-- migration EnsureErpCompanyContactMobilesColumn on startup).
-- 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
-- (re-running only reports "Duplicate column name", which is safe)
-- (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
ALTER TABLE `yz_backend_crm_entity_contact`
ADD COLUMN `mobiles` TEXT NULL COMMENT 'phones in JSON array, max 5' AFTER `mobile`;
-- 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`
@@ -28,8 +31,10 @@ UPDATE `yz_backend_crm_entity_contact`
AND `mobile` IS NOT NULL AND `mobile` <> '';
-- 2) global address book contacts
ALTER TABLE `yz_backend_erp_contact`
ADD COLUMN `mobiles` TEXT NULL COMMENT 'phones in JSON array, max 5' AFTER `phone`;
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`)
@@ -38,9 +43,11 @@ UPDATE `yz_backend_erp_contact`
-- 3) company contacts (customers / suppliers) - shared by project contact
-- picker / address book / customer-supplier conversion.
-- NOTE: also auto-added on server startup (EnsureErpCompanyContactMobilesColumn).
ALTER TABLE `yz_backend_contact_company`
ADD COLUMN `mobiles` TEXT NULL COMMENT 'phones in JSON array, max 5' AFTER `phone`;
-- 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`)
@@ -12,8 +12,11 @@
-- (re-running only reports "Duplicate column name", which is safe)
-- =============================================================
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`;
-- 可重复执行:列不存在时才执行 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`
+5 -2
View File
@@ -10,8 +10,11 @@
-- (re-running only reports "Duplicate column name", which is safe)
-- =============================================================
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`;
-- 可重复执行:列不存在时才执行 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`
@@ -0,0 +1,48 @@
-- =============================================================
-- 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;
+11 -4
View File
@@ -9,8 +9,15 @@
-- (re-running only reports "Duplicate column name", which is safe)
-- =============================================================
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`;
-- 可重复执行:列不存在时才执行 ALTER,已存在自动跳过(不再报 1060 Duplicate column)。
ALTER TABLE `yz_backend_crm_payback_record`
ADD COLUMN `item_name` VARCHAR(50) NULL COMMENT 'plan item name snapshot' AFTER `item_seq`;
-- 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;
+11 -4
View File
@@ -14,8 +14,15 @@
-- NOTE: keep this file ASCII-only; piping through some shells mangles non-ASCII.
-- =============================================================
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`;
-- 可重复执行:列不存在时才执行 ALTER,已存在自动跳过(不再报 1060 Duplicate column)。
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`;
-- 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;
@@ -0,0 +1,21 @@
-- =============================================================
-- 站内信消息表补齐「多选接收目标」字段: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;
@@ -0,0 +1,20 @@
-- =============================================================
-- 角色表补齐列: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;
@@ -0,0 +1,51 @@
-- =============================================================
-- 附件表补齐「文件存储分层」改造字段与索引: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;
@@ -0,0 +1,20 @@
-- =============================================================
-- 租户用户表补齐列: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;
@@ -0,0 +1,47 @@
-- =============================================================
-- 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合同表';
@@ -0,0 +1,62 @@
-- =============================================================
-- 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回款记录表(回款进度流水)';
+49
View File
@@ -0,0 +1,49 @@
-- =============================================================
-- 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
@@ -0,0 +1,39 @@
-- =============================================================
-- 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
@@ -0,0 +1,46 @@
-- =============================================================
-- 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
@@ -0,0 +1,133 @@
-- =============================================================
-- 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
@@ -0,0 +1,31 @@
-- =============================================================
-- 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通知公告表';
+2 -1
View File
@@ -1,7 +1,8 @@
-- 平台官网站点设置表 yz_platform_site_setting(通用键值结构)
-- 一行一个设置项:title 名称 / value 值 / tag 标签名 / remark 说明。
-- 模板里用 {yz:tag} 输出对应 value;新增设置项只需插一行,无需加字段。
-- 服务端启动时会自动建表并补齐默认项(models.EnsurePlatformSiteSettingTable)。
-- 表结构需手动执行本文件(服务端已取消自动建表);
-- 建表后,已登记的默认设置项会由服务端启动时自动补齐(纯数据初始化)。
-- 已登记的默认设置项:
-- sitename 站点名称
-- subtitle 副标题
@@ -0,0 +1,20 @@
-- =============================================================
-- 租户站点扩展设置表: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
@@ -0,0 +1,29 @@
-- =============================================================
-- 官网前端导航:全局默认菜单(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;
+67
View File
@@ -0,0 +1,67 @@
-- =============================================================
-- 角色体系整理:命名规范 + 全局角色统一
-- 表: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 -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()
}
})
}
+12 -3
View File
@@ -1,6 +1,8 @@
package models
import "time"
import (
"time"
)
// SystemReminderList 站内信消息列表表 yz_system_reminderlist
type SystemReminderList struct {
@@ -16,9 +18,16 @@ type SystemReminderList struct {
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"`
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"`
// 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()
})
}
-6
View File
@@ -162,8 +162,6 @@ func queryTenantSettingItems(tid uint64, keys ...string) (map[string]string, err
// navProvider 导航菜单:一级菜单循环,[field:children/] 输出子菜单 HTML。
// 一级菜单含全局默认(tenant_id=0,不可删)与租户自定义;子菜单仅租户自有。
func navProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([]map[string]string, error) {
models.EnsureGlobalDefaultFrontMenus()
var rows []models.BackendMenuFront
_, err := models.Orm.QueryTable(new(models.BackendMenuFront)).
Filter("tenant_id__in", []uint64{0, tid}).
@@ -268,7 +266,6 @@ func friendlinkProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([
// productlistProvider 产品列表循环,支持 row/titlelen 参数
func productlistProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([]map[string]string, error) {
_ = models.EnsureCmsProductTables()
row := atoiDefault(params["row"], 10)
if row <= 0 || row > 50 {
row = 10
@@ -307,7 +304,6 @@ func productlistProvider(tid uint64, params map[string]string, ctx *RenderCtx) (
// producttypeProvider 产品分类循环,支持 row 参数
func producttypeProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([]map[string]string, error) {
_ = models.EnsureCmsProductTables()
row := atoiDefault(params["row"], 20)
if row <= 0 || row > 50 {
row = 20
@@ -338,7 +334,6 @@ func producttypeProvider(tid uint64, params map[string]string, ctx *RenderCtx) (
// solutionlistProvider 解决方案列表循环,支持 row/titlelen 参数
func solutionlistProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([]map[string]string, error) {
_ = models.EnsureCmsSolutionTables()
row := atoiDefault(params["row"], 10)
if row <= 0 || row > 50 {
row = 10
@@ -376,7 +371,6 @@ func solutionlistProvider(tid uint64, params map[string]string, ctx *RenderCtx)
// solutiontypeProvider 解决方案分类循环,支持 row 参数
func solutiontypeProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([]map[string]string, error) {
_ = models.EnsureCmsSolutionTables()
row := atoiDefault(params["row"], 20)
if row <= 0 || row > 50 {
row = 20
-2
View File
@@ -31,7 +31,6 @@ func EnsureCrmProjectDocCategory(tid int, projectID uint64, projectName string,
return 0, nil
}
models.EnsureOaDocumentTables()
models.EnsureOaDocDefaultCategories(tid)
// 1. 已绑定且分类仍有效:直接返回
@@ -165,7 +164,6 @@ type ProjectDocFolder struct {
// CrmProjectDocFolderTree 构建项目文件夹树(根为项目文件夹,含全部子孙文件夹)。
// 「项目文档」下的同名文件夹即项目根,用户在项目内新建的文件夹为其子分类。
func CrmProjectDocFolderTree(a OaDocActor, rootID uint64) ([]*ProjectDocFolder, error) {
models.EnsureOaDocumentTables()
empty := []*ProjectDocFolder{}
if rootID == 0 {
return empty, nil
-4
View File
@@ -39,7 +39,6 @@ func FindDuplicate(source, scope string, tid, tuid uint64, md5Str string) (*mode
if md5Str == "" {
return nil, nil
}
models.EnsureSystemFileStorageColumns()
qs := models.Orm.QueryTable(new(models.SystemFile)).
Filter("source", source).
@@ -63,7 +62,6 @@ func FindDuplicate(source, scope string, tid, tuid uint64, md5Str string) (*mode
// CreateFileRecord 写入文件记录(同步写入归属与存储字段)
func CreateFileRecord(p FileUploadParams) (uint64, error) {
models.EnsureSystemFileStorageColumns()
uid := p.Uid
row := &models.SystemFile{
@@ -197,7 +195,6 @@ func SoftDeleteFiles(ids []uint64) (int64, error) {
if len(ids) == 0 {
return 0, nil
}
models.EnsureSystemFileStorageColumns()
return models.Orm.QueryTable(new(models.SystemFile)).
Filter("id__in", ids).
Filter("delete_time__isnull", true).
@@ -209,7 +206,6 @@ func DeleteFilesPermanently(ids []uint64) (int64, error) {
if len(ids) == 0 {
return 0, nil
}
models.EnsureSystemFileStorageColumns()
return models.Orm.QueryTable(new(models.SystemFile)).
Filter("id__in", ids).
Delete()
-20
View File
@@ -55,8 +55,6 @@ type OaDocActor struct {
// NewOaDocActor 构造访问者上下文。
// uid 为 backend 端 JWT 的 UserID,即 yz_system_tenant_user.uid。
func NewOaDocActor(tid int, uid uint64) OaDocActor {
models.EnsureOaDocumentTables()
models.EnsureTenantUserOrgColumn()
// 补建租户自己的系统内置分类(项目文档),按 tid 隔离
models.EnsureOaDocDefaultCategories(tid)
@@ -363,7 +361,6 @@ func oaDocApplyUncategorized(qs orm.QuerySeter, a OaDocActor, scope string) orm.
// scope="shared" 返回共享空间树(租户级),scope="personal" 返回当前用户的私密空间树。
// 系统内置分类(项目文档)置顶;数量统计受可见性约束:无权限的私密文档不计入。
func OaDocCategoryTree(a OaDocActor, scope string) ([]*OaDocCategoryItem, error) {
models.EnsureOaDocumentTables()
scope = oaDocScopeOf(scope)
var list []models.OaDocCategory
@@ -416,7 +413,6 @@ func OaDocCategoryTree(a OaDocActor, scope string) ([]*OaDocCategoryItem, error)
// visibility<0 时按范围自动取对应可见性(共享→公开,私密→私密);可见性约束同样生效:无权限的私密文档不计入。
// scope="all" 时跨两个空间统计(不限定 visibility)。
func OaDocCountByCategory(a OaDocActor, scope string, visibility int) (map[uint64]int64, error) {
models.EnsureOaDocumentTables()
scope = oaDocScopeOf(scope)
sql := "SELECT category_id, COUNT(*) AS cnt FROM yz_backend_oa_doc WHERE tid = ? AND is_deleted = 0"
@@ -461,7 +457,6 @@ func oaDocIsReservedName(name string) bool {
// scope="shared" 创建到共享空间(user_id=0,租户级);scope="personal" 创建到当前用户的私密空间(user_id=uid)。
// 父级分类必须与目标空间一致,否则视为不存在。
func OaDocCategoryCreate(a OaDocActor, scope string, parentID uint64, name, remark string, sort int) (*models.OaDocCategory, error) {
models.EnsureOaDocumentTables()
scope = oaDocScopeOf(scope)
name = strings.TrimSpace(name)
@@ -499,7 +494,6 @@ func OaDocCategoryCreate(a OaDocActor, scope string, parentID uint64, name, rema
// OaDocCategoryUpdate 修改分类名称/备注/排序,parentID < 0 表示不调整父级。
func OaDocCategoryUpdate(a OaDocActor, id uint64, name, remark string, sort int, parentID int64) (*models.OaDocCategory, error) {
models.EnsureOaDocumentTables()
var item models.OaDocCategory
if err := oaDocCategoryBaseAny(a.Tid).Filter("id", id).One(&item); err != nil {
@@ -608,7 +602,6 @@ func oaDocCategoryIDsWithChildren(a OaDocActor, scope string, rootID uint64) ([]
// OaDocCategoryDelete 删除分类:系统内置分类、存在子分类或文档时拒绝删除。
// 共享空间分类任何租户成员均可删除(内置分类除外);私密空间分类仅本人可删除。
func OaDocCategoryDelete(a OaDocActor, id uint64) error {
models.EnsureOaDocumentTables()
var item models.OaDocCategory
if err := oaDocCategoryBaseAny(a.Tid).Filter("id", id).One(&item); err != nil {
@@ -709,7 +702,6 @@ type OaDocListParams struct {
// OaDocList 分页查询文档(自动应用可见性与范围过滤)。
func OaDocList(a OaDocActor, p OaDocListParams) ([]models.OaDoc, int64, error) {
models.EnsureOaDocumentTables()
scope := oaDocScopeOf(p.Scope)
qs := oaDocApplyVisible(oaDocBase(a.Tid), a)
@@ -790,7 +782,6 @@ func OaDocList(a OaDocActor, p OaDocListParams) ([]models.OaDoc, int64, error) {
// OaDocGet 读取文档详情(不累加查看次数)。私密文档对无权限者返回未找到。
func OaDocGet(a OaDocActor, id uint64) (*models.OaDoc, error) {
models.EnsureOaDocumentTables()
var item models.OaDoc
qs := oaDocApplyVisible(oaDocBase(a.Tid).Filter("id", id), a)
@@ -854,7 +845,6 @@ type OaDocSaveParams struct {
// OaDocCreate 新增文档。
// 若未传标题则回退为原始文件名(去扩展名);doc_type 由扩展名推断。
func OaDocCreate(a OaDocActor, operatorName string, p OaDocSaveParams) (*models.OaDoc, error) {
models.EnsureOaDocumentTables()
tid := a.Tid
title := strings.TrimSpace(p.Title)
@@ -925,7 +915,6 @@ func visibilityOf(v int8) int8 {
// OaDocUpdate 编辑文档(仅更新传入的字段)。
func OaDocUpdate(a OaDocActor, id uint64, p OaDocSaveParams) (*models.OaDoc, error) {
models.EnsureOaDocumentTables()
var item models.OaDoc
if err := oaDocApplyVisible(oaDocBase(a.Tid).Filter("id", id), a).One(&item); err != nil {
@@ -1034,7 +1023,6 @@ func OaDocUpdate(a OaDocActor, id uint64, p OaDocSaveParams) (*models.OaDoc, err
// OaDocDelete 批量软删除文档,并清理与被删文档相关的关联关系。
// 仅对"可见且可管理"的文档生效;若给定 ID 全部无权操作则返回 ErrDocNoPermission。
func OaDocDelete(a OaDocActor, ids []uint64) (int64, error) {
models.EnsureOaDocumentTables()
ids = normalizeIDs(ids)
if len(ids) == 0 {
@@ -1078,7 +1066,6 @@ func OaDocDelete(a OaDocActor, ids []uint64) (int64, error) {
// OaDocMove 批量移动文档到目标分类(0 表示未分类)。仅对可管理文档生效。
func OaDocMove(a OaDocActor, ids []uint64, categoryID uint64) (int64, error) {
models.EnsureOaDocumentTables()
ids = normalizeIDs(ids)
if len(ids) == 0 {
@@ -1121,7 +1108,6 @@ func OaDocMove(a OaDocActor, ids []uint64, categoryID uint64) (int64, error) {
// OaDocToggleStar 收藏 / 取消收藏切换,返回切换后的状态。
func OaDocToggleStar(a OaDocActor, id uint64) (int8, error) {
models.EnsureOaDocumentTables()
var item models.OaDoc
if err := oaDocApplyVisible(oaDocBase(a.Tid).Filter("id", id), a).One(&item); err != nil {
@@ -1165,7 +1151,6 @@ type OaDocTypeCount struct {
// OaDocStats 文档库概览:总数、状态分布、类型分布、最近更新与热门文档。
// scope 限定统计口径("shared" 共享文档空间 / "personal" 私密文档空间 / "all" 跨空间);可见性约束同样生效。
func OaDocStats(a OaDocActor, scope string) (*OaDocStatsResult, error) {
models.EnsureOaDocumentTables()
scope = oaDocScopeOf(scope)
vis := int8(-1)
@@ -1290,7 +1275,6 @@ type OaDocLinkItem struct {
// OaDocLinkCreate 建立两个文档之间的关联。
func OaDocLinkCreate(a OaDocActor, creatorID, sourceID, targetID uint64, relation, remark string) (*models.OaDocLink, error) {
models.EnsureOaDocumentTables()
tid := a.Tid
if sourceID == 0 || targetID == 0 {
@@ -1345,7 +1329,6 @@ func OaDocLinkCreate(a OaDocActor, creatorID, sourceID, targetID uint64, relatio
// OaDocLinkDelete 删除关联关系(同时清理反向关系,保证图谱双向一致)。
func OaDocLinkDelete(a OaDocActor, id uint64) error {
models.EnsureOaDocumentTables()
tid := a.Tid
var item models.OaDocLink
@@ -1380,7 +1363,6 @@ func OaDocLinkDelete(a OaDocActor, id uint64) error {
// OaDocLinkList 查询与某文档相关的全部关联(含反向),all 为 true 时返回租户全部关联。
// 仅返回两端文档对当前访问者均可见的关联,避免私密文档被越权探查。
func OaDocLinkList(a OaDocActor, docID uint64, all bool) ([]OaDocLinkItem, error) {
models.EnsureOaDocumentTables()
sql := `
SELECT l.id, l.tid, l.source_id, l.target_id, l.relation, l.remark, l.create_time,
@@ -1576,7 +1558,6 @@ type OaDocMemberItem struct {
// OaDocMembers 查询租户内用户(用于私密文档共享选人)。
func OaDocMembers(tid int, keyword string, limit int) ([]OaDocMemberItem, error) {
models.EnsureOaDocumentTables()
if limit < 1 || limit > 200 {
limit = 100
}
@@ -1705,7 +1686,6 @@ type OaDocGraphResult struct {
// - kind=doc :文档之间的关联关系(yz_backend_oa_doc_link)
// - kind=belong :文档到所属分类的归属关系,使孤立文档也能在图谱中定位
func OaDocGraph(a OaDocActor, p OaDocGraphParams) (*OaDocGraphResult, error) {
models.EnsureOaDocumentTables()
limit := p.Limit
if limit < 1 || limit > 1000 {
+196 -103
View File
@@ -2,6 +2,7 @@ package services
import (
"context"
"encoding/json"
"fmt"
"strconv"
"time"
@@ -89,83 +90,213 @@ func SaveSiteReminderConfig(retentionDays int, autoRead int8) error {
return nil
}
// SendSiteReminder 发送站内信
// targetType: platform (平台端), tenant_all (管理端所有用户), role (平台角色), tenant (特定租户)
func SendSiteReminder(title, content string, senderID uint64, senderType string, targetType string, targetRoleID uint64, targetTenantID uint64) error {
var receiverIDs []uint64
var receiverType string
// reminderReceiver 站内信接收人(用户ID + 所属端类型)
type reminderReceiver struct {
ID uint64
Type string // platform-平台端 tenant-租户端
}
// queryEnabledAdminUserIDs 查询启用的平台用户ID;cond 为附加过滤条件(可为 nil)。
func queryEnabledAdminUserIDs(cond *orm.Condition) ([]uint64, error) {
qs := models.Orm.QueryTable(new(models.AdminUser)).
Filter("status", 1).
Filter("delete_time__isnull", true)
if cond != nil {
qs = qs.SetCond(orm.NewCondition().
And("status", 1).
And("delete_time__isnull", true).
AndCond(cond))
}
var list []models.AdminUser
if _, err := qs.All(&list, "id"); err != nil {
return nil, err
}
ids := make([]uint64, 0, len(list))
for _, u := range list {
ids = append(ids, u.ID)
}
return ids, nil
}
// queryEnabledTenantUserIDs 查询启用的租户用户ID;cond 为附加过滤条件(可为 nil)。
func queryEnabledTenantUserIDs(cond *orm.Condition) ([]uint64, error) {
qs := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("status", 1).
Filter("delete_time__isnull", true)
if cond != nil {
qs = qs.SetCond(orm.NewCondition().
And("status", 1).
And("delete_time__isnull", true).
AndCond(cond))
}
var list []models.SystemTenantUser
if _, err := qs.All(&list, "id"); err != nil {
return nil, err
}
ids := make([]uint64, 0, len(list))
for _, u := range list {
ids = append(ids, u.ID)
}
return ids, nil
}
// marshalTargetIDs 将多选目标ID序列化为 JSON 字符串(空列表返回 nil)。
func marshalTargetIDs(targetIDs []uint64) *string {
if len(targetIDs) == 0 {
return nil
}
b, err := json.Marshal(targetIDs)
if err != nil {
return nil
}
s := string(b)
return &s
}
// legacyTargetColumns 旧字段兼容写入:role → target_role_id,tenant → target_tenant_id(取首个)
func legacyTargetColumns(targetType string, targetIDs []uint64) (roleID, tenantID uint64) {
if len(targetIDs) == 0 {
return 0, 0
}
switch targetType {
case "role":
return targetIDs[0], 0
case "tenant":
return 0, targetIDs[0]
}
return 0, 0
}
// resolveReminderReceivers 解析接收目标 → 接收人集合(按端区分类型,已去重)。
// targetType:
// all 全平台(平台端 + 租户端所有启用用户)
// platform 平台端所有启用用户
// tenant_all 租户端所有租户的启用用户
// tenant_role 指定租户角色(可多选)下的启用用户
// tenant 指定租户(可多选)下的启用用户
// tenant_user 指定租户用户(可多选)
// role 兼容旧数据:指定平台角色(可多选)下的启用用户
func resolveReminderReceivers(targetType string, targetIDs []uint64) ([]reminderReceiver, error) {
receivers := make([]reminderReceiver, 0)
seen := map[string]bool{}
appendIDs := func(ids []uint64, rtype string) {
for _, id := range ids {
if id == 0 {
continue
}
key := fmt.Sprintf("%s_%d", rtype, id)
if seen[key] {
continue
}
seen[key] = true
receivers = append(receivers, reminderReceiver{ID: id, Type: rtype})
}
}
switch targetType {
case "platform":
receiverType = "platform"
var list []models.AdminUser
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
case "all": // 全平台:平台端 + 租户端所有启用用户
adminIDs, err := queryEnabledAdminUserIDs(nil)
if err != nil {
return fmt.Errorf("查询平台用户失败: %w", err)
return nil, fmt.Errorf("查询平台用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "tenant_all":
receiverType = "tenant"
var list []models.SystemTenantUser
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
tenantIDs, err := queryEnabledTenantUserIDs(nil)
if err != nil {
return fmt.Errorf("查询租户用户失败: %w", err)
return nil, fmt.Errorf("查询租户用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "role":
receiverType = "platform"
var list []models.AdminUser
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("role_id", targetRoleID).Filter("delete_time__isnull", true).All(&list, "id")
appendIDs(adminIDs, "platform")
appendIDs(tenantIDs, "tenant")
case "platform": // 平台端所有启用用户
adminIDs, err := queryEnabledAdminUserIDs(nil)
if err != nil {
return fmt.Errorf("根据角色查询用户失败: %w", err)
return nil, fmt.Errorf("查询平台用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "tenant":
receiverType = "tenant"
var list []models.SystemTenantUser
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("tid", targetTenantID).Filter("delete_time__isnull", true).All(&list, "id")
appendIDs(adminIDs, "platform")
case "tenant_all": // 租户端所有租户的启用用户
tenantIDs, err := queryEnabledTenantUserIDs(nil)
if err != nil {
return fmt.Errorf("根据租户查询用户失败: %w", err)
return nil, fmt.Errorf("查询租户用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
appendIDs(tenantIDs, "tenant")
case "tenant_role": // 指定租户角色(可多选)下的启用用户
if len(targetIDs) == 0 {
return nil, fmt.Errorf("请选择接收的租户角色")
}
tenantIDs, err := queryEnabledTenantUserIDs(orm.NewCondition().And("group_id__in", targetIDs))
if err != nil {
return nil, fmt.Errorf("根据租户角色查询用户失败: %w", err)
}
appendIDs(tenantIDs, "tenant")
case "tenant": // 指定租户(可多选)下的启用用户
if len(targetIDs) == 0 {
return nil, fmt.Errorf("请选择接收的租户")
}
tenantIDs, err := queryEnabledTenantUserIDs(orm.NewCondition().And("tid__in", targetIDs))
if err != nil {
return nil, fmt.Errorf("根据租户查询用户失败: %w", err)
}
appendIDs(tenantIDs, "tenant")
case "tenant_user": // 指定租户用户(可多选)
if len(targetIDs) == 0 {
return nil, fmt.Errorf("请选择接收的租户用户")
}
tenantIDs, err := queryEnabledTenantUserIDs(orm.NewCondition().And("id__in", targetIDs))
if err != nil {
return nil, fmt.Errorf("查询租户用户失败: %w", err)
}
appendIDs(tenantIDs, "tenant")
case "role": // 兼容旧数据:平台角色(可多选)下的启用用户
if len(targetIDs) == 0 {
return nil, fmt.Errorf("请选择接收的角色")
}
adminIDs, err := queryEnabledAdminUserIDs(orm.NewCondition().And("role_id__in", targetIDs))
if err != nil {
return nil, fmt.Errorf("根据角色查询用户失败: %w", err)
}
appendIDs(adminIDs, "platform")
default:
return fmt.Errorf("未知的发送目标类型: %s", targetType)
return nil, fmt.Errorf("未知的发送目标类型: %s", targetType)
}
return receivers, nil
}
if len(receiverIDs) == 0 {
// SendSiteReminder 发送站内信
// targetType: all (全平台), platform (平台端), tenant_all (租户端所有租户),
// tenant_role (租户角色), tenant (指定租户), tenant_user (指定租户用户), role (平台角色-旧)
func SendSiteReminder(title, content string, senderID uint64, senderType string, targetType string, targetIDs []uint64) error {
receivers, err := resolveReminderReceivers(targetType, targetIDs)
if err != nil {
return err
}
if len(receivers) == 0 {
return nil
}
targetIDsJSON := marshalTargetIDs(targetIDs)
legacyRoleID, legacyTenantID := legacyTargetColumns(targetType, targetIDs)
now := time.Now()
batchID := fmt.Sprintf("%d_%d", now.UnixNano(), senderID)
var reminders []models.SystemReminderList
for _, rid := range receiverIDs {
reminders := make([]models.SystemReminderList, 0, len(receivers))
for _, r := range receivers {
reminders = append(reminders, models.SystemReminderList{
Title: title,
Content: content,
SenderID: senderID,
SenderType: senderType,
ReceiverID: rid,
ReceiverType: receiverType,
ReceiverID: r.ID,
ReceiverType: r.Type,
IsRead: 0,
CreateTime: &now,
BatchID: batchID,
TargetType: targetType,
TargetRoleID: targetRoleID,
TargetTenantID: targetTenantID,
TargetIDs: targetIDsJSON,
TargetRoleID: legacyRoleID,
TargetTenantID: legacyTenantID,
})
}
// 批量插入
_, err := models.Orm.InsertMulti(100, reminders)
_, err = models.Orm.InsertMulti(100, reminders)
return err
}
@@ -280,7 +411,7 @@ func ListSentReminders(senderID uint64, page, pageSize int) ([]models.SystemRemi
}
// UpdateSentReminder 更新已发出的消息(更新该批次下所有接收者的消息,支持修改目标接收群体)
func UpdateSentReminder(batchID string, title, content, targetType string, targetRoleID, targetTenantID uint64) error {
func UpdateSentReminder(batchID string, title, content, targetType string, targetIDs []uint64) error {
// 1. 获取当前发送者ID (从该批次中任意一条记录中获取)
var firstRecord models.SystemReminderList
err := models.Orm.QueryTable(new(models.SystemReminderList)).Filter("batch_id", batchID).Limit(1).One(&firstRecord)
@@ -291,53 +422,13 @@ func UpdateSentReminder(batchID string, title, content, targetType string, targe
senderType := firstRecord.SenderType
// 2. 根据新的目标接收群体获取接收人列表
var receiverIDs []uint64
var receiverType string
receivers, err := resolveReminderReceivers(targetType, targetIDs)
if err != nil {
return err
}
switch targetType {
case "platform":
receiverType = "platform"
var list []models.AdminUser
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("查询平台用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "tenant_all":
receiverType = "tenant"
var list []models.SystemTenantUser
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("查询租户用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "role":
receiverType = "platform"
var list []models.AdminUser
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("role_id", targetRoleID).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("根据角色查询用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
case "tenant":
receiverType = "tenant"
var list []models.SystemTenantUser
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("tid", targetTenantID).Filter("delete_time__isnull", true).All(&list, "id")
if err != nil {
return fmt.Errorf("根据租户查询用户失败: %w", err)
}
for _, u := range list {
receiverIDs = append(receiverIDs, u.ID)
}
default:
return fmt.Errorf("未知的发送目标类型: %s", targetType)
}
targetIDsJSON := marshalTargetIDs(targetIDs)
legacyRoleID, legacyTenantID := legacyTargetColumns(targetType, targetIDs)
// 3. 获取该批次中现有的所有记录 (包括已删除的)
var existingRecords []models.SystemReminderList
@@ -354,8 +445,8 @@ func UpdateSentReminder(batchID string, title, content, targetType string, targe
}
newReceiverMap := make(map[string]bool)
for _, rid := range receiverIDs {
key := fmt.Sprintf("%s_%d", receiverType, rid)
for _, r := range receivers {
key := fmt.Sprintf("%s_%d", r.Type, r.ID)
newReceiverMap[key] = true
}
@@ -377,16 +468,17 @@ func UpdateSentReminder(batchID string, title, content, targetType string, targe
// B. 对于仍然在新接收者列表中的用户,更新标题、内容、以及 target 信息;如果原来被删除了,清除 delete_time
var newInserts []models.SystemReminderList
for _, rid := range receiverIDs {
key := fmt.Sprintf("%s_%d", receiverType, rid)
for _, r := range receivers {
key := fmt.Sprintf("%s_%d", r.Type, r.ID)
if rec, exists := existingMap[key]; exists {
rec.Title = title
rec.Content = content
rec.TargetType = targetType
rec.TargetRoleID = targetRoleID
rec.TargetTenantID = targetTenantID
rec.TargetIDs = targetIDsJSON
rec.TargetRoleID = legacyRoleID
rec.TargetTenantID = legacyTenantID
cols := []string{"Title", "Content", "TargetType", "TargetRoleID", "TargetTenantID"}
cols := []string{"Title", "Content", "TargetType", "TargetIDs", "TargetRoleID", "TargetTenantID"}
if rec.DeleteTime != nil {
rec.DeleteTime = nil
rec.IsRead = 0
@@ -403,14 +495,15 @@ func UpdateSentReminder(batchID string, title, content, targetType string, targe
Content: content,
SenderID: senderID,
SenderType: senderType,
ReceiverID: rid,
ReceiverType: receiverType,
ReceiverID: r.ID,
ReceiverType: r.Type,
IsRead: 0,
CreateTime: &now,
BatchID: batchID,
TargetType: targetType,
TargetRoleID: targetRoleID,
TargetTenantID: targetTenantID,
TargetIDs: targetIDsJSON,
TargetRoleID: legacyRoleID,
TargetTenantID: legacyTenantID,
})
}
}
-4
View File
@@ -8,10 +8,6 @@ import (
// BindTenantUser 绑定用户到租户(若已存在则更新状态/默认值)
func BindTenantUser(tid, uid uint64, account, name, phone, email *string, sex *uint8, birth *string, password *string, isDefault, status int8, remark *string) (uint64, error) {
// 确保 group_id(角色)列存在,避免老表插入时报 Unknown column。
// INSERT 会带上该列,列不存在会直接失败,故必须在写入前补齐。
models.EnsureTenantUserGroupColumn()
var existed models.SystemTenantUser
err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tid).
+1 -6
View File
@@ -117,6 +117,7 @@ import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled, Expand, Loa
import { ElMessage } from 'element-plus';
import { getMySiteReminders, readAllSiteReminders } from "@/api/sitereminder";
import MessageDetailDialog from "@/views/basicSettings/sitereminder/components/detail.vue";
import { formatTime } from "@/utils/datetime";
const router = useRouter();
const route = useRoute();
@@ -178,12 +179,6 @@ const loadingMessages = ref(false);
const detailVisible = ref(false);
const currentReminder = ref<any>({});
const formatTime = (timeStr: any) => {
if (!timeStr) return "-";
const date = new Date(timeStr);
return date.toLocaleString();
};
const fetchUnreadCount = async () => {
if (!authStore.token) return;
try {
+55
View File
@@ -0,0 +1,55 @@
/**
* 统一日期时间格式化工具(platform 端)
*
* 后端(Go)返回的时间字段为 RFC3339 / ISO 8601 字符串,
* 例如 "2026-03-31T23:34:07+08:00",直接渲染不友好。
* 页面上显示时间统一使用本工具,输出:
* formatDateTime / formatTime -> 2026-03-31 23:34:07(YYYY-MM-DD HH:mm:ss)
* formatDate / formatDateOnly -> 2026-03-31(YYYY-MM-DD)
*/
const pad = (n: number) => String(n).padStart(2, "0");
function toDate(value: any): Date | null {
if (value === null || value === undefined || value === "") return null;
if (value instanceof Date) return isNaN(value.getTime()) ? null : value;
// 兼容秒级 / 毫秒级时间戳
if (typeof value === "number") {
const d = new Date(value < 1e12 ? value * 1000 : value);
return isNaN(d.getTime()) ? null : d;
}
// "2026-03-31 23:34:07" 形式在部分浏览器解析异常,统一先替换为 ISO 形式
const str = String(value).trim().replace(" ", "T");
const d = new Date(str);
return isNaN(d.getTime()) ? null : d;
}
/** 2026-03-31 23:34:07(YYYY-MM-DD HH:mm:ss);非法值返回 "-" */
export function formatDateTime(value: any): string {
const d = toDate(value);
if (!d) return "-";
return (
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
);
}
/** 与 formatDateTime 相同(兼容旧调用名) */
export function formatTime(value: any): string {
return formatDateTime(value);
}
/** 2026-03-31(YYYY-MM-DD);非法值返回 "-";纯日期串不做时区换算 */
export function formatDate(value: any): string {
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value.trim())) {
return value.trim();
}
const d = toDate(value);
if (!d) return "-";
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** 与 formatDate 相同(兼容旧调用名) */
export function formatDateOnly(value: any): string {
return formatDate(value);
}

Some files were not shown because too many files have changed in this diff Show More