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