diff --git a/backend/components.d.ts b/backend/components.d.ts index 4e1b2cb..bab72bc 100644 --- a/backend/components.d.ts +++ b/backend/components.d.ts @@ -19,6 +19,8 @@ declare module 'vue' { ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElBacktop: typeof import('element-plus/es')['ElBacktop'] ElBadge: typeof import('element-plus/es')['ElBadge'] + ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'] + ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'] ElButton: typeof import('element-plus/es')['ElButton'] ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] ElCard: typeof import('element-plus/es')['ElCard'] diff --git a/backend/src/api/dict.js b/backend/src/api/dict.js index 6021512..da2c96e 100644 --- a/backend/src/api/dict.js +++ b/backend/src/api/dict.js @@ -1,114 +1,124 @@ import request from '@/utils/request' -// 获取字典类型列表 +/** + * 数据字典接口(租户端) + * + * 表:yz_backend_dict / yz_backend_dict_item + * 租户从 JWT 解析,前端不传 tenant_id。 + */ +const PREFIX = '/backend/dict' + +/* ================= 字典列表(字典类型) ================= */ + +/** 字典列表(分页)params: { page, pageSize, keyword, status } */ export function getDictTypes(params) { return request({ - url: '/backend/dict/types', + url: `${PREFIX}/types`, method: 'get', params }) } -// 根据ID获取字典类型 -export function getDictTypeById(id) { +/** 新增字典 */ +export function createDictType(data) { return request({ - url: `/backend/dict/types/${id}`, - method: 'get' - }) -} - -// 添加字典类型 -export function addDictType(data) { - return request({ - url: '/backend/dict/types', + url: `${PREFIX}/types`, method: 'post', - data: { - ...data, - is_global: data.is_global !== undefined ? data.is_global : 0 - } + data }) } -// 更新字典类型 +/** 编辑字典(dict_code 不可改,由后端忽略) */ export function updateDictType(id, data) { return request({ - url: `/backend/dict/types/${id}`, + url: `${PREFIX}/types/${id}`, method: 'put', - data: { - ...data, - is_global: data.is_global !== undefined ? data.is_global : 0 - } + data }) } -// 删除字典类型 +/** 删除字典(其下仍有字典项时后端会拒绝) */ export function deleteDictType(id) { return request({ - url: `/backend/dict/types/${id}`, + url: `${PREFIX}/types/${id}`, method: 'delete' }) } -// 获取字典项列表 +/* ================= 字典项 ================= */ + +/** 字典项列表(分页)params: { dict_id, page, pageSize, keyword, status } */ export function getDictItems(params) { return request({ - url: '/backend/dict/items', + url: `${PREFIX}/items`, method: 'get', params }) } -// 根据ID获取字典项 -export function getDictItemById(id) { +/** 新增字典项 */ +export function createDictItem(data) { return request({ - url: `/backend/dict/items/${id}`, - method: 'get' - }) -} - -// 添加字典项 -export function addDictItem(data) { - return request({ - url: '/backend/dict/items', + url: `${PREFIX}/items`, method: 'post', data }) } -// 更新字典项 +/** 编辑字典项 */ export function updateDictItem(id, data) { return request({ - url: `/backend/dict/items/${id}`, + url: `${PREFIX}/items/${id}`, method: 'put', data }) } -// 删除字典项 +/** 启用 / 停用字典项 */ +export function toggleDictItemStatus(id, status) { + return request({ + url: `${PREFIX}/items/${id}/status`, + method: 'post', + data: { status } + }) +} + +/** 删除字典项 */ export function deleteDictItem(id) { return request({ - url: `/backend/dict/items/${id}`, + url: `${PREFIX}/items/${id}`, method: 'delete' }) } -// 根据字典编码获取字典项(用于业务查询) -export function getDictItemsByCode(code, includeDisabled = false) { +/* ================= 业务读取 ================= */ + +/** 按字典编码取「字典类型与字典项都启用」的选项,供业务页面下拉使用 */ +export function getDictOptions(code) { return request({ - url: `/backend/dict/items/code/${code}`, - method: 'get', - params: { - include_disabled: includeDisabled ? '1' : '0' - } + url: `${PREFIX}/options/${code}`, + method: 'get' }) } -// 批量更新字典项排序 -export function batchUpdateDictItemSort(data) { +/** + * 批量取多个字典的选项(一次请求),供页面/应用启动时预加载。 + * 返回 data.dicts = { [dict_code]: 字典项[] }。 + * 同编码时后端优先返回本租户自己的字典,没有再回落全局内置字典。 + * @param {string[]} codes 字典编码数组 + */ +export function getDictOptionsBatch(codes) { return request({ - url: '/backend/dict/items/sort', - method: 'put', - data + url: `${PREFIX}/options`, + method: 'post', + data: { codes } }) } +/** 一次取回全部「启用」字典的字典项,供登录后预热缓存 */ +export function getAllDictOptions() { + return request({ + url: `${PREFIX}/options`, + method: 'get' + }) +} diff --git a/backend/src/composables/useDict.js b/backend/src/composables/useDict.js new file mode 100644 index 0000000..aed3803 --- /dev/null +++ b/backend/src/composables/useDict.js @@ -0,0 +1,59 @@ +import { computed } from 'vue' +import { useDictStore } from '@/stores/dict' + +/** + * 字典 Composable:把「加载 + 取值 + 文案/标签色查询」收敛成一行调用。 + * + * 用法: + * const { items, options, label, tagType } = useDictionary('payment_channel') + * // 模板: + * // {{ label(row.status) }} + * + * @param {string} code 字典编码(库里真实的 dict_code) + */ +export function useDictionary(code) { + const dictStore = useDictStore() + // setup 阶段同步触发加载:缓存命中时不发请求,首次会自动拉取并响应式刷新 + dictStore.ensure(code) + + /** 字典项原始数组(含 item_value / item_label / tag_type 等) */ + const items = computed(() => dictStore.getDictItemsSync(code)) + /** el-select / el-option 直接可用的 { label, value } 列表 */ + const options = computed(() => items.value.map((it) => ({ + label: it.item_label, + value: it.item_value, + tag_type: it.tag_type + }))) + + return { + items, + options, + label: (value, fallback) => dictStore.getDictLabel(code, value, fallback), + tagType: (value, fallback) => dictStore.getDictTagType(code, value, fallback), + find: (value) => dictStore.getDictItem(code, value) + } +} + +/** + * 只要「值 → 文案」或「值 → 标签色」时的轻量用法。 + * const channelLabel = useDictLabel('payment_channel') + */ +export function useDictLabel(code) { + const dictStore = useDictStore() + dictStore.ensure(code) + return (value, fallback) => dictStore.getDictLabel(code, value, fallback) +} + +export function useDictTagType(code) { + const dictStore = useDictStore() + dictStore.ensure(code) + return (value, fallback) => dictStore.getDictTagType(code, value, fallback) +} + +/** 预加载多个字典(页面 onMounted 里调用,一次批量请求) */ +export function useDictPreload(codes) { + const dictStore = useDictStore() + return () => dictStore.preloadDicts(codes) +} + +export default useDictionary diff --git a/backend/src/stores/dict.js b/backend/src/stores/dict.js new file mode 100644 index 0000000..d7f7770 --- /dev/null +++ b/backend/src/stores/dict.js @@ -0,0 +1,187 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { getDictOptions, getDictOptionsBatch, getAllDictOptions } from '@/api/dict' + +/** + * 字典 Store(平台端) + * + * 全部字典数据都来自数据库,前端不维护编码清单: + * - 登录后调用一次 loadAll(),把库里所有「启用」字典读进缓存; + * - 页面按库里真实的 dict_code 取值即可(字符串直接写,无需常量文件); + * - 之后在字典管理页新增的字典,第一次被引用时会按需自动加载。 + * + * 用法: + * const dictStore = useDictStore() + * dictStore.getDictLabel('payment_order_status', row.status) // 文案 + * dictStore.getDictTagType('payment_order_status', row.status) // 标签色 + * dictStore.getSelectOptions('payment_channel') // 下拉选项 + */ +export const useDictStore = defineStore('dict', () => { + /** dict_code -> 字典项数组(渲染依赖它,必须是响应式的) */ + const dictMap = ref({}) + + /** 已加载过的编码(仅用于去重,不参与渲染) */ + let loaded = {} + /** 正在请求中的编码 -> Promise(并发调用复用同一个请求) */ + const pending = new Map() + /** 是否已完成全量预热 */ + let allLoaded = false + let allPending = null + + function setItems(code, list) { + dictMap.value = { ...dictMap.value, [code]: Array.isArray(list) ? list : [] } + } + + function itemsOf(code) { + return dictMap.value[code] || [] + } + + /** 把入参规范成去重后的编码数组 */ + function normalizeCodes(codes) { + return [...new Set((codes || []).map((c) => String(c || '').trim()).filter(Boolean))] + } + + /** + * 加载字典(带缓存与并发去重)。 + * 请求失败也会标记为已加载,避免页面反复重试;需要重试请先 clearCache。 + */ + async function load(codes) { + const list = normalizeCodes(codes) + const missing = list.filter((c) => !loaded[c] && !pending.has(c)) + + if (missing.length) { + const promise = (async () => { + if (missing.length === 1) { + // 单个字典走专用接口 + const code = missing[0] + try { + const res = await getDictOptions(code) + setItems(code, res?.code === 200 ? res.data?.list || [] : []) + } catch { + setItems(code, []) + } + } else { + // 多个字典合并成一次请求 + try { + const res = await getDictOptionsBatch(missing) + const dicts = res?.code === 200 ? res.data?.dicts || {} : {} + missing.forEach((code) => setItems(code, dicts[code] || [])) + } catch { + missing.forEach((code) => setItems(code, [])) + } + } + })().finally(() => { + missing.forEach((code) => { + loaded[code] = true + pending.delete(code) + }) + }) + + missing.forEach((code) => pending.set(code, promise)) + } + + await Promise.all(list.map((c) => pending.get(c) || Promise.resolve())) + return list.flatMap((c) => itemsOf(c)) + } + + /** + * 全量预热:一次请求把库里所有启用字典读进缓存(登录后调用一次即可)。 + * 之后再新增的字典仍会走 load() 按需加载。 + */ + function loadAll() { + if (allLoaded) return Promise.resolve() + if (allPending) return allPending + + allPending = (async () => { + try { + const res = await getAllDictOptions() + const dicts = res?.code === 200 ? res.data?.dicts || {} : {} + const next = { ...dictMap.value } + Object.entries(dicts).forEach(([code, list]) => { + loaded[code] = true + next[code] = Array.isArray(list) ? list : [] + }) + dictMap.value = next + } catch { + // 预热失败不影响使用:后续按需加载 + } finally { + allLoaded = true + allPending = null + } + })() + return allPending + } + + /** 同步触发加载(不等待),用于组件 setup/onMounted 里“先渲染后填充” */ + function ensure(code) { + const list = normalizeCodes([code]) + if (list.some((c) => !loaded[c] && !pending.has(c))) { + load(list) + } + } + + /** 同步取字典项;未加载时顺带触发加载,拿到后由响应式自动刷新 */ + function getDictItemsSync(code) { + ensure(code) + return itemsOf(code) + } + + /** 值 → 字典项(值按字符串比较,兼容 1 / '1' 混用) */ + function getDictItem(code, value) { + if (value === undefined || value === null || value === '') return null + const key = String(value) + return itemsOf(code).find((it) => String(it.item_value) === key) || null + } + + /** 值 → 文案;取不到时返回 fallback,未传则原样返回取值 */ + function getDictLabel(code, value, fallback) { + const hit = getDictItem(code, value) + if (hit) return hit.item_label + if (fallback !== undefined) return fallback + return value === undefined || value === null ? '' : String(value) + } + + /** 值 → el-tag 的 type;取不到时返回 fallback(默认 info) */ + function getDictTagType(code, value, fallback = 'info') { + const hit = getDictItem(code, value) + return hit && hit.tag_type ? hit.tag_type : fallback + } + + /** 值 → 下拉选项 { label, value },供 el-select / el-option 直接使用 */ + function getSelectOptions(code) { + return getDictItemsSync(code).map((it) => ({ label: it.item_label, value: it.item_value, tag_type: it.tag_type })) + } + + /** 预加载多个字典(一次批量请求) */ + function preloadDicts(codes) { + return load(codes) + } + + /** 清空缓存:传编码清单个,不传清全部(同时允许重新全量预热) */ + function clearCache(code) { + if (code) { + delete loaded[code] + const next = { ...dictMap.value } + delete next[code] + dictMap.value = next + return + } + loaded = {} + dictMap.value = {} + allLoaded = false + } + + return { + dictMap, + ensure, + loadAll, + getDictItems: load, + getDictItemsSync, + getDictItem, + getDictLabel, + getDictTagType, + getSelectOptions, + preloadDicts, + clearCache + } +}) diff --git a/backend/src/utils/datetime.ts b/backend/src/utils/datetime.ts index 6cf5ba8..8957bdd 100644 --- a/backend/src/utils/datetime.ts +++ b/backend/src/utils/datetime.ts @@ -53,3 +53,4 @@ export function formatDate(value: any): string { export function formatDateOnly(value: any): string { return formatDate(value); } + diff --git a/backend/src/views/Main.vue b/backend/src/views/Main.vue index 0d910c9..f631f61 100644 --- a/backend/src/views/Main.vue +++ b/backend/src/views/Main.vue @@ -2,12 +2,14 @@ import CommonAside from '@/components/CommonAside.vue'; import CommonHeader from '@/components/CommonHeader.vue'; import { useTabsStore } from '@/stores'; +import { useDictStore } from '@/stores/dict'; import { useRouter, useRoute } from 'vue-router'; import { ref, watch, reactive, nextTick, onMounted, onBeforeUnmount, computed } from 'vue'; import { More, Close, CircleClose, ArrowUp, Fold, Expand } from '@element-plus/icons-vue'; import { ElMessage } from 'element-plus'; const tabsStore = useTabsStore(); +const dictStore = useDictStore(); const router = useRouter(); const route = useRoute(); const defaultDashboardPath = '/home'; @@ -76,6 +78,9 @@ function restoreTabFromRoute() { // 组件挂载后,根据当前路由恢复 tab onMounted(() => { + // 进入后台后把库里所有字典预热到缓存(失败不影响按需加载) + dictStore.loadAll(); + // 等待路由完全加载后再恢复 nextTick(() => { // 刷新时,localStorage 中已保存的 tabs 会在 store 初始化时恢复 diff --git a/backend/src/views/apps/erp/customer/index.vue b/backend/src/views/apps/erp/customer/index.vue index e3c0665..c70f840 100644 --- a/backend/src/views/apps/erp/customer/index.vue +++ b/backend/src/views/apps/erp/customer/index.vue @@ -25,27 +25,17 @@ - - - - - + - - - - + - - - - + @@ -210,13 +200,23 @@ diff --git a/backend/src/views/apps/erp/supplier/dict.js b/backend/src/views/apps/erp/supplier/dict.js new file mode 100644 index 0000000..01d94da --- /dev/null +++ b/backend/src/views/apps/erp/supplier/dict.js @@ -0,0 +1,5 @@ +/** + * 供应商模块的字典取值:实现已上移到 ../dict.js(客户/供应商共用同一套往来单位字典), + * 这里保留原有导出名,避免各页面改 import 路径。 + */ +export { useErpPartyDict as useSupplierDict, formatDate, formatDateTime } from '../dict' diff --git a/backend/src/views/apps/erp/supplier/index.vue b/backend/src/views/apps/erp/supplier/index.vue index ef9d09f..f1086d4 100644 --- a/backend/src/views/apps/erp/supplier/index.vue +++ b/backend/src/views/apps/erp/supplier/index.vue @@ -25,26 +25,17 @@ - - - - + - - - - + - - - - + @@ -134,65 +125,12 @@ @success="fetchList" /> - - -
-
- 转为客户 - 同一家单位可能既是供应商又是客户,转化后原供应商记录保留 -
- - {{ detailData.supplier_name }} - {{ supplierTypeText(detailData.supplier_type) }} - {{ supplierLevelText(detailData.supplier_level) }} - {{ detailData.industry || '-' }} - - {{ statusText(detailData.status) }} - - {{ detailData.registered_capital ? detailData.registered_capital + ' 万元' : '-' }} - {{ detailData.paid_capital ? detailData.paid_capital + ' 万元' : '-' }} - {{ formatDate(detailData.establish_date) }} - {{ detailData.administrative_division || '-' }} - {{ detailData.enterprise_type || '-' }} - {{ detailData.taxpayer_qualification || '-' }} - {{ detailData.business_scope || '-' }} - {{ formatDateTime(detailData.update_time) }} - + - - {{ detailData.contact_person || '-' }} - {{ detailData.contact_phone || '-' }} - {{ detailData.contact_email || '-' }} - {{ detailData.address || '-' }} - - - - - {{ detailData.invoice_title || '-' }} - {{ detailData.tax_number || '-' }} - {{ detailData.registered_phone || '-' }} - {{ detailData.registered_address || '-' }} - {{ detailData.bank_name || '-' }} - {{ detailData.bank_account || '-' }} - - - - {{ detailData.remark || '-' }} - -
-
- - diff --git a/backend/src/views/system/dict/components/DictItemEdit.vue b/backend/src/views/system/dict/components/DictItemEdit.vue index ed57bd9..4fb0f04 100644 --- a/backend/src/views/system/dict/components/DictItemEdit.vue +++ b/backend/src/views/system/dict/components/DictItemEdit.vue @@ -1,335 +1,173 @@ - - - - - - \ No newline at end of file +} + diff --git a/backend/src/views/system/dict/components/DictItemEditDialog.vue b/backend/src/views/system/dict/components/DictItemEditDialog.vue deleted file mode 100644 index 9717310..0000000 --- a/backend/src/views/system/dict/components/DictItemEditDialog.vue +++ /dev/null @@ -1,316 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/backend/src/views/system/dict/components/DictItemList.vue b/backend/src/views/system/dict/components/DictItemList.vue deleted file mode 100644 index 594b2bb..0000000 --- a/backend/src/views/system/dict/components/DictItemList.vue +++ /dev/null @@ -1,414 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/backend/src/views/system/dict/components/DictTypeEdit.vue b/backend/src/views/system/dict/components/DictTypeEdit.vue index 23a2f97..18dbf5b 100644 --- a/backend/src/views/system/dict/components/DictTypeEdit.vue +++ b/backend/src/views/system/dict/components/DictTypeEdit.vue @@ -1,241 +1,145 @@ - - - - - - \ No newline at end of file +} + diff --git a/backend/src/views/system/dict/components/DictTypeList.vue b/backend/src/views/system/dict/components/DictTypeList.vue deleted file mode 100644 index 2b7b999..0000000 --- a/backend/src/views/system/dict/components/DictTypeList.vue +++ /dev/null @@ -1,308 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/backend/src/views/system/dict/index.vue b/backend/src/views/system/dict/index.vue index dc0b13e..279fb8b 100644 --- a/backend/src/views/system/dict/index.vue +++ b/backend/src/views/system/dict/index.vue @@ -1,357 +1,517 @@ - - diff --git a/go/controllers/backend_dict.go b/go/controllers/backend_dict.go new file mode 100644 index 0000000..30ce2bc --- /dev/null +++ b/go/controllers/backend_dict.go @@ -0,0 +1,815 @@ +package controllers + +import ( + "encoding/json" + "fmt" + "io" + "regexp" + "strconv" + "strings" + "time" + + "server/models" + "server/pkg/jwtutil" + + "github.com/beego/beego/v2/client/orm" + beego "github.com/beego/beego/v2/server/web" +) + +// BackendDictController 租户端数据字典:字典列表 + 字典项 +// +// 表:yz_backend_dict / yz_backend_dict_item(建表 SQL:docs/sql/create_dict.sql)。 +// 按 tenant_id 隔离(取 JWT 里的租户ID,前端不传),与 yz_backend_erp_supplier 一致。 +// 平台端字典见 PlatformDictController(/platform/dict/*)。 +type BackendDictController struct { + beego.Controller +} + +// globalDictTenantID 全局内置字典的租户标记:所有租户可见、只读, +// 与 yz_system_admin_role 中 tenant_id=0 的「全局租户角色」同一套约定。 +// 租户要改动时就按相同 dict_code 建一份自己的字典覆盖它。 +const globalDictTenantID = "0" + +var backendDictCodeRe = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]{0,99}$`) + +var backendDictTagTypes = map[string]bool{ + "": true, + "primary": true, + "success": true, + "warning": true, + "danger": true, + "info": true, +} + +func (c *BackendDictController) dictClaims() (*jwtutil.Claims, error) { + auth := c.Ctx.Request.Header.Get("Authorization") + if auth == "" { + return nil, fmt.Errorf("未登录") + } + parts := strings.SplitN(auth, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + return nil, fmt.Errorf("认证信息格式错误") + } + claims, err := jwtutil.ParseToken(parts[1]) + if err != nil { + return nil, fmt.Errorf("无效的token") + } + if claims.UserType != "backend" { + return nil, fmt.Errorf("无权访问") + } + return claims, nil +} + +func (c *BackendDictController) dictErr(httpStatus, bizCode int, msg string) { + c.Ctx.Output.SetStatus(httpStatus) + c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} + _ = c.ServeJSON() +} + +func (c *BackendDictController) dictOk(data interface{}, msg string) { + if msg == "" { + msg = "success" + } + resp := map[string]interface{}{"code": 200, "msg": msg} + if data != nil { + resp["data"] = data + } + c.Data["json"] = resp + _ = c.ServeJSON() +} + +type backendDictTypePayload struct { + DictName string `json:"dict_name"` + DictCode string `json:"dict_code"` + Remark string `json:"remark"` + Sort int `json:"sort"` + Status *int8 `json:"status"` +} + +type backendDictItemPayload struct { + DictID uint64 `json:"dict_id"` + ItemLabel string `json:"item_label"` + ItemValue string `json:"item_value"` + TagType string `json:"tag_type"` + Remark string `json:"remark"` + Sort int `json:"sort"` + Status *int8 `json:"status"` + IsDefault *int8 `json:"is_default"` +} + +// backendDictRow 字典列表行:在实体上追加「是否全局内置」,供前端禁用编辑/删除 +type backendDictRow struct { + models.BackendDict + IsGlobal bool `json:"is_global"` +} + +// dictTenantScope 当前请求可见的字典租户范围:全局内置 + 本租户自己的 +func dictTenantScope(claims *jwtutil.Claims) []string { + return []string{globalDictTenantID, strconv.Itoa(claims.TenantId)} +} + +// findVisibleDict 按编码取当前租户可见的字典:优先本租户自己的,没有再回落全局内置 +func findVisibleDict(tenantID, code string) (*models.BackendDict, error) { + var dicts []models.BackendDict + if _, err := models.Orm.QueryTable(new(models.BackendDict)). + Filter("dict_code", code). + Filter("tenant_id__in", []string{globalDictTenantID, tenantID}). + Filter("status", 1). + Filter("delete_time__isnull", true). + All(&dicts); err != nil { + return nil, err + } + for i := range dicts { + if dicts[i].TenantID == tenantID { + return &dicts[i], nil + } + } + if len(dicts) > 0 { + return &dicts[0], nil + } + return nil, orm.ErrNoRows +} + +// isGlobalDictRow 全局内置字典:任何租户都不允许修改 / 删除 +func isGlobalDictRow(tenantID string) bool { return tenantID == globalDictTenantID } + +/* ---------------- 字典列表 ---------------- */ + +// ListTypes GET /backend/dict/types +func (c *BackendDictController) ListTypes() { + claims, err := c.dictClaims() + if err != nil { + c.dictErr(401, 401, err.Error()) + return + } + page, pageSize, offset := dictPageSize(&c.Controller) + keyword := strings.TrimSpace(c.GetString("keyword")) + status := strings.TrimSpace(c.GetString("status")) + + // 可见范围:全局内置 + 本租户自己的(同编码时两条都会出现,各自带 is_global 标记) + cond := orm.NewCondition(). + And("tenant_id__in", dictTenantScope(claims)). + And("delete_time__isnull", true) + if keyword != "" { + cond = cond.AndCond(orm.NewCondition(). + Or("dict_name__contains", keyword). + Or("dict_code__contains", keyword)) + } + if status != "" { + if v, err := strconv.Atoi(status); err == nil { + cond = cond.And("status", v) + } + } + + qs := models.Orm.QueryTable(new(models.BackendDict)).SetCond(cond) + total, err := qs.Count() + if err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + var list []models.BackendDict + if _, err := qs.OrderBy("sort", "id").Limit(pageSize, offset).All(&list); err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + rows := make([]backendDictRow, 0, len(list)) + for _, r := range list { + rows = append(rows, backendDictRow{BackendDict: r, IsGlobal: isGlobalDictRow(r.TenantID)}) + } + c.dictOk(map[string]interface{}{ + "list": rows, + "total": total, + "page": page, + "pageSize": pageSize, + }, "") +} + +// CreateType POST /backend/dict/types +func (c *BackendDictController) CreateType() { + claims, err := c.dictClaims() + if err != nil { + c.dictErr(401, 401, err.Error()) + return + } + body, _ := io.ReadAll(c.Ctx.Request.Body) + var p backendDictTypePayload + if err := json.Unmarshal(body, &p); err != nil { + c.dictErr(400, 400, "参数错误") + return + } + p.DictName = strings.TrimSpace(p.DictName) + p.DictCode = strings.TrimSpace(p.DictCode) + p.Remark = strings.TrimSpace(p.Remark) + if p.DictName == "" { + c.dictErr(400, 400, "字典名称不能为空") + return + } + if !backendDictCodeRe.MatchString(p.DictCode) { + c.dictErr(400, 400, "字典编码以字母开头,只能包含字母、数字与下划线") + return + } + + tenantID := fmt.Sprintf("%d", claims.TenantId) + exist, _ := models.Orm.QueryTable(new(models.BackendDict)). + Filter("tenant_id", tenantID). + Filter("dict_code", p.DictCode). + Filter("delete_time__isnull", true). + Count() + if exist > 0 { + c.dictErr(400, 400, "字典编码已存在") + return + } + + status := int8(1) + if p.Status != nil { + status = *p.Status + } + now := time.Now() + row := &models.BackendDict{ + TenantID: tenantID, + DictName: p.DictName, + DictCode: p.DictCode, + Remark: p.Remark, + Sort: p.Sort, + Status: status, + CreateUserID: strconv.Itoa(claims.UserID), + UpdateTime: &now, + } + id, err := models.Orm.Insert(row) + if err != nil { + c.dictErr(500, 500, "新增失败: "+err.Error()) + return + } + row.ID = uint64(id) + c.dictOk(row, "新增成功") +} + +// UpdateType PUT /backend/dict/types/:id +// 编码是业务取值依据,不允许修改。 +func (c *BackendDictController) UpdateType() { + claims, err := c.dictClaims() + if err != nil { + c.dictErr(401, 401, err.Error()) + return + } + id := dictPathID(&c.Controller) + if id == 0 { + c.dictErr(400, 400, "无效的ID") + return + } + body, _ := io.ReadAll(c.Ctx.Request.Body) + var p backendDictTypePayload + if err := json.Unmarshal(body, &p); err != nil { + c.dictErr(400, 400, "参数错误") + return + } + p.DictName = strings.TrimSpace(p.DictName) + p.Remark = strings.TrimSpace(p.Remark) + if p.DictName == "" { + c.dictErr(400, 400, "字典名称不能为空") + return + } + + var row models.BackendDict + if err := models.Orm.QueryTable(new(models.BackendDict)). + Filter("id", id). + Filter("tenant_id__in", dictTenantScope(claims)). + Filter("delete_time__isnull", true). + One(&row); err != nil { + c.dictErr(404, 404, "字典不存在") + return + } + if isGlobalDictRow(row.TenantID) { + c.dictErr(403, 403, "系统内置字典不可修改,如需调整请在当前租户下按相同编码新建一份") + return + } + now := time.Now() + row.DictName = p.DictName + row.Remark = p.Remark + row.Sort = p.Sort + if p.Status != nil { + row.Status = *p.Status + } + row.UpdateTime = &now + if _, err := models.Orm.Update(&row); err != nil { + c.dictErr(500, 500, "保存失败: "+err.Error()) + return + } + c.dictOk(row, "保存成功") +} + +// DeleteType DELETE /backend/dict/types/:id +// 字典下还有字典项时拒绝删除,避免留下孤儿数据。 +func (c *BackendDictController) DeleteType() { + claims, err := c.dictClaims() + if err != nil { + c.dictErr(401, 401, err.Error()) + return + } + id := dictPathID(&c.Controller) + if id == 0 { + c.dictErr(400, 400, "无效的ID") + return + } + var row models.BackendDict + if err := models.Orm.QueryTable(new(models.BackendDict)). + Filter("id", id). + Filter("tenant_id__in", dictTenantScope(claims)). + Filter("delete_time__isnull", true). + One(&row); err != nil { + c.dictErr(404, 404, "字典不存在") + return + } + if isGlobalDictRow(row.TenantID) { + c.dictErr(403, 403, "系统内置字典不可删除,如需调整请在当前租户下按相同编码新建一份") + return + } + cnt, _ := models.Orm.QueryTable(new(models.BackendDictItem)). + Filter("dict_id", id). + Filter("delete_time__isnull", true). + Count() + if cnt > 0 { + c.dictErr(400, 400, fmt.Sprintf("该字典下还有 %d 个字典项,请先删除字典项", cnt)) + return + } + + now := time.Now() + if _, err := models.Orm.QueryTable(new(models.BackendDict)). + Filter("id", id). + Update(orm.Params{"delete_time": now, "update_time": now}); err != nil { + c.dictErr(500, 500, "删除失败: "+err.Error()) + return + } + c.dictOk(nil, "删除成功") +} + +/* ---------------- 字典项 ---------------- */ + +// ListItems GET /backend/dict/items +func (c *BackendDictController) ListItems() { + claims, err := c.dictClaims() + if err != nil { + c.dictErr(401, 401, err.Error()) + return + } + dictID, _ := c.GetInt64("dict_id") + if dictID <= 0 { + c.dictErr(400, 400, "dict_id 不能为空") + return + } + // 字典必须在本租户可见范围内(自己的或全局内置),避免越权读取他人字典 + var dict models.BackendDict + if err := models.Orm.QueryTable(new(models.BackendDict)). + Filter("id", dictID). + Filter("tenant_id__in", dictTenantScope(claims)). + Filter("delete_time__isnull", true). + One(&dict); err != nil { + c.dictErr(404, 404, "字典不存在") + return + } + + page, pageSize, offset := dictPageSize(&c.Controller) + keyword := strings.TrimSpace(c.GetString("keyword")) + status := strings.TrimSpace(c.GetString("status")) + + // 字典项跟随所属字典的租户:全局内置字典的项对所有租户可见 + cond := orm.NewCondition(). + And("tenant_id", dict.TenantID). + And("dict_id", dictID). + And("delete_time__isnull", true) + if keyword != "" { + cond = cond.AndCond(orm.NewCondition(). + Or("item_label__contains", keyword). + Or("item_value__contains", keyword)) + } + if status != "" { + if v, err := strconv.Atoi(status); err == nil { + cond = cond.And("status", v) + } + } + + qs := models.Orm.QueryTable(new(models.BackendDictItem)).SetCond(cond) + total, err := qs.Count() + if err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + var list []models.BackendDictItem + if _, err := qs.OrderBy("sort", "id").Limit(pageSize, offset).All(&list); err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + c.dictOk(map[string]interface{}{ + "list": list, + "total": total, + "page": page, + "pageSize": pageSize, + }, "") +} + +// CreateItem POST /backend/dict/items +func (c *BackendDictController) CreateItem() { + claims, err := c.dictClaims() + if err != nil { + c.dictErr(401, 401, err.Error()) + return + } + body, _ := io.ReadAll(c.Ctx.Request.Body) + var p backendDictItemPayload + if err := json.Unmarshal(body, &p); err != nil { + c.dictErr(400, 400, "参数错误") + return + } + p.ItemLabel = strings.TrimSpace(p.ItemLabel) + p.ItemValue = strings.TrimSpace(p.ItemValue) + p.TagType = strings.TrimSpace(p.TagType) + p.Remark = strings.TrimSpace(p.Remark) + if p.DictID == 0 { + c.dictErr(400, 400, "dict_id 不能为空") + return + } + if p.ItemLabel == "" { + c.dictErr(400, 400, "字典项名称不能为空") + return + } + if p.ItemValue == "" { + c.dictErr(400, 400, "字典项值不能为空") + return + } + if !backendDictTagTypes[p.TagType] { + c.dictErr(400, 400, "标签色只能是 primary/success/warning/danger/info 之一") + return + } + + tenantID := fmt.Sprintf("%d", claims.TenantId) + var dict models.BackendDict + if err := models.Orm.QueryTable(new(models.BackendDict)). + Filter("id", p.DictID). + Filter("tenant_id__in", dictTenantScope(claims)). + Filter("delete_time__isnull", true). + One(&dict); err != nil { + c.dictErr(404, 404, "字典不存在") + return + } + if isGlobalDictRow(dict.TenantID) { + c.dictErr(403, 403, "系统内置字典不可新增字典项,如需调整请在当前租户下按相同编码新建一份") + return + } + cnt, _ := models.Orm.QueryTable(new(models.BackendDictItem)). + Filter("tenant_id", tenantID). + Filter("dict_id", p.DictID). + Filter("item_value", p.ItemValue). + Filter("delete_time__isnull", true). + Count() + if cnt > 0 { + c.dictErr(400, 400, "同一字典下的字典项值不能重复") + return + } + + status := int8(1) + if p.Status != nil { + status = *p.Status + } + isDefault := int8(0) + if p.IsDefault != nil { + isDefault = *p.IsDefault + } + now := time.Now() + row := &models.BackendDictItem{ + TenantID: tenantID, + DictID: p.DictID, + ItemLabel: p.ItemLabel, + ItemValue: p.ItemValue, + TagType: p.TagType, + Sort: p.Sort, + Status: status, + IsDefault: isDefault, + Remark: p.Remark, + UpdateTime: &now, + } + id, err := models.Orm.Insert(row) + if err != nil { + c.dictErr(500, 500, "新增失败: "+err.Error()) + return + } + row.ID = uint64(id) + c.dictOk(row, "新增成功") +} + +// UpdateItem PUT /backend/dict/items/:id +// 字典项值是业务入库值,不允许修改,避免存量数据对不上。 +func (c *BackendDictController) UpdateItem() { + claims, err := c.dictClaims() + if err != nil { + c.dictErr(401, 401, err.Error()) + return + } + id := dictPathID(&c.Controller) + if id == 0 { + c.dictErr(400, 400, "无效的ID") + return + } + body, _ := io.ReadAll(c.Ctx.Request.Body) + var p backendDictItemPayload + if err := json.Unmarshal(body, &p); err != nil { + c.dictErr(400, 400, "参数错误") + return + } + p.ItemLabel = strings.TrimSpace(p.ItemLabel) + p.TagType = strings.TrimSpace(p.TagType) + p.Remark = strings.TrimSpace(p.Remark) + if p.ItemLabel == "" { + c.dictErr(400, 400, "字典项名称不能为空") + return + } + if !backendDictTagTypes[p.TagType] { + c.dictErr(400, 400, "标签色只能是 primary/success/warning/danger/info 之一") + return + } + + var row models.BackendDictItem + if err := models.Orm.QueryTable(new(models.BackendDictItem)). + Filter("id", id). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + Filter("delete_time__isnull", true). + One(&row); err != nil { + c.dictErr(404, 404, "字典项不存在") + return + } + now := time.Now() + row.ItemLabel = p.ItemLabel + row.TagType = p.TagType + row.Sort = p.Sort + row.Remark = p.Remark + if p.Status != nil { + row.Status = *p.Status + } + if p.IsDefault != nil { + row.IsDefault = *p.IsDefault + } + row.UpdateTime = &now + if _, err := models.Orm.Update(&row); err != nil { + c.dictErr(500, 500, "保存失败: "+err.Error()) + return + } + c.dictOk(row, "保存成功") +} + +// ToggleItemStatus POST /backend/dict/items/:id/status +func (c *BackendDictController) ToggleItemStatus() { + claims, err := c.dictClaims() + if err != nil { + c.dictErr(401, 401, err.Error()) + return + } + id := dictPathID(&c.Controller) + if id == 0 { + c.dictErr(400, 400, "无效的ID") + return + } + body, _ := io.ReadAll(c.Ctx.Request.Body) + var p struct { + Status *int8 `json:"status"` + } + if err := json.Unmarshal(body, &p); err != nil || p.Status == nil { + c.dictErr(400, 400, "参数错误") + return + } + if *p.Status != 0 && *p.Status != 1 { + c.dictErr(400, 400, "状态只能是 0 或 1") + return + } + + tenantID := fmt.Sprintf("%d", claims.TenantId) + var row models.BackendDictItem + if err := models.Orm.QueryTable(new(models.BackendDictItem)). + Filter("id", id). + Filter("tenant_id", tenantID). + Filter("delete_time__isnull", true). + One(&row); err != nil { + c.dictErr(404, 404, "字典项不存在") + return + } + now := time.Now() + if _, err := models.Orm.QueryTable(new(models.BackendDictItem)). + Filter("id", id). + Update(orm.Params{"status": *p.Status, "update_time": now}); err != nil { + c.dictErr(500, 500, "操作失败: "+err.Error()) + return + } + c.dictOk(nil, "操作成功") +} + +// DeleteItem DELETE /backend/dict/items/:id +func (c *BackendDictController) DeleteItem() { + claims, err := c.dictClaims() + if err != nil { + c.dictErr(401, 401, err.Error()) + return + } + id := dictPathID(&c.Controller) + if id == 0 { + c.dictErr(400, 400, "无效的ID") + return + } + var row models.BackendDictItem + if err := models.Orm.QueryTable(new(models.BackendDictItem)). + Filter("id", id). + Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)). + Filter("delete_time__isnull", true). + One(&row); err != nil { + c.dictErr(404, 404, "字典项不存在") + return + } + now := time.Now() + if _, err := models.Orm.QueryTable(new(models.BackendDictItem)). + Filter("id", id). + Update(orm.Params{"delete_time": now, "update_time": now}); err != nil { + c.dictErr(500, 500, "删除失败: "+err.Error()) + return + } + c.dictOk(nil, "删除成功") +} + +/* ---------------- 业务读取 ---------------- */ + +// OptionsAll GET /backend/dict/options +// 一次取回本租户可见的全部「启用」字典项(全局内置 + 本租户自己的,同编码租户优先), +// 供前端登录后预热缓存:之后页面按任意 dict_code 都能同步读到。 +func (c *BackendDictController) OptionsAll() { + claims, err := c.dictClaims() + if err != nil { + c.dictErr(401, 401, err.Error()) + return + } + tenantID := strconv.Itoa(claims.TenantId) + result := map[string][]models.BackendDictItem{} + + var dicts []models.BackendDict + if _, err := models.Orm.QueryTable(new(models.BackendDict)). + Filter("tenant_id__in", dictTenantScope(claims)). + Filter("status", 1). + Filter("delete_time__isnull", true). + All(&dicts); err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + // 同编码优先本租户:先记录全局,再用租户自己的覆盖 + chosen := map[string]uint64{} + for _, d := range dicts { + if _, ok := chosen[d.DictCode]; !ok || d.TenantID == tenantID { + chosen[d.DictCode] = d.ID + } + } + if len(chosen) == 0 { + c.dictOk(map[string]interface{}{"dicts": result}, "") + return + } + + ids := make([]uint64, 0, len(chosen)) + codeByID := make(map[uint64]string, len(chosen)) + for code, id := range chosen { + ids = append(ids, id) + codeByID[id] = code + result[code] = []models.BackendDictItem{} + } + + var items []models.BackendDictItem + if _, err := models.Orm.QueryTable(new(models.BackendDictItem)). + Filter("dict_id__in", ids). + Filter("status", 1). + Filter("delete_time__isnull", true). + OrderBy("sort", "id"). + All(&items); err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + for _, it := range items { + code := codeByID[it.DictID] + result[code] = append(result[code], it) + } + c.dictOk(map[string]interface{}{"dicts": result}, "") +} + +// OptionsBatch POST /backend/dict/options +// 一次取多个字典的启用项,供前端预加载(body: {"codes":[...]})。 +// 同编码时优先本租户自己的字典,没有再回落全局内置字典。 +func (c *BackendDictController) OptionsBatch() { + claims, err := c.dictClaims() + if err != nil { + c.dictErr(401, 401, err.Error()) + return + } + body, _ := io.ReadAll(c.Ctx.Request.Body) + var p struct { + Codes []string `json:"codes"` + } + if err := json.Unmarshal(body, &p); err != nil { + c.dictErr(400, 400, "参数错误") + return + } + + result := map[string][]models.BackendDictItem{} + codes := make([]string, 0, len(p.Codes)) + for _, code := range p.Codes { + code = strings.TrimSpace(code) + if code == "" { + continue + } + if _, ok := result[code]; ok { + continue + } + result[code] = []models.BackendDictItem{} + codes = append(codes, code) + } + if len(codes) == 0 { + c.dictOk(map[string]interface{}{"dicts": result}, "") + return + } + + tenantID := strconv.Itoa(claims.TenantId) + var dicts []models.BackendDict + if _, err := models.Orm.QueryTable(new(models.BackendDict)). + Filter("dict_code__in", codes). + Filter("tenant_id__in", dictTenantScope(claims)). + Filter("status", 1). + Filter("delete_time__isnull", true). + All(&dicts); err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + + // 同编码优先本租户:先记录全局,再用租户自己的覆盖 + chosen := map[string]uint64{} + for _, d := range dicts { + if _, ok := chosen[d.DictCode]; !ok || d.TenantID == tenantID { + chosen[d.DictCode] = d.ID + } + } + if len(chosen) == 0 { + c.dictOk(map[string]interface{}{"dicts": result}, "") + return + } + + ids := make([]uint64, 0, len(chosen)) + codeByID := make(map[uint64]string, len(chosen)) + for code, id := range chosen { + ids = append(ids, id) + codeByID[id] = code + } + + var items []models.BackendDictItem + if _, err := models.Orm.QueryTable(new(models.BackendDictItem)). + Filter("dict_id__in", ids). + Filter("status", 1). + Filter("delete_time__isnull", true). + OrderBy("sort", "id"). + All(&items); err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + for _, it := range items { + code := codeByID[it.DictID] + result[code] = append(result[code], it) + } + c.dictOk(map[string]interface{}{"dicts": result}, "") +} + +// Options GET /backend/dict/options/:code +// 业务页面按编码取选项:只返回「字典与字典项都启用」的项,按 sort 升序。 +func (c *BackendDictController) Options() { + claims, err := c.dictClaims() + if err != nil { + c.dictErr(401, 401, err.Error()) + return + } + code := strings.TrimSpace(c.Ctx.Input.Param(":code")) + if code == "" { + c.dictErr(400, 400, "字典编码不能为空") + return + } + + // 优先取本租户自己的字典,没有再回落全局内置字典 + dict, err := findVisibleDict(strconv.Itoa(claims.TenantId), code) + if err != nil { + c.dictErr(404, 404, "字典不存在或已停用") + return + } + + var list []models.BackendDictItem + if _, err := models.Orm.QueryTable(new(models.BackendDictItem)). + Filter("dict_id", dict.ID). + Filter("status", 1). + Filter("delete_time__isnull", true). + OrderBy("sort", "id"). + All(&list); err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + c.dictOk(map[string]interface{}{ + "dict_id": dict.ID, + "dict_code": dict.DictCode, + "dict_name": dict.DictName, + "list": list, + }, "") +} diff --git a/go/controllers/platform_dict.go b/go/controllers/platform_dict.go new file mode 100644 index 0000000..27872d0 --- /dev/null +++ b/go/controllers/platform_dict.go @@ -0,0 +1,733 @@ +package controllers + +import ( + "encoding/json" + "fmt" + "io" + "regexp" + "strconv" + "strings" + "time" + + "server/models" + "server/pkg/jwtutil" + + "github.com/beego/beego/v2/client/orm" + beego "github.com/beego/beego/v2/server/web" +) + +// PlatformDictController 平台端数据字典:字典列表 + 字典项 +// +// 表:yz_platform_dict / yz_platform_dict_item(建表 SQL:docs/sql/create_dict.sql)。 +// 平台端字典不按租户隔离;租户端字典见 BackendDictController(/backend/dict/*)。 +type PlatformDictController struct { + beego.Controller +} + +// dictCodeRe 字典编码:字母开头,仅字母/数字/下划线 +var dictCodeRe = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]{0,99}$`) + +// dictTagTypes 字典项标签色(与前端 el-tag 的 type 对齐),空串表示不渲染标签 +var dictTagTypes = map[string]bool{ + "": true, + "primary": true, + "success": true, + "warning": true, + "danger": true, + "info": true, +} + +func (c *PlatformDictController) dictClaims() (*jwtutil.Claims, error) { + auth := c.Ctx.Request.Header.Get("Authorization") + if auth == "" { + return nil, fmt.Errorf("未登录") + } + parts := strings.SplitN(auth, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + return nil, fmt.Errorf("认证信息格式错误") + } + claims, err := jwtutil.ParseToken(parts[1]) + if err != nil { + return nil, fmt.Errorf("无效的token") + } + if claims.UserType != "platform" { + return nil, fmt.Errorf("无权访问") + } + return claims, nil +} + +func (c *PlatformDictController) dictErr(httpStatus, bizCode int, msg string) { + c.Ctx.Output.SetStatus(httpStatus) + c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} + _ = c.ServeJSON() +} + +func (c *PlatformDictController) dictOk(data interface{}, msg string) { + if msg == "" { + msg = "success" + } + resp := map[string]interface{}{"code": 200, "msg": msg} + if data != nil { + resp["data"] = data + } + c.Data["json"] = resp + _ = c.ServeJSON() +} + +// dictPageSize 统一分页参数处理:page 从 1 开始,pageSize 限制在 1~100 +func dictPageSize(c *beego.Controller) (page, pageSize, offset int) { + page, _ = c.GetInt("page", 1) + pageSize, _ = c.GetInt("pageSize", 20) + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + return page, pageSize, (page - 1) * pageSize +} + +type dictTypePayload struct { + DictName string `json:"dict_name"` + DictCode string `json:"dict_code"` + Remark string `json:"remark"` + Sort int `json:"sort"` + Status *int8 `json:"status"` +} + +type dictItemPayload struct { + DictID uint64 `json:"dict_id"` + ItemLabel string `json:"item_label"` + ItemValue string `json:"item_value"` + TagType string `json:"tag_type"` + Remark string `json:"remark"` + Sort int `json:"sort"` + Status *int8 `json:"status"` + IsDefault *int8 `json:"is_default"` +} + +func parseDictTypeBody(c *beego.Controller) (*dictTypePayload, error) { + body, _ := io.ReadAll(c.Ctx.Request.Body) + var p dictTypePayload + if err := json.Unmarshal(body, &p); err != nil { + return nil, fmt.Errorf("参数错误") + } + p.DictName = strings.TrimSpace(p.DictName) + p.DictCode = strings.TrimSpace(p.DictCode) + p.Remark = strings.TrimSpace(p.Remark) + return &p, nil +} + +func parseDictItemBody(c *beego.Controller) (*dictItemPayload, error) { + body, _ := io.ReadAll(c.Ctx.Request.Body) + var p dictItemPayload + if err := json.Unmarshal(body, &p); err != nil { + return nil, fmt.Errorf("参数错误") + } + p.ItemLabel = strings.TrimSpace(p.ItemLabel) + p.ItemValue = strings.TrimSpace(p.ItemValue) + p.TagType = strings.TrimSpace(p.TagType) + p.Remark = strings.TrimSpace(p.Remark) + return &p, nil +} + +func dictPathID(c *beego.Controller) uint64 { + id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + return id +} + +/* ---------------- 字典列表 ---------------- */ + +// ListTypes GET /platform/dict/types +// 参数:page / pageSize / keyword(名称或编码模糊)/ status +func (c *PlatformDictController) ListTypes() { + if _, err := c.dictClaims(); err != nil { + c.dictErr(401, 401, err.Error()) + return + } + page, pageSize, offset := dictPageSize(&c.Controller) + keyword := strings.TrimSpace(c.GetString("keyword")) + status := strings.TrimSpace(c.GetString("status")) + + cond := orm.NewCondition().And("delete_time__isnull", true) + if keyword != "" { + cond = cond.AndCond(orm.NewCondition(). + Or("dict_name__contains", keyword). + Or("dict_code__contains", keyword)) + } + if status != "" { + if v, err := strconv.Atoi(status); err == nil { + cond = cond.And("status", v) + } + } + + qs := models.Orm.QueryTable(new(models.PlatformDict)).SetCond(cond) + total, err := qs.Count() + if err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + var list []models.PlatformDict + if _, err := qs.OrderBy("sort", "id").Limit(pageSize, offset).All(&list); err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + c.dictOk(map[string]interface{}{ + "list": list, + "total": total, + "page": page, + "pageSize": pageSize, + }, "") +} + +// CreateType POST /platform/dict/types +func (c *PlatformDictController) CreateType() { + claims, err := c.dictClaims() + if err != nil { + c.dictErr(401, 401, err.Error()) + return + } + p, err := parseDictTypeBody(&c.Controller) + if err != nil { + c.dictErr(400, 400, err.Error()) + return + } + if p.DictName == "" { + c.dictErr(400, 400, "字典名称不能为空") + return + } + if !dictCodeRe.MatchString(p.DictCode) { + c.dictErr(400, 400, "字典编码以字母开头,只能包含字母、数字与下划线") + return + } + exist, _ := models.Orm.QueryTable(new(models.PlatformDict)). + Filter("dict_code", p.DictCode). + Filter("delete_time__isnull", true). + Count() + if exist > 0 { + c.dictErr(400, 400, "字典编码已存在") + return + } + + status := int8(1) + if p.Status != nil { + status = *p.Status + } + now := time.Now() + row := &models.PlatformDict{ + DictName: p.DictName, + DictCode: p.DictCode, + Remark: p.Remark, + Sort: p.Sort, + Status: status, + CreateUserID: strconv.Itoa(claims.UserID), + UpdateTime: &now, + } + id, err := models.Orm.Insert(row) + if err != nil { + c.dictErr(500, 500, "新增失败: "+err.Error()) + return + } + row.ID = uint64(id) + c.dictOk(row, "新增成功") +} + +// UpdateType PUT /platform/dict/types/:id +// 编码是业务取值依据,不允许修改。 +func (c *PlatformDictController) UpdateType() { + if _, err := c.dictClaims(); err != nil { + c.dictErr(401, 401, err.Error()) + return + } + id := dictPathID(&c.Controller) + if id == 0 { + c.dictErr(400, 400, "无效的ID") + return + } + p, err := parseDictTypeBody(&c.Controller) + if err != nil { + c.dictErr(400, 400, err.Error()) + return + } + if p.DictName == "" { + c.dictErr(400, 400, "字典名称不能为空") + return + } + + var row models.PlatformDict + if err := models.Orm.QueryTable(new(models.PlatformDict)). + Filter("id", id). + Filter("delete_time__isnull", true). + One(&row); err != nil { + c.dictErr(404, 404, "字典不存在") + return + } + now := time.Now() + row.DictName = p.DictName + row.Remark = p.Remark + row.Sort = p.Sort + if p.Status != nil { + row.Status = *p.Status + } + row.UpdateTime = &now + if _, err := models.Orm.Update(&row); err != nil { + c.dictErr(500, 500, "保存失败: "+err.Error()) + return + } + c.dictOk(row, "保存成功") +} + +// DeleteType DELETE /platform/dict/types/:id +// 字典下还有字典项时拒绝删除,避免留下孤儿数据。 +func (c *PlatformDictController) DeleteType() { + if _, err := c.dictClaims(); err != nil { + c.dictErr(401, 401, err.Error()) + return + } + id := dictPathID(&c.Controller) + if id == 0 { + c.dictErr(400, 400, "无效的ID") + return + } + var row models.PlatformDict + if err := models.Orm.QueryTable(new(models.PlatformDict)). + Filter("id", id). + Filter("delete_time__isnull", true). + One(&row); err != nil { + c.dictErr(404, 404, "字典不存在") + return + } + cnt, _ := models.Orm.QueryTable(new(models.PlatformDictItem)). + Filter("dict_id", id). + Filter("delete_time__isnull", true). + Count() + if cnt > 0 { + c.dictErr(400, 400, fmt.Sprintf("该字典下还有 %d 个字典项,请先删除字典项", cnt)) + return + } + + now := time.Now() + if _, err := models.Orm.QueryTable(new(models.PlatformDict)). + Filter("id", id). + Update(orm.Params{"delete_time": now, "update_time": now}); err != nil { + c.dictErr(500, 500, "删除失败: "+err.Error()) + return + } + c.dictOk(nil, "删除成功") +} + +/* ---------------- 字典项 ---------------- */ + +// ListItems GET /platform/dict/items +// 参数:dict_id(必填)/ page / pageSize / keyword(名称或值模糊)/ status +func (c *PlatformDictController) ListItems() { + if _, err := c.dictClaims(); err != nil { + c.dictErr(401, 401, err.Error()) + return + } + dictID, _ := c.GetInt64("dict_id") + if dictID <= 0 { + c.dictErr(400, 400, "dict_id 不能为空") + return + } + page, pageSize, offset := dictPageSize(&c.Controller) + keyword := strings.TrimSpace(c.GetString("keyword")) + status := strings.TrimSpace(c.GetString("status")) + + cond := orm.NewCondition(). + And("delete_time__isnull", true). + And("dict_id", dictID) + if keyword != "" { + cond = cond.AndCond(orm.NewCondition(). + Or("item_label__contains", keyword). + Or("item_value__contains", keyword)) + } + if status != "" { + if v, err := strconv.Atoi(status); err == nil { + cond = cond.And("status", v) + } + } + + qs := models.Orm.QueryTable(new(models.PlatformDictItem)).SetCond(cond) + total, err := qs.Count() + if err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + var list []models.PlatformDictItem + if _, err := qs.OrderBy("sort", "id").Limit(pageSize, offset).All(&list); err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + c.dictOk(map[string]interface{}{ + "list": list, + "total": total, + "page": page, + "pageSize": pageSize, + }, "") +} + +// CreateItem POST /platform/dict/items +func (c *PlatformDictController) CreateItem() { + if _, err := c.dictClaims(); err != nil { + c.dictErr(401, 401, err.Error()) + return + } + p, err := parseDictItemBody(&c.Controller) + if err != nil { + c.dictErr(400, 400, err.Error()) + return + } + if p.DictID == 0 { + c.dictErr(400, 400, "dict_id 不能为空") + return + } + if p.ItemLabel == "" { + c.dictErr(400, 400, "字典项名称不能为空") + return + } + if p.ItemValue == "" { + c.dictErr(400, 400, "字典项值不能为空") + return + } + if !dictTagTypes[p.TagType] { + c.dictErr(400, 400, "标签色只能是 primary/success/warning/danger/info 之一") + return + } + + var dict models.PlatformDict + if err := models.Orm.QueryTable(new(models.PlatformDict)). + Filter("id", p.DictID). + Filter("delete_time__isnull", true). + One(&dict); err != nil { + c.dictErr(404, 404, "字典不存在") + return + } + cnt, _ := models.Orm.QueryTable(new(models.PlatformDictItem)). + Filter("dict_id", p.DictID). + Filter("item_value", p.ItemValue). + Filter("delete_time__isnull", true). + Count() + if cnt > 0 { + c.dictErr(400, 400, "同一字典下的字典项值不能重复") + return + } + + status := int8(1) + if p.Status != nil { + status = *p.Status + } + isDefault := int8(0) + if p.IsDefault != nil { + isDefault = *p.IsDefault + } + now := time.Now() + row := &models.PlatformDictItem{ + DictID: p.DictID, + ItemLabel: p.ItemLabel, + ItemValue: p.ItemValue, + TagType: p.TagType, + Sort: p.Sort, + Status: status, + IsDefault: isDefault, + Remark: p.Remark, + UpdateTime: &now, + } + id, err := models.Orm.Insert(row) + if err != nil { + c.dictErr(500, 500, "新增失败: "+err.Error()) + return + } + row.ID = uint64(id) + c.dictOk(row, "新增成功") +} + +// UpdateItem PUT /platform/dict/items/:id +// 字典项值是业务入库值,不允许修改,避免存量数据对不上。 +func (c *PlatformDictController) UpdateItem() { + if _, err := c.dictClaims(); err != nil { + c.dictErr(401, 401, err.Error()) + return + } + id := dictPathID(&c.Controller) + if id == 0 { + c.dictErr(400, 400, "无效的ID") + return + } + p, err := parseDictItemBody(&c.Controller) + if err != nil { + c.dictErr(400, 400, err.Error()) + return + } + if p.ItemLabel == "" { + c.dictErr(400, 400, "字典项名称不能为空") + return + } + if !dictTagTypes[p.TagType] { + c.dictErr(400, 400, "标签色只能是 primary/success/warning/danger/info 之一") + return + } + + var row models.PlatformDictItem + if err := models.Orm.QueryTable(new(models.PlatformDictItem)). + Filter("id", id). + Filter("delete_time__isnull", true). + One(&row); err != nil { + c.dictErr(404, 404, "字典项不存在") + return + } + now := time.Now() + row.ItemLabel = p.ItemLabel + row.TagType = p.TagType + row.Sort = p.Sort + row.Remark = p.Remark + if p.Status != nil { + row.Status = *p.Status + } + if p.IsDefault != nil { + row.IsDefault = *p.IsDefault + } + row.UpdateTime = &now + if _, err := models.Orm.Update(&row); err != nil { + c.dictErr(500, 500, "保存失败: "+err.Error()) + return + } + c.dictOk(row, "保存成功") +} + +// ToggleItemStatus POST /platform/dict/items/:id/status +func (c *PlatformDictController) ToggleItemStatus() { + if _, err := c.dictClaims(); err != nil { + c.dictErr(401, 401, err.Error()) + return + } + id := dictPathID(&c.Controller) + if id == 0 { + c.dictErr(400, 400, "无效的ID") + return + } + body, _ := io.ReadAll(c.Ctx.Request.Body) + var p struct { + Status *int8 `json:"status"` + } + if err := json.Unmarshal(body, &p); err != nil || p.Status == nil { + c.dictErr(400, 400, "参数错误") + return + } + if *p.Status != 0 && *p.Status != 1 { + c.dictErr(400, 400, "状态只能是 0 或 1") + return + } + + var row models.PlatformDictItem + if err := models.Orm.QueryTable(new(models.PlatformDictItem)). + Filter("id", id). + Filter("delete_time__isnull", true). + One(&row); err != nil { + c.dictErr(404, 404, "字典项不存在") + return + } + now := time.Now() + if _, err := models.Orm.QueryTable(new(models.PlatformDictItem)). + Filter("id", id). + Update(orm.Params{"status": *p.Status, "update_time": now}); err != nil { + c.dictErr(500, 500, "操作失败: "+err.Error()) + return + } + c.dictOk(nil, "操作成功") +} + +// DeleteItem DELETE /platform/dict/items/:id +func (c *PlatformDictController) DeleteItem() { + if _, err := c.dictClaims(); err != nil { + c.dictErr(401, 401, err.Error()) + return + } + id := dictPathID(&c.Controller) + if id == 0 { + c.dictErr(400, 400, "无效的ID") + return + } + var row models.PlatformDictItem + if err := models.Orm.QueryTable(new(models.PlatformDictItem)). + Filter("id", id). + Filter("delete_time__isnull", true). + One(&row); err != nil { + c.dictErr(404, 404, "字典项不存在") + return + } + now := time.Now() + if _, err := models.Orm.QueryTable(new(models.PlatformDictItem)). + Filter("id", id). + Update(orm.Params{"delete_time": now, "update_time": now}); err != nil { + c.dictErr(500, 500, "删除失败: "+err.Error()) + return + } + c.dictOk(nil, "删除成功") +} + +/* ---------------- 业务读取 ---------------- */ + +// OptionsAll GET /platform/dict/options +// 一次取回全部「启用」字典的字典项,供前端登录后预热缓存: +// 之后页面按任意 dict_code 都能同步读到,新增字典不用改前端。 +func (c *PlatformDictController) OptionsAll() { + if _, err := c.dictClaims(); err != nil { + c.dictErr(401, 401, err.Error()) + return + } + result := map[string][]models.PlatformDictItem{} + + var dicts []models.PlatformDict + if _, err := models.Orm.QueryTable(new(models.PlatformDict)). + Filter("status", 1). + Filter("delete_time__isnull", true). + All(&dicts); err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + if len(dicts) == 0 { + c.dictOk(map[string]interface{}{"dicts": result}, "") + return + } + + ids := make([]uint64, 0, len(dicts)) + codeByID := make(map[uint64]string, len(dicts)) + for _, d := range dicts { + ids = append(ids, d.ID) + codeByID[d.ID] = d.DictCode + result[d.DictCode] = []models.PlatformDictItem{} + } + + var items []models.PlatformDictItem + if _, err := models.Orm.QueryTable(new(models.PlatformDictItem)). + Filter("dict_id__in", ids). + Filter("status", 1). + Filter("delete_time__isnull", true). + OrderBy("sort", "id"). + All(&items); err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + for _, it := range items { + code := codeByID[it.DictID] + result[code] = append(result[code], it) + } + c.dictOk(map[string]interface{}{"dicts": result}, "") +} + +// OptionsBatch POST /platform/dict/options +// 一次取多个字典的启用项,供前端预加载(body: {"codes":["payment_channel",...]})。 +// 编码不存在或已停用时返回空数组,前端不必再单独重试。 +// 注意:这里刻意与 GET /options/:code 分开成「同段不同方法」,避免静态段与通配段同级。 +func (c *PlatformDictController) OptionsBatch() { + if _, err := c.dictClaims(); err != nil { + c.dictErr(401, 401, err.Error()) + return + } + body, _ := io.ReadAll(c.Ctx.Request.Body) + var p struct { + Codes []string `json:"codes"` + } + if err := json.Unmarshal(body, &p); err != nil { + c.dictErr(400, 400, "参数错误") + return + } + + result := map[string][]models.PlatformDictItem{} + codes := make([]string, 0, len(p.Codes)) + for _, code := range p.Codes { + code = strings.TrimSpace(code) + if code == "" { + continue + } + if _, ok := result[code]; ok { + continue + } + result[code] = []models.PlatformDictItem{} + codes = append(codes, code) + } + if len(codes) == 0 { + c.dictOk(map[string]interface{}{"dicts": result}, "") + return + } + + // 先按编码取字典,再按字典ID一次取回全部字典项,避免逐个字典查一轮(N+1) + var dicts []models.PlatformDict + if _, err := models.Orm.QueryTable(new(models.PlatformDict)). + Filter("dict_code__in", codes). + Filter("status", 1). + Filter("delete_time__isnull", true). + All(&dicts); err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + if len(dicts) == 0 { + c.dictOk(map[string]interface{}{"dicts": result}, "") + return + } + + ids := make([]uint64, 0, len(dicts)) + codeByID := make(map[uint64]string, len(dicts)) + for _, d := range dicts { + ids = append(ids, d.ID) + codeByID[d.ID] = d.DictCode + } + + var items []models.PlatformDictItem + if _, err := models.Orm.QueryTable(new(models.PlatformDictItem)). + Filter("dict_id__in", ids). + Filter("status", 1). + Filter("delete_time__isnull", true). + OrderBy("sort", "id"). + All(&items); err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + for _, it := range items { + code := codeByID[it.DictID] + result[code] = append(result[code], it) + } + c.dictOk(map[string]interface{}{"dicts": result}, "") +} + +// Options GET /platform/dict/options/:code +// 业务页面按编码取选项:只返回「字典与字典项都启用」的项,按 sort 升序。 +func (c *PlatformDictController) Options() { + if _, err := c.dictClaims(); err != nil { + c.dictErr(401, 401, err.Error()) + return + } + code := strings.TrimSpace(c.Ctx.Input.Param(":code")) + if code == "" { + c.dictErr(400, 400, "字典编码不能为空") + return + } + + var dict models.PlatformDict + if err := models.Orm.QueryTable(new(models.PlatformDict)). + Filter("dict_code", code). + Filter("status", 1). + Filter("delete_time__isnull", true). + One(&dict); err != nil { + c.dictErr(404, 404, "字典不存在或已停用") + return + } + + var list []models.PlatformDictItem + if _, err := models.Orm.QueryTable(new(models.PlatformDictItem)). + Filter("dict_id", dict.ID). + Filter("status", 1). + Filter("delete_time__isnull", true). + OrderBy("sort", "id"). + All(&list); err != nil { + c.dictErr(500, 500, "查询失败: "+err.Error()) + return + } + c.dictOk(map[string]interface{}{ + "dict_id": dict.ID, + "dict_code": dict.DictCode, + "dict_name": dict.DictName, + "list": list, + }, "") +} diff --git a/go/docs/sql/create_dict.sql b/go/docs/sql/create_dict.sql new file mode 100644 index 0000000..cb49e4f --- /dev/null +++ b/go/docs/sql/create_dict.sql @@ -0,0 +1,112 @@ +-- ============================================================= +-- 数据字典 —— 平台端 / 租户端各一套 +-- +-- 表清单: +-- yz_platform_dict 平台端「字典列表」(字典类型) +-- yz_platform_dict_item 平台端「字典项」 +-- yz_backend_dict 租户端「字典列表」(按 tenant_id 租户隔离) +-- yz_backend_dict_item 租户端「字典项」 +-- +-- 设计说明: +-- 1. 平台端字典与租户端字典完全独立:平台端字典供平台系统自身使用 +-- (不按租户隔离);租户端字典是每个租户可自行维护的业务选项 +-- (如供应商类型、结算方式),按 tenant_id 隔离,与 +-- yz_backend_erp_supplier 的隔离方式保持一致。 +-- 2. 字典项通过 dict_id 关联字典;租户端额外冗余 tenant_id, +-- 便于不带 JOIN 直接按租户过滤,写入时须与父字典 tenant_id 一致。 +-- 3. tag_type 对应前端 el-tag 的 type(primary/success/warning/danger/info), +-- 留空表示该项在列表里不渲染彩色标签。 +-- 4. 与 yz_backend_erp_* / yz_backend_crm_* 等表一致,这里只建普通索引、 +-- 不建唯一索引:一是软删除(delete_time)后编码仍会占用唯一键, +-- 二是库中既有表也普遍不用唯一索引。dict_code / item_value 的唯一性 +-- 由后端接口在写入时校验。 +-- 5. 不显式指定 COLLATE,沿用库默认排序规则,避免 JOIN 时报 +-- Illegal mix of collations。 +-- +-- 执行方式:人工在数据库执行,代码中不做任何自动建表 / 补列。 +-- 说明:下方 DDL 幂等(IF NOT EXISTS),可重复执行。 +-- ============================================================= + +SET NAMES utf8mb4; + + +-- ============================================================= +-- 平台端 +-- ============================================================= + +CREATE TABLE IF NOT EXISTS `yz_platform_dict` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `dict_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '字典名称,如:供应商类型', + `dict_code` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '字典编码(业务唯一标识,后端按此取字典),如:supplier_type', + `remark` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序,数字越小越靠前', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1 启用 0 停用', + `create_user_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人用户ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `delete_time` DATETIME DEFAULT NULL COMMENT '删除时间(软删除)', + PRIMARY KEY (`id`), + KEY `idx_dict_code` (`dict_code`), + KEY `idx_status_sort` (`status`, `sort`, `id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台端-数据字典(字典列表)'; + + +CREATE TABLE IF NOT EXISTS `yz_platform_dict_item` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `dict_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '所属字典ID(yz_platform_dict.id)', + `item_label` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '字典项名称(显示文案),如:原材料供应商', + `item_value` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '字典项值(业务入库值),如:1', + `tag_type` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '标签色(el-tag type):primary/success/warning/danger/info,留空不渲染标签', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序,数字越小越靠前', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1 启用 0 停用', + `is_default` TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认选中:0 否 1 是', + `remark` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `delete_time` DATETIME DEFAULT NULL COMMENT '删除时间(软删除)', + PRIMARY KEY (`id`), + KEY `idx_dict_sort` (`dict_id`, `sort`, `id`), + KEY `idx_dict_value` (`dict_id`, `item_value`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台端-字典项'; + + +-- ============================================================= +-- 租户端(按 tenant_id 隔离) +-- ============================================================= + +CREATE TABLE IF NOT EXISTS `yz_backend_dict` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '租户ID(同 yz_backend_erp_supplier.tenant_id)', + `dict_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '字典名称,如:供应商类型', + `dict_code` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '字典编码(租户内唯一),如:supplier_type', + `remark` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序,数字越小越靠前', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1 启用 0 停用', + `create_user_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人用户ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `delete_time` DATETIME DEFAULT NULL COMMENT '删除时间(软删除)', + PRIMARY KEY (`id`), + KEY `idx_tenant_dict_code` (`tenant_id`, `dict_code`), + KEY `idx_tenant_status_sort` (`tenant_id`, `status`, `sort`, `id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户端-数据字典(字典列表)'; + + +CREATE TABLE IF NOT EXISTS `yz_backend_dict_item` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '租户ID,须与所属字典一致', + `dict_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '所属字典ID(yz_backend_dict.id)', + `item_label` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '字典项名称(显示文案),如:原材料供应商', + `item_value` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '字典项值(业务入库值),如:1', + `tag_type` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '标签色(el-tag type):primary/success/warning/danger/info,留空不渲染标签', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序,数字越小越靠前', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1 启用 0 停用', + `is_default` TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认选中:0 否 1 是', + `remark` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `delete_time` DATETIME DEFAULT NULL COMMENT '删除时间(软删除)', + PRIMARY KEY (`id`), + KEY `idx_tenant_dict_sort` (`tenant_id`, `dict_id`, `sort`, `id`), + KEY `idx_tenant_dict_value` (`tenant_id`, `dict_id`, `item_value`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户端-字典项'; diff --git a/go/docs/sql/insert_backend_dict_data.sql b/go/docs/sql/insert_backend_dict_data.sql new file mode 100644 index 0000000..78de691 --- /dev/null +++ b/go/docs/sql/insert_backend_dict_data.sql @@ -0,0 +1,448 @@ +-- ============================================================= +-- 租户端数据字典 —— 初始数据(系统内置,全局共享) +-- +-- 表:yz_backend_dict / yz_backend_dict_item(建表见 create_dict.sql) +-- +-- 重要:本文件写入的是 tenant_id = '0' 的「全局内置字典」,与 yz_system_admin_role +-- 中 tenant_id=0 的全局租户角色同一套约定: +-- - 所有租户都能通过 /backend/dict/options/:code 读到; +-- - 租户若需要改动,请按相同 dict_code 新建一份自己的字典(读接口优先取租户自己的, +-- 字典管理页里全局字典显示为「内置」只读)。 +-- +-- 内容来源:backend 前端散落的硬编码映射,其中 CRM 部分取自 views/apps/crm/dict.js。 +-- 幂等:字典按 (tenant_id='0', dict_code) 判重,字典项按 (dict_id, item_value) 判重,可重复执行。 +-- 说明:未给出标签色的字典项 tag_type 留空,可在「系统设置 → 数据字典」里按需补。 +-- ============================================================= + +SET NAMES utf8mb4; + + +-- ============================================================= +-- 1. 字典列表(全局内置) +-- ============================================================= + +INSERT INTO `yz_backend_dict` (`tenant_id`, `dict_name`, `dict_code`, `remark`, `sort`, `status`, `create_user_id`, `create_time`) +SELECT '0', t.dict_name, t.dict_code, t.remark, t.sort, 1, 'sql', NOW() +FROM ( + SELECT '通用状态' AS dict_name, 'common_status' AS dict_code, '0 禁用 / 1 正常 / 2 冻结 / 3 已注销(客户、供应商等共用)' AS remark, 10 AS sort + UNION ALL SELECT '往来单位等级' AS dict_name, 'common_object_level' AS dict_code, '客户/供应商等级' AS remark, 20 AS sort + UNION ALL SELECT '性别' AS dict_name, 'common_gender' AS dict_code, '0 未知 / 1 男 / 2 女' AS remark, 30 AS sort + UNION ALL SELECT '通用启用状态' AS dict_name, 'common_enable_status' AS dict_code, '1 启用 / 0 禁用' AS remark, 40 AS sort + UNION ALL SELECT '待办状态' AS dict_name, 'common_todo_status' AS dict_code, '0 待办 / 1 已完成' AS remark, 50 AS sort + UNION ALL SELECT '优先级' AS dict_name, 'common_priority' AS dict_code, '日程优先级' AS remark, 60 AS sort + UNION ALL SELECT '联系人类型' AS dict_name, 'common_contact_type' AS dict_code, '1 企业 / 2 个人' AS remark, 70 AS sort + UNION ALL SELECT '提醒渠道' AS dict_name, 'common_notify_channel' AS dict_code, '站内提醒的推送渠道(remind_channels)' AS remark, 80 AS sort + UNION ALL SELECT '移入公海原因' AS dict_name, 'crm_pool_reason' AS dict_code, 'CRM 客户移入公海的原因' AS remark, 90 AS sort + UNION ALL SELECT '联系人关联类型' AS dict_name, 'erp_related_type' AS dict_code, '1 客户 / 2 供应商' AS remark, 100 AS sort + UNION ALL SELECT '客户类型' AS dict_name, 'erp_customer_type' AS dict_code, 'crm/dict.js CUSTOMER_TYPE_OPTIONS' AS remark, 110 AS sort + UNION ALL SELECT '供应商类型' AS dict_name, 'erp_supplier_type' AS dict_code, '与 CRM 供应商类型共用' AS remark, 120 AS sort + UNION ALL SELECT '线索来源' AS dict_name, 'crm_clue_source' AS dict_code, '' AS remark, 130 AS sort + UNION ALL SELECT '线索状态' AS dict_name, 'crm_clue_status' AS dict_code, '' AS remark, 140 AS sort + UNION ALL SELECT '线索级别' AS dict_name, 'crm_clue_level' AS dict_code, '' AS remark, 150 AS sort + UNION ALL SELECT '商机阶段' AS dict_name, 'crm_business_stage' AS dict_code, '' AS remark, 160 AS sort + UNION ALL SELECT '商机状态' AS dict_name, 'crm_business_status' AS dict_code, '' AS remark, 170 AS sort + UNION ALL SELECT '跟进类型' AS dict_name, 'crm_follow_type' AS dict_code, '' AS remark, 180 AS sort + UNION ALL SELECT '跟进关联类型' AS dict_name, 'crm_pipeline_related_type' AS dict_code, '' AS remark, 190 AS sort + UNION ALL SELECT '项目状态' AS dict_name, 'crm_project_status' AS dict_code, '' AS remark, 200 AS sort + UNION ALL SELECT '项目类型' AS dict_name, 'crm_project_type' AS dict_code, '' AS remark, 210 AS sort + UNION ALL SELECT '合同分类' AS dict_name, 'crm_contract_category' AS dict_code, '' AS remark, 220 AS sort + UNION ALL SELECT '合同状态' AS dict_name, 'crm_contract_status' AS dict_code, '' AS remark, 230 AS sort + UNION ALL SELECT '合同形式' AS dict_name, 'crm_contract_party_count' AS dict_code, '2 双方 / 3 三方 / 4 四方' AS remark, 240 AS sort + UNION ALL SELECT '我方角色' AS dict_name, 'crm_contract_our_role' AS dict_code, '' AS remark, 250 AS sort + UNION ALL SELECT '合同产品类别' AS dict_name, 'crm_contract_product_category' AS dict_code, '' AS remark, 260 AS sort + UNION ALL SELECT '回款周期' AS dict_name, 'crm_payback_cycle' AS dict_code, '' AS remark, 270 AS sort + UNION ALL SELECT '回款方式' AS dict_name, 'crm_payback_method' AS dict_code, '' AS remark, 280 AS sort + UNION ALL SELECT '回款计划状态' AS dict_name, 'crm_payback_status' AS dict_code, '' AS remark, 290 AS sort + UNION ALL SELECT '回款明细状态' AS dict_name, 'crm_payback_item_status' AS dict_code, '' AS remark, 300 AS sort + UNION ALL SELECT '产品状态' AS dict_name, 'crm_product_status' AS dict_code, '1 启用 / 0 停用' AS remark, 310 AS sort + UNION ALL SELECT '产品单位' AS dict_name, 'crm_product_unit' AS dict_code, '计量单位,偏业务参数,可自行增删' AS remark, 320 AS sort + UNION ALL SELECT '产品研发方式' AS dict_name, 'crm_product_dev_mode' AS dict_code, '' AS remark, 330 AS sort + UNION ALL SELECT '招标状态' AS dict_name, 'crm_tender_status' AS dict_code, '' AS remark, 340 AS sort + UNION ALL SELECT '招标方式' AS dict_name, 'crm_tender_type' AS dict_code, '' AS remark, 350 AS sort + UNION ALL SELECT '招标结果' AS dict_name, 'crm_tender_result_type' AS dict_code, '' AS remark, 360 AS sort + UNION ALL SELECT '招标供应商状态' AS dict_name, 'crm_tender_supplier_status' AS dict_code, '' AS remark, 370 AS sort + UNION ALL SELECT '招标公告类型' AS dict_name, 'crm_tender_announce_type' AS dict_code, '' AS remark, 380 AS sort + UNION ALL SELECT '评标方式' AS dict_name, 'crm_evaluation_method' AS dict_code, '' AS remark, 390 AS sort + UNION ALL SELECT '评标结果' AS dict_name, 'crm_evaluation_result' AS dict_code, '' AS remark, 400 AS sort + UNION ALL SELECT '投标状态' AS dict_name, 'crm_bid_status' AS dict_code, '' AS remark, 410 AS sort + UNION ALL SELECT '投标方式' AS dict_name, 'crm_bid_type' AS dict_code, '' AS remark, 420 AS sort + UNION ALL SELECT '投标来源' AS dict_name, 'crm_bid_source_type' AS dict_code, '' AS remark, 430 AS sort + UNION ALL SELECT '投标结果' AS dict_name, 'crm_bid_result_type' AS dict_code, '' AS remark, 440 AS sort + UNION ALL SELECT '投标任务类型' AS dict_name, 'crm_bid_task_type' AS dict_code, '' AS remark, 450 AS sort + UNION ALL SELECT '投标任务状态' AS dict_name, 'crm_bid_task_status' AS dict_code, '' AS remark, 460 AS sort + UNION ALL SELECT '投标模板类型' AS dict_name, 'crm_bid_template_type' AS dict_code, '' AS remark, 470 AS sort + UNION ALL SELECT '投标报价状态' AS dict_name, 'crm_bid_quote_status' AS dict_code, '' AS remark, 480 AS sort + UNION ALL SELECT '投标文件状态' AS dict_name, 'crm_bid_document_status' AS dict_code, '' AS remark, 490 AS sort + UNION ALL SELECT '投标费用类型' AS dict_name, 'crm_bid_expense_type' AS dict_code, '' AS remark, 500 AS sort + UNION ALL SELECT '竞争对手类型' AS dict_name, 'crm_competitor_type' AS dict_code, '' AS remark, 510 AS sort + UNION ALL SELECT '资质类型' AS dict_name, 'crm_qualification_type' AS dict_code, '' AS remark, 520 AS sort + UNION ALL SELECT '资质状态' AS dict_name, 'crm_qualification_status' AS dict_code, '' AS remark, 530 AS sort + UNION ALL SELECT '收付款状态' AS dict_name, 'crm_payment_status' AS dict_code, '' AS remark, 540 AS sort + UNION ALL SELECT '审批类型' AS dict_name, 'crm_approval_type' AS dict_code, '' AS remark, 550 AS sort + UNION ALL SELECT '审批状态' AS dict_name, 'crm_approval_status' AS dict_code, '' AS remark, 560 AS sort + UNION ALL SELECT '账套会计准则' AS dict_name, 'erp_account_set_standard' AS dict_code, '1 企业会计准则 / 2 小企业会计准则' AS remark, 570 AS sort + UNION ALL SELECT '账套本位币' AS dict_name, 'erp_account_set_currency' AS dict_code, '账套支持的币种' AS remark, 580 AS sort + UNION ALL SELECT '职位类型' AS dict_name, 'erp_position_type' AS dict_code, '0 普通职位 / 1 主管 / 2 负责人' AS remark, 590 AS sort + UNION ALL SELECT '员工账号状态' AS dict_name, 'common_employee_status' AS dict_code, '1 正常 / 0 禁用 / 2 离职' AS remark, 600 AS sort + UNION ALL SELECT '用工状态' AS dict_name, 'oa_employee_hire_status' AS dict_code, '人事档案:1 试用 / 2 正式 / 3 离职' AS remark, 610 AS sort + UNION ALL SELECT '婚姻状况' AS dict_name, 'oa_employee_marital' AS dict_code, '人事档案' AS remark, 620 AS sort + UNION ALL SELECT '报销单状态' AS dict_name, 'oa_reimburse_status' AS dict_code, '0 草稿 / 1 审批中 / 2 已通过 / 3 已驳回 / 4 已撤回 / 5 已打款' AS remark, 630 AS sort + UNION ALL SELECT '薪酬单状态' AS dict_name, 'oa_compensation_status' AS dict_code, '0 草稿 / 1 已确认 / 2 已发放 / 3 已作废' AS remark, 640 AS sort + UNION ALL SELECT '薪酬项类型' AS dict_name, 'oa_compensation_item_type' AS dict_code, '1 增项 / 2 扣项' AS remark, 650 AS sort + UNION ALL SELECT '公告类型' AS dict_name, 'oa_notice_type' AS dict_code, '0 通知 / 1 公告 / 2 活动' AS remark, 660 AS sort + UNION ALL SELECT '公告状态' AS dict_name, 'oa_notice_status' AS dict_code, '0 草稿 / 1 已发布 / 2 已下架' AS remark, 670 AS sort + UNION ALL SELECT 'OA文档状态' AS dict_name, 'oa_doc_status' AS dict_code, '0 草稿 / 1 已发布 / 2 已归档' AS remark, 680 AS sort + UNION ALL SELECT 'OA文档关联类型' AS dict_name, 'oa_doc_relation' AS dict_code, 'reference 引用 / related 相关 / version 版本 / belong 从属' AS remark, 690 AS sort + UNION ALL SELECT 'OA文档可见性' AS dict_name, 'oa_doc_visibility' AS dict_code, '0 租户公开 / 1 私密' AS remark, 700 AS sort + UNION ALL SELECT '登录方式' AS dict_name, 'system_login_type' AS dict_code, '登录日志' AS remark, 710 AS sort + UNION ALL SELECT '请求方法' AS dict_name, 'system_http_method' AS dict_code, '操作日志' AS remark, 720 AS sort + UNION ALL SELECT '菜单类型' AS dict_name, 'system_menu_type' AS dict_code, '1 目录 / 2 菜单 / 3 按钮' AS remark, 730 AS sort + UNION ALL SELECT '订购单状态' AS dict_name, 'common_order_status' AS dict_code, '产品/套餐购买单状态' AS remark, 740 AS sort + UNION ALL SELECT '前台导航类型' AS dict_name, 'cms_front_menu_type' AS dict_code, '1 目录 / 2 页面 / 3 外链 / 4 单页' AS remark, 750 AS sort + UNION ALL SELECT '需求状态' AS dict_name, 'cms_demand_status' AS dict_code, '1 待处理 / 2 处理中 / 3 已完成 / 4 已拒绝' AS remark, 760 AS sort + UNION ALL SELECT '域名核验状态' AS dict_name, 'tenant_domain_verify_status' AS dict_code, '0 未检测 / 1 已通过 / 2 未通过' AS remark, 770 AS sort + UNION ALL SELECT '域名证书状态' AS dict_name, 'tenant_domain_ssl_status' AS dict_code, '0 待处理 / 1 签发中 / 2 已签发 / 3 签发失败' AS remark, 780 AS sort +) AS t +WHERE NOT EXISTS ( + SELECT 1 FROM `yz_backend_dict` AS d + WHERE d.tenant_id = '0' AND d.dict_code = t.dict_code AND d.delete_time IS NULL +); + + +-- ============================================================= +-- 2. 字典项(全局内置) +-- ============================================================= + +INSERT INTO `yz_backend_dict_item` (`tenant_id`, `dict_id`, `item_label`, `item_value`, `tag_type`, `sort`, `status`, `is_default`, `create_time`) +SELECT '0', d.id, t.item_label, t.item_value, t.tag_type, t.sort, 1, 0, NOW() +FROM ( + SELECT 'common_status' AS dict_code, '正常' AS item_label, '1' AS item_value, 'success' AS tag_type, 10 AS sort + UNION ALL SELECT 'common_status' AS dict_code, '禁用' AS item_label, '0' AS item_value, 'info' AS tag_type, 20 AS sort + UNION ALL SELECT 'common_status' AS dict_code, '冻结' AS item_label, '2' AS item_value, 'warning' AS tag_type, 30 AS sort + UNION ALL SELECT 'common_status' AS dict_code, '已注销' AS item_label, '3' AS item_value, 'danger' AS tag_type, 40 AS sort + UNION ALL SELECT 'common_object_level' AS dict_code, '核心' AS item_label, '1' AS item_value, 'danger' AS tag_type, 10 AS sort + UNION ALL SELECT 'common_object_level' AS dict_code, '重要' AS item_label, '2' AS item_value, 'warning' AS tag_type, 20 AS sort + UNION ALL SELECT 'common_object_level' AS dict_code, '普通' AS item_label, '3' AS item_value, 'info' AS tag_type, 30 AS sort + UNION ALL SELECT 'common_object_level' AS dict_code, '潜在' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'common_gender' AS dict_code, '未知' AS item_label, '0' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'common_gender' AS dict_code, '男' AS item_label, '1' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'common_gender' AS dict_code, '女' AS item_label, '2' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'common_enable_status' AS dict_code, '禁用' AS item_label, '0' AS item_value, 'info' AS tag_type, 10 AS sort + UNION ALL SELECT 'common_enable_status' AS dict_code, '启用' AS item_label, '1' AS item_value, 'success' AS tag_type, 20 AS sort + UNION ALL SELECT 'common_todo_status' AS dict_code, '待办' AS item_label, '0' AS item_value, 'info' AS tag_type, 10 AS sort + UNION ALL SELECT 'common_todo_status' AS dict_code, '已完成' AS item_label, '1' AS item_value, 'success' AS tag_type, 20 AS sort + UNION ALL SELECT 'common_priority' AS dict_code, '重要' AS item_label, '1' AS item_value, 'warning' AS tag_type, 10 AS sort + UNION ALL SELECT 'common_priority' AS dict_code, '紧急' AS item_label, '2' AS item_value, 'danger' AS tag_type, 20 AS sort + UNION ALL SELECT 'common_contact_type' AS dict_code, '企业' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'common_contact_type' AS dict_code, '个人' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'common_notify_channel' AS dict_code, '短信' AS item_label, 'SMS' AS item_value, 'success' AS tag_type, 10 AS sort + UNION ALL SELECT 'common_notify_channel' AS dict_code, '邮件' AS item_label, 'EMAIL' AS item_value, 'warning' AS tag_type, 20 AS sort + UNION ALL SELECT 'common_notify_channel' AS dict_code, 'Bark推送' AS item_label, 'BARK' AS item_value, 'danger' AS tag_type, 30 AS sort + UNION ALL SELECT 'common_notify_channel' AS dict_code, '站内信' AS item_label, 'SITE_MSG' AS item_value, 'primary' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_pool_reason' AS dict_code, '长期未跟进' AS item_label, '长期未跟进' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_pool_reason' AS dict_code, '合作终止' AS item_label, '合作终止' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_pool_reason' AS dict_code, '重复建档' AS item_label, '重复建档' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_pool_reason' AS dict_code, '负责人离职' AS item_label, '负责人离职' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_pool_reason' AS dict_code, '手动释放' AS item_label, '手动释放' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_pool_reason' AS dict_code, '其他' AS item_label, '其他' AS item_value, '' AS tag_type, 60 AS sort + UNION ALL SELECT 'erp_related_type' AS dict_code, '客户' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'erp_related_type' AS dict_code, '供应商' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'erp_customer_type' AS dict_code, '企业' AS item_label, '1' AS item_value, 'primary' AS tag_type, 10 AS sort + UNION ALL SELECT 'erp_customer_type' AS dict_code, '政府机构' AS item_label, '2' AS item_value, 'warning' AS tag_type, 20 AS sort + UNION ALL SELECT 'erp_customer_type' AS dict_code, '国企' AS item_label, '3' AS item_value, 'success' AS tag_type, 30 AS sort + UNION ALL SELECT 'erp_customer_type' AS dict_code, '教育机构' AS item_label, '4' AS item_value, 'info' AS tag_type, 40 AS sort + UNION ALL SELECT 'erp_customer_type' AS dict_code, '个人' AS item_label, '5' AS item_value, 'info' AS tag_type, 50 AS sort + UNION ALL SELECT 'erp_supplier_type' AS dict_code, '原材料供应商' AS item_label, '1' AS item_value, 'primary' AS tag_type, 10 AS sort + UNION ALL SELECT 'erp_supplier_type' AS dict_code, '设备供应商' AS item_label, '2' AS item_value, 'warning' AS tag_type, 20 AS sort + UNION ALL SELECT 'erp_supplier_type' AS dict_code, '服务供应商' AS item_label, '3' AS item_value, 'success' AS tag_type, 30 AS sort + UNION ALL SELECT 'erp_supplier_type' AS dict_code, '其他' AS item_label, '4' AS item_value, 'info' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_clue_source' AS dict_code, '官网咨询' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_clue_source' AS dict_code, '电话咨询' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_clue_source' AS dict_code, '朋友介绍' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_clue_source' AS dict_code, '展会活动' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_clue_source' AS dict_code, '广告投放' AS item_label, '5' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_clue_source' AS dict_code, '陌生拜访' AS item_label, '6' AS item_value, '' AS tag_type, 60 AS sort + UNION ALL SELECT 'crm_clue_source' AS dict_code, '其他' AS item_label, '7' AS item_value, '' AS tag_type, 70 AS sort + UNION ALL SELECT 'crm_clue_status' AS dict_code, '跟进中' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_clue_status' AS dict_code, '已转化' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_clue_status' AS dict_code, '已废弃' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_clue_level' AS dict_code, '重点' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_clue_level' AS dict_code, '普通' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_clue_level' AS dict_code, '非优先' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_business_stage' AS dict_code, '初步接洽' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_business_stage' AS dict_code, '需求确认' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_business_stage' AS dict_code, '方案报价' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_business_stage' AS dict_code, '商务谈判' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_business_stage' AS dict_code, '赢单' AS item_label, '5' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_business_stage' AS dict_code, '输单' AS item_label, '6' AS item_value, '' AS tag_type, 60 AS sort + UNION ALL SELECT 'crm_business_status' AS dict_code, '跟进中' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_business_status' AS dict_code, '已转化项目' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_business_status' AS dict_code, '已关闭' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_follow_type' AS dict_code, '电话' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_follow_type' AS dict_code, '微信' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_follow_type' AS dict_code, '上门' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_follow_type' AS dict_code, '邮件' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_follow_type' AS dict_code, '其他' AS item_label, '5' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_pipeline_related_type' AS dict_code, '线索' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_pipeline_related_type' AS dict_code, '商机' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_pipeline_related_type' AS dict_code, '项目' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_project_status' AS dict_code, '未开始' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_project_status' AS dict_code, '进行中' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_project_status' AS dict_code, '已完成' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_project_status' AS dict_code, '已暂停' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_project_status' AS dict_code, '异常' AS item_label, '5' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_project_type' AS dict_code, '单一项目' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_project_type' AS dict_code, '上下游项目' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_project_type' AS dict_code, '上中游项目' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_contract_category' AS dict_code, '开发合同' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_contract_category' AS dict_code, '服务合同' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_contract_category' AS dict_code, '销售合同' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_contract_category' AS dict_code, '租赁合同' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_contract_category' AS dict_code, '采购合同' AS item_label, '5' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_contract_category' AS dict_code, '运维合同' AS item_label, '6' AS item_value, '' AS tag_type, 60 AS sort + UNION ALL SELECT 'crm_contract_category' AS dict_code, '咨询合同' AS item_label, '7' AS item_value, '' AS tag_type, 70 AS sort + UNION ALL SELECT 'crm_contract_category' AS dict_code, '其他' AS item_label, '8' AS item_value, '' AS tag_type, 80 AS sort + UNION ALL SELECT 'crm_contract_status' AS dict_code, '草稿' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_contract_status' AS dict_code, '已完成' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_contract_status' AS dict_code, '履约中' AS item_label, '4' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_contract_status' AS dict_code, '异常' AS item_label, '5' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_contract_status' AS dict_code, '已作废' AS item_label, '3' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_contract_party_count' AS dict_code, '双方合同' AS item_label, '2' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_contract_party_count' AS dict_code, '三方合同' AS item_label, '3' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_contract_party_count' AS dict_code, '四方合同' AS item_label, '4' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_contract_our_role' AS dict_code, '甲方' AS item_label, '1' AS item_value, 'danger' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_contract_our_role' AS dict_code, '乙方' AS item_label, '2' AS item_value, 'primary' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_contract_our_role' AS dict_code, '丙方' AS item_label, '3' AS item_value, 'warning' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_contract_our_role' AS dict_code, '丁方' AS item_label, '4' AS item_value, 'success' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_contract_product_category' AS dict_code, '硬件(货物)' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_contract_product_category' AS dict_code, '软件(许可)' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_contract_product_category' AS dict_code, '服务' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_contract_product_category' AS dict_code, '开发' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_contract_product_category' AS dict_code, '其他' AS item_label, '5' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_payback_cycle' AS dict_code, '月度' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_payback_cycle' AS dict_code, '季度' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_payback_cycle' AS dict_code, '年度' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_payback_cycle' AS dict_code, '进度' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_payback_cycle' AS dict_code, '自定义' AS item_label, '5' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_payback_method' AS dict_code, '对公转账' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_payback_method' AS dict_code, '网银转账' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_payback_method' AS dict_code, '现金' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_payback_method' AS dict_code, '支票' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_payback_method' AS dict_code, '支付宝' AS item_label, '5' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_payback_method' AS dict_code, '微信' AS item_label, '6' AS item_value, '' AS tag_type, 60 AS sort + UNION ALL SELECT 'crm_payback_method' AS dict_code, '其他' AS item_label, '7' AS item_value, '' AS tag_type, 70 AS sort + UNION ALL SELECT 'crm_payback_status' AS dict_code, '进行中' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_payback_status' AS dict_code, '已完成' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_payback_item_status' AS dict_code, '未回款' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_payback_item_status' AS dict_code, '部分回款' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_payback_item_status' AS dict_code, '已回款' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_product_status' AS dict_code, '启用' AS item_label, '1' AS item_value, 'success' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_product_status' AS dict_code, '停用' AS item_label, '0' AS item_value, 'info' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_product_unit' AS dict_code, '套' AS item_label, '套' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_product_unit' AS dict_code, '台' AS item_label, '台' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_product_unit' AS dict_code, '个' AS item_label, '个' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_product_unit' AS dict_code, '件' AS item_label, '件' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_product_unit' AS dict_code, '批' AS item_label, '批' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_product_unit' AS dict_code, '次' AS item_label, '次' AS item_value, '' AS tag_type, 60 AS sort + UNION ALL SELECT 'crm_product_unit' AS dict_code, '人月' AS item_label, '人月' AS item_value, '' AS tag_type, 70 AS sort + UNION ALL SELECT 'crm_product_unit' AS dict_code, '人天' AS item_label, '人天' AS item_value, '' AS tag_type, 80 AS sort + UNION ALL SELECT 'crm_product_unit' AS dict_code, '年' AS item_label, '年' AS item_value, '' AS tag_type, 90 AS sort + UNION ALL SELECT 'crm_product_dev_mode' AS dict_code, '自行研发' AS item_label, 'self' AS item_value, 'success' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_product_dev_mode' AS dict_code, '外协研发' AS item_label, 'outsource' AS item_value, 'warning' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_tender_status' AS dict_code, '草稿' AS item_label, '0' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_tender_status' AS dict_code, '公告中' AS item_label, '1' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_tender_status' AS dict_code, '报名中' AS item_label, '2' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_tender_status' AS dict_code, '投标中' AS item_label, '3' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_tender_status' AS dict_code, '开标中' AS item_label, '4' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_tender_status' AS dict_code, '评标中' AS item_label, '5' AS item_value, '' AS tag_type, 60 AS sort + UNION ALL SELECT 'crm_tender_status' AS dict_code, '已定标' AS item_label, '6' AS item_value, '' AS tag_type, 70 AS sort + UNION ALL SELECT 'crm_tender_status' AS dict_code, '已归档' AS item_label, '7' AS item_value, '' AS tag_type, 80 AS sort + UNION ALL SELECT 'crm_tender_type' AS dict_code, '公开招标' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_tender_type' AS dict_code, '邀请招标' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_tender_type' AS dict_code, '竞争性谈判' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_tender_type' AS dict_code, '询比采购' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_tender_type' AS dict_code, '单一来源' AS item_label, '5' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_tender_result_type' AS dict_code, '定标' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_tender_result_type' AS dict_code, '流标' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_tender_result_type' AS dict_code, '废标' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_tender_supplier_status' AS dict_code, '待审核' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_tender_supplier_status' AS dict_code, '已通过' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_tender_supplier_status' AS dict_code, '已拒绝' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_tender_supplier_status' AS dict_code, '已退标' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_tender_announce_type' AS dict_code, '招标公告' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_tender_announce_type' AS dict_code, '变更公告' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_tender_announce_type' AS dict_code, '中标公告' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_tender_announce_type' AS dict_code, '终止公告' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_evaluation_method' AS dict_code, '综合评估法' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_evaluation_method' AS dict_code, '最低价法' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_evaluation_method' AS dict_code, '性价比法' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_evaluation_result' AS dict_code, '待评' AS item_label, '0' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_evaluation_result' AS dict_code, '合格' AS item_label, '1' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_evaluation_result' AS dict_code, '不合格' AS item_label, '2' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_evaluation_result' AS dict_code, '废标' AS item_label, '3' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_bid_status' AS dict_code, '草稿' AS item_label, '0' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_bid_status' AS dict_code, '立项审批中' AS item_label, '1' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_bid_status' AS dict_code, '投标中' AS item_label, '2' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_bid_status' AS dict_code, '已开标' AS item_label, '3' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_bid_status' AS dict_code, '中标' AS item_label, '4' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_bid_status' AS dict_code, '落标' AS item_label, '5' AS item_value, '' AS tag_type, 60 AS sort + UNION ALL SELECT 'crm_bid_status' AS dict_code, '已转合同' AS item_label, '6' AS item_value, '' AS tag_type, 70 AS sort + UNION ALL SELECT 'crm_bid_status' AS dict_code, '已归档' AS item_label, '7' AS item_value, '' AS tag_type, 80 AS sort + UNION ALL SELECT 'crm_bid_type' AS dict_code, '公开招标' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_bid_type' AS dict_code, '邀请招标' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_bid_type' AS dict_code, '竞争性谈判' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_bid_type' AS dict_code, '询比采购' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_bid_type' AS dict_code, '单一来源' AS item_label, '5' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_bid_source_type' AS dict_code, '独立创建' AS item_label, '0' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_bid_source_type' AS dict_code, '商机' AS item_label, '1' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_bid_source_type' AS dict_code, '项目' AS item_label, '2' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_bid_result_type' AS dict_code, '中标' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_bid_result_type' AS dict_code, '未中标' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_bid_result_type' AS dict_code, '废标' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_bid_result_type' AS dict_code, '流标' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_bid_task_type' AS dict_code, '商务' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_bid_task_type' AS dict_code, '技术' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_bid_task_type' AS dict_code, '报价' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_bid_task_type' AS dict_code, '法务' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_bid_task_type' AS dict_code, '财务' AS item_label, '5' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_bid_task_type' AS dict_code, '其他' AS item_label, '6' AS item_value, '' AS tag_type, 60 AS sort + UNION ALL SELECT 'crm_bid_task_status' AS dict_code, '未开始' AS item_label, '0' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_bid_task_status' AS dict_code, '进行中' AS item_label, '1' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_bid_task_status' AS dict_code, '已完成' AS item_label, '2' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_bid_task_status' AS dict_code, '已逾期' AS item_label, '3' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_bid_task_status' AS dict_code, '已取消' AS item_label, '4' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_bid_template_type' AS dict_code, '技术标' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_bid_template_type' AS dict_code, '商务标' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_bid_template_type' AS dict_code, '综合标' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_bid_quote_status' AS dict_code, '草稿' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_bid_quote_status' AS dict_code, '待审批' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_bid_quote_status' AS dict_code, '已审批' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_bid_quote_status' AS dict_code, '已使用' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_bid_document_status' AS dict_code, '编辑中' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_bid_document_status' AS dict_code, '待审核' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_bid_document_status' AS dict_code, '已定稿' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_bid_document_status' AS dict_code, '已提交' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_bid_expense_type' AS dict_code, '标书费' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_bid_expense_type' AS dict_code, '保证金' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_bid_expense_type' AS dict_code, '差旅费' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_bid_expense_type' AS dict_code, '咨询费' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_bid_expense_type' AS dict_code, '代理费' AS item_label, '5' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_bid_expense_type' AS dict_code, '其他' AS item_label, '6' AS item_value, '' AS tag_type, 60 AS sort + UNION ALL SELECT 'crm_competitor_type' AS dict_code, '同类厂商' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_competitor_type' AS dict_code, '关联企业' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_competitor_type' AS dict_code, '其他' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_qualification_type' AS dict_code, '营业执照' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_qualification_type' AS dict_code, '资质证书' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_qualification_type' AS dict_code, '业绩证明' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_qualification_type' AS dict_code, '认证证书' AS item_label, '4' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_qualification_type' AS dict_code, '其他' AS item_label, '5' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'crm_qualification_status' AS dict_code, '有效' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_qualification_status' AS dict_code, '即将过期' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_qualification_status' AS dict_code, '失效' AS item_label, '0' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_payment_status' AS dict_code, '未付' AS item_label, '0' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_payment_status' AS dict_code, '已付' AS item_label, '1' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_payment_status' AS dict_code, '已退还' AS item_label, '2' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_payment_status' AS dict_code, '已抵扣' AS item_label, '3' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'crm_approval_type' AS dict_code, '立项审批' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_approval_type' AS dict_code, '报价审批' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_approval_type' AS dict_code, '合同审批' AS item_label, '3' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_approval_status' AS dict_code, '待审批' AS item_label, '0' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'crm_approval_status' AS dict_code, '已通过' AS item_label, '1' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'crm_approval_status' AS dict_code, '已拒绝' AS item_label, '2' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'crm_approval_status' AS dict_code, '已撤回' AS item_label, '3' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'erp_account_set_standard' AS dict_code, '企业会计准则' AS item_label, '1' AS item_value, 'primary' AS tag_type, 10 AS sort + UNION ALL SELECT 'erp_account_set_standard' AS dict_code, '小企业会计准则' AS item_label, '2' AS item_value, 'info' AS tag_type, 20 AS sort + UNION ALL SELECT 'erp_account_set_currency' AS dict_code, '人民币' AS item_label, 'CNY' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'erp_account_set_currency' AS dict_code, '美元' AS item_label, 'USD' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'erp_account_set_currency' AS dict_code, '欧元' AS item_label, 'EUR' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'erp_account_set_currency' AS dict_code, '港币' AS item_label, 'HKD' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'erp_position_type' AS dict_code, '普通职位' AS item_label, '0' AS item_value, 'info' AS tag_type, 10 AS sort + UNION ALL SELECT 'erp_position_type' AS dict_code, '主管' AS item_label, '1' AS item_value, 'warning' AS tag_type, 20 AS sort + UNION ALL SELECT 'erp_position_type' AS dict_code, '负责人' AS item_label, '2' AS item_value, 'primary' AS tag_type, 30 AS sort + UNION ALL SELECT 'common_employee_status' AS dict_code, '禁用' AS item_label, '0' AS item_value, 'info' AS tag_type, 10 AS sort + UNION ALL SELECT 'common_employee_status' AS dict_code, '正常' AS item_label, '1' AS item_value, 'success' AS tag_type, 20 AS sort + UNION ALL SELECT 'common_employee_status' AS dict_code, '离职' AS item_label, '2' AS item_value, 'danger' AS tag_type, 30 AS sort + UNION ALL SELECT 'oa_employee_hire_status' AS dict_code, '试用' AS item_label, '1' AS item_value, 'warning' AS tag_type, 10 AS sort + UNION ALL SELECT 'oa_employee_hire_status' AS dict_code, '正式' AS item_label, '2' AS item_value, 'success' AS tag_type, 20 AS sort + UNION ALL SELECT 'oa_employee_hire_status' AS dict_code, '离职' AS item_label, '3' AS item_value, 'danger' AS tag_type, 30 AS sort + UNION ALL SELECT 'oa_employee_marital' AS dict_code, '未知' AS item_label, '0' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'oa_employee_marital' AS dict_code, '未婚' AS item_label, '1' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'oa_employee_marital' AS dict_code, '已婚' AS item_label, '2' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'oa_employee_marital' AS dict_code, '离异' AS item_label, '3' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'oa_employee_marital' AS dict_code, '丧偶' AS item_label, '4' AS item_value, '' AS tag_type, 50 AS sort + UNION ALL SELECT 'oa_reimburse_status' AS dict_code, '草稿' AS item_label, '0' AS item_value, 'info' AS tag_type, 10 AS sort + UNION ALL SELECT 'oa_reimburse_status' AS dict_code, '审批中' AS item_label, '1' AS item_value, 'warning' AS tag_type, 20 AS sort + UNION ALL SELECT 'oa_reimburse_status' AS dict_code, '已通过' AS item_label, '2' AS item_value, 'success' AS tag_type, 30 AS sort + UNION ALL SELECT 'oa_reimburse_status' AS dict_code, '已驳回' AS item_label, '3' AS item_value, 'danger' AS tag_type, 40 AS sort + UNION ALL SELECT 'oa_reimburse_status' AS dict_code, '已撤回' AS item_label, '4' AS item_value, 'info' AS tag_type, 50 AS sort + UNION ALL SELECT 'oa_reimburse_status' AS dict_code, '已打款' AS item_label, '5' AS item_value, 'success' AS tag_type, 60 AS sort + UNION ALL SELECT 'oa_compensation_status' AS dict_code, '草稿' AS item_label, '0' AS item_value, 'info' AS tag_type, 10 AS sort + UNION ALL SELECT 'oa_compensation_status' AS dict_code, '已确认' AS item_label, '1' AS item_value, 'warning' AS tag_type, 20 AS sort + UNION ALL SELECT 'oa_compensation_status' AS dict_code, '已发放' AS item_label, '2' AS item_value, 'success' AS tag_type, 30 AS sort + UNION ALL SELECT 'oa_compensation_status' AS dict_code, '已作废' AS item_label, '3' AS item_value, 'danger' AS tag_type, 40 AS sort + UNION ALL SELECT 'oa_compensation_item_type' AS dict_code, '增项' AS item_label, '1' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'oa_compensation_item_type' AS dict_code, '扣项' AS item_label, '2' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'oa_notice_type' AS dict_code, '通知' AS item_label, '0' AS item_value, 'info' AS tag_type, 10 AS sort + UNION ALL SELECT 'oa_notice_type' AS dict_code, '公告' AS item_label, '1' AS item_value, 'primary' AS tag_type, 20 AS sort + UNION ALL SELECT 'oa_notice_type' AS dict_code, '活动' AS item_label, '2' AS item_value, 'success' AS tag_type, 30 AS sort + UNION ALL SELECT 'oa_notice_status' AS dict_code, '草稿' AS item_label, '0' AS item_value, 'info' AS tag_type, 10 AS sort + UNION ALL SELECT 'oa_notice_status' AS dict_code, '已发布' AS item_label, '1' AS item_value, 'success' AS tag_type, 20 AS sort + UNION ALL SELECT 'oa_notice_status' AS dict_code, '已下架' AS item_label, '2' AS item_value, 'warning' AS tag_type, 30 AS sort + UNION ALL SELECT 'oa_doc_status' AS dict_code, '草稿' AS item_label, '0' AS item_value, 'warning' AS tag_type, 10 AS sort + UNION ALL SELECT 'oa_doc_status' AS dict_code, '已发布' AS item_label, '1' AS item_value, 'success' AS tag_type, 20 AS sort + UNION ALL SELECT 'oa_doc_status' AS dict_code, '已归档' AS item_label, '2' AS item_value, 'info' AS tag_type, 30 AS sort + UNION ALL SELECT 'oa_doc_relation' AS dict_code, '引用' AS item_label, 'reference' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'oa_doc_relation' AS dict_code, '相关' AS item_label, 'related' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'oa_doc_relation' AS dict_code, '版本' AS item_label, 'version' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'oa_doc_relation' AS dict_code, '从属' AS item_label, 'belong' AS item_value, '' AS tag_type, 40 AS sort + UNION ALL SELECT 'oa_doc_visibility' AS dict_code, '租户公开' AS item_label, '0' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'oa_doc_visibility' AS dict_code, '私密' AS item_label, '1' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'system_login_type' AS dict_code, '密码登录' AS item_label, 'password' AS item_value, '' AS tag_type, 10 AS sort + UNION ALL SELECT 'system_login_type' AS dict_code, '短信验证码' AS item_label, 'sms' AS item_value, '' AS tag_type, 20 AS sort + UNION ALL SELECT 'system_login_type' AS dict_code, '邮箱验证码' AS item_label, 'email' AS item_value, '' AS tag_type, 30 AS sort + UNION ALL SELECT 'system_http_method' AS dict_code, 'GET' AS item_label, 'GET' AS item_value, 'info' AS tag_type, 10 AS sort + UNION ALL SELECT 'system_http_method' AS dict_code, 'POST' AS item_label, 'POST' AS item_value, 'success' AS tag_type, 20 AS sort + UNION ALL SELECT 'system_http_method' AS dict_code, 'PUT' AS item_label, 'PUT' AS item_value, 'warning' AS tag_type, 30 AS sort + UNION ALL SELECT 'system_http_method' AS dict_code, 'DELETE' AS item_label, 'DELETE' AS item_value, 'danger' AS tag_type, 40 AS sort + UNION ALL SELECT 'system_http_method' AS dict_code, 'PATCH' AS item_label, 'PATCH' AS item_value, 'primary' AS tag_type, 50 AS sort + UNION ALL SELECT 'system_menu_type' AS dict_code, '目录' AS item_label, '1' AS item_value, 'primary' AS tag_type, 10 AS sort + UNION ALL SELECT 'system_menu_type' AS dict_code, '菜单' AS item_label, '2' AS item_value, 'success' AS tag_type, 20 AS sort + UNION ALL SELECT 'system_menu_type' AS dict_code, '按钮' AS item_label, '3' AS item_value, 'warning' AS tag_type, 30 AS sort + UNION ALL SELECT 'common_order_status' AS dict_code, '待支付' AS item_label, 'pending' AS item_value, 'warning' AS tag_type, 10 AS sort + UNION ALL SELECT 'common_order_status' AS dict_code, '已支付' AS item_label, 'paid' AS item_value, 'primary' AS tag_type, 20 AS sort + UNION ALL SELECT 'common_order_status' AS dict_code, '已开通' AS item_label, 'activated' AS item_value, 'success' AS tag_type, 30 AS sort + UNION ALL SELECT 'common_order_status' AS dict_code, '已关闭' AS item_label, 'closed' AS item_value, 'info' AS tag_type, 40 AS sort + UNION ALL SELECT 'cms_front_menu_type' AS dict_code, '目录' AS item_label, '1' AS item_value, 'primary' AS tag_type, 10 AS sort + UNION ALL SELECT 'cms_front_menu_type' AS dict_code, '页面' AS item_label, '2' AS item_value, 'success' AS tag_type, 20 AS sort + UNION ALL SELECT 'cms_front_menu_type' AS dict_code, '外链' AS item_label, '3' AS item_value, 'info' AS tag_type, 30 AS sort + UNION ALL SELECT 'cms_front_menu_type' AS dict_code, '单页' AS item_label, '4' AS item_value, 'warning' AS tag_type, 40 AS sort + UNION ALL SELECT 'cms_demand_status' AS dict_code, '待处理' AS item_label, '1' AS item_value, 'info' AS tag_type, 10 AS sort + UNION ALL SELECT 'cms_demand_status' AS dict_code, '处理中' AS item_label, '2' AS item_value, 'warning' AS tag_type, 20 AS sort + UNION ALL SELECT 'cms_demand_status' AS dict_code, '已完成' AS item_label, '3' AS item_value, 'success' AS tag_type, 30 AS sort + UNION ALL SELECT 'cms_demand_status' AS dict_code, '已拒绝' AS item_label, '4' AS item_value, 'danger' AS tag_type, 40 AS sort + UNION ALL SELECT 'tenant_domain_verify_status' AS dict_code, '未检测' AS item_label, '0' AS item_value, 'info' AS tag_type, 10 AS sort + UNION ALL SELECT 'tenant_domain_verify_status' AS dict_code, '已通过' AS item_label, '1' AS item_value, 'success' AS tag_type, 20 AS sort + UNION ALL SELECT 'tenant_domain_verify_status' AS dict_code, '未通过' AS item_label, '2' AS item_value, 'danger' AS tag_type, 30 AS sort + UNION ALL SELECT 'tenant_domain_ssl_status' AS dict_code, '待处理' AS item_label, '0' AS item_value, 'info' AS tag_type, 10 AS sort + UNION ALL SELECT 'tenant_domain_ssl_status' AS dict_code, '签发中' AS item_label, '1' AS item_value, 'warning' AS tag_type, 20 AS sort + UNION ALL SELECT 'tenant_domain_ssl_status' AS dict_code, '已签发' AS item_label, '2' AS item_value, 'success' AS tag_type, 30 AS sort + UNION ALL SELECT 'tenant_domain_ssl_status' AS dict_code, '签发失败' AS item_label, '3' AS item_value, 'danger' AS tag_type, 40 AS sort +) AS t +JOIN `yz_backend_dict` AS d + ON d.tenant_id = '0' AND d.dict_code = t.dict_code AND d.delete_time IS NULL +WHERE NOT EXISTS ( + SELECT 1 FROM `yz_backend_dict_item` AS i + WHERE i.dict_id = d.id AND i.item_value = t.item_value AND i.delete_time IS NULL +); + + +-- ============================================================= +-- 3. 校验 +-- ============================================================= + +SELECT d.dict_name, d.dict_code, COUNT(i.id) AS item_count +FROM `yz_backend_dict` d +LEFT JOIN `yz_backend_dict_item` i ON i.dict_id = d.id AND i.delete_time IS NULL +WHERE d.tenant_id = '0' AND d.delete_time IS NULL +GROUP BY d.id, d.dict_name, d.dict_code +ORDER BY d.sort, d.id; diff --git a/go/docs/sql/insert_platform_dict_data.sql b/go/docs/sql/insert_platform_dict_data.sql new file mode 100644 index 0000000..10d02a8 --- /dev/null +++ b/go/docs/sql/insert_platform_dict_data.sql @@ -0,0 +1,305 @@ +-- ============================================================= +-- 平台端数据字典 —— 初始数据 +-- +-- 表:yz_platform_dict / yz_platform_dict_item(建表见 create_dict.sql) +-- 内容来源:platform 前端散落的硬编码映射(值→文案 / 标签色), +-- 以及各平台端接口返回的状态字段取值。 +-- +-- 幂等: +-- - 字典按 dict_code 判重,已存在则跳过; +-- - 字典项按 (dict_id, item_value) 判重,已存在则跳过。 +-- 因此可重复执行,不会产生重复数据。 +-- +-- 说明: +-- - tag_type 对应前端 el-tag 的 type,留空表示不渲染彩色标签; +-- - sort 按 10 递增,便于后续插入新字典项; +-- - 执行完这份数据后,前端改为调用 /platform/dict/options/:code 读取。 +-- ============================================================= + +SET NAMES utf8mb4; + + +-- ============================================================= +-- 1. 字典列表 +-- ============================================================= + +INSERT INTO `yz_platform_dict` (`dict_name`, `dict_code`, `remark`, `sort`, `status`, `create_user_id`, `create_time`) +SELECT t.dict_name, t.dict_code, t.remark, t.sort, 1, 'sql', NOW() +FROM ( + -- 通用 + SELECT '通用启用状态' AS dict_name, 'common_enable_status' AS dict_code, '1 启用 / 0 停用,部分页面把 0 显示为「禁用」' AS remark, 10 AS sort + UNION ALL SELECT '是否' , 'common_yes_no' , '1 是 / 0 否' , 20 + -- 支付 + UNION ALL SELECT '支付渠道' , 'payment_channel' , 'yz_platform_payment_channel.channel' , 100 + UNION ALL SELECT '支付单状态' , 'payment_order_status' , 'yz_platform_payment_order.status' , 110 + UNION ALL SELECT '支付订单类型' , 'payment_order_type' , 'yz_platform_payment_order.order_type' , 120 + UNION ALL SELECT '支付渠道状态' , 'payment_channel_state' , '渠道配置页展示状态:启用/停用/未配置' , 130 + UNION ALL SELECT '对账处理状态' , 'payment_reconcile_status' , 'yz_platform_payment_reconcile_diff.handle_status' , 140 + UNION ALL SELECT '对账差异类型' , 'payment_reconcile_diff_type', 'yz_platform_payment_reconcile_diff.diff_type' , 150 + UNION ALL SELECT '佣金状态' , 'payment_commission_status', 'yz_platform_commission_order.status' , 160 + UNION ALL SELECT '佣金发放方式' , 'payment_commission_pay_method', 'yz_platform_commission_order.pay_method' , 170 + UNION ALL SELECT '佣金计算方式' , 'payment_calc_type' , 'yz_platform_commission_rule.calc_type' , 180 + -- 通知 / 短信 + UNION ALL SELECT '通知渠道' , 'notification_channel' , '站内提醒的推送渠道' , 200 + UNION ALL SELECT '短信任务状态' , 'sms_task_status' , 'yz_system_sms_task.status' , 210 + -- 系统 + UNION ALL SELECT '请求方法' , 'http_method' , '操作日志的 HTTP 方法' , 220 + UNION ALL SELECT '登录方式' , 'login_type' , '登录日志的登录方式' , 230 + UNION ALL SELECT '菜单类型' , 'menu_type' , 'yz_system_menu.type' , 240 + -- 官网内容 + UNION ALL SELECT '导航类型' , 'website_nav_type' , 'yz_platform_website_nav.type' , 250 + UNION ALL SELECT '评论状态' , 'comment_status' , 'yz_platform_website_comment.status' , 260 + UNION ALL SELECT '文章状态' , 'article_status' , 'yz_platform_website_article.status' , 270 + UNION ALL SELECT '公告类型' , 'notice_type' , 'yz_platform_website_notice.type' , 280 + UNION ALL SELECT '公告状态' , 'notice_status' , 'yz_platform_website_notice.status' , 290 + -- 账号池 / Cursor + UNION ALL SELECT '账号池平台' , 'account_pool_platform' , '账号来源平台(本地/闲鱼/淘宝…)' , 300 + UNION ALL SELECT '账号池数据类型' , 'account_pool_data_type' , '账号密码 / 账号密码+Token / Token' , 310 + UNION ALL SELECT '账号池提取状态' , 'account_pool_extract_status', '账号池记录的提取状态' , 320 + UNION ALL SELECT 'Cursor 设备状态' , 'cursor_equipment_status' , 'yz_platform_cursor_equipment.status' , 330 + UNION ALL SELECT 'Cursor 激活码状态' , 'cursor_activation_status' , 'yz_platform_cursor_activation_code.status' , 340 + UNION ALL SELECT '提取记录状态' , 'cursor_extract_record_status', 'Cursor 设备提取记录的状态' , 350 + UNION ALL SELECT '激活记录状态' , 'cursor_activation_record_status', 'Cursor 设备激活记录的状态' , 360 + -- 模块商店 + UNION ALL SELECT '模块发布状态' , 'moduleshop_publish_status' , '模块商店上下架状态' , 370 + UNION ALL SELECT '模块分类状态' , 'moduleshop_category_status' , '模块商店分类启用状态' , 380 + -- 投诉工单 + UNION ALL SELECT '投诉工单状态' , 'complain_status' , 'yz_platform_complaint.status' , 390 + -- 统一认证 + UNION ALL SELECT '接入应用类型' , 'auth_app_type' , 'yz_auth_client.app_type' , 400 + UNION ALL SELECT '认证域' , 'auth_realm' , '租户域 / 平台域' , 410 + UNION ALL SELECT '登录验证方式' , 'auth_verify_type' , '图形验证码 / 短信 / 邮箱 / 极验 / 不开启' , 420 + -- 域名 + UNION ALL SELECT '域名类型' , 'domain_type' , 'yz_system_tenant_domain.domain_type' , 430 + UNION ALL SELECT '域名状态' , 'domain_status' , 'yz_system_tenant_domain.status' , 440 + -- 租户 / 组织 + UNION ALL SELECT '员工状态' , 'employee_status' , 'yz_backend_employee.account_status' , 450 + UNION ALL SELECT '性别' , 'gender' , '用户/员工性别' , 460 + UNION ALL SELECT '角色分类' , 'role_category' , 'yz_system_admin_role.cid:1 平台角色 / 2 租户角色' , 470 + UNION ALL SELECT '角色来源' , 'role_source' , 'yz_system_admin_role.is_custom' , 480 + UNION ALL SELECT '站内信接收对象' , 'sitereminder_target_type' , 'yz_system_reminder_list.target_type' , 490 +) AS t +WHERE NOT EXISTS ( + SELECT 1 FROM `yz_platform_dict` AS d + WHERE d.dict_code = t.dict_code AND d.delete_time IS NULL +); + + +-- ============================================================= +-- 2. 字典项 +-- ============================================================= + +INSERT INTO `yz_platform_dict_item` (`dict_id`, `item_label`, `item_value`, `tag_type`, `sort`, `status`, `is_default`, `create_time`) +SELECT d.id, t.item_label, t.item_value, t.tag_type, t.sort, 1, 0, NOW() +FROM ( + -- 通用启用状态 + SELECT 'common_enable_status' AS dict_code, '启用' AS item_label, '1' AS item_value, 'success' AS tag_type, 10 AS sort + UNION ALL SELECT 'common_enable_status' , '停用' , '0' , 'info' , 20 + -- 是否 + UNION ALL SELECT 'common_yes_no' , '是' , '1' , 'success' , 10 + UNION ALL SELECT 'common_yes_no' , '否' , '0' , 'info' , 20 + -- 支付渠道 + UNION ALL SELECT 'payment_channel' , '微信支付' , 'wechat' , '' , 10 + UNION ALL SELECT 'payment_channel' , '支付宝' , 'alipay' , '' , 20 + UNION ALL SELECT 'payment_channel' , '银联' , 'unionpay' , '' , 30 + UNION ALL SELECT 'payment_channel' , '云闪付' , 'cloudpay' , '' , 40 + UNION ALL SELECT 'payment_channel' , 'PayPal' , 'paypal' , '' , 50 + -- 支付单状态 + UNION ALL SELECT 'payment_order_status' , '已创建' , 'created' , 'info' , 10 + UNION ALL SELECT 'payment_order_status' , '待支付' , 'pending' , 'warning' , 20 + UNION ALL SELECT 'payment_order_status' , '支付中' , 'paying' , 'warning' , 30 + UNION ALL SELECT 'payment_order_status' , '支付成功' , 'paid' , 'success' , 40 + UNION ALL SELECT 'payment_order_status' , '支付成功' , 'success' , 'success' , 50 + UNION ALL SELECT 'payment_order_status' , '支付失败' , 'failed' , 'danger' , 60 + UNION ALL SELECT 'payment_order_status' , '已关闭' , 'closed' , 'info' , 70 + UNION ALL SELECT 'payment_order_status' , '已超时' , 'timeout' , 'info' , 80 + UNION ALL SELECT 'payment_order_status' , '退款中' , 'refunding' , 'warning' , 90 + UNION ALL SELECT 'payment_order_status' , '已退款' , 'refunded' , 'info' , 100 + UNION ALL SELECT 'payment_order_status' , '部分退款' , 'partial_refunded' , 'warning' , 110 + -- 支付订单类型 + UNION ALL SELECT 'payment_order_type' , '平台使用费' , 'platform_usage' , '' , 10 + UNION ALL SELECT 'payment_order_type' , '模块购买' , 'module_shop' , '' , 20 + UNION ALL SELECT 'payment_order_type' , '服务费' , 'service_fee' , '' , 30 + -- 支付渠道状态 + UNION ALL SELECT 'payment_channel_state' , '已启用' , 'enabled' , 'success' , 10 + UNION ALL SELECT 'payment_channel_state' , '已停用' , 'disabled' , 'info' , 20 + UNION ALL SELECT 'payment_channel_state' , '未配置' , 'unconfigured' , 'warning' , 30 + -- 对账处理状态 + UNION ALL SELECT 'payment_reconcile_status' , '待处理' , 'unhandled' , 'warning' , 10 + UNION ALL SELECT 'payment_reconcile_status' , '处理中' , 'handling' , 'primary' , 20 + UNION ALL SELECT 'payment_reconcile_status' , '已处理' , 'handled' , 'success' , 30 + UNION ALL SELECT 'payment_reconcile_status' , '已忽略' , 'ignored' , 'info' , 40 + -- 对账差异类型 + UNION ALL SELECT 'payment_reconcile_diff_type' , '长款' , 'long' , 'warning' , 10 + UNION ALL SELECT 'payment_reconcile_diff_type' , '短款' , 'short' , 'danger' , 20 + UNION ALL SELECT 'payment_reconcile_diff_type' , '状态不一致' , 'status_mismatch' , 'warning' , 30 + -- 佣金状态 + UNION ALL SELECT 'payment_commission_status' , '应付' , 'payable' , 'warning' , 10 + UNION ALL SELECT 'payment_commission_status' , '已付' , 'settled' , 'success' , 20 + UNION ALL SELECT 'payment_commission_status' , '税务处理中' , 'tax_processing' , 'primary' , 30 + UNION ALL SELECT 'payment_commission_status' , '已作废' , 'canceled' , 'info' , 40 + -- 佣金发放方式 + UNION ALL SELECT 'payment_commission_pay_method', '余额抵扣' , 'balance' , '' , 10 + UNION ALL SELECT 'payment_commission_pay_method', '银行转账' , 'bank' , '' , 20 + UNION ALL SELECT 'payment_commission_pay_method', '人工发放' , 'manual' , '' , 30 + UNION ALL SELECT 'payment_commission_pay_method', '下期抵扣' , 'offset' , '' , 40 + -- 佣金计算方式 + UNION ALL SELECT 'payment_calc_type' , '按比例' , 'percent' , 'primary' , 10 + UNION ALL SELECT 'payment_calc_type' , '固定金额' , 'fixed' , 'success' , 20 + UNION ALL SELECT 'payment_calc_type' , '阶梯' , 'tiered' , 'warning' , 30 + -- 通知渠道 + UNION ALL SELECT 'notification_channel' , '短信' , 'SMS' , 'success' , 10 + UNION ALL SELECT 'notification_channel' , '邮件' , 'EMAIL' , 'warning' , 20 + UNION ALL SELECT 'notification_channel' , 'Bark推送' , 'BARK' , 'danger' , 30 + UNION ALL SELECT 'notification_channel' , '站内信' , 'SITE_MSG' , 'primary' , 40 + -- 短信任务状态 + UNION ALL SELECT 'sms_task_status' , '待发送' , '0' , 'info' , 10 + UNION ALL SELECT 'sms_task_status' , '发送中' , '1' , 'warning' , 20 + UNION ALL SELECT 'sms_task_status' , '发送失败' , '2' , 'danger' , 30 + UNION ALL SELECT 'sms_task_status' , '发送成功' , '3' , 'success' , 40 + -- 请求方法 + UNION ALL SELECT 'http_method' , 'GET' , 'GET' , 'info' , 10 + UNION ALL SELECT 'http_method' , 'POST' , 'POST' , 'success' , 20 + UNION ALL SELECT 'http_method' , 'PUT' , 'PUT' , 'warning' , 30 + UNION ALL SELECT 'http_method' , 'DELETE' , 'DELETE' , 'danger' , 40 + UNION ALL SELECT 'http_method' , 'PATCH' , 'PATCH' , 'primary' , 50 + -- 登录方式 + UNION ALL SELECT 'login_type' , '密码登录' , 'password' , '' , 10 + UNION ALL SELECT 'login_type' , '短信验证码' , 'sms' , '' , 20 + UNION ALL SELECT 'login_type' , '邮箱验证码' , 'email' , '' , 30 + -- 菜单类型 + UNION ALL SELECT 'menu_type' , '目录' , '1' , 'primary' , 10 + UNION ALL SELECT 'menu_type' , '页面' , '2' , 'success' , 20 + UNION ALL SELECT 'menu_type' , '接口' , '3' , 'info' , 30 + -- 导航类型 + UNION ALL SELECT 'website_nav_type' , '目录' , '1' , 'primary' , 10 + UNION ALL SELECT 'website_nav_type' , '页面' , '2' , 'success' , 20 + UNION ALL SELECT 'website_nav_type' , '外链' , '3' , 'info' , 30 + UNION ALL SELECT 'website_nav_type' , '单页' , '4' , 'warning' , 40 + -- 评论状态 + UNION ALL SELECT 'comment_status' , '待审核' , '0' , 'warning' , 10 + UNION ALL SELECT 'comment_status' , '已通过' , '1' , 'success' , 20 + UNION ALL SELECT 'comment_status' , '已拒绝' , '2' , 'danger' , 30 + -- 文章状态 + UNION ALL SELECT 'article_status' , '草稿' , '0' , 'info' , 10 + UNION ALL SELECT 'article_status' , '待审核' , '1' , 'warning' , 20 + UNION ALL SELECT 'article_status' , '已发布' , '2' , 'success' , 30 + UNION ALL SELECT 'article_status' , '已隐藏' , '3' , 'danger' , 40 + -- 公告类型 + UNION ALL SELECT 'notice_type' , '通知公告' , '1' , '' , 10 + UNION ALL SELECT 'notice_type' , '活动公告' , '2' , '' , 20 + UNION ALL SELECT 'notice_type' , '系统维护' , '3' , '' , 30 + -- 公告状态 + UNION ALL SELECT 'notice_status' , '草稿' , '0' , 'info' , 10 + UNION ALL SELECT 'notice_status' , '已发布' , '1' , 'success' , 20 + UNION ALL SELECT 'notice_status' , '已下线' , '2' , 'warning' , 30 + -- 账号池平台 + UNION ALL SELECT 'account_pool_platform' , '本地' , 'local' , 'info' , 10 + UNION ALL SELECT 'account_pool_platform' , '闲鱼' , 'xianyu' , 'warning' , 20 + UNION ALL SELECT 'account_pool_platform' , '拼多多' , 'pinduoduo' , 'danger' , 30 + UNION ALL SELECT 'account_pool_platform' , '京东' , 'jingdong' , 'primary' , 40 + UNION ALL SELECT 'account_pool_platform' , '抖音' , 'douyin' , 'success' , 50 + UNION ALL SELECT 'account_pool_platform' , '淘宝' , 'taobao' , 'info' , 60 + UNION ALL SELECT 'account_pool_platform' , '自有商城' , 'ziyoushangcheng' , 'warning' , 70 + UNION ALL SELECT 'account_pool_platform' , '续杯' , 'xubei' , 'primary' , 80 + -- 账号池数据类型 + UNION ALL SELECT 'account_pool_data_type' , '账号密码' , 'account' , 'success' , 10 + UNION ALL SELECT 'account_pool_data_type' , '账号密码+Token' , 'account_tk' , 'primary' , 20 + UNION ALL SELECT 'account_pool_data_type' , 'Token' , 'tk' , 'warning' , 30 + -- 账号池提取状态 + UNION ALL SELECT 'account_pool_extract_status' , '未提取' , '0' , 'info' , 10 + UNION ALL SELECT 'account_pool_extract_status' , '补号' , '2' , 'warning' , 20 + UNION ALL SELECT 'account_pool_extract_status' , '续杯' , '3' , 'primary' , 30 + UNION ALL SELECT 'account_pool_extract_status' , '已提取' , 'extracted' , 'success' , 40 + -- Cursor 设备状态 + UNION ALL SELECT 'cursor_equipment_status' , '已激活' , 'active' , 'success' , 10 + UNION ALL SELECT 'cursor_equipment_status' , '未激活' , 'inactive' , 'info' , 20 + UNION ALL SELECT 'cursor_equipment_status' , '禁用' , 'disabled' , 'danger' , 30 + UNION ALL SELECT 'cursor_equipment_status' , '已过期' , 'expired' , 'warning' , 40 + -- Cursor 激活码状态 + UNION ALL SELECT 'cursor_activation_status' , '未使用' , '0' , 'info' , 10 + UNION ALL SELECT 'cursor_activation_status' , '已使用' , '1' , 'success' , 20 + UNION ALL SELECT 'cursor_activation_status' , '已过期' , '2' , 'warning' , 30 + UNION ALL SELECT 'cursor_activation_status' , '已禁用' , '3' , 'danger' , 40 + -- 提取记录状态 + UNION ALL SELECT 'cursor_extract_record_status', '未提取' , '0' , 'info' , 10 + UNION ALL SELECT 'cursor_extract_record_status', '已提取' , '1' , 'success' , 20 + UNION ALL SELECT 'cursor_extract_record_status', '补号' , '2' , 'warning' , 30 + UNION ALL SELECT 'cursor_extract_record_status', '异常' , '3' , 'danger' , 40 + -- 激活记录状态 + UNION ALL SELECT 'cursor_activation_record_status', '失败' , '0' , 'danger' , 10 + UNION ALL SELECT 'cursor_activation_record_status', '成功' , '1' , 'success' , 20 + -- 模块发布状态 + UNION ALL SELECT 'moduleshop_publish_status' , '草稿' , '0' , 'info' , 10 + UNION ALL SELECT 'moduleshop_publish_status' , '已发布' , '1' , 'success' , 20 + UNION ALL SELECT 'moduleshop_publish_status' , '已撤回' , '2' , 'warning' , 30 + -- 模块分类状态 + UNION ALL SELECT 'moduleshop_category_status' , '禁用' , '0' , 'info' , 10 + UNION ALL SELECT 'moduleshop_category_status' , '启用' , '1' , 'success' , 20 + -- 投诉工单状态 + UNION ALL SELECT 'complain_status' , '待处理' , '0' , 'warning' , 10 + UNION ALL SELECT 'complain_status' , '处理中' , '1' , 'warning' , 20 + UNION ALL SELECT 'complain_status' , '已回复' , '2' , 'success' , 30 + UNION ALL SELECT 'complain_status' , '已关闭' , '3' , 'info' , 40 + -- 接入应用类型 + UNION ALL SELECT 'auth_app_type' , 'Web 后端' , '1' , '' , 10 + UNION ALL SELECT 'auth_app_type' , 'SPA' , '2' , '' , 20 + UNION ALL SELECT 'auth_app_type' , '原生 APP' , '3' , '' , 30 + UNION ALL SELECT 'auth_app_type' , '小程序' , '4' , '' , 40 + -- 认证域 + UNION ALL SELECT 'auth_realm' , '租户域' , 'tenant' , '' , 10 + UNION ALL SELECT 'auth_realm' , '平台域' , 'platform' , '' , 20 + -- 登录验证方式 + UNION ALL SELECT 'auth_verify_type' , '图形验证码' , 'captcha' , '' , 10 + UNION ALL SELECT 'auth_verify_type' , '短信验证码' , 'sms' , '' , 20 + UNION ALL SELECT 'auth_verify_type' , '邮箱验证码' , 'email' , '' , 30 + UNION ALL SELECT 'auth_verify_type' , '极验人机验证' , 'geetest' , '' , 40 + UNION ALL SELECT 'auth_verify_type' , '不开启' , 'none' , '' , 50 + -- 域名类型 + UNION ALL SELECT 'domain_type' , '平台二级域名' , '1' , '' , 10 + UNION ALL SELECT 'domain_type' , '自有域名' , '2' , '' , 20 + -- 域名状态 + UNION ALL SELECT 'domain_status' , '审核中' , '0' , 'warning' , 10 + UNION ALL SELECT 'domain_status' , '已生效' , '1' , 'success' , 20 + UNION ALL SELECT 'domain_status' , '已禁用' , '2' , 'info' , 30 + -- 员工状态 + UNION ALL SELECT 'employee_status' , '启用' , '1' , 'success' , 10 + UNION ALL SELECT 'employee_status' , '禁用' , '0' , 'info' , 20 + UNION ALL SELECT 'employee_status' , '离职' , '2' , 'danger' , 30 + -- 性别 + UNION ALL SELECT 'gender' , '保密' , '0' , 'info' , 10 + UNION ALL SELECT 'gender' , '男' , '1' , 'primary' , 20 + UNION ALL SELECT 'gender' , '女' , '2' , 'danger' , 30 + -- 角色分类 + UNION ALL SELECT 'role_category' , '平台角色' , '1' , 'primary' , 10 + UNION ALL SELECT 'role_category' , '租户角色' , '2' , 'warning' , 20 + -- 角色来源 + UNION ALL SELECT 'role_source' , '预置' , '0' , 'info' , 10 + UNION ALL SELECT 'role_source' , '自建' , '1' , 'warning' , 20 + -- 站内信接收对象 + UNION ALL SELECT 'sitereminder_target_type' , '全平台(所有人)' , 'all' , 'danger' , 10 + UNION ALL SELECT 'sitereminder_target_type' , '平台端(所有人)' , 'platform' , 'primary' , 20 + UNION ALL SELECT 'sitereminder_target_type' , '租户端(所有租户)' , 'tenant_all' , 'success' , 30 + UNION ALL SELECT 'sitereminder_target_type' , '租户角色' , 'tenant_role' , 'warning' , 40 + UNION ALL SELECT 'sitereminder_target_type' , '租户' , 'tenant' , 'warning' , 50 + UNION ALL SELECT 'sitereminder_target_type' , '租户用户' , 'tenant_user' , 'info' , 60 + UNION ALL SELECT 'sitereminder_target_type' , '平台角色' , 'role' , 'warning' , 70 +) AS t +JOIN `yz_platform_dict` AS d + ON d.dict_code = t.dict_code AND d.delete_time IS NULL +WHERE NOT EXISTS ( + SELECT 1 FROM `yz_platform_dict_item` AS i + WHERE i.dict_id = d.id AND i.item_value = t.item_value AND i.delete_time IS NULL +); + + +-- ============================================================= +-- 3. 校验 +-- ============================================================= + +SELECT d.dict_name, d.dict_code, COUNT(i.id) AS item_count +FROM `yz_platform_dict` d +LEFT JOIN `yz_platform_dict_item` i ON i.dict_id = d.id AND i.delete_time IS NULL +WHERE d.delete_time IS NULL +GROUP BY d.id, d.dict_name, d.dict_code +ORDER BY d.sort, d.id; diff --git a/go/models/backend_dict.go b/go/models/backend_dict.go new file mode 100644 index 0000000..c496513 --- /dev/null +++ b/go/models/backend_dict.go @@ -0,0 +1,45 @@ +package models + +import "time" + +// BackendDict 租户端数据字典(字典列表): yz_backend_dict +// 表结构见 docs/sql/create_dict.sql。租户隔离字段 tenant_id 与 yz_backend_erp_supplier 一致。 +type BackendDict struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + DictName string `orm:"column(dict_name);size(100)" json:"dict_name"` + DictCode string `orm:"column(dict_code);size(100)" json:"dict_code"` + Remark string `orm:"column(remark);size(500)" json:"remark"` + Sort int `orm:"column(sort);default(0)" json:"sort"` + Status int8 `orm:"column(status);default(1)" json:"status"` + CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"` + CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *BackendDict) TableName() string { + return "yz_backend_dict" +} + +// BackendDictItem 租户端字典项: yz_backend_dict_item +// tenant_id 冗余自所属字典,写入时由后端保证与字典一致。 +type BackendDictItem struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + DictID uint64 `orm:"column(dict_id);default(0)" json:"dict_id"` + ItemLabel string `orm:"column(item_label);size(100)" json:"item_label"` + ItemValue string `orm:"column(item_value);size(100)" json:"item_value"` + TagType string `orm:"column(tag_type);size(20)" json:"tag_type"` + Sort int `orm:"column(sort);default(0)" json:"sort"` + Status int8 `orm:"column(status);default(1)" json:"status"` + IsDefault int8 `orm:"column(is_default);default(0)" json:"is_default"` + Remark string `orm:"column(remark);size(500)" json:"remark"` + CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *BackendDictItem) TableName() string { + return "yz_backend_dict_item" +} diff --git a/go/models/init.go b/go/models/init.go index 21bb297..9249333 100644 --- a/go/models/init.go +++ b/go/models/init.go @@ -210,6 +210,12 @@ func Init(_ string) { new(AuthTenantIdp), new(AuthTenantAuthConfig), new(AuthLoginLog), + + // 数据字典(docs/sql/create_dict.sql):平台端 / 租户端各一套 + new(PlatformDict), + new(PlatformDictItem), + new(BackendDict), + new(BackendDictItem), ) // 创建全局 Ormer diff --git a/go/models/platform_dict.go b/go/models/platform_dict.go new file mode 100644 index 0000000..dc9e003 --- /dev/null +++ b/go/models/platform_dict.go @@ -0,0 +1,42 @@ +package models + +import "time" + +// PlatformDict 平台端数据字典(字典列表): yz_platform_dict +// 表结构见 docs/sql/create_dict.sql(人工执行建表,代码不做自动建表)。 +type PlatformDict struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + DictName string `orm:"column(dict_name);size(100)" json:"dict_name"` + DictCode string `orm:"column(dict_code);size(100)" json:"dict_code"` + Remark string `orm:"column(remark);size(500)" json:"remark"` + Sort int `orm:"column(sort);default(0)" json:"sort"` + Status int8 `orm:"column(status);default(1)" json:"status"` + CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"` + CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *PlatformDict) TableName() string { + return "yz_platform_dict" +} + +// PlatformDictItem 平台端字典项: yz_platform_dict_item +type PlatformDictItem struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + DictID uint64 `orm:"column(dict_id);default(0)" json:"dict_id"` + ItemLabel string `orm:"column(item_label);size(100)" json:"item_label"` + ItemValue string `orm:"column(item_value);size(100)" json:"item_value"` + TagType string `orm:"column(tag_type);size(20)" json:"tag_type"` + Sort int `orm:"column(sort);default(0)" json:"sort"` + Status int8 `orm:"column(status);default(1)" json:"status"` + IsDefault int8 `orm:"column(is_default);default(0)" json:"is_default"` + Remark string `orm:"column(remark);size(500)" json:"remark"` + CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *PlatformDictItem) TableName() string { + return "yz_platform_dict_item" +} diff --git a/go/routers/backend/backend.go b/go/routers/backend/backend.go index 7b5c640..7ae4752 100644 --- a/go/routers/backend/backend.go +++ b/go/routers/backend/backend.go @@ -14,6 +14,7 @@ func Register() { RegisterPaymentRoutes() RegisterProductRoutes() RegisterWechatMpRoutes() + RegisterDictRoutes() } // RegisterProductRoutes 注册租户端「产品购买」路由。 diff --git a/go/routers/backend/dict.go b/go/routers/backend/dict.go new file mode 100644 index 0000000..9401e5d --- /dev/null +++ b/go/routers/backend/dict.go @@ -0,0 +1,24 @@ +package backend + +import ( + "server/controllers" + + beego "github.com/beego/beego/v2/server/web" +) + +// RegisterDictRoutes 注册租户端数据字典路由(表结构见 docs/sql/create_dict.sql)。 +// 租户隔离由 JWT 里的租户ID 完成,前端不传 tenant_id。 +func RegisterDictRoutes() { + // 字典列表(字典类型) + beego.Router("/backend/dict/types", &controllers.BackendDictController{}, "get:ListTypes;post:CreateType") + beego.Router("/backend/dict/types/:id", &controllers.BackendDictController{}, "put:UpdateType;delete:DeleteType") + + // 字典项 + beego.Router("/backend/dict/items", &controllers.BackendDictController{}, "get:ListItems;post:CreateItem") + beego.Router("/backend/dict/items/:id", &controllers.BackendDictController{}, "put:UpdateItem;delete:DeleteItem") + beego.Router("/backend/dict/items/:id/status", &controllers.BackendDictController{}, "post:ToggleItemStatus") + + // 业务读取:单个编码 / 批量(指定编码)/ 全量(前端预热) + beego.Router("/backend/dict/options/:code", &controllers.BackendDictController{}, "get:Options") + beego.Router("/backend/dict/options", &controllers.BackendDictController{}, "get:OptionsAll;post:OptionsBatch") +} diff --git a/go/routers/backend/dict_test.go b/go/routers/backend/dict_test.go new file mode 100644 index 0000000..3b66dfa --- /dev/null +++ b/go/routers/backend/dict_test.go @@ -0,0 +1,15 @@ +package backend + +import "testing" + +// TestRegisterDictRoutes 确认字典路由能完成注册且不与既有路由冲突。 +// beego 在注册重复/歧义路由或非法方法名时会 panic,这里通过实际注册来兜住这类错误。 +func TestRegisterDictRoutes(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatalf("注册租户端字典路由失败: %v", r) + } + }() + + RegisterDictRoutes() +} diff --git a/go/routers/platform/dict.go b/go/routers/platform/dict.go new file mode 100644 index 0000000..ec12493 --- /dev/null +++ b/go/routers/platform/dict.go @@ -0,0 +1,23 @@ +package platform + +import ( + "server/controllers" + + beego "github.com/beego/beego/v2/server/web" +) + +// RegisterDictRoutes 注册平台端数据字典路由(表结构见 docs/sql/create_dict.sql)。 +func RegisterDictRoutes() { + // 字典列表(字典类型) + beego.Router("/platform/dict/types", &controllers.PlatformDictController{}, "get:ListTypes;post:CreateType") + beego.Router("/platform/dict/types/:id", &controllers.PlatformDictController{}, "put:UpdateType;delete:DeleteType") + + // 字典项 + beego.Router("/platform/dict/items", &controllers.PlatformDictController{}, "get:ListItems;post:CreateItem") + beego.Router("/platform/dict/items/:id", &controllers.PlatformDictController{}, "put:UpdateItem;delete:DeleteItem") + beego.Router("/platform/dict/items/:id/status", &controllers.PlatformDictController{}, "post:ToggleItemStatus") + + // 业务读取:单个编码 / 批量(指定编码)/ 全量(前端预热) + beego.Router("/platform/dict/options/:code", &controllers.PlatformDictController{}, "get:Options") + beego.Router("/platform/dict/options", &controllers.PlatformDictController{}, "get:OptionsAll;post:OptionsBatch") +} diff --git a/go/routers/platform/dict_test.go b/go/routers/platform/dict_test.go new file mode 100644 index 0000000..39bd558 --- /dev/null +++ b/go/routers/platform/dict_test.go @@ -0,0 +1,15 @@ +package platform + +import "testing" + +// TestRegisterDictRoutes 确认字典路由能完成注册且不与既有路由冲突。 +// beego 在注册重复/歧义路由或非法方法名时会 panic,这里通过实际注册来兜住这类错误。 +func TestRegisterDictRoutes(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatalf("注册平台端字典路由失败: %v", r) + } + }() + + RegisterDictRoutes() +} diff --git a/go/routers/platform/platform.go b/go/routers/platform/platform.go index 0272584..35b1faf 100644 --- a/go/routers/platform/platform.go +++ b/go/routers/platform/platform.go @@ -14,6 +14,9 @@ func Register() { // 微信公众号(服务号):配置 / 绑定 / 粉丝 / 推送 / 微信服务器回调 RegisterWechatMpRoutes() + // 数据字典(字典列表 / 字典项) + RegisterDictRoutes() + // 统一认证中心 - 租户登录策略配置 beego.Router("/platform/authConfig/detail", &controllers.PlatformAuthConfigController{}, "get:Detail") beego.Router("/platform/authConfig/save", &controllers.PlatformAuthConfigController{}, "post:Save") diff --git a/platform/components.d.ts b/platform/components.d.ts index 32ee612..be750b5 100644 --- a/platform/components.d.ts +++ b/platform/components.d.ts @@ -18,6 +18,8 @@ declare module 'vue' { ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElBacktop: typeof import('element-plus/es')['ElBacktop'] ElBadge: typeof import('element-plus/es')['ElBadge'] + ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'] + ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'] ElButton: typeof import('element-plus/es')['ElButton'] ElCard: typeof import('element-plus/es')['ElCard'] ElCascader: typeof import('element-plus/es')['ElCascader'] diff --git a/platform/docs/pinia-dict-guide.md b/platform/docs/pinia-dict-guide.md deleted file mode 100644 index f3365a4..0000000 --- a/platform/docs/pinia-dict-guide.md +++ /dev/null @@ -1,393 +0,0 @@ -# Pinia 字典管理系统使用指南 - -## 系统架构 - -``` -┌─────────────────────────────────────┐ -│ API 接口 (getDictItemsByCode) │ -│ /api/dict/items/code/{code} │ -└──────────────┬──────────────────────┘ - │ - ↓ -┌─────────────────────────────────────┐ -│ Pinia Store (useDictStore) │ -│ ✅ 自动缓存字典数据 │ -│ ✅ 避免重复请求 │ -│ ✅ 支持同步/异步访问 │ -└──────────────┬──────────────────────┘ - │ - ┌──────┴──────┐ - ↓ ↓ - ┌────────┐ ┌──────────────┐ - │组件 │ │Composable │ - │直接用 │ │useDict Hook │ - └────────┘ └──────────────┘ -``` - ---- - -## 核心文件说明 - -### 1. **Store**: `src/stores/dict.js` - -字典数据的全局管理器 - -**主要方法**: -```javascript -import { useDictStore } from '@/stores/dict' - -const dictStore = useDictStore() - -// ✅ 异步获取字典(推荐) -const items = await dictStore.getDictItems('user_status') - -// ✅ 同步获取字典(已缓存时) -const items = dictStore.getDictItemsSync('user_status') - -// ✅ 获取字典值对应的标签 -const label = dictStore.getDictLabel('user_status', 1) - -// ✅ 预加载多个字典 -await dictStore.preloadDicts(['user_status', 'user_role']) - -// ✅ 清空缓存 -dictStore.clearCache('user_status') -``` - ---- - -### 2. **常量**: `src/constants/dictCodes.js` - -集中管理所有字典编码 - -**使用示例**: -```javascript -import { DICT_CODES } from '@/constants/dictCodes' - -// 好处:避免硬编码,IDE 有自动完成 -const items = await dictStore.getDictItems(DICT_CODES.USER_STATUS) - -// 所有可用的编码: -DICT_CODES.USER_STATUS // 用户状态 -DICT_CODES.USER_GENDER // 用户性别 -DICT_CODES.USER_ROLE // 用户角色 -DICT_CODES.DEPT_STATUS // 部门状态 -DICT_CODES.POSITION_STATUS // 职位状态 -// ... 更多编码 -``` - ---- - -### 3. **Composable**: `src/composables/useDict.js` - -简化在组件中使用字典的 Hook - -**基础用法**: -```javascript -import { useDictionary, useUserStatusDict } from '@/composables/useDict' -import { DICT_CODES } from '@/constants/dictCodes' - -// 方式1:使用常量 -const { statusDict, loading } = useDictionary(DICT_CODES.USER_STATUS) - -// 方式2:使用字符串 -const { dicts, loading } = useDictionary('user_status') - -// 方式3:使用特化 Hook(推荐) -const { user_statusDict, loading } = useUserStatusDict() -``` - ---- - -## 使用场景 - -### 场景1:在列表页加载字典 - -**文件**: `src/views/system/users/index.vue` - -```vue - - - -``` - ---- - -### 场景2:在编辑对话框使用字典 - -**文件**: `src/views/system/users/components/UserEdit.vue` - -```vue - - - -``` - ---- - -### 场景3:快速使用 Composable Hook - -最简单的方式,自动处理加载: - -```vue - - - -``` - ---- - -### 场景4:预加载应用启动时需要的字典 - -**文件**: `src/main.js` - -```javascript -import { createApp } from 'vue' -import { createPinia } from 'pinia' -import { useDictStore } from '@/stores/dict' -import { DICT_CODES } from '@/constants/dictCodes' - -const app = createApp(App) -const pinia = createPinia() - -app.use(pinia) - -// 在应用启动后预加载常用字典 -const dictStore = useDictStore() -await dictStore.preloadDicts([ - DICT_CODES.USER_STATUS, - DICT_CODES.COMMON_STATUS, - DICT_CODES.YES_NO, -]) - -app.mount('#app') -``` - ---- - -## 字典数据结构 - -后端返回的字典数据结构: - -```json -[ - { - "dict_id": 1, - "dict_value": "1", - "dict_label": "启用", - "dict_type": "user_status", - "remarks": "用户启用状态", - "remark": "用户启用状态" - }, - { - "dict_id": 2, - "dict_value": "0", - "dict_label": "禁用", - "dict_type": "user_status", - "remarks": "用户禁用状态", - "remark": "用户禁用状态" - } -] -``` - -**关键字段**: -- `dict_value`: 字典值(存储在数据库中) -- `dict_label`: 字典标签(显示给用户) -- `dict_type`: 字典类型编码(如 'user_status') - ---- - -## 最佳实践 - -### ✅ DO - -1. **使用常量而不是硬编码字符串** - ```javascript - // ✅ 好 - dictStore.getDictItems(DICT_CODES.USER_STATUS) - - // ❌ 差 - dictStore.getDictItems('user_status') - ``` - -2. **在父组件加载,通过 props 传给子组件** - ```javascript - // ✅ 父组件负责数据,子组件负责展示 - // index.vue - const statusDict = await dictStore.getDictItems(DICT_CODES.USER_STATUS) - - // UserEdit.vue - const props = defineProps({ statusDict: Array }) - ``` - -3. **用 Composable 简化组件逻辑** - ```javascript - // ✅ 一行代码搞定 - const { user_statusDict, loading } = useUserStatusDict() - ``` - -4. **预加载常用字典** - ```javascript - // ✅ 应用启动时预加载,避免页面初始化时加载 - await dictStore.preloadDicts([...]) - ``` - -### ❌ DON'T - -1. **不要在多个地方重复加载同一个字典** - ```javascript - // ❌ 糟糕:重复加载 - // 页面A - const dict1 = await dictStore.getDictItems('user_status') - - // 页面B(Store 会自动缓存,但代码看起来重复) - const dict2 = await dictStore.getDictItems('user_status') - ``` - -2. **不要忘记处理加载状态** - ```javascript - // ❌ 可能展示空白 - const { statusDict } = useUserStatusDict() - - // ✅ 处理加载状态 - const { statusDict, loading } = useUserStatusDict() - if (loading) { /* 显示加载中 */ } - ``` - -3. **不要混用不同的字典访问方式** - ```javascript - // ❌ 混乱 - const dict1 = await dictStore.getDictItems('user_status') - const dict2 = dictStore.getDictItemsSync('user_role') - - // ✅ 统一使用 - const dict1 = await dictStore.getDictItems('user_status') - const dict2 = await dictStore.getDictItems('user_role') - ``` - ---- - -## 性能优化建议 - -| 优化项 | 说明 | -|------|------| -| **缓存** | Store 自动缓存,同一个字典只请求一次 | -| **预加载** | 在路由切换前预加载需要的字典 | -| **同步访问** | 已加载的字典可用 `getDictItemsSync` 同步获取 | -| **避免重复** | 不要在多个组件重复请求同一个字典 | - ---- - -## 故障排查 - -### 问题1:状态选项为空 - -**原因**:字典未加载 -**解决**: -```javascript -// ❌ 错误:字典还未加载 -const statusDict = dictStore.getDictItemsSync('user_status') // 返回 [] - -// ✅ 正确:等待异步加载完成 -const statusDict = await dictStore.getDictItems('user_status') -``` - -### 问题2:重复加载字典 - -**原因**:没有使用 Store 的缓存 -**解决**: -```javascript -// 所有调用都会自动使用缓存,只请求一次 -await dictStore.getDictItems('user_status') // 首次:发送请求 -await dictStore.getDictItems('user_status') // 第二次:返回缓存 -``` - -### 问题3:字典显示不对 - -**原因**:value 类型不匹配(如 1 vs "1") -**解决**: -```javascript -// Store 会自动处理类型匹配 -const item = items.find(i => - String(i.dict_value) === String(value) || i.dict_value === value -) -``` - ---- - -## 集成检清表 - -- [ ] 创建 `src/stores/dict.js` - Store -- [ ] 创建 `src/constants/dictCodes.js` - 常量 -- [ ] 创建 `src/composables/useDict.js` - Composable -- [ ] 在 `index.vue` 中导入 `useDictStore` -- [ ] 在 `UserEdit.vue` 中接收 `statusDict` props -- [ ] 测试字典加载和显示 -- [ ] 验证缓存功能(打开浏览器 DevTools 检查 Network) -- [ ] 预加载常用字典(可选) - ---- - -## 相关文件修改 - -已修改的文件: -- ✅ `src/stores/dict.js` - 新建 -- ✅ `src/constants/dictCodes.js` - 新建 -- ✅ `src/composables/useDict.js` - 新建 -- ✅ `src/views/system/users/index.vue` - 使用 `useDictStore` -- ✅ `src/views/system/users/components/UserEdit.vue` - 导入字典库 - diff --git a/platform/src/api/dict.js b/platform/src/api/dict.js index 3a5cb07..b452fd3 100644 --- a/platform/src/api/dict.js +++ b/platform/src/api/dict.js @@ -1,114 +1,123 @@ import request from '@/utils/request' -// 获取字典类型列表 +/** + * 数据字典接口(平台端) + * + * 表:yz_platform_dict / yz_platform_dict_item + * 平台端字典不做租户隔离,与租户端(/backend/dict)各自独立。 + */ +const PREFIX = '/platform/dict' + +/* ================= 字典列表(字典类型) ================= */ + +/** 字典列表(分页)params: { page, pageSize, keyword, status } */ export function getDictTypes(params) { return request({ - url: '/platform/dict/types', + url: `${PREFIX}/types`, method: 'get', params }) } -// 根据ID获取字典类型 -export function getDictTypeById(id) { +/** 新增字典 */ +export function createDictType(data) { return request({ - url: `/platform/dict/types/${id}`, - method: 'get' - }) -} - -// 添加字典类型 -export function addDictType(data) { - return request({ - url: '/platform/dict/types', + url: `${PREFIX}/types`, method: 'post', - data: { - ...data, - is_global: data.is_global !== undefined ? data.is_global : 0 - } + data }) } -// 更新字典类型 +/** 编辑字典(dict_code 不可改,由后端忽略) */ export function updateDictType(id, data) { return request({ - url: `/platform/dict/types/${id}`, + url: `${PREFIX}/types/${id}`, method: 'put', - data: { - ...data, - is_global: data.is_global !== undefined ? data.is_global : 0 - } + data }) } -// 删除字典类型 +/** 删除字典(其下仍有字典项时后端会拒绝) */ export function deleteDictType(id) { return request({ - url: `/platform/dict/types/${id}`, + url: `${PREFIX}/types/${id}`, method: 'delete' }) } -// 获取字典项列表 +/* ================= 字典项 ================= */ + +/** 字典项列表(分页)params: { dict_id, page, pageSize, keyword, status } */ export function getDictItems(params) { return request({ - url: '/platform/dict/items', + url: `${PREFIX}/items`, method: 'get', params }) } -// 根据ID获取字典项 -export function getDictItemById(id) { +/** 新增字典项 */ +export function createDictItem(data) { return request({ - url: `/platform/dict/items/${id}`, - method: 'get' - }) -} - -// 添加字典项 -export function addDictItem(data) { - return request({ - url: '/platform/dict/items', + url: `${PREFIX}/items`, method: 'post', data }) } -// 更新字典项 +/** 编辑字典项 */ export function updateDictItem(id, data) { return request({ - url: `/platform/dict/items/${id}`, + url: `${PREFIX}/items/${id}`, method: 'put', data }) } -// 删除字典项 +/** 启用 / 停用字典项 */ +export function toggleDictItemStatus(id, status) { + return request({ + url: `${PREFIX}/items/${id}/status`, + method: 'post', + data: { status } + }) +} + +/** 删除字典项 */ export function deleteDictItem(id) { return request({ - url: `/platform/dict/items/${id}`, + url: `${PREFIX}/items/${id}`, method: 'delete' }) } -// 根据字典编码获取字典项(用于业务查询) -export function getDictItemsByCode(code, includeDisabled = false) { +/* ================= 业务读取 ================= */ + +/** 按字典编码取「字典类型与字典项都启用」的选项,供业务页面下拉使用 */ +export function getDictOptions(code) { return request({ - url: `/platform/dict/items/code/${code}`, - method: 'get', - params: { - include_disabled: includeDisabled ? '1' : '0' - } + url: `${PREFIX}/options/${code}`, + method: 'get' }) } -// 批量更新字典项排序 -export function batchUpdateDictItemSort(data) { +/** + * 批量取多个字典的选项(一次请求),供页面/应用启动时预加载。 + * 返回 data.dicts = { [dict_code]: 字典项[] },不存在的编码返回空数组。 + * @param {string[]} codes 字典编码数组 + */ +export function getDictOptionsBatch(codes) { return request({ - url: '/platform/dict/items/sort', - method: 'put', - data + url: `${PREFIX}/options`, + method: 'post', + data: { codes } }) } +/** 一次取回全部「启用」字典的字典项,供登录后预热缓存 */ +export function getAllDictOptions() { + return request({ + url: `${PREFIX}/options`, + method: 'get' + }) +} diff --git a/platform/src/composables/useDict.js b/platform/src/composables/useDict.js new file mode 100644 index 0000000..aed3803 --- /dev/null +++ b/platform/src/composables/useDict.js @@ -0,0 +1,59 @@ +import { computed } from 'vue' +import { useDictStore } from '@/stores/dict' + +/** + * 字典 Composable:把「加载 + 取值 + 文案/标签色查询」收敛成一行调用。 + * + * 用法: + * const { items, options, label, tagType } = useDictionary('payment_channel') + * // 模板: + * // {{ label(row.status) }} + * + * @param {string} code 字典编码(库里真实的 dict_code) + */ +export function useDictionary(code) { + const dictStore = useDictStore() + // setup 阶段同步触发加载:缓存命中时不发请求,首次会自动拉取并响应式刷新 + dictStore.ensure(code) + + /** 字典项原始数组(含 item_value / item_label / tag_type 等) */ + const items = computed(() => dictStore.getDictItemsSync(code)) + /** el-select / el-option 直接可用的 { label, value } 列表 */ + const options = computed(() => items.value.map((it) => ({ + label: it.item_label, + value: it.item_value, + tag_type: it.tag_type + }))) + + return { + items, + options, + label: (value, fallback) => dictStore.getDictLabel(code, value, fallback), + tagType: (value, fallback) => dictStore.getDictTagType(code, value, fallback), + find: (value) => dictStore.getDictItem(code, value) + } +} + +/** + * 只要「值 → 文案」或「值 → 标签色」时的轻量用法。 + * const channelLabel = useDictLabel('payment_channel') + */ +export function useDictLabel(code) { + const dictStore = useDictStore() + dictStore.ensure(code) + return (value, fallback) => dictStore.getDictLabel(code, value, fallback) +} + +export function useDictTagType(code) { + const dictStore = useDictStore() + dictStore.ensure(code) + return (value, fallback) => dictStore.getDictTagType(code, value, fallback) +} + +/** 预加载多个字典(页面 onMounted 里调用,一次批量请求) */ +export function useDictPreload(codes) { + const dictStore = useDictStore() + return () => dictStore.preloadDicts(codes) +} + +export default useDictionary diff --git a/platform/src/stores/dict.js b/platform/src/stores/dict.js new file mode 100644 index 0000000..d7f7770 --- /dev/null +++ b/platform/src/stores/dict.js @@ -0,0 +1,187 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { getDictOptions, getDictOptionsBatch, getAllDictOptions } from '@/api/dict' + +/** + * 字典 Store(平台端) + * + * 全部字典数据都来自数据库,前端不维护编码清单: + * - 登录后调用一次 loadAll(),把库里所有「启用」字典读进缓存; + * - 页面按库里真实的 dict_code 取值即可(字符串直接写,无需常量文件); + * - 之后在字典管理页新增的字典,第一次被引用时会按需自动加载。 + * + * 用法: + * const dictStore = useDictStore() + * dictStore.getDictLabel('payment_order_status', row.status) // 文案 + * dictStore.getDictTagType('payment_order_status', row.status) // 标签色 + * dictStore.getSelectOptions('payment_channel') // 下拉选项 + */ +export const useDictStore = defineStore('dict', () => { + /** dict_code -> 字典项数组(渲染依赖它,必须是响应式的) */ + const dictMap = ref({}) + + /** 已加载过的编码(仅用于去重,不参与渲染) */ + let loaded = {} + /** 正在请求中的编码 -> Promise(并发调用复用同一个请求) */ + const pending = new Map() + /** 是否已完成全量预热 */ + let allLoaded = false + let allPending = null + + function setItems(code, list) { + dictMap.value = { ...dictMap.value, [code]: Array.isArray(list) ? list : [] } + } + + function itemsOf(code) { + return dictMap.value[code] || [] + } + + /** 把入参规范成去重后的编码数组 */ + function normalizeCodes(codes) { + return [...new Set((codes || []).map((c) => String(c || '').trim()).filter(Boolean))] + } + + /** + * 加载字典(带缓存与并发去重)。 + * 请求失败也会标记为已加载,避免页面反复重试;需要重试请先 clearCache。 + */ + async function load(codes) { + const list = normalizeCodes(codes) + const missing = list.filter((c) => !loaded[c] && !pending.has(c)) + + if (missing.length) { + const promise = (async () => { + if (missing.length === 1) { + // 单个字典走专用接口 + const code = missing[0] + try { + const res = await getDictOptions(code) + setItems(code, res?.code === 200 ? res.data?.list || [] : []) + } catch { + setItems(code, []) + } + } else { + // 多个字典合并成一次请求 + try { + const res = await getDictOptionsBatch(missing) + const dicts = res?.code === 200 ? res.data?.dicts || {} : {} + missing.forEach((code) => setItems(code, dicts[code] || [])) + } catch { + missing.forEach((code) => setItems(code, [])) + } + } + })().finally(() => { + missing.forEach((code) => { + loaded[code] = true + pending.delete(code) + }) + }) + + missing.forEach((code) => pending.set(code, promise)) + } + + await Promise.all(list.map((c) => pending.get(c) || Promise.resolve())) + return list.flatMap((c) => itemsOf(c)) + } + + /** + * 全量预热:一次请求把库里所有启用字典读进缓存(登录后调用一次即可)。 + * 之后再新增的字典仍会走 load() 按需加载。 + */ + function loadAll() { + if (allLoaded) return Promise.resolve() + if (allPending) return allPending + + allPending = (async () => { + try { + const res = await getAllDictOptions() + const dicts = res?.code === 200 ? res.data?.dicts || {} : {} + const next = { ...dictMap.value } + Object.entries(dicts).forEach(([code, list]) => { + loaded[code] = true + next[code] = Array.isArray(list) ? list : [] + }) + dictMap.value = next + } catch { + // 预热失败不影响使用:后续按需加载 + } finally { + allLoaded = true + allPending = null + } + })() + return allPending + } + + /** 同步触发加载(不等待),用于组件 setup/onMounted 里“先渲染后填充” */ + function ensure(code) { + const list = normalizeCodes([code]) + if (list.some((c) => !loaded[c] && !pending.has(c))) { + load(list) + } + } + + /** 同步取字典项;未加载时顺带触发加载,拿到后由响应式自动刷新 */ + function getDictItemsSync(code) { + ensure(code) + return itemsOf(code) + } + + /** 值 → 字典项(值按字符串比较,兼容 1 / '1' 混用) */ + function getDictItem(code, value) { + if (value === undefined || value === null || value === '') return null + const key = String(value) + return itemsOf(code).find((it) => String(it.item_value) === key) || null + } + + /** 值 → 文案;取不到时返回 fallback,未传则原样返回取值 */ + function getDictLabel(code, value, fallback) { + const hit = getDictItem(code, value) + if (hit) return hit.item_label + if (fallback !== undefined) return fallback + return value === undefined || value === null ? '' : String(value) + } + + /** 值 → el-tag 的 type;取不到时返回 fallback(默认 info) */ + function getDictTagType(code, value, fallback = 'info') { + const hit = getDictItem(code, value) + return hit && hit.tag_type ? hit.tag_type : fallback + } + + /** 值 → 下拉选项 { label, value },供 el-select / el-option 直接使用 */ + function getSelectOptions(code) { + return getDictItemsSync(code).map((it) => ({ label: it.item_label, value: it.item_value, tag_type: it.tag_type })) + } + + /** 预加载多个字典(一次批量请求) */ + function preloadDicts(codes) { + return load(codes) + } + + /** 清空缓存:传编码清单个,不传清全部(同时允许重新全量预热) */ + function clearCache(code) { + if (code) { + delete loaded[code] + const next = { ...dictMap.value } + delete next[code] + dictMap.value = next + return + } + loaded = {} + dictMap.value = {} + allLoaded = false + } + + return { + dictMap, + ensure, + loadAll, + getDictItems: load, + getDictItemsSync, + getDictItem, + getDictLabel, + getDictTagType, + getSelectOptions, + preloadDicts, + clearCache + } +}) diff --git a/platform/src/utils/datetime.ts b/platform/src/utils/datetime.ts index 6acb1c6..1fcde59 100644 --- a/platform/src/utils/datetime.ts +++ b/platform/src/utils/datetime.ts @@ -53,3 +53,4 @@ export function formatDate(value: any): string { export function formatDateOnly(value: any): string { return formatDate(value); } + diff --git a/platform/src/views/Main.vue b/platform/src/views/Main.vue index e7dcbe4..c71e10a 100644 --- a/platform/src/views/Main.vue +++ b/platform/src/views/Main.vue @@ -2,12 +2,14 @@ import CommonAside from '@/components/CommonAside.vue'; import CommonHeader from '@/components/CommonHeader.vue'; import { useTabsStore } from '@/stores'; +import { useDictStore } from '@/stores/dict'; import { useRouter, useRoute } from 'vue-router'; import { ref, watch, reactive, nextTick, onMounted, computed } from 'vue'; import { More, Close, CircleClose, ArrowUp } from '@element-plus/icons-vue'; import { ElMessage } from 'element-plus'; const tabsStore = useTabsStore(); +const dictStore = useDictStore(); const router = useRouter(); const route = useRoute(); const asideRef = ref(null); @@ -39,6 +41,9 @@ function restoreTabFromRoute() { // 组件挂载后,根据当前路由恢复 tab onMounted(() => { + // 进入后台后把库里所有字典预热到缓存(失败不影响按需加载) + dictStore.loadAll(); + // 等待路由完全加载后再恢复 nextTick(() => { // 刷新时,localStorage 中已保存的 tabs 会在 store 初始化时恢复 diff --git a/platform/src/views/system/dict/components/DictItemEdit.vue b/platform/src/views/system/dict/components/DictItemEdit.vue index ed57bd9..4fb0f04 100644 --- a/platform/src/views/system/dict/components/DictItemEdit.vue +++ b/platform/src/views/system/dict/components/DictItemEdit.vue @@ -1,335 +1,173 @@ - - - - - - \ No newline at end of file +} + diff --git a/platform/src/views/system/dict/components/DictItemEditDialog.vue b/platform/src/views/system/dict/components/DictItemEditDialog.vue deleted file mode 100644 index 9717310..0000000 --- a/platform/src/views/system/dict/components/DictItemEditDialog.vue +++ /dev/null @@ -1,316 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/platform/src/views/system/dict/components/DictItemList.vue b/platform/src/views/system/dict/components/DictItemList.vue deleted file mode 100644 index e7303d6..0000000 --- a/platform/src/views/system/dict/components/DictItemList.vue +++ /dev/null @@ -1,422 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/platform/src/views/system/dict/components/DictTypeEdit.vue b/platform/src/views/system/dict/components/DictTypeEdit.vue index 23a2f97..18dbf5b 100644 --- a/platform/src/views/system/dict/components/DictTypeEdit.vue +++ b/platform/src/views/system/dict/components/DictTypeEdit.vue @@ -1,241 +1,145 @@ - - - - - - \ No newline at end of file +} + diff --git a/platform/src/views/system/dict/components/DictTypeList.vue b/platform/src/views/system/dict/components/DictTypeList.vue deleted file mode 100644 index 60ad101..0000000 --- a/platform/src/views/system/dict/components/DictTypeList.vue +++ /dev/null @@ -1,316 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/platform/src/views/system/dict/index.vue b/platform/src/views/system/dict/index.vue index 60273da..279fb8b 100644 --- a/platform/src/views/system/dict/index.vue +++ b/platform/src/views/system/dict/index.vue @@ -1,368 +1,517 @@ - - diff --git a/platform/src/views/system/payment/channels/alipay.vue b/platform/src/views/system/payment/channels/alipay.vue index 28b1988..45d9ab4 100644 --- a/platform/src/views/system/payment/channels/alipay.vue +++ b/platform/src/views/system/payment/channels/alipay.vue @@ -5,7 +5,7 @@

