更新代码
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
// 状态管理
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())}`
|
||||
}
|
||||
|
||||
// 获取字典类型列表
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -40,7 +40,6 @@ func main() {
|
||||
fmt.Println("数据库未初始化")
|
||||
os.Exit(1)
|
||||
}
|
||||
models.EnsureSystemFileStorageColumns()
|
||||
|
||||
storageSvc, err := services.GetStorageService()
|
||||
if err != nil {
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -62,6 +62,7 @@ var crmDefaultUnits = []struct {
|
||||
}
|
||||
|
||||
// crmEnsureDefaultUnits 租户首次使用(从未有过任何单位记录,含已软删除的)时写入默认单位。
|
||||
// 【租户级初始化】新租户首次使用时补建默认业务数据(非表结构/全局预置数据;结构变更须走 docs/sql/ SQL 文件)。
|
||||
// 用户手动清空后不再自动补,避免越删越有;需要恢复时用「恢复默认单位」接口。
|
||||
// 已有记录时顺带修复存量默认单位的 is_default 标记(历史数据升级)。
|
||||
func crmEnsureDefaultUnits(tenantID string) {
|
||||
|
||||
@@ -79,11 +79,6 @@ func (c *BackendFileController) effectiveTid(claims *jwtutil.Claims) uint64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Prepare 所有接口执行前补齐文件存储分层所需字段,避免历史库缺列导致查询/写入报错
|
||||
func (c *BackendFileController) Prepare() {
|
||||
models.EnsureSystemFileStorageColumns()
|
||||
}
|
||||
|
||||
// resolveTuid 解析归属用户 ID,返回 0 表示租户共享文件。
|
||||
//
|
||||
// 取值顺序:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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").
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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}$`)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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回款记录表(回款进度流水)';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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通知公告表';
|
||||
@@ -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='租户站点扩展设置';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user