支付宝配置

平台使用费收款渠道,支持电脑网站支付与手机网站支付(RSA2 签名)。

- + diff --git a/platform/src/views/system/payment/channels/cloudpay.vue b/platform/src/views/system/payment/channels/cloudpay.vue index 7d6c035..54632cb 100644 --- a/platform/src/views/system/payment/channels/cloudpay.vue +++ b/platform/src/views/system/payment/channels/cloudpay.vue @@ -5,7 +5,7 @@

云闪付配置

云闪付走银联通道收款,复用银联商户参数,本页只控制收银台展示与开关。

- + diff --git a/platform/src/views/system/payment/channels/paypal.vue b/platform/src/views/system/payment/channels/paypal.vue index 5413a1d..a795826 100644 --- a/platform/src/views/system/payment/channels/paypal.vue +++ b/platform/src/views/system/payment/channels/paypal.vue @@ -5,7 +5,7 @@

PayPal 配置

面向港澳台及海外租户的收款渠道,当前业务不跨境,默认保持停用。

- + diff --git a/platform/src/views/system/payment/channels/unionpay.vue b/platform/src/views/system/payment/channels/unionpay.vue index 7fbd777..79bd77c 100644 --- a/platform/src/views/system/payment/channels/unionpay.vue +++ b/platform/src/views/system/payment/channels/unionpay.vue @@ -5,7 +5,7 @@

银联配置

银联全渠道收单(PC 网关支付 / 手机控件支付),参数由收单机构下发。

- + diff --git a/platform/src/views/system/payment/channels/wechat.vue b/platform/src/views/system/payment/channels/wechat.vue index b893a45..fbd36ad 100644 --- a/platform/src/views/system/payment/channels/wechat.vue +++ b/platform/src/views/system/payment/channels/wechat.vue @@ -5,7 +5,7 @@

微信支付配置

平台使用费收款主渠道,支持 JSAPI(公众号内)、Native(PC 扫码)支付。

- + diff --git a/platform/src/views/system/payment/commission/index.vue b/platform/src/views/system/payment/commission/index.vue index d9fc696..232ecce 100644 --- a/platform/src/views/system/payment/commission/index.vue +++ b/platform/src/views/system/payment/commission/index.vue @@ -97,7 +97,7 @@ @@ -139,12 +139,13 @@ diff --git a/platform/src/views/system/payment/guide/config.vue b/platform/src/views/system/payment/guide/config.vue index 6231063..c8233d9 100644 --- a/platform/src/views/system/payment/guide/config.vue +++ b/platform/src/views/system/payment/guide/config.vue @@ -18,7 +18,7 @@ diff --git a/platform/src/views/system/payment/guide/index.vue b/platform/src/views/system/payment/guide/index.vue index 965cf6a..7ce4b6f 100644 --- a/platform/src/views/system/payment/guide/index.vue +++ b/platform/src/views/system/payment/guide/index.vue @@ -72,7 +72,7 @@ diff --git a/platform/src/views/system/payment/index.vue b/platform/src/views/system/payment/index.vue index 468d4b5..5c5eada 100644 --- a/platform/src/views/system/payment/index.vue +++ b/platform/src/views/system/payment/index.vue @@ -88,7 +88,7 @@ diff --git a/platform/src/views/system/payment/orders/detail.vue b/platform/src/views/system/payment/orders/detail.vue index 0ec4997..b15fcf1 100644 --- a/platform/src/views/system/payment/orders/detail.vue +++ b/platform/src/views/system/payment/orders/detail.vue @@ -22,13 +22,13 @@ {{ detail.pay_no || '-' }} - + ¥{{ fenToYuan(detail.amount) }} @@ -56,7 +56,7 @@ ¥{{ fenToYuan(detail.order_amount) }} - + {{ detail.subject || '-' }} @@ -108,7 +108,7 @@ placement="top" >
- + {{ item.remark }}
操作来源:{{ item.operator }}
@@ -126,6 +126,7 @@ import { ElMessage, ElMessageBox } from 'element-plus' import { Refresh } from '@element-plus/icons-vue' import StatusTag from '../components/StatusTag.vue' import { getOrderDetail, queryPayment, refundPayment } from '@/api/payment' +import { useDictStore } from '@/stores/dict' const route = useRoute() const router = useRouter() @@ -174,21 +175,12 @@ const detail = reactive>({ /** 仅支付成功且未退款时允许手动原路退回(兜底能力) */ const canRefund = computed(() => detail.status === 'paid') -const channelMap: Record = { - wechat: '微信支付', - alipay: '支付宝', - unionpay: '银联', - cloudpay: '云闪付', - paypal: 'PayPal' -} -const channelLabel = (channel: string) => channelMap[channel] || channel || '-' - -const orderTypeMap: Record = { - platform_usage: '平台使用费', - module_shop: '模块购买', - service_fee: '服务费' -} -const orderTypeLabel = (type: string) => orderTypeMap[type] || type || '-' +/** 渠道与订单类型文案统一取自数据字典 */ +const dictStore = useDictStore() +const channelLabel = (channel: string) => + dictStore.getDictLabel('payment_channel', channel, channel || '-') +const orderTypeLabel = (type: string) => + dictStore.getDictLabel('payment_order_type', type, type || '-') /* ---------------- 数据加载 ---------------- */ diff --git a/platform/src/views/system/payment/orders/index.vue b/platform/src/views/system/payment/orders/index.vue index 0d0631c..6b71dbd 100644 --- a/platform/src/views/system/payment/orders/index.vue +++ b/platform/src/views/system/payment/orders/index.vue @@ -94,7 +94,7 @@
@@ -121,13 +121,14 @